Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 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
167 changes: 167 additions & 0 deletions internal/mcp/mcp_parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temporarily ignored


type MCPToolDef struct {
Name string `json:"name"`
Description string `json:"description"`
InputSchema Schema `json:"inputSchema"`
OutputSchema Schema `json:"outputSchema"`
}

type Schema struct {
Schema string `json:"$schema"`
SchemaObject
}

type RawSchema struct {
Type string `json:"type"`
Description string `json:"description"`
Schema 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 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]*MCPToolDef, 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 {
// TODO: think we should panic instead
return nil, err
}

tools := map[string]*MCPToolDef{}
parser := &parser{}

for _, t := range defs.Tools {
tools[t.Name] = &MCPToolDef{
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.Schema,
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
}
100 changes: 100 additions & 0 deletions internal/mcp/mcp_parse_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
File renamed without changes.
Loading