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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ This Go-based MCP server acts as a bridge between AI applications and Collibra,
- [`add_data_classification_match`](pkg/tools/add_data_classification_match/) - Associate a data class with an asset. **Requires:** `dgc.classify`, `dgc.catalog`
- [`create_assessment`](pkg/tools/create_assessment/) - Conduct a new assessment from a template (given by name or UUID) in the Assessments application. Returns the template's (unanswered) questions to fill in afterward with `edit_assessment` — no separate prepare step needed. The owner and USER assignees accept a UUID, email, username, or full name
- [`create_asset`](pkg/tools/create_asset/) - Create a new asset of any type. Resolves `assetType` (UUID, publicId, or display name), `domain` (UUID or name), `status` (UUID or name), and attributes (by name or typeId) server-side; converts Markdown to HTML for `RICH_TEXT` attributes; gates on duplicate-name (default `allowDuplicate: false`)
- [`create_community`](pkg/tools/create_community/) - Create a community, optionally nested under an existing `parentCommunity` (UUID or name). Confirm checkpoint: `confirm=false` (default) previews the community without creating it; `confirm=true` creates it. Requires permission to create communities
- [`create_domain`](pkg/tools/create_domain/) - Create a domain in an existing `community` with a given `domainType` (both accept a UUID or name; an unknown domain type returns the valid ones). Confirm checkpoint: `confirm=false` (default) previews the domain without creating it; `confirm=true` creates it. Requires permission to create domains in the community
- [`create_data_quality_job`](pkg/tools/create_dq_job/) - Set up automated data-quality monitoring on a database table in Collibra, and start the first run. A data quality job profiles a table and watches it over time for problems — sudden row-count drops, spikes in null or empty values, duplicates, and other anomalies. Spans discovery (connection → dataSource → schema → table, one field per turn, or a catalog Table asset), preview, and create; optional tuning covers selected columns, monitors and adaptive-baseline settings, sample size, a row filter, a time slice, a recurring schedule, back-runs and notifications. `jobType` is PUSHDOWN (checks run inside the source database) or PULLUP (data is pulled into Spark). Confirm checkpoint: `confirm=false` (default) returns the exact request without creating anything; `confirm=true` creates and runs the job. **Gated:** the `data-quality` capability (`--data-quality`)
- [`create_data_quality_rule`](pkg/tools/create_dq_rule/) - Create a data quality rule (monitor) on an existing DQ job. `monitorType` is `FREEFORM_SQL` (full SQL query) or `SIMPLE_SQL` (single-column check); defaults to active and not suppressed. Confirm checkpoint: `confirm=false` (default) returns a preview of the rule + SQL without creating; `confirm=true` creates. Uses the DQ monitoring API and requires permission to create rules on the target job. **Gated:** the `data-quality` capability (`--data-quality`)
- [`deploy_data_quality_rule_template`](pkg/tools/deploy_dq_rule_template/) - Instantiate a rule template as concrete rules across one or more job/column targets (bulk). The DQ service resolves dialect-specific SQL and names each rule `{templateName}_{columnName}`. Confirm checkpoint: `confirm=false` (default) previews the template + targets without deploying; `confirm=true` deploys. Requires permission to deploy templates and create rules on the target jobs. **Gated:** the `data-quality` capability (`--data-quality`)
Expand Down
64 changes: 64 additions & 0 deletions pkg/clients/create_community_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package clients

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

// CreateCommunityRequest is the request body for POST /rest/2.0/communities.
type CreateCommunityRequest struct {
Name string `json:"name"`
ParentID string `json:"parentId,omitempty"`
Description string `json:"description,omitempty"`
}

// CreateCommunityResponse is the response from POST /rest/2.0/communities.
type CreateCommunityResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Parent *NamedIDRef `json:"parent,omitempty"`
}

// NamedIDRef is a {id, name} reference to another resource, as nested in
// create responses.
type NamedIDRef struct {
ID string `json:"id"`
Name string `json:"name"`
}

// CreateCommunity creates a new community via POST /rest/2.0/communities.
func CreateCommunity(ctx context.Context, client *http.Client, request CreateCommunityRequest) (*CreateCommunityResponse, error) {
var result CreateCommunityResponse
if err := postJSON(ctx, client, "/rest/2.0/communities", request, &result); err != nil {
return nil, fmt.Errorf("creating community: %w", err)
}
return &result, nil
}

