Skip to content

Commit 3877d02

Browse files
authored
Replace hyphen encoding with fuzzy matching for MCP tool names (#10775)
1 parent 83f123f commit 3877d02

6 files changed

Lines changed: 358 additions & 159 deletions

File tree

src/core/prompts/tools/native-tools/__tests__/mcp_server.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ describe("getMcpServerTools", () => {
8989

9090
// Should only have one tool (from project server)
9191
expect(result).toHaveLength(1)
92-
expect(getFunction(result[0]).name).toBe("mcp--context7--resolve___library___id")
92+
expect(getFunction(result[0]).name).toBe("mcp--context7--resolve-library-id")
9393
// Project server takes priority
9494
expect(getFunction(result[0]).description).toBe("Project description")
9595
})

src/core/tools/UseMcpToolTool.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Task } from "../task/Task"
44
import { formatResponse } from "../prompts/responses"
55
import { t } from "../../i18n"
66
import type { ToolUse } from "../../shared/tools"
7+
import { toolNamesMatch } from "../../utils/mcp-name"
78

89
import { BaseTool, ToolCallbacks } from "./BaseTool"
910

@@ -43,14 +44,18 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
4344
return
4445
}
4546

47+
// Use the resolved tool name (original name from the server) for MCP calls
48+
// This handles cases where models mangle hyphens to underscores
49+
const resolvedToolName = toolValidation.resolvedToolName ?? toolName
50+
4651
// Reset mistake count on successful validation
4752
task.consecutiveMistakeCount = 0
4853

4954
// Get user approval
5055
const completeMessage = JSON.stringify({
5156
type: "use_mcp_tool",
5257
serverName,
53-
toolName,
58+
toolName: resolvedToolName,
5459
arguments: params.arguments ? JSON.stringify(params.arguments) : undefined,
5560
} satisfies ClineAskUseMcpServer)
5661

@@ -65,7 +70,7 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
6570
await this.executeToolAndProcessResult(
6671
task,
6772
serverName,
68-
toolName,
73+
resolvedToolName,
6974
parsedArguments,
7075
executionId,
7176
pushToolResult,
@@ -137,7 +142,7 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
137142
serverName: string,
138143
toolName: string,
139144
pushToolResult: (content: string) => void,
140-
): Promise<{ isValid: boolean; availableTools?: string[] }> {
145+
): Promise<{ isValid: boolean; availableTools?: string[]; resolvedToolName?: string }> {
141146
try {
142147
// Get the MCP hub to access server information
143148
const provider = task.providerRef.deref()
@@ -186,8 +191,8 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
186191
return { isValid: false, availableTools: [] }
187192
}
188193

189-
// Check if the requested tool exists
190-
const tool = server.tools.find((tool) => tool.name === toolName)
194+
// Check if the requested tool exists (using fuzzy matching to handle model mangling of hyphens)
195+
const tool = server.tools.find((t) => toolNamesMatch(t.name, toolName))
191196

192197
if (!tool) {
193198
// Tool not found - provide list of available tools
@@ -232,8 +237,8 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
232237
return { isValid: false, availableTools: enabledToolNames }
233238
}
234239

235-
// Tool exists and is enabled
236-
return { isValid: true, availableTools: server.tools.map((tool) => tool.name) }
240+
// Tool exists and is enabled - return the original tool name for use with the MCP server
241+
return { isValid: true, availableTools: server.tools.map((t) => t.name), resolvedToolName: tool.name }
237242
} catch (error) {
238243
// If there's an error during validation, log it but don't block the tool execution
239244
// The actual tool call might still fail with a proper error

src/core/tools/__tests__/useMcpToolTool.spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,5 +577,60 @@ describe("useMcpToolTool", () => {
577577
expect(callToolMock).not.toHaveBeenCalled()
578578
expect(mockAskApproval).not.toHaveBeenCalled()
579579
})
580+
581+
it("should match tool names using fuzzy matching (hyphens vs underscores)", async () => {
582+
// This tests the scenario where models mangle hyphens to underscores
583+
// e.g., model sends "get_user_profile" but actual tool name is "get-user-profile"
584+
mockTask.consecutiveMistakeCount = 0
585+
586+
const callToolMock = vi.fn().mockResolvedValue({
587+
content: [{ type: "text", text: "Success" }],
588+
})
589+
590+
const mockServers = [
591+
{
592+
name: "test-server",
593+
tools: [{ name: "get-user-profile", description: "Gets a user profile" }],
594+
},
595+
]
596+
597+
mockProviderRef.deref.mockReturnValue({
598+
getMcpHub: () => ({
599+
getAllServers: vi.fn().mockReturnValue(mockServers),
600+
callTool: callToolMock,
601+
}),
602+
postMessageToWebview: vi.fn(),
603+
})
604+
605+
// Model sends the mangled version with underscores
606+
const block: ToolUse = {
607+
type: "tool_use",
608+
name: "use_mcp_tool",
609+
params: {
610+
server_name: "test-server",
611+
tool_name: "get_user_profile", // Model mangled hyphens to underscores
612+
arguments: "{}",
613+
},
614+
partial: false,
615+
}
616+
617+
mockAskApproval.mockResolvedValue(true)
618+
619+
await useMcpToolTool.handle(mockTask as Task, block as any, {
620+
askApproval: mockAskApproval,
621+
handleError: mockHandleError,
622+
pushToolResult: mockPushToolResult,
623+
removeClosingTag: mockRemoveClosingTag,
624+
toolProtocol: "xml",
625+
})
626+
627+
// Tool should be found and executed
628+
expect(mockTask.consecutiveMistakeCount).toBe(0)
629+
expect(mockTask.recordToolError).not.toHaveBeenCalled()
630+
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")
631+
632+
// The original tool name (with hyphens) should be passed to callTool
633+
expect(callToolMock).toHaveBeenCalledWith("test-server", "get-user-profile", {})
634+
})
580635
})
581636
})

