Skip to content
Merged
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
12 changes: 10 additions & 2 deletions cmd/chip/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ func initConfigOptions() {
pflag.StringSlice("disabled-tools", []string{}, "Optional comma-separated list of tool names to disable while enabling the remaining tools (cannot be used with enabled-tools) (env: COLLIBRA_MCP_DISABLED_TOOLS)")
_ = viper.BindEnv("mcp.disabled-tools", "COLLIBRA_MCP_DISABLED_TOOLS")
_ = viper.BindPFlag("mcp.disabled-tools", pflag.Lookup("disabled-tools"))

pflag.Bool("enable-debug-tools", false, "Enable debug tools (e.g. get_debug_mcp_init_request); off by default (env: COLLIBRA_MCP_ENABLE_DEBUG_TOOLS)")
_ = viper.BindEnv("mcp.enable-debug-tools", "COLLIBRA_MCP_ENABLE_DEBUG_TOOLS")
_ = viper.BindPFlag("mcp.enable-debug-tools", pflag.Lookup("enable-debug-tools"))
viper.SetDefault("mcp.enable-debug-tools", false)
}

func printUsage(version string) {
Expand All @@ -114,6 +119,7 @@ ENVIRONMENT VARIABLES:
COLLIBRA_MCP_HTTP_PORT HTTP server port (default: 8080)
COLLIBRA_MCP_ENABLED_TOOLS Optional comma-separated list of tool names to enable instead of enabling all tools, cannot be used with disabled-tools
COLLIBRA_MCP_DISABLED_TOOLS Optional comma-separated list of tool names to disable while enabling the remaining tools, cannot be used with enabled-tools
COLLIBRA_MCP_ENABLE_DEBUG_TOOLS Enable debug tools (default: false)

CONFIGURATION:
Configuration can be provided in the following order of precedence: command-line flags (highest), environment variables, or a YAML configuration file (lowest).
Expand All @@ -139,6 +145,7 @@ CONFIGURATION FILE EXAMPLE:
# disabled-tools: # Optional: list of tools to disable (cannot be used with enabled-tools)
# - "tool3"
# - "tool4"
enable-debug-tools: false # Optional: enable debug tools (default: false)
`)
}

Expand Down Expand Up @@ -193,8 +200,9 @@ type McpConfig struct {
Mode string `mapstructure:"mode"` // "stdio", "http", "http-sse", or "http-streamable"
Http HttpConfig `mapstructure:"http"`
Stdio StdioConfig `mapstructure:"stdio"`
EnabledTools []string `mapstructure:"enabled-tools"`
DisabledTools []string `mapstructure:"disabled-tools"`
EnabledTools []string `mapstructure:"enabled-tools"`
DisabledTools []string `mapstructure:"disabled-tools"`
EnableDebugTools bool `mapstructure:"enable-debug-tools"`
}

type HttpConfig struct {
Expand Down
5 changes: 3 additions & 2 deletions cmd/chip/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ func main() {
client := newCollibraClient(config)
server := chip.NewServer(chip.WithToolMiddleware(chip.ToolMiddlewareFunc(setCollibraHost(config.Api.Url))))
toolConfig := &chip.ServerToolConfig{
EnabledTools: config.Mcp.EnabledTools,
DisabledTools: config.Mcp.DisabledTools,
EnabledTools: config.Mcp.EnabledTools,
DisabledTools: config.Mcp.DisabledTools,
EnableDebugTools: config.Mcp.EnableDebugTools,
}
tools.RegisterAll(server, client, toolConfig)

Expand Down
6 changes: 6 additions & 0 deletions docs/CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The server can be configured using the following environment variables:
- `COLLIBRA_MCP_API_PROXY` | `HTTP_PROXY` | `HTTPS_PROXY` - HTTP proxy URL for API requests (e.g., `http://proxy.example.com:8080`)
- `COLLIBRA_MCP_ENABLED_TOOLS` - Comma-separated list of tool names to enable instead of enabling all tools (cannot be used with `COLLIBRA_MCP_DISABLED_TOOLS`)
- `COLLIBRA_MCP_DISABLED_TOOLS` - Comma-separated list of tool names to disable while enabling the remaining tools (cannot be used with `COLLIBRA_MCP_ENABLED_TOOLS`)
- `COLLIBRA_MCP_ENABLE_DEBUG_TOOLS` - Register debug tools (e.g. `get_debug_mcp_init_request`) that are hidden by default. Set to `true` to enable. Off by default.

## Configuration File

Expand Down Expand Up @@ -51,6 +52,9 @@ mcp:
# optionally enable OR disable specific tools using the tool names listed in the README.md file.
# enabled-tools: []
# disabled-tools: []

# optionally register debug tools that are hidden by default (e.g. get_debug_mcp_init_request).
# enable-debug-tools: false
```

## Configuration Structure
Expand All @@ -71,6 +75,7 @@ The configuration is organized into two main sections:
- `stdio` section: (currently empty, reserved for future stdio-specific settings)
- `enabled-tools` - optional list of tool names to be enabled instead of enabling all tools. Cannot be used with `disabled-tools`
- `disabled-tools` - optional list of tool names to be disabled while enabling remaining tools. Cannot be used with `enabled-tools`
- `enable-debug-tools` - optional boolean. When `true`, registers debug tools that are hidden by default (e.g. `get_debug_mcp_init_request`). Defaults to `false`.

## Authentication Approaches

Expand Down Expand Up @@ -208,3 +213,4 @@ All environment variables use the `COLLIBRA_MCP_` prefix. The configuration syst
- `COLLIBRA_MCP_HTTP_PORT` → `mcp.http.port`
- `COLLIBRA_MCP_ENABLED_TOOLS` → `mcp.enabled-tools`
- `COLLIBRA_MCP_DISABLED_TOOLS` → `mcp.disabled-tools`
- `COLLIBRA_MCP_ENABLE_DEBUG_TOOLS` → `mcp.enable-debug-tools`
10 changes: 10 additions & 0 deletions pkg/chip/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type contextKey int
const (
callToolRequestKey contextKey = iota
collibraHostKey
initParamsKey
)

func SetCallToolRequest(ctx context.Context, toolRequest *mcp.CallToolRequest) context.Context {
Expand All @@ -32,6 +33,15 @@ func GetCollibraHost(ctx context.Context) (string, bool) {
return collibraHost, ok
}

func SetInitParams(ctx context.Context, params *mcp.InitializeParams) context.Context {
return context.WithValue(ctx, initParamsKey, params)
}

func GetInitParams(ctx context.Context) (*mcp.InitializeParams, bool) {
params, ok := ctx.Value(initParamsKey).(*mcp.InitializeParams)
return params, ok
}

func GetSessionId(ctx context.Context) string {
toolRequest, ok := GetCallToolRequest(ctx)
if ok {
Expand Down
42 changes: 42 additions & 0 deletions pkg/chip/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log"
"log/slog"
"slices"
"sync"

"github.com/google/jsonschema-go/jsonschema"
"github.com/modelcontextprotocol/go-sdk/mcp"
Expand All @@ -29,13 +30,35 @@ func (f ToolMiddlewareFunc) ToolHandle(ctx context.Context, toolRequest *mcp.Cal
return f(ctx, toolRequest, next)
}

// initParamsStore holds the last InitializeParams received from a client.
// In stateless HTTP mode each call gets a fresh session, so the params from
// the initial handshake are captured here and re-injected into the per-request
// context by the receiving middleware.
type initParamsStore struct {
mu sync.RWMutex
params *mcp.InitializeParams
}

func (s *initParamsStore) set(p *mcp.InitializeParams) {
s.mu.Lock()
defer s.mu.Unlock()
s.params = p
}

func (s *initParamsStore) get() *mcp.InitializeParams {
s.mu.RLock()
defer s.mu.RUnlock()
return s.params
}

type Server struct {
toolMiddlewares []ToolMiddleware
toolMetadata map[string]*ToolMetadata
mcp.Server
}

func NewServer(opts ...ServerOption) *Server {
store := &initParamsStore{}
s := &Server{
toolMiddlewares: []ToolMiddleware{},
toolMetadata: make(map[string]*ToolMetadata),
Expand All @@ -48,6 +71,20 @@ func NewServer(opts ...ServerOption) *Server {
}),
}

s.AddReceivingMiddleware(func(next mcp.MethodHandler) mcp.MethodHandler {
return func(ctx context.Context, method string, req mcp.Request) (mcp.Result, error) {
if method == "initialize" {
if params, ok := req.GetParams().(*mcp.InitializeParams); ok {
store.set(params)
}
}
if p := store.get(); p != nil {
ctx = SetInitParams(ctx, p)
}
return next(ctx, method, req)
}
})

for _, opt := range opts {
opt(s)
}
Expand All @@ -70,6 +107,8 @@ type ToolMetadata struct {
type ServerToolConfig struct {
EnabledTools []string
DisabledTools []string
// EnableDebugTools, when true, registers debug tools that are otherwise hidden.
EnableDebugTools bool
}

func (tc *ServerToolConfig) IsToolEnabled(toolName string) bool {
Expand Down Expand Up @@ -147,6 +186,9 @@ func buildSchema[Schema any]() *jsonschema.Schema {
if err != nil {
log.Fatal(err)
}
if inputSchema == nil {
log.Fatalf("jsonschema.For returned nil schema for %T", *new(Schema))
}
inputSchema.AdditionalProperties = nil
return inputSchema
}
57 changes: 57 additions & 0 deletions pkg/chip/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,63 @@ func TestTool_IgnoreUnknownFields(t *testing.T) {
}
}

func TestServerToolConfig_IsToolEnabled(t *testing.T) {
cases := []struct {
name string
cfg ServerToolConfig
tool string
expected bool
}{
{"empty config enables everything", ServerToolConfig{}, "foo", true},
{"explicitly disabled", ServerToolConfig{DisabledTools: []string{"foo"}}, "foo", false},
{"allow-list excludes others", ServerToolConfig{EnabledTools: []string{"bar"}}, "foo", false},
{"allow-list includes self", ServerToolConfig{EnabledTools: []string{"foo"}}, "foo", true},
{"disabled wins over enabled", ServerToolConfig{EnabledTools: []string{"foo"}, DisabledTools: []string{"foo"}}, "foo", false},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := tc.cfg.IsToolEnabled(tc.tool); got != tc.expected {
t.Fatalf("IsToolEnabled(%q) = %v, want %v", tc.tool, got, tc.expected)
}
})
}
}

func TestServer_InitParamsAvailableOnToolContext(t *testing.T) {
chipServer := NewServer()
var captured *mcp.InitializeParams
RegisterTool(chipServer, &Tool[toolInput, toolOutput]{
Name: "capture_init",
Description: "Captures init params for testing.",
Handler: func(ctx context.Context, _ toolInput) (toolOutput, error) {
p, _ := GetInitParams(ctx)
captured = p
return toolOutput{}, nil
},
})
chipSession := newChipSession(t.Context(), chipServer)
defer closeSilently(chipSession)

if _, err := chipSession.CallTool(t.Context(), &mcp.CallToolParams{
Name: "capture_init",
Arguments: map[string]any{"input": "x"},
}); err != nil {
t.Fatalf("CallTool failed: %v", err)
}
if captured == nil {
t.Fatal("expected init params to be captured on tool context")
}
if captured.ClientInfo == nil {
t.Fatal("expected ClientInfo on captured init params")
}
if captured.ClientInfo.Name != "client" {
t.Fatalf("expected ClientInfo.Name=client, got %q", captured.ClientInfo.Name)
}
if captured.ClientInfo.Version != "v0.0.1" {
t.Fatalf("expected ClientInfo.Version=v0.0.1, got %q", captured.ClientInfo.Version)
}
}

func newChipSession(ctx context.Context, chipServer *Server) *mcp.ClientSession {
t1, t2 := mcp.NewInMemoryTransports()
if _, err := chipServer.Connect(ctx, t1, nil); err != nil {
Expand Down
34 changes: 34 additions & 0 deletions pkg/tools/get_debug_mcp_init_request/tool.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package get_debug_mcp_init_request

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

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

type Input struct{}

type Output = mcp.InitializeParams

func NewTool(_ *http.Client) *chip.Tool[Input, Output] {
return &chip.Tool[Input, Output]{
Name: "get_debug_mcp_init_request",
Description: "Returns the MCP initialize request the connected client sent during the handshake, including protocolVersion, clientInfo, and declared capabilities (e.g. whether elicitation, sampling, or roots are supported). Useful for debugging MCP client/server compatibility.",
Handler: handler(),
Permissions: []string{},
Annotations: &mcp.ToolAnnotations{ReadOnlyHint: true},
}
}

func handler() chip.ToolHandlerFunc[Input, Output] {
return func(ctx context.Context, _ Input) (Output, error) {
params, ok := chip.GetInitParams(ctx)
if !ok {
return Output{}, fmt.Errorf("no InitializeParams available on context")
}
return *params, nil
}
}
53 changes: 53 additions & 0 deletions pkg/tools/get_debug_mcp_init_request/tool_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package get_debug_mcp_init_request_test

import (
"context"
"strings"
"testing"

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

func TestHandler_ReturnsParamsFromContext(t *testing.T) {
tool := get_debug_mcp_init_request.NewTool(nil)
params := &mcp.InitializeParams{
ProtocolVersion: "2024-11-05",
ClientInfo: &mcp.Implementation{Name: "test-client", Version: "v1.2.3"},
Capabilities: &mcp.ClientCapabilities{},
}
ctx := chip.SetInitParams(context.Background(), params)

out, err := tool.Handler(ctx, get_debug_mcp_init_request.Input{})
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if out.ProtocolVersion != "2024-11-05" {
t.Fatalf("expected ProtocolVersion=2024-11-05, got %q", out.ProtocolVersion)
}
if out.ClientInfo == nil {
t.Fatal("expected ClientInfo on output")
}
if out.ClientInfo.Name != "test-client" {
t.Fatalf("expected ClientInfo.Name=test-client, got %q", out.ClientInfo.Name)
}
if out.ClientInfo.Version != "v1.2.3" {
t.Fatalf("expected ClientInfo.Version=v1.2.3, got %q", out.ClientInfo.Version)
}
if out.Capabilities == nil {
t.Fatal("expected Capabilities on output")
}
}

func TestHandler_ErrorsWhenContextMissingParams(t *testing.T) {
tool := get_debug_mcp_init_request.NewTool(nil)

_, err := tool.Handler(context.Background(), get_debug_mcp_init_request.Input{})
if err == nil {
t.Fatal("expected error when context has no init params")
}
if !strings.Contains(err.Error(), "no InitializeParams") {
t.Fatalf("expected error mentioning missing InitializeParams, got %q", err.Error())
}
}
5 changes: 5 additions & 0 deletions pkg/tools/register.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/collibra/chip/pkg/tools/get_asset_details"
"github.com/collibra/chip/pkg/tools/get_business_term_data"
"github.com/collibra/chip/pkg/tools/get_column_semantics"
"github.com/collibra/chip/pkg/tools/get_debug_mcp_init_request"
"github.com/collibra/chip/pkg/tools/get_lineage_downstream"
"github.com/collibra/chip/pkg/tools/get_lineage_entity"
"github.com/collibra/chip/pkg/tools/get_lineage_transformation"
Expand Down Expand Up @@ -65,6 +66,10 @@ func RegisterAll(server *chip.Server, client *http.Client, toolConfig *chip.Serv
toolRegister(server, toolConfig, prepare_create_asset.NewTool(client))
toolRegister(server, toolConfig, create_asset.NewTool(client))
toolRegister(server, toolConfig, edit_asset.NewTool(client))

if toolConfig.EnableDebugTools {
toolRegister(server, toolConfig, get_debug_mcp_init_request.NewTool(client))
}
}

func toolRegister[In, Out any](server *chip.Server, toolConfig *chip.ServerToolConfig, tool *chip.Tool[In, Out]) {
Expand Down
Loading
Loading