// postJSON POSTs body as JSON and decodes the response into out. Non-2xx
// responses surface Collibra's error envelope via executeCollibraRequest.
func postJSON(ctx context.Context, client *http.Client, path string, body, out any) error {
payload, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("marshaling request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, path, bytes.NewReader(payload))
if err != nil {
return fmt.Errorf("building request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")

respBody, err := executeCollibraRequest(client, req)
if err != nil {
return err
}
if err := json.Unmarshal(respBody, out); err != nil {
return fmt.Errorf("decoding response: %w", err)
}
return nil
}
33 changes: 33 additions & 0 deletions pkg/clients/create_domain_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package clients

import (
"context"
"fmt"
"net/http"
)

// CreateDomainRequest is the request body for POST /rest/2.0/domains.
type CreateDomainRequest struct {
Name string `json:"name"`
CommunityID string `json:"communityId"`
TypeID string `json:"typeId"`
Description string `json:"description,omitempty"`
}

// CreateDomainResponse is the response from POST /rest/2.0/domains.
type CreateDomainResponse struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Type *NamedIDRef `json:"type,omitempty"`
Community *NamedIDRef `json:"community,omitempty"`
}

// CreateDomain creates a new domain via POST /rest/2.0/domains.
func CreateDomain(ctx context.Context, client *http.Client, request CreateDomainRequest) (*CreateDomainResponse, error) {
var result CreateDomainResponse
if err := postJSON(ctx, client, "/rest/2.0/domains", request, &result); err != nil {
return nil, fmt.Errorf("creating domain: %w", err)
}
return &result, nil
}
170 changes: 170 additions & 0 deletions pkg/tools/create_community/tool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Package create_community implements the create_community MCP tool: a single
// write tool that creates a Collibra community, optionally nested under an
// existing parent community. It is built around the confirm checkpoint of
// docs/TOOL_CONTRIBUTION_STANDARDS.md section 5: confirm=false (the default)
// returns a preview of everything that would be written and writes nothing;
// confirm=true performs the write.
package create_community

import (
"context"
"fmt"
"net/http"
"strings"
"unicode/utf8"

"github.com/collibra/chip/pkg/chip"
"github.com/collibra/chip/pkg/clients"
"github.com/collibra/chip/pkg/tools/resolve"
"github.com/modelcontextprotocol/go-sdk/mcp"
)

// maxNameLength is the longest community name Collibra accepts.
const maxNameLength = 255

// OutputStatus is the overall outcome of a create_community call.
type OutputStatus string

const (
// StatusPreview means confirm was not set: nothing was written.
StatusPreview OutputStatus = "preview"
// StatusSuccess means the community was created.
StatusSuccess OutputStatus = "success"
// StatusValidationError means validation or name resolution failed before
// any write.
StatusValidationError OutputStatus = "validation_error"
// StatusError means Collibra rejected the write.
StatusError OutputStatus = "error"
)

// Input is the tool's typed input.
type Input struct {
Name string `json:"name" jsonschema:"Required. Name of the new community, 1-255 characters. Collibra requires community names to be unique across the whole instance, e.g. 'Employee Wellness'."`
ParentCommunity string `json:"parentCommunity,omitempty" jsonschema:"Optional. The existing community to nest the new one under, as a UUID or the parent's exact display name (case-insensitive). Omit to create a top-level community."`
Description string `json:"description,omitempty" jsonschema:"Optional. Free-text description of the community."`
Confirm bool `json:"confirm,omitempty" jsonschema:"Safety checkpoint. false (the default) returns a PREVIEW of the community and writes nothing, so it can be reviewed with the user. Set true only after the user approved the preview."`
}

// Preview echoes every field that confirm=true would write.
type Preview struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
ParentCommunityID string `json:"parentCommunityId,omitempty"`
ParentCommunityName string `json:"parentCommunityName,omitempty"`
}

// CommunitySummary is the created community as Collibra returned it.
type CommunitySummary struct {
ID string `json:"id" jsonschema:"UUID of the newly created community."`
Name string `json:"name"`
Description string `json:"description,omitempty"`
ParentCommunityID string `json:"parentCommunityId,omitempty" jsonschema:"UUID of the parent community, when nested."`
}

