From d46ac71a3a5a2c650e7aa60f049603c4c910c1eb Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Wed, 10 Dec 2025 12:42:46 +0200 Subject: [PATCH 1/4] add tool registry registry tracks tools and from which endpoints they originated from --- internal/mcp/registry.go | 81 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 internal/mcp/registry.go diff --git a/internal/mcp/registry.go b/internal/mcp/registry.go new file mode 100644 index 0000000000..d846def224 --- /dev/null +++ b/internal/mcp/registry.go @@ -0,0 +1,81 @@ +package mcp + +import ( + "context" + "encoding/json" + "iter" + + "github.com/sourcegraph/src-cli/internal/api" + + "github.com/sourcegraph/sourcegraph/lib/errors" +) + +// ToolRegistry keeps track of tools and the endpoints they originated from +type ToolRegistry struct { + tools map[string]*ToolDef + endpoints map[string]string +} + +func NewToolRegistry() *ToolRegistry { + return &ToolRegistry{ + tools: make(map[string]*ToolDef), + endpoints: make(map[string]string), + } +} + +// LoadTools loads the tool definitions from the Mcp tool endpoints constants McpURLPath and McpDeepSearchURLPath +func (r *ToolRegistry) LoadTools(ctx context.Context, client api.Client) error { + endpoints := []string{McpURLPath, McpDeepSearchURLPath} + + var errs []error + for _, endpoint := range endpoints { + tools, err := fetchToolDefinitions(ctx, client, endpoint) + if err != nil { + errs = append(errs, errors.Wrapf(err, "failed to load tools from %s", endpoint)) + continue + } + r.register(endpoint, tools) + } + + if len(errs) > 0 { + return errors.Append(nil, errs...) + } + return nil +} + +// register associates a collection of tools with the given endpoint +func (r *ToolRegistry) register(endpoint string, tools map[string]*ToolDef) { + for name, def := range tools { + r.tools[name] = def + r.endpoints[name] = endpoint + } +} + +// Get returns the tool definition for the given name +func (r *ToolRegistry) Get(name string) (*ToolDef, bool) { + tool, ok := r.tools[name] + return tool, ok +} + +// CallTool calls the given tool with the given arguments. It constructs the Tool request and decodes the Tool response +func (r *ToolRegistry) CallTool(ctx context.Context, client api.Client, name string, args map[string]any) (map[string]json.RawMessage, error) { + tool := r.tools[name] + endpoint := r.endpoints[name] + resp, err := doToolCall(ctx, client, endpoint, tool.RawName, args) + if err != nil { + return nil, err + } + defer resp.Body.Close() + return decodeToolResponse(resp) +} + +// All returns an iterator that yields the name and Tool definition of all registered tools +func (r *ToolRegistry) All() iter.Seq2[string, *ToolDef] { + return func(yield func(string, *ToolDef) bool) { + for name, def := range r.tools { + if !yield(name, def) { + return + } + } + } +} From a744b31e76360137590ae903f4ddb4e15b83632f Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Wed, 10 Dec 2025 12:43:21 +0200 Subject: [PATCH 2/4] unexport DoToolCall and DecodeToolResponse --- internal/mcp/mcp_request.go | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/internal/mcp/mcp_request.go b/internal/mcp/mcp_request.go index 4b41db1570..d584da2ea0 100644 --- a/internal/mcp/mcp_request.go +++ b/internal/mcp/mcp_request.go @@ -14,9 +14,10 @@ import ( ) const McpURLPath = ".api/mcp/v1" +const McpDeepSearchURLPath = ".api/mcp/deepsearch" -func FetchToolDefinitions(ctx context.Context, client api.Client) (map[string]*ToolDef, error) { - resp, err := doJSONRPC(ctx, client, "tools/list", nil) +func fetchToolDefinitions(ctx context.Context, client api.Client, endpoint string) (map[string]*ToolDef, error) { + resp, err := doJSONRPC(ctx, client, endpoint, "tools/list", nil) if err != nil { return nil, errors.Wrap(err, "failed to list tools from mcp endpoint") } @@ -44,7 +45,7 @@ func FetchToolDefinitions(ctx context.Context, client api.Client) (map[string]*T return loadToolDefinitions(rpcResp.Result) } -func DoToolCall(ctx context.Context, client api.Client, tool string, vars map[string]any) (*http.Response, error) { +func doToolCall(ctx context.Context, client api.Client, endpoint string, tool string, vars map[string]any) (*http.Response, error) { params := struct { Name string `json:"name"` Arguments map[string]any `json:"arguments"` @@ -53,10 +54,10 @@ func DoToolCall(ctx context.Context, client api.Client, tool string, vars map[st Arguments: vars, } - return doJSONRPC(ctx, client, "tools/call", params) + return doJSONRPC(ctx, client, endpoint, "tools/call", params) } -func doJSONRPC(ctx context.Context, client api.Client, method string, params any) (*http.Response, error) { +func doJSONRPC(ctx context.Context, client api.Client, endpoint string, method string, params any) (*http.Response, error) { jsonRPC := struct { Version string `json:"jsonrpc"` ID int `json:"id"` @@ -75,7 +76,7 @@ func doJSONRPC(ctx context.Context, client api.Client, method string, params any } buf.Write(data) - req, err := client.NewHTTPRequest(ctx, http.MethodPost, McpURLPath, buf) + req, err := client.NewHTTPRequest(ctx, http.MethodPost, endpoint, buf) if err != nil { return nil, err } @@ -91,13 +92,13 @@ func doJSONRPC(ctx context.Context, client api.Client, method string, params any body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096)) resp.Body.Close() return nil, errors.Newf("MCP endpoint %s returned %d: %s", - McpURLPath, resp.StatusCode, strings.TrimSpace(string(body))) + endpoint, resp.StatusCode, strings.TrimSpace(string(body))) } return resp, nil } -func DecodeToolResponse(resp *http.Response) (map[string]json.RawMessage, error) { +func decodeToolResponse(resp *http.Response) (map[string]json.RawMessage, error) { data, err := readSSEResponseData(resp) if err != nil { return nil, err From 5dc08417290316e9b8a28e8c60205d0a549d3e8e Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Wed, 10 Dec 2025 12:44:24 +0200 Subject: [PATCH 3/4] use tool registry in mcp command for tool execution --- cmd/src/mcp.go | 41 +++++++++++++++-------------------------- 1 file changed, 15 insertions(+), 26 deletions(-) diff --git a/cmd/src/mcp.go b/cmd/src/mcp.go index 33dbd8f99d..af07a5453d 100644 --- a/cmd/src/mcp.go +++ b/cmd/src/mcp.go @@ -7,7 +7,6 @@ import ( "fmt" "strings" - "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/mcp" "github.com/sourcegraph/sourcegraph/lib/errors" @@ -37,8 +36,8 @@ func mcpMain(args []string) error { apiClient := cfg.apiClient(nil, mcpFlagSet.Output()) ctx := context.Background() - tools, err := mcp.FetchToolDefinitions(ctx, apiClient) - if err != nil { + registry := mcp.NewToolRegistry() + if err := registry.LoadTools(ctx, apiClient); err != nil { return err } @@ -50,7 +49,7 @@ func mcpMain(args []string) error { subcmd := args[0] if subcmd == "list-tools" { fmt.Println("The following tools are available:") - for name := range tools { + for name := range registry.All() { fmt.Printf(" %s\n", name) } fmt.Println("\nUSAGE:") @@ -59,7 +58,7 @@ func mcpMain(args []string) error { fmt.Println(" src mcp -h List the available flags of a tool") return nil } - tool, ok := tools[subcmd] + tool, ok := registry.Get(subcmd) if !ok { return errors.Newf("tool definition for %q not found - run src mcp list-tools to see a list of available tools", subcmd) } @@ -82,7 +81,17 @@ func mcpMain(args []string) error { return err } - return handleMcpTool(context.Background(), apiClient, tool, vars) + result, err := registry.CallTool(ctx, apiClient, tool.Name, vars) + if err != nil { + return err + } + + output, err := json.MarshalIndent(result, "", " ") + if err != nil { + return err + } + fmt.Println(string(output)) + return nil } func printSchemas(tool *mcp.ToolDef) error { @@ -112,23 +121,3 @@ func validateToolArgs(inputSchema mcp.SchemaObject, args []string, vars map[stri return nil } - -func handleMcpTool(ctx context.Context, client api.Client, tool *mcp.ToolDef, vars map[string]any) error { - resp, err := mcp.DoToolCall(ctx, client, tool.RawName, vars) - if err != nil { - return err - } - - result, err := mcp.DecodeToolResponse(resp) - if err != nil { - return err - } - defer resp.Body.Close() - - output, err := json.MarshalIndent(result, "", " ") - if err != nil { - return err - } - fmt.Println(string(output)) - return nil -} From 42ca15da04b0a207d4972c4cc13d5095f01855c4 Mon Sep 17 00:00:00 2001 From: William Bezuidenhout Date: Wed, 10 Dec 2025 14:39:02 +0200 Subject: [PATCH 4/4] fix MCP casing --- internal/mcp/mcp_request.go | 4 ++-- internal/mcp/registry.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/mcp/mcp_request.go b/internal/mcp/mcp_request.go index d584da2ea0..eaba8377b9 100644 --- a/internal/mcp/mcp_request.go +++ b/internal/mcp/mcp_request.go @@ -13,8 +13,8 @@ import ( "github.com/sourcegraph/sourcegraph/lib/errors" ) -const McpURLPath = ".api/mcp/v1" -const McpDeepSearchURLPath = ".api/mcp/deepsearch" +const MCPURLPath = ".api/mcp/v1" +const MCPDeepSearchURLPath = ".api/mcp/deepsearch" func fetchToolDefinitions(ctx context.Context, client api.Client, endpoint string) (map[string]*ToolDef, error) { resp, err := doJSONRPC(ctx, client, endpoint, "tools/list", nil) diff --git a/internal/mcp/registry.go b/internal/mcp/registry.go index d846def224..d428c48e4f 100644 --- a/internal/mcp/registry.go +++ b/internal/mcp/registry.go @@ -25,7 +25,7 @@ func NewToolRegistry() *ToolRegistry { // LoadTools loads the tool definitions from the Mcp tool endpoints constants McpURLPath and McpDeepSearchURLPath func (r *ToolRegistry) LoadTools(ctx context.Context, client api.Client) error { - endpoints := []string{McpURLPath, McpDeepSearchURLPath} + endpoints := []string{MCPURLPath, MCPDeepSearchURLPath} var errs []error for _, endpoint := range endpoints {