From af2831ca8b31912e49391e4c8bdc0beb60943698 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Thu, 15 Jan 2026 14:38:43 +0200 Subject: [PATCH] feat: add Anthropic tool_search_tool_bm25_20251119 for lazy loading Add support for Anthropic's Advanced Tool Use standard by implementing the tool_search_tool_bm25_20251119 tool alongside existing retrieve_tools. This tool name is fine-tuned into Claude models for on-demand tool discovery. Returns tool_reference content blocks per Anthropic's custom implementation format, enabling lazy loading of tools. Key changes: - Add tool_search_tool_bm25_20251119 tool registration - Implement handleToolSearchBM25 handler with BM25 search - Return tool_reference format: [{"type":"tool_reference","tool_name":"..."}] - Fixed 5-result limit per Anthropic standard - Full activity logging (Spec 024) References: - https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool - https://www.anthropic.com/engineering/advanced-tool-use Closes: anthropics/claude-code#7336 Co-Authored-By: Claude Opus 4.5 --- internal/server/mcp.go | 77 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/internal/server/mcp.go b/internal/server/mcp.go index 52b6d0c7..96040cfc 100644 --- a/internal/server/mcp.go +++ b/internal/server/mcp.go @@ -300,6 +300,21 @@ func (p *MCPProxyServer) registerTools(_ bool) { ) p.server.AddTool(retrieveToolsTool, p.handleRetrieveTools) + // tool_search_tool_bm25_20251119 - Anthropic-compatible tool search (lazy loading) + // Claude models are fine-tuned to recognize this specific tool name for on-demand tool discovery. + // Returns tool_reference content blocks per Anthropic's custom implementation format. + // See: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool + toolSearchBM25Tool := mcp.NewTool("tool_search_tool_bm25_20251119", + mcp.WithDescription("Search for tools by keyword or description. Use this to find relevant tools before calling them."), + mcp.WithTitleAnnotation("Tool Search (BM25)"), + mcp.WithReadOnlyHintAnnotation(true), + mcp.WithString("query", + mcp.Required(), + mcp.Description("The search query (e.g. 'weather', 'git commit', 'database query')."), + ), + ) + p.server.AddTool(toolSearchBM25Tool, p.handleToolSearchBM25) + // Intent-based tool variants (Spec 018) // These replace the legacy call_tool with three operation-specific variants // that enable granular IDE permission control and require explicit intent declaration. @@ -900,6 +915,68 @@ func (p *MCPProxyServer) handleRetrieveTools(ctx context.Context, request mcp.Ca return mcp.NewToolResultText(string(jsonResult)), nil } +// handleToolSearchBM25 implements the Anthropic-standard tool search tool +// This tool name (tool_search_tool_bm25_20251119) is fine-tuned into Claude models +// for lazy loading tool discovery. Returns tool_reference content blocks per +// Anthropic's custom implementation format. +// See: https://platform.claude.com/docs/en/agents-and-tools/tool-use/tool-search-tool +func (p *MCPProxyServer) handleToolSearchBM25(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { + startTime := time.Now() + + // Extract session info for activity logging (Spec 024) + var sessionID string + if sess := mcpserver.ClientSessionFromContext(ctx); sess != nil { + sessionID = sess.SessionID() + } + requestID := fmt.Sprintf("%d-tool_search_bm25", time.Now().UnixNano()) + + query, err := request.RequireString("query") + if err != nil { + p.emitActivityInternalToolCall("tool_search_tool_bm25_20251119", "", "", "", + sessionID, requestID, "error", err.Error(), + time.Since(startTime).Milliseconds(), nil, nil, nil) + return mcp.NewToolResultError(fmt.Sprintf("Missing required parameter 'query': %v", err)), nil + } + + // Build arguments map for activity logging + args := map[string]interface{}{"query": query} + + // Search using existing BM25 index (limit to 5 per Anthropic standard) + results, err := p.index.Search(query, 5) + if err != nil { + p.logger.Error("Tool search failed", zap.String("query", query), zap.Error(err)) + p.emitActivityInternalToolCall("tool_search_tool_bm25_20251119", "", "", "", + sessionID, requestID, "error", err.Error(), + time.Since(startTime).Milliseconds(), args, nil, nil) + return mcp.NewToolResultError(fmt.Sprintf("Search failed: %v", err)), nil + } + + // Build tool_reference content blocks (Anthropic custom implementation format) + content := make([]map[string]interface{}, 0, len(results)) + for _, result := range results { + content = append(content, map[string]interface{}{ + "type": "tool_reference", + "tool_name": result.Tool.Name, + }) + } + + // Return as JSON array (Claude will parse this) + jsonResult, err := json.Marshal(content) + if err != nil { + p.emitActivityInternalToolCall("tool_search_tool_bm25_20251119", "", "", "", + sessionID, requestID, "error", err.Error(), + time.Since(startTime).Milliseconds(), args, nil, nil) + return mcp.NewToolResultError(fmt.Sprintf("Failed to serialize results: %v", err)), nil + } + + // Emit success event + p.emitActivityInternalToolCall("tool_search_tool_bm25_20251119", "", "", "", + sessionID, requestID, "success", "", + time.Since(startTime).Milliseconds(), args, content, nil) + + return mcp.NewToolResultText(string(jsonResult)), nil +} + // handleCallToolRead implements the call_tool_read functionality (Spec 018) func (p *MCPProxyServer) handleCallToolRead(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { return p.handleCallToolVariant(ctx, request, contracts.ToolVariantRead)