From add42aa9dcd110b8a8d1cd4df46f136f98cb773a Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 10 Mar 2026 21:39:51 +0200 Subject: [PATCH 1/7] feat: give each routing endpoint its own focused tool set /mcp/code now only exposes code_execution + retrieve_tools (with instructions to use code_execution, not call_tool_*). /mcp/call now has a dedicated server with retrieve_tools (with instructions to use call_tool_read/write/destructive) + the three call_tool variants + read_cache. No code_execution, upstream_servers, or other tools that don't belong. Previously /mcp/call served the full default server with all tools. Co-Authored-By: Claude Opus 4.6 --- internal/server/mcp.go | 1 + internal/server/mcp_routing.go | 166 +++++++++++++++++++++++++++- internal/server/mcp_routing_test.go | 6 +- internal/server/server.go | 4 +- 4 files changed, 171 insertions(+), 6 deletions(-) diff --git a/internal/server/mcp.go b/internal/server/mcp.go index 04492923..af03548d 100644 --- a/internal/server/mcp.go +++ b/internal/server/mcp.go @@ -75,6 +75,7 @@ type MCPProxyServer struct { // Each instance has different tools registered for its routing mode. directServer *mcpserver.MCPServer // Direct mode: upstream tools with serverName__toolName naming codeExecServer *mcpserver.MCPServer // Code execution mode: code_execution + retrieve_tools + callToolServer *mcpserver.MCPServer // Call tool mode: retrieve_tools + call_tool_read/write/destructive // Docker availability cache dockerAvailableCache *bool diff --git a/internal/server/mcp_routing.go b/internal/server/mcp_routing.go index 5451dbc8..850b4366 100644 --- a/internal/server/mcp_routing.go +++ b/internal/server/mcp_routing.go @@ -264,9 +264,12 @@ func (p *MCPProxyServer) buildCodeExecModeTools() []mcpserver.ServerTool { }) } - // retrieve_tools for discovery + // retrieve_tools for discovery — instructs to use code_execution (NOT call_tool_*) retrieveToolsTool := mcp.NewTool("retrieve_tools", - mcp.WithDescription("Search and discover available upstream tools using BM25 full-text search. Use this to find tools before orchestrating them with code_execution. Use natural language to describe what you want to accomplish."), + mcp.WithDescription("Search and discover available upstream tools using BM25 full-text search. "+ + "Use this to find tools, then use the `code_execution` tool to call them via `call_tool(serverName, toolName, args)` in JavaScript. "+ + "Do NOT use call_tool_read/write/destructive — they are not available in this mode. "+ + "Use natural language to describe what you want to accomplish."), mcp.WithTitleAnnotation("Retrieve Tools"), mcp.WithReadOnlyHintAnnotation(true), mcp.WithString("query", @@ -288,6 +291,142 @@ func (p *MCPProxyServer) buildCodeExecModeTools() []mcpserver.ServerTool { return tools } +// buildCallToolModeTools builds the tool set for retrieve_tools routing mode (/mcp/call). +// Includes: retrieve_tools (with call_tool_* instructions) + call_tool_read/write/destructive + read_cache. +// Does NOT include code_execution or upstream_servers. +func (p *MCPProxyServer) buildCallToolModeTools() []mcpserver.ServerTool { + tools := make([]mcpserver.ServerTool, 0, 5) + + // retrieve_tools — instructs to use call_tool_read/write/destructive + retrieveToolsTool := mcp.NewTool("retrieve_tools", + mcp.WithDescription("Search and discover available upstream tools using BM25 full-text search. "+ + "WORKFLOW: 1) Call this tool first to find relevant tools, 2) Check the 'call_with' field in results "+ + "to determine which variant to use, 3) Call the tool using call_tool_read, call_tool_write, or call_tool_destructive. "+ + "Results include 'annotations' (tool behavior hints like destructiveHint) and 'call_with' recommendation. "+ + "Use natural language to describe what you want to accomplish."), + mcp.WithTitleAnnotation("Retrieve Tools"), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithString("query", + mcp.Required(), + mcp.Description("Natural language description of what you want to accomplish. Be specific (e.g., 'create a new GitHub repository', 'get weather for London')."), + ), + mcp.WithNumber("limit", + mcp.Description("Maximum number of tools to return (default: configured tools_limit, max: 100)"), + ), + mcp.WithBoolean("include_stats", + mcp.Description("Include usage statistics for returned tools (default: false)"), + ), + mcp.WithBoolean("debug", + mcp.Description("Enable debug mode with detailed scoring and ranking explanations (default: false)"), + ), + mcp.WithString("explain_tool", + mcp.Description("When debug=true, explain why a specific tool was ranked low (format: 'server:tool')"), + ), + ) + tools = append(tools, mcpserver.ServerTool{ + Tool: retrieveToolsTool, + Handler: p.handleRetrieveTools, + }) + + // call_tool_read + callToolReadTool := mcp.NewTool(contracts.ToolVariantRead, + mcp.WithDescription("Execute a READ-ONLY tool. WORKFLOW: 1) Call retrieve_tools first to find tools, 2) Use the exact 'name' field from results. Use this for: search, query, list, get, fetch, find, check, view, read, show, describe, lookup, retrieve, browse, explore, discover, scan, inspect, analyze, examine, validate, verify. This is the DEFAULT choice when unsure."), + mcp.WithTitleAnnotation("Call Tool (Read)"), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Tool name in format 'server:tool' (e.g., 'github:get_user'). Use exact names from retrieve_tools results."), + ), + mcp.WithString("args_json", + mcp.Description("Arguments as JSON string. Refer to the tool's inputSchema from retrieve_tools."), + ), + mcp.WithString("intent_data_sensitivity", + mcp.Description("Classify data: public, internal, private, or unknown."), + ), + mcp.WithString("intent_reason", + mcp.Description("Why is this tool being called? Provide context."), + ), + ) + tools = append(tools, mcpserver.ServerTool{ + Tool: callToolReadTool, + Handler: p.handleCallToolRead, + }) + + // call_tool_write + callToolWriteTool := mcp.NewTool(contracts.ToolVariantWrite, + mcp.WithDescription("Execute a STATE-MODIFYING tool. WORKFLOW: 1) Call retrieve_tools first to find tools, 2) Use the exact 'name' field from results. Use this for: create, update, modify, add, set, send, edit, change, write, post, put, patch, insert, upload, submit. Use only when explicitly modifying state."), + mcp.WithTitleAnnotation("Call Tool (Write)"), + mcp.WithDestructiveHintAnnotation(false), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Tool name in format 'server:tool' (e.g., 'github:create_issue'). Use exact names from retrieve_tools results."), + ), + mcp.WithString("args_json", + mcp.Description("Arguments as JSON string. Refer to the tool's inputSchema from retrieve_tools."), + ), + mcp.WithString("intent_data_sensitivity", + mcp.Description("Classify data: public, internal, private, or unknown."), + ), + mcp.WithString("intent_reason", + mcp.Description("Why is this modification needed? Provide context."), + ), + ) + tools = append(tools, mcpserver.ServerTool{ + Tool: callToolWriteTool, + Handler: p.handleCallToolWrite, + }) + + // call_tool_destructive + callToolDestructiveTool := mcp.NewTool(contracts.ToolVariantDestructive, + mcp.WithDescription("Execute a DESTRUCTIVE tool. WORKFLOW: 1) Call retrieve_tools first to find tools, 2) Use the exact 'name' field from results. Use this for: delete, remove, drop, revoke, disable, destroy, purge, reset, clear, terminate. Use for irreversible or high-impact operations."), + mcp.WithTitleAnnotation("Call Tool (Destructive)"), + mcp.WithDestructiveHintAnnotation(true), + mcp.WithString("name", + mcp.Required(), + mcp.Description("Tool name in format 'server:tool' (e.g., 'github:delete_repo'). Use exact names from retrieve_tools results."), + ), + mcp.WithString("args_json", + mcp.Description("Arguments as JSON string. Refer to the tool's inputSchema from retrieve_tools."), + ), + mcp.WithString("intent_data_sensitivity", + mcp.Description("Classify data: public, internal, private, or unknown."), + ), + mcp.WithString("intent_reason", + mcp.Description("Why is this deletion needed? Provide justification."), + ), + ) + tools = append(tools, mcpserver.ServerTool{ + Tool: callToolDestructiveTool, + Handler: p.handleCallToolDestructive, + }) + + // read_cache for paginated responses + readCacheTool := mcp.NewTool("read_cache", + mcp.WithDescription("Retrieve paginated data when mcpproxy indicates a tool response was truncated. Use the cache key provided in truncation messages."), + mcp.WithTitleAnnotation("Read Cache"), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithString("key", + mcp.Required(), + mcp.Description("Cache key provided by mcpproxy when a response was truncated."), + ), + mcp.WithNumber("offset", + mcp.Description("Starting record offset for pagination (default: 0)"), + ), + mcp.WithNumber("limit", + mcp.Description("Maximum number of records to return per page (default: 50, max: 1000)"), + ), + ) + tools = append(tools, mcpserver.ServerTool{ + Tool: readCacheTool, + Handler: p.handleReadCache, + }) + + p.logger.Info("built call tool mode tools", + zap.Int("tool_count", len(tools))) + + return tools +} + // buildToolCatalogDescription builds a human-readable catalog of available tools for the code_execution description. func (p *MCPProxyServer) buildToolCatalogDescription(ctx context.Context) string { tools, err := p.upstreamManager.DiscoverTools(ctx) @@ -340,12 +479,26 @@ func (p *MCPProxyServer) initRoutingModeServers() { mcpserver.WithRecovery(), ) + // Create call tool mode server (/mcp/call) + p.callToolServer = mcpserver.NewMCPServer( + "mcpproxy-go", + "1.0.0", + mcpserver.WithToolCapabilities(true), + mcpserver.WithRecovery(), + ) + // Register tools for code execution mode (static tools that don't change) codeExecTools := p.buildCodeExecModeTools() for _, st := range codeExecTools { p.codeExecServer.AddTool(st.Tool, st.Handler) } + // Register tools for call tool mode + callToolModeTools := p.buildCallToolModeTools() + for _, st := range callToolModeTools { + p.callToolServer.AddTool(st.Tool, st.Handler) + } + // Note: Direct mode tools are built lazily/on-demand via RefreshDirectModeTools // because upstream servers may not be connected yet during initialization. // The servers.changed event will trigger a refresh. @@ -403,6 +556,10 @@ func (p *MCPProxyServer) GetMCPServerForMode(mode string) *mcpserver.MCPServer { if p.codeExecServer != nil { return p.codeExecServer } + case config.RoutingModeRetrieveTools: + if p.callToolServer != nil { + return p.callToolServer + } } // Default: retrieve_tools mode (the original server) return p.server @@ -417,3 +574,8 @@ func (p *MCPProxyServer) GetDirectServer() *mcpserver.MCPServer { func (p *MCPProxyServer) GetCodeExecServer() *mcpserver.MCPServer { return p.codeExecServer } + +// GetCallToolServer returns the call tool mode MCP server instance. +func (p *MCPProxyServer) GetCallToolServer() *mcpserver.MCPServer { + return p.callToolServer +} diff --git a/internal/server/mcp_routing_test.go b/internal/server/mcp_routing_test.go index 5b46d23b..b66603b0 100644 --- a/internal/server/mcp_routing_test.go +++ b/internal/server/mcp_routing_test.go @@ -165,10 +165,12 @@ func TestGetMCPServerForMode(t *testing.T) { mainServer := mcpserver.NewMCPServer("main", "1.0.0", mcpserver.WithToolCapabilities(true)) directServer := mcpserver.NewMCPServer("direct", "1.0.0", mcpserver.WithToolCapabilities(true)) codeExecServer := mcpserver.NewMCPServer("code_exec", "1.0.0", mcpserver.WithToolCapabilities(true)) + callToolServer := mcpserver.NewMCPServer("call_tool", "1.0.0", mcpserver.WithToolCapabilities(true)) proxy.server = mainServer proxy.directServer = directServer proxy.codeExecServer = codeExecServer + proxy.callToolServer = callToolServer tests := []struct { name string @@ -176,9 +178,9 @@ func TestGetMCPServerForMode(t *testing.T) { expected *mcpserver.MCPServer }{ { - name: "retrieve_tools returns main server", + name: "retrieve_tools returns call tool server", mode: "retrieve_tools", - expected: mainServer, + expected: callToolServer, }, { name: "direct returns direct server", diff --git a/internal/server/server.go b/internal/server/server.go index 5c0360fe..20aaca64 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1511,8 +1511,8 @@ func (s *Server) startCustomHTTPServer(ctx context.Context, streamableServer *se mux.Handle("/mcp/code", codeExecHandler) mux.Handle("/mcp/code/", codeExecHandler) - // /mcp/call → retrieve_tools mode (explicit, same as default server) - callToolStreamable := server.NewStreamableHTTPServer(s.mcpProxy.GetMCPServer()) + // /mcp/call → retrieve_tools mode (focused: retrieve_tools + call_tool_read/write/destructive) + callToolStreamable := server.NewStreamableHTTPServer(s.mcpProxy.GetMCPServerForMode(config.RoutingModeRetrieveTools)) callToolHandler := s.mcpAuthMiddleware(loggingHandler(callToolStreamable)) mux.Handle("/mcp/call", callToolHandler) mux.Handle("/mcp/call/", callToolHandler) From 218de1f74396f326b5db24ec4ad5ad1f56e52def Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 10 Mar 2026 22:00:08 +0200 Subject: [PATCH 2/7] feat: MCP endpoints dropdown, CLI endpoints, version, management tools Web UI: - Replace single "Proxy" copy button with dropdown showing all 4 MCP endpoints (/mcp, /mcp/call, /mcp/all, /mcp/code) with descriptions and per-endpoint copy buttons CLI: - `mcpproxy status` now shows MCP Endpoints section with all URLs Backend: - MCP server instances now report actual build version (not "1.0.0") - Management tools (upstream_servers, quarantine_security, search_servers, list_registries) are now available on all routing endpoints - Extract buildManagementTools() for sharing across routing modes Docs: - Add pros/cons and comparison table for each routing mode - Add "Choosing the Right Mode" decision matrix Co-Authored-By: Claude Opus 4.6 --- cmd/mcpproxy/main.go | 5 +- cmd/mcpproxy/status_cmd.go | 63 ++++++++-- docs/features/routing-modes.md | 104 ++++++++++++---- frontend/src/components/TopHeader.vue | 117 ++++++++++++++---- internal/server/mcp.go | 56 +++++++-- internal/server/mcp_routing.go | 12 +- web/frontend/dist/assets/Activity-3jsAMS7R.js | 1 - .../dist/assets/AdminDashboard-_SN62PSb.js | 1 - .../dist/assets/AdminServers-BS1vVmqX.js | 1 - .../dist/assets/AdminUsers-LpYNmcL8.js | 1 - .../dist/assets/AgentTokens-Cr2jVCc5.js | 1 - web/frontend/dist/assets/Login-BO9OPmov.js | 1 - web/frontend/dist/assets/NotFound-alLFYf5-.js | 1 - .../dist/assets/Repositories-Ob_RpJWO.js | 1 - web/frontend/dist/assets/Search-DItjCgfV.js | 7 -- web/frontend/dist/assets/Secrets-D_Jzkx2b.js | 23 ---- .../dist/assets/ServerDetail-BASjhAMZ.js | 12 -- web/frontend/dist/assets/Servers-DInf8P5a.js | 16 --- web/frontend/dist/assets/Sessions-B3Q2H5AK.js | 1 - web/frontend/dist/assets/Settings-Bk3uo8hY.js | 22 ---- .../dist/assets/UserActivity-D77DruWK.js | 1 - .../dist/assets/UserDiagnostics-CYaZlbXF.js | 1 - .../dist/assets/UserServers-BBrm1aSk.js | 3 - .../dist/assets/UserTokens-DOuVGX0Y.js | 1 - web/frontend/dist/assets/index-BR76citP.css | 1 - web/frontend/dist/assets/index-DMP0eryo.js | 85 ------------- web/frontend/dist/index.html | 4 +- 27 files changed, 281 insertions(+), 261 deletions(-) delete mode 100644 web/frontend/dist/assets/Activity-3jsAMS7R.js delete mode 100644 web/frontend/dist/assets/AdminDashboard-_SN62PSb.js delete mode 100644 web/frontend/dist/assets/AdminServers-BS1vVmqX.js delete mode 100644 web/frontend/dist/assets/AdminUsers-LpYNmcL8.js delete mode 100644 web/frontend/dist/assets/AgentTokens-Cr2jVCc5.js delete mode 100644 web/frontend/dist/assets/Login-BO9OPmov.js delete mode 100644 web/frontend/dist/assets/NotFound-alLFYf5-.js delete mode 100644 web/frontend/dist/assets/Repositories-Ob_RpJWO.js delete mode 100644 web/frontend/dist/assets/Search-DItjCgfV.js delete mode 100644 web/frontend/dist/assets/Secrets-D_Jzkx2b.js delete mode 100644 web/frontend/dist/assets/ServerDetail-BASjhAMZ.js delete mode 100644 web/frontend/dist/assets/Servers-DInf8P5a.js delete mode 100644 web/frontend/dist/assets/Sessions-B3Q2H5AK.js delete mode 100644 web/frontend/dist/assets/Settings-Bk3uo8hY.js delete mode 100644 web/frontend/dist/assets/UserActivity-D77DruWK.js delete mode 100644 web/frontend/dist/assets/UserDiagnostics-CYaZlbXF.js delete mode 100644 web/frontend/dist/assets/UserServers-BBrm1aSk.js delete mode 100644 web/frontend/dist/assets/UserTokens-DOuVGX0Y.js delete mode 100644 web/frontend/dist/assets/index-BR76citP.css delete mode 100644 web/frontend/dist/assets/index-DMP0eryo.js diff --git a/cmd/mcpproxy/main.go b/cmd/mcpproxy/main.go index 2277120b..048420d1 100644 --- a/cmd/mcpproxy/main.go +++ b/cmd/mcpproxy/main.go @@ -41,8 +41,8 @@ import ( clioutput "github.com/smart-mcp-proxy/mcpproxy-go/internal/cli/output" "github.com/smart-mcp-proxy/mcpproxy-go/internal/config" - "github.com/smart-mcp-proxy/mcpproxy-go/internal/httpapi" "github.com/smart-mcp-proxy/mcpproxy-go/internal/experiments" + "github.com/smart-mcp-proxy/mcpproxy-go/internal/httpapi" "github.com/smart-mcp-proxy/mcpproxy-go/internal/logs" "github.com/smart-mcp-proxy/mcpproxy-go/internal/registries" "github.com/smart-mcp-proxy/mcpproxy-go/internal/server" @@ -476,8 +476,9 @@ func runServer(cmd *cobra.Command, _ []string) error { zap.String("log_level", cmdLogLevel), zap.Bool("log_to_file", cmdLogToFile)) - // Pass edition to httpapi for status endpoint + // Pass edition and version to internal packages httpapi.SetEdition(Edition) + server.SetMCPServerVersion(version) // Override other settings from command line cfg.DebugSearch = cmdDebugSearch diff --git a/cmd/mcpproxy/status_cmd.go b/cmd/mcpproxy/status_cmd.go index bc85f169..81df7ed8 100644 --- a/cmd/mcpproxy/status_cmd.go +++ b/cmd/mcpproxy/status_cmd.go @@ -21,19 +21,20 @@ import ( // StatusInfo holds the collected status data for display. type StatusInfo struct { - State string `json:"state"` - Edition string `json:"edition"` - ListenAddr string `json:"listen_addr"` - Uptime string `json:"uptime,omitempty"` - UptimeSeconds float64 `json:"uptime_seconds,omitempty"` - APIKey string `json:"api_key"` - WebUIURL string `json:"web_ui_url"` - RoutingMode string `json:"routing_mode"` - Servers *ServerCounts `json:"servers,omitempty"` - SocketPath string `json:"socket_path,omitempty"` - ConfigPath string `json:"config_path,omitempty"` - Version string `json:"version,omitempty"` - TeamsInfo *TeamsStatusInfo `json:"teams,omitempty"` + State string `json:"state"` + Edition string `json:"edition"` + ListenAddr string `json:"listen_addr"` + Uptime string `json:"uptime,omitempty"` + UptimeSeconds float64 `json:"uptime_seconds,omitempty"` + APIKey string `json:"api_key"` + WebUIURL string `json:"web_ui_url"` + RoutingMode string `json:"routing_mode"` + Endpoints map[string]string `json:"endpoints"` + Servers *ServerCounts `json:"servers,omitempty"` + SocketPath string `json:"socket_path,omitempty"` + ConfigPath string `json:"config_path,omitempty"` + Version string `json:"version,omitempty"` + TeamsInfo *TeamsStatusInfo `json:"teams,omitempty"` } // TeamsStatusInfo holds teams-specific status information. @@ -218,6 +219,9 @@ func collectStatusFromDaemon(cfg *config.Config, socketPath, configPath string) info.WebUIURL = statusBuildWebUIURL(info.ListenAddr, cfg.APIKey) } + // Build MCP endpoint URLs + info.Endpoints = statusBuildEndpoints(info.ListenAddr) + return info, nil } @@ -239,6 +243,7 @@ func collectStatusFromConfig(cfg *config.Config, socketPath, configPath string) APIKey: cfg.APIKey, WebUIURL: statusBuildWebUIURL(listenAddr, cfg.APIKey), RoutingMode: routingMode, + Endpoints: statusBuildEndpoints(listenAddr), ConfigPath: configPath, } @@ -273,6 +278,21 @@ func statusMaskAPIKey(apiKey string) string { return apiKey[:4] + "****" + apiKey[len(apiKey)-4:] } +// statusBuildEndpoints constructs the MCP endpoint URLs map. +func statusBuildEndpoints(listenAddr string) map[string]string { + addr := listenAddr + if strings.HasPrefix(addr, ":") { + addr = "127.0.0.1" + addr + } + base := "http://" + addr + return map[string]string{ + "default": base + "/mcp", + "retrieve_tools": base + "/mcp/call", + "direct": base + "/mcp/all", + "code_execution": base + "/mcp/code", + } +} + // statusBuildWebUIURL constructs the Web UI URL with embedded API key. func statusBuildWebUIURL(listenAddr, apiKey string) string { addr := listenAddr @@ -379,6 +399,23 @@ func printStatusTable(info *StatusInfo) { fmt.Printf(" %-12s %s\n", "Config:", info.ConfigPath) } + if info.Endpoints != nil { + fmt.Println() + fmt.Println("MCP Endpoints") + if v, ok := info.Endpoints["default"]; ok { + fmt.Printf(" %-16s %s (default, %s mode)\n", "/mcp", v, info.RoutingMode) + } + if v, ok := info.Endpoints["retrieve_tools"]; ok { + fmt.Printf(" %-16s %s (retrieve + call tools)\n", "/mcp/call", v) + } + if v, ok := info.Endpoints["direct"]; ok { + fmt.Printf(" %-16s %s (all tools, direct access)\n", "/mcp/all", v) + } + if v, ok := info.Endpoints["code_execution"]; ok { + fmt.Printf(" %-16s %s (code execution)\n", "/mcp/code", v) + } + } + if info.TeamsInfo != nil { fmt.Println() fmt.Println("Server Edition") diff --git a/docs/features/routing-modes.md b/docs/features/routing-modes.md index ca1aff1d..e114ce4d 100644 --- a/docs/features/routing-modes.md +++ b/docs/features/routing-modes.md @@ -52,36 +52,51 @@ This means you can point different AI clients at different endpoints. For exampl ### retrieve_tools (Default) -The default mode uses BM25 full-text search to help AI agents discover relevant tools without exposing the entire tool catalog. This is the most token-efficient mode. +The default mode uses BM25 full-text search to help AI agents discover relevant tools without exposing the entire tool catalog. This approach — sometimes called "lazy tool loading" or "tool search" — is used by Anthropic's own MCP implementation and is the recommended pattern for large tool sets. + +**Endpoint:** `/mcp/call` **Tools exposed:** -- `retrieve_tools` -- Search for tools by natural language query -- `call_tool_read` -- Execute read-only tool calls -- `call_tool_write` -- Execute write tool calls -- `call_tool_destructive` -- Execute destructive tool calls -- `upstream_servers` -- Manage upstream servers (if management enabled) -- `code_execution` -- JavaScript orchestration (if enabled) +- `retrieve_tools` — Search for tools by natural language query +- `call_tool_read` — Execute read-only tool calls +- `call_tool_write` — Execute write tool calls +- `call_tool_destructive` — Execute destructive tool calls +- `read_cache` — Access paginated responses **How it works:** 1. AI agent calls `retrieve_tools` with a natural language query -2. MCPProxy returns matching tools ranked by BM25 relevance -3. AI agent calls the appropriate `call_tool_*` variant with the tool name +2. MCPProxy returns matching tools ranked by BM25 relevance, with `call_with` recommendations +3. AI agent calls the appropriate `call_tool_*` variant with the exact tool name from results + +**Pros:** +- Massive token savings: only matched tools are sent to the model, not the full catalog +- Scales to hundreds of tools across many servers without context window bloat +- Intent-based permission control (read/write/destructive variants) enables granular IDE approval flows +- Activity logging captures operation type and intent metadata for auditing + +**Cons:** +- Two-step workflow (search then call) adds one round-trip compared to direct mode +- BM25 search quality depends on tool descriptions — poorly described tools may not surface +- The AI agent must learn the retrieve-then-call pattern (most modern models handle this well) **When to use:** - You have many upstream servers with dozens or hundreds of tools -- Token usage is a concern (only tool metadata for matched tools is sent) -- You want intent-based permission control (read/write/destructive variants) +- Token usage is a concern (common with paid API usage) +- You want intent-based permission control in IDE auto-approve settings +- Production deployments where audit trails matter ### direct -Direct mode exposes every upstream tool directly to the AI agent. Each tool is named `serverName__toolName` (double underscore separator). +Direct mode exposes every upstream tool directly to the AI agent. Each tool appears in the standard MCP `tools/list` with a `serverName__toolName` name. This is the simplest mode and the closest to how individual MCP servers work natively. + +**Endpoint:** `/mcp/all` **Tools exposed:** - Every tool from every connected, enabled, non-quarantined upstream server - Named as `serverName__toolName` (e.g., `github__create_issue`, `filesystem__read_file`) **How it works:** -1. AI agent sees all available tools in the tools list +1. AI agent sees all available tools in `tools/list` 2. AI agent calls tools directly by their `serverName__toolName` name 3. MCPProxy routes the call to the correct upstream server @@ -94,31 +109,74 @@ Direct mode exposes every upstream tool directly to the AI agent. Each tool is n - Agent tokens with server restrictions are enforced (access denied if token lacks server access) - Permission levels are derived from tool annotations (read-only, destructive, etc.) +**Pros:** +- Zero learning curve: tools work exactly like native MCP tools +- Single round-trip: no search step needed, call any tool directly +- Maximum compatibility: works with any MCP client without special handling +- Tool annotations (readOnlyHint, destructiveHint) are preserved from upstream + +**Cons:** +- High token cost: all tool definitions are sent in every request context +- Does not scale well beyond ~50 tools (context window fills up, model accuracy degrades) +- No intent-based permission tiers (the model just calls tools) +- All tools visible upfront increases attack surface for prompt injection + **When to use:** -- You have a small number of upstream servers (fewer than 50 total tools) -- You want maximum simplicity and compatibility -- AI clients that work better with a flat tool list +- Small setups with fewer than 50 total tools +- Quick prototyping and testing +- AI clients that don't support the retrieve-then-call pattern +- CI/CD agents that know exactly which tools they need ### code_execution -Code execution mode is designed for multi-step orchestration workflows. It exposes the `code_execution` tool with an enhanced description that includes a catalog of all available upstream tools. +Code execution mode is designed for multi-step orchestration workflows. Instead of making separate tool calls for each step, the AI agent writes JavaScript or TypeScript code that chains multiple tool calls together in a single request. This is inspired by patterns from OpenAI's code interpreter and similar "tool-as-code" approaches. + +**Endpoint:** `/mcp/code` **Tools exposed:** -- `code_execution` -- Execute JavaScript/TypeScript that orchestrates upstream tools -- `retrieve_tools` -- Search for tools (useful for discovery before writing code) +- `code_execution` — Execute JavaScript/TypeScript that orchestrates upstream tools (includes a catalog of all available tools in the description) +- `retrieve_tools` — Search for tools (instructs to use `code_execution`, not `call_tool_*`) **How it works:** 1. AI agent sees the `code_execution` tool with a listing of all available upstream tools 2. AI agent writes JavaScript/TypeScript code that calls `call_tool(serverName, toolName, args)` -3. MCPProxy executes the code in a sandboxed VM, routing tool calls to upstream servers +3. MCPProxy executes the code in a sandboxed ES2020+ VM, routing tool calls to upstream servers +4. Results are returned as a single response + +**Pros:** +- Minimal round-trips: complex multi-step workflows execute in one request +- Full programming power: conditionals, loops, error handling, data transformation +- TypeScript support with type safety (auto-transpiled via esbuild) +- Sandboxed execution: no filesystem or network access, timeout enforcement +- Tool catalog in description means no separate search step needed + +**Cons:** +- Requires the AI model to write correct JavaScript/TypeScript code +- Debugging is harder: errors come from inside the sandbox, not from MCP tool calls +- Higher latency per request (VM startup + multiple sequential tool calls) +- Must be explicitly enabled (`"enable_code_execution": true`) +- Not all AI models are equally good at writing code for tool orchestration **When to use:** -- Workflows that require chaining 2+ tool calls together -- You want to minimize model round-trips -- Complex conditional logic or data transformation between tool calls +- Workflows that chain 2+ tool calls with data dependencies between them +- Batch operations (e.g., "for each repo, check CI status and create issue if failing") +- Complex conditional logic that would require many round-trips in other modes +- Data transformation pipelines (fetch from one tool, transform, send to another) **Note:** Code execution must be enabled in config (`"enable_code_execution": true`). If disabled, the `code_execution` tool appears but returns an error message directing the user to enable it. +## Choosing the Right Mode + +| Factor | retrieve_tools | direct | code_execution | +|--------|---------------|--------|----------------| +| **Token cost** | Low (only matched tools) | High (all tools) | Medium (catalog in description) | +| **Round-trips per task** | 2 (search + call) | 1 (direct call) | 1 (code handles all) | +| **Max practical tools** | 500+ | ~50 | 500+ | +| **Setup complexity** | None (default) | None | Requires enablement | +| **Model requirements** | Any modern LLM | Any LLM | Code-capable LLM | +| **Audit granularity** | High (intent metadata) | Medium (annotations) | Medium (code logged) | +| **IDE auto-approve** | Per-variant rules | Per-tool rules | Single rule | + ## Viewing Current Routing Mode ### CLI diff --git a/frontend/src/components/TopHeader.vue b/frontend/src/components/TopHeader.vue index 40937b77..300cf55c 100644 --- a/frontend/src/components/TopHeader.vue +++ b/frontend/src/components/TopHeader.vue @@ -70,19 +70,45 @@ {{ routingModeLabel }} - -
- Proxy: - {{ systemStore.listenAddr }} - + +
@@ -97,7 +123,7 @@ - + +
From d44365aa0341a52a26da5d83bd912451422d7bbe Mon Sep 17 00:00:00 2001 From: Claude Code Date: Tue, 10 Mar 2026 22:10:50 +0200 Subject: [PATCH 3/7] fix: MCP endpoints dropdown not opening due to overflow-x-hidden clipping Remove overflow-x-hidden from header and inner div that was causing the absolutely-positioned dropdown to be clipped. Replace DaisyUI focus-based dropdown with Vue-controlled v-if dropdown for reliable click behavior. Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/TopHeader.vue | 23 ++++++++++++++++------- web/frontend/dist/index.html | 4 ++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/TopHeader.vue b/frontend/src/components/TopHeader.vue index 300cf55c..640ef0a0 100644 --- a/frontend/src/components/TopHeader.vue +++ b/frontend/src/components/TopHeader.vue @@ -1,6 +1,6 @@