Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ This Go-based MCP server acts as a bridge between AI applications and Collibra,
- `set_owner` - set the owner by user UUID, email, username, or full name
- `set_assignees` - replace the assignee list with the given users (UUID, email, username, or full name) and groups (UUID)
- `set_visibility` - set whether the assessment is visible to everyone
- [`edit_community`](pkg/tools/edit_community/) - Edit an existing community (identified by name or UUID) via a list of typed operations, each validated and applied independently (`success` / `partial_success` / `error`):
- `set_name`, `set_description` - rename the community or change its description
- `set_parent` - move the community under another parent (name or UUID), or `makeTopLevel: true` to remove the parent; exactly one of the two is required, and the result is verified against the response
- `set_responsibility`, `remove_responsibility` - assign or unassign a user or group on a resource role (e.g. `Steward`); only directly-assigned responsibilities can be removed
- [`edit_domain`](pkg/tools/edit_domain/) - Edit an existing domain (identified by name or UUID) via a list of typed operations, each validated and applied independently (`success` / `partial_success` / `error`):
- `set_name`, `set_description` - rename the domain or change its description
- `set_community` - move the domain to a different community (name or UUID)
- `set_responsibility`, `remove_responsibility` - assign or unassign a user or group on a resource role (e.g. `Steward`); only directly-assigned responsibilities can be removed
- [`edit_asset`](pkg/tools/edit_asset/) - Edit an existing asset via a list of typed operations:
- `set_attribute`, `add_attribute`, `remove_attribute` - set an attribute value (creates if empty, updates if present), append an extra value to a multi-valued attribute, or clear one (e.g. `Definition`, `Note`)
- `update_property` - rename the asset (`name`), change its `displayName`, or change its `statusId` (status name or UUID accepted)
Expand Down
167 changes: 167 additions & 0 deletions pkg/clients/edit_container_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package clients

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
)

// ContainerRef is a reference to a community or domain (id + name).
type ContainerRef struct {
ID string `json:"id"`
Name string `json:"name"`
}

// DomainDetails is the slim view of a domain used by edit_domain and the name resolver.
type DomainDetails struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Community *ContainerRef `json:"community,omitempty"`
}

// CommunityDetails is the slim view of a community used by edit_community and the
// name resolver. Parent is nil for a top-level community.
type CommunityDetails struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parent *ContainerRef `json:"parent,omitempty"`
}

// DomainPatch is the body for PATCH /rest/2.0/domains/{id}. Nil fields are
// left unchanged by Collibra.
type DomainPatch struct {
Name *string `json:"name,omitempty"`
Description *string `json:"description,omitempty"`
CommunityID *string `json:"communityId,omitempty"`
}

// CommunityPatch is the body for PATCH /rest/2.0/communities/{id}. Nil fields
// are left unchanged by Collibra. ClearParent sends an explicit
// "parentId": null, which the API is supposed to treat as "make top-level".
type CommunityPatch struct {
Name *string
Description *string
ParentID *string
ClearParent bool
}

// MarshalJSON emits only the fields that are set; ClearParent forces an
// explicit null parentId, which omitempty on a pointer could not express.
func (p CommunityPatch) MarshalJSON() ([]byte, error) {
body := map[string]any{}
if p.Name != nil {
body["name"] = *p.Name
}
if p.Description != nil {
body["description"] = *p.Description
}
if p.ParentID != nil {
body["parentId"] = *p.ParentID
} else if p.ClearParent {
body["parentId"] = nil
}
return json.Marshal(body)
}

type domainList struct {
Results []DomainDetails `json:"results"`
}

type communityList struct {
Results []CommunityDetails `json:"results"`
}

// GetDomain fetches a domain via GET /rest/2.0/domains/{id}.
func GetDomain(ctx context.Context, client *http.Client, domainID string) (*DomainDetails, error) {
var out DomainDetails
if err := getJSON(ctx, client, "/rest/2.0/domains/"+url.PathEscape(domainID), &out); err != nil {
return nil, err
}
return &out, nil
}

// SearchDomains returns domains whose name contains the given text (the API's
// substring match); callers narrow to an exact match.
func SearchDomains(ctx context.Context, client *http.Client, name string) ([]DomainDetails, error) {
var out domainList
if err := getJSON(ctx, client, "/rest/2.0/domains?limit=50&name="+url.QueryEscape(name), &out); err != nil {
return nil, err
}
return out.Results, nil
}

// GetCommunity fetches a community via GET /rest/2.0/communities/{id}.
func GetCommunity(ctx context.Context, client *http.Client, communityID string) (*CommunityDetails, error) {
var out CommunityDetails
if err := getJSON(ctx, client, "/rest/2.0/communities/"+url.PathEscape(communityID), &out); err != nil {
return nil, err
}
return &out, nil
}

// SearchCommunities returns communities whose name contains the given text
// (the API's substring match); callers narrow to an exact match.
func SearchCommunities(ctx context.Context, client *http.Client, name string) ([]CommunityDetails, error) {
var out communityList
if err := getJSON(ctx, client, "/rest/2.0/communities?limit=50&name="+url.QueryEscape(name), &out); err != nil {
return nil, err
}
return out.Results, nil
}

// PatchDomain applies a partial update via PATCH /rest/2.0/domains/{id} and
// returns the domain as Collibra reports it afterwards.
func PatchDomain(ctx context.Context, client *http.Client, domainID string, patch DomainPatch) (*DomainDetails, error) {
var out DomainDetails
if err := patchJSON(ctx, client, "/rest/2.0/domains/"+url.PathEscape(domainID), patch, &out); err != nil {
return nil, fmt.Errorf("patch domain: %w", err)
}
return &out, nil
}

// PatchCommunity applies a partial update via PATCH /rest/2.0/communities/{id}
// and returns the community as Collibra reports it afterwards.
func PatchCommunity(ctx context.Context, client *http.Client, communityID string, patch CommunityPatch) (*CommunityDetails, error) {
var out CommunityDetails
if err := patchJSON(ctx, client, "/rest/2.0/communities/"+url.PathEscape(communityID), patch, &out); err != nil {
return nil, fmt.Errorf("patch community: %w", err)
}
return &out, nil
}

func patchJSON(ctx context.Context, client *http.Client, reqURL string, payload, out any) error {
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("marshaling request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPatch, reqURL, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("building request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("sending request: %w", err)
}
defer func() { _ = resp.Body.Close() }()

respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("reading response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status %d: %s", resp.StatusCode, string(respBody))
}
if err := json.Unmarshal(respBody, out); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
return nil
}
Loading
Loading