diff --git a/internal/mcp/mcp_parse.go b/internal/mcp/mcp_parse.go new file mode 100644 index 0000000000..b5fb843804 --- /dev/null +++ b/internal/mcp/mcp_parse.go @@ -0,0 +1,166 @@ +//go:generate ../../scripts/gen-mcp-tool-json.sh mcp_tools.json +package mcp + +import ( + _ "embed" + "encoding/json" + "fmt" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +//go:embed mcp_tools.json +var _ []byte + +type ToolDef struct { + Name string `json:"name"` + Description string `json:"description"` + InputSchema Schema `json:"inputSchema"` + OutputSchema Schema `json:"outputSchema"` +} + +type RawSchema struct { + Type string `json:"type"` + Description string `json:"description"` + SchemaVersion string `json:"$schema"` + Required []string `json:"required,omitempty"` + AdditionalProperties bool `json:"additionalProperties"` + Properties map[string]json.RawMessage `json:"properties"` + Items json.RawMessage `json:"items"` +} + +type Schema struct { + Schema string `json:"$schema"` + SchemaObject +} + +type SchemaValue interface { + Type() string +} + +type SchemaObject struct { + Kind string `json:"type"` + Description string `json:"description"` + Required []string `json:"required,omitempty"` + AdditionalProperties bool `json:"additionalProperties"` + Properties map[string]SchemaValue `json:"properties"` +} + +func (s SchemaObject) Type() string { return s.Kind } + +type SchemaArray struct { + Kind string `json:"type"` + Description string `json:"description"` + Items SchemaValue `json:"items,omitempty"` +} + +func (s SchemaArray) Type() string { return s.Kind } + +type SchemaPrimitive struct { + Description string `json:"description"` + Kind string `json:"type"` +} + +func (s SchemaPrimitive) Type() string { return s.Kind } + +type parser struct { + errors []error +} + +func LoadToolDefinitions(data []byte) (map[string]*ToolDef, error) { + defs := struct { + Tools []struct { + Name string `json:"name"` + Description string `json:"description"` + InputSchema RawSchema `json:"inputSchema"` + OutputSchema RawSchema `json:"outputSchema"` + } `json:"tools"` + }{} + + if err := json.Unmarshal(data, &defs); err != nil { + return nil, err + } + + tools := map[string]*ToolDef{} + parser := &parser{} + + for _, t := range defs.Tools { + tools[t.Name] = &ToolDef{ + Name: t.Name, + Description: t.Description, + InputSchema: parser.parseRootSchema(t.InputSchema), + OutputSchema: parser.parseRootSchema(t.OutputSchema), + } + } + + if len(parser.errors) > 0 { + return tools, errors.Append(nil, parser.errors...) + } + + return tools, nil +} + +func (p *parser) parseRootSchema(r RawSchema) Schema { + return Schema{ + Schema: r.SchemaVersion, + SchemaObject: SchemaObject{ + Kind: r.Type, + Description: r.Description, + Required: r.Required, + AdditionalProperties: r.AdditionalProperties, + Properties: p.parseProperties(r.Properties), + }, + } +} + +func (p *parser) parseSchema(r *RawSchema) SchemaValue { + switch r.Type { + case "object": + return &SchemaObject{ + Kind: r.Type, + Description: r.Description, + Required: r.Required, + AdditionalProperties: r.AdditionalProperties, + Properties: p.parseProperties(r.Properties), + } + case "array": + var items SchemaValue + if len(r.Items) > 0 { + var boolItems bool + if err := json.Unmarshal(r.Items, &boolItems); err == nil { + // Sometimes items is defined as "items: true", so we handle it here and + // consider it "empty" array + } else { + var itemRaw RawSchema + if err := json.Unmarshal(r.Items, &itemRaw); err == nil { + items = p.parseSchema(&itemRaw) + } else { + p.errors = append(p.errors, errors.Errorf("failed to unmarshal array items: %w", err)) + } + } + } + return &SchemaArray{ + Kind: r.Type, + Description: r.Description, + Items: items, + } + default: + return &SchemaPrimitive{ + Kind: r.Type, + Description: r.Description, + } + } +} + +func (p *parser) parseProperties(props map[string]json.RawMessage) map[string]SchemaValue { + res := make(map[string]SchemaValue) + for name, raw := range props { + var r RawSchema + if err := json.Unmarshal(raw, &r); err != nil { + p.errors = append(p.errors, fmt.Errorf("failed to parse property %q: %w", name, err)) + continue + } + res[name] = p.parseSchema(&r) + } + return res +} diff --git a/internal/mcp/mcp_parse_test.go b/internal/mcp/mcp_parse_test.go new file mode 100644 index 0000000000..e29281e9a3 --- /dev/null +++ b/internal/mcp/mcp_parse_test.go @@ -0,0 +1,100 @@ +package mcp + +import ( + "testing" +) + +func TestLoadToolDefinitions(t *testing.T) { + toolJSON := []byte(`{ + "tools": [ + { + "name": "test_tool", + "description": "test description", + "inputSchema": { + "type": "object", + "$schema": "https://localhost/schema-draft/2025-07", + "properties": { + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "key": { "type": "string" }, + "value": { "type": "string" } + } + } + } + } + }, + "outputSchema": { + "type": "object", + "$schema": "https://localhost/schema-draft/2025-07", + "properties": { + "result": { "type": "string" } + } + } + } + ] + }`) + + tools, err := LoadToolDefinitions(toolJSON) + if err != nil { + t.Fatalf("Failed to load tool definitions: %v", err) + } + + if len(tools) != 1 { + t.Fatalf("Expected 1 tool, got %d", len(tools)) + } + + tool := tools["test_tool"] + if tool == nil { + t.Fatal("Tool 'test_tool' not found") + } + + if tool.Name != "test_tool" { + t.Errorf("Expected name 'test_tool', got '%s'", tool.Name) + } + + inputSchema := tool.InputSchema + outputSchema := tool.OutputSchema + schemaVersion := "https://localhost/schema-draft/2025-07" + + if inputSchema.Schema != schemaVersion { + t.Errorf("Expected input schema version %q, got %q", schemaVersion, inputSchema.Schema) + } + if outputSchema.Schema != schemaVersion { + t.Errorf("Expected output schema version %q, got %q", schemaVersion, outputSchema.Schema) + } + + tagsProp, ok := inputSchema.Properties["tags"] + if !ok { + t.Fatal("Property 'tags' not found in inputSchema") + } + + if tagsProp.Type() != "array" { + t.Errorf("Expected tags type 'array', got '%s'", tagsProp.Type()) + } + + arraySchema, ok := tagsProp.(*SchemaArray) + if !ok { + t.Fatal("Expected SchemaArray for tags") + } + + if arraySchema.Items == nil { + t.Fatal("Expected items schema in array, got nil") + } + + itemSchema := arraySchema.Items + if itemSchema.Type() != "object" { + t.Errorf("Expected item type 'object', got '%s'", itemSchema.Type()) + } + + objectSchema, ok := itemSchema.(*SchemaObject) + if !ok { + t.Fatal("Expected SchemaObject for item") + } + + if _, ok := objectSchema.Properties["key"]; !ok { + t.Error("Property 'key' not found in item schema") + } +} diff --git a/cmd/src/mcp_tools.json b/internal/mcp/mcp_tools.json similarity index 100% rename from cmd/src/mcp_tools.json rename to internal/mcp/mcp_tools.json