// Output is the typed response.
type Output struct {
Status OutputStatus `json:"status" jsonschema:"'preview' when confirm was not set (nothing written; review it, then call again with confirm=true); 'success' when the community was created; 'validation_error' for invalid or unresolvable inputs; 'error' when Collibra rejected the write."`
Message string `json:"message" jsonschema:"Human-readable summary."`
Preview *Preview `json:"preview,omitempty" jsonschema:"The community that would be created, when status is 'preview'."`
Community *CommunitySummary `json:"community,omitempty" jsonschema:"The created community, when status is 'success'."`
}

// NewTool returns the registered tool.
func NewTool(collibraClient *http.Client) *chip.Tool[Input, Output] {
return &chip.Tool[Input, Output]{
Name: "create_community",
Title: "Create Community",
Description: "Create a new Collibra community — the top-level organizational container that groups domains (a domain being a container for assets), " +
"for example 'Finance' or 'Employee Wellness'. A community can be nested under another community by passing parentCommunity, " +
"which accepts the parent's UUID or its exact display name, so there is no need to look the UUID up first. " +
"Use this to create a community; to create a domain inside a community use create_domain, and to create assets use create_asset. " +
"The name must be unique across all communities on the instance. " +
"Built around a confirm checkpoint: confirm=false (the default) returns a PREVIEW of exactly what would be created and writes nothing — show it to the user; " +
"call again with confirm=true to create it. " +
"Returns the new community's id on success. " +
"Requires permission to create communities (typically an administrator action); without it the write fails with a permission error. " +
"Example requests: 'create a community called Employee Wellness', 'add a sub-community Payroll under Finance', 'set up a new community for the marketing team'.",
Handler: handler(collibraClient),
Permissions: []string{},
Annotations: &mcp.ToolAnnotations{ReadOnlyHint: false, DestructiveHint: chip.Ptr(false), IdempotentHint: false, OpenWorldHint: chip.Ptr(false)},
}
}

func handler(collibraClient *http.Client) chip.ToolHandlerFunc[Input, Output] {
return func(ctx context.Context, input Input) (Output, error) {
name := strings.TrimSpace(input.Name)
if msg := validateName(name); msg != "" {
return Output{Status: StatusValidationError, Message: msg}, nil
}

var parent resolve.Ref
if raw := strings.TrimSpace(input.ParentCommunity); raw != "" {
var err error
parent, err = resolve.Community(ctx, collibraClient, "parentCommunity", raw)
if err != nil {
return Output{Status: StatusValidationError, Message: fmt.Sprintf("parentCommunity could not be resolved: %v", err)}, nil
}
}

if !input.Confirm {
return previewOutput(name, input.Description, parent), nil
}

resp, err := clients.CreateCommunity(ctx, collibraClient, clients.CreateCommunityRequest{
Name: name,
ParentID: parent.ID,
Description: input.Description,
})
if err != nil {
return Output{Status: StatusError, Message: fmt.Sprintf("Could not create community: %v", err)}, nil
}
return successOutput(resp, parent), nil
}
}

func validateName(name string) string {
switch {
case name == "":
return "name is required: provide a community name of 1-255 characters."
case utf8.RuneCountInString(name) > maxNameLength:
return fmt.Sprintf("name is %d characters long; Collibra allows at most %d. Shorten it.", utf8.RuneCountInString(name), maxNameLength)
}
return ""
}

func previewOutput(name, description string, parent resolve.Ref) Output {
return Output{
Status: StatusPreview,
Message: fmt.Sprintf("Preview only — nothing was created. Will create community %q%s. "+
"Review this with the user, then call again with confirm=true.", name, nestingSuffix(parent)),
Preview: &Preview{
Name: name,
Description: description,
ParentCommunityID: parent.ID,
ParentCommunityName: parent.Name,
},
}
}

func successOutput(resp *clients.CreateCommunityResponse, parent resolve.Ref) Output {
summary := &CommunitySummary{ID: resp.ID, Name: resp.Name, Description: resp.Description}
if resp.Parent != nil {
summary.ParentCommunityID = resp.Parent.ID
parent.Name = resp.Parent.Name
}
return Output{
Status: StatusSuccess,
Message: fmt.Sprintf("Created community %q (id %s)%s.", resp.Name, resp.ID, nestingSuffix(parent)),
Community: summary,
}
}

func nestingSuffix(parent resolve.Ref) string {
switch {
case parent.Name != "":
return fmt.Sprintf(" under parent community %q", parent.Name)
case parent.ID != "":
return fmt.Sprintf(" under parent community %s", parent.ID)
}
return " as a top-level community"
}
Loading
Loading