src/services/mcp/McpHub.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { fileExistsAtPath } from "../../utils/fs"
3838
import { arePathsEqual, getWorkspacePath } from "../../utils/path"
3939
import { injectVariables } from "../../utils/config"
4040
import { safeWriteJson } from "../../utils/safeWriteJson"
41-
import { sanitizeMcpName } from "../../utils/mcp-name"
41+
import { sanitizeMcpName, toolNamesMatch } from "../../utils/mcp-name"
4242

4343
// Discriminated union for connection states
4444
export type ConnectedMcpConnection = {
@@ -940,16 +940,30 @@ export class McpHub {
940940
* Find a connection by sanitized server name.
941941
* This is used when parsing MCP tool responses where the server name has been
942942
* sanitized (e.g., hyphens replaced with underscores) for API compliance.
943+
* Uses fuzzy matching to handle cases where models convert hyphens to underscores.
943944
* @param sanitizedServerName The sanitized server name from the API tool call
944945
* @returns The original server name if found, or null if no match
945946
*/
946947
public findServerNameBySanitizedName(sanitizedServerName: string): string | null {
948+
// First, check for an exact match
947949
const exactMatch = this.connections.find((conn) => conn.server.name === sanitizedServerName)
948950
if (exactMatch) {
949951
return exactMatch.server.name
950952
}
951953

952-
return this.sanitizedNameRegistry.get(sanitizedServerName) ?? null
954+
// Check the registry for sanitized name mapping
955+
const registryMatch = this.sanitizedNameRegistry.get(sanitizedServerName)
956+
if (registryMatch) {
957+
return registryMatch
958+
}
959+
960+
// Use fuzzy matching: treat hyphens and underscores as equivalent
961+
const fuzzyMatch = this.connections.find((conn) => toolNamesMatch(conn.server.name, sanitizedServerName))
962+
if (fuzzyMatch) {
963+
return fuzzyMatch.server.name
964+
}
965+
966+
return null
953967
}
954968

955969
private async fetchToolsList(serverName: string, source?: "global" | "project"): Promise<McpTool[]> {

0 commit comments

Comments
 (0)