Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ describe("getMcpServerTools", () => {

// Should only have one tool (from project server)
expect(result).toHaveLength(1)
expect(getFunction(result[0]).name).toBe("mcp--context7--resolve___library___id")
expect(getFunction(result[0]).name).toBe("mcp--context7--resolve-library-id")
// Project server takes priority
expect(getFunction(result[0]).description).toBe("Project description")
})
Expand Down
19 changes: 12 additions & 7 deletions src/core/tools/UseMcpToolTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Task } from "../task/Task"
import { formatResponse } from "../prompts/responses"
import { t } from "../../i18n"
import type { ToolUse } from "../../shared/tools"
import { toolNamesMatch } from "../../utils/mcp-name"

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

Expand Down Expand Up @@ -53,14 +54,18 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
return
}

// Use the resolved tool name (original name from the server) for MCP calls
// This handles cases where models mangle hyphens to underscores
const resolvedToolName = toolValidation.resolvedToolName ?? toolName

// Reset mistake count on successful validation
task.consecutiveMistakeCount = 0

// Get user approval
const completeMessage = JSON.stringify({
type: "use_mcp_tool",
serverName,
toolName,
toolName: resolvedToolName,
arguments: params.arguments ? JSON.stringify(params.arguments) : undefined,
} satisfies ClineAskUseMcpServer)

Expand All @@ -75,7 +80,7 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
await this.executeToolAndProcessResult(
task,
serverName,
toolName,
resolvedToolName,
parsedArguments,
executionId,
pushToolResult,
Expand Down Expand Up @@ -156,7 +161,7 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
serverName: string,
toolName: string,
pushToolResult: (content: string) => void,
): Promise<{ isValid: boolean; availableTools?: string[] }> {
): Promise<{ isValid: boolean; availableTools?: string[]; resolvedToolName?: string }> {
try {
// Get the MCP hub to access server information
const provider = task.providerRef.deref()
Expand Down Expand Up @@ -205,8 +210,8 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
return { isValid: false, availableTools: [] }
}

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

if (!tool) {
// Tool not found - provide list of available tools
Expand Down Expand Up @@ -251,8 +256,8 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> {
return { isValid: false, availableTools: enabledToolNames }
}

// Tool exists and is enabled
return { isValid: true, availableTools: server.tools.map((tool) => tool.name) }
// Tool exists and is enabled - return the original tool name for use with the MCP server
return { isValid: true, availableTools: server.tools.map((t) => t.name), resolvedToolName: tool.name }
} catch (error) {
// If there's an error during validation, log it but don't block the tool execution
// The actual tool call might still fail with a proper error
Expand Down
55 changes: 55 additions & 0 deletions src/core/tools/__tests__/useMcpToolTool.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,5 +536,60 @@ describe("useMcpToolTool", () => {
expect(callToolMock).not.toHaveBeenCalled()
expect(mockAskApproval).not.toHaveBeenCalled()
})

it("should match tool names using fuzzy matching (hyphens vs underscores)", async () => {
// This tests the scenario where models mangle hyphens to underscores
// e.g., model sends "get_user_profile" but actual tool name is "get-user-profile"
mockTask.consecutiveMistakeCount = 0

const callToolMock = vi.fn().mockResolvedValue({
content: [{ type: "text", text: "Success" }],
})

const mockServers = [
{
name: "test-server",
tools: [{ name: "get-user-profile", description: "Gets a user profile" }],
},
]

mockProviderRef.deref.mockReturnValue({
getMcpHub: () => ({
getAllServers: vi.fn().mockReturnValue(mockServers),
callTool: callToolMock,
}),
postMessageToWebview: vi.fn(),
})

// Model sends the mangled version with underscores
const block: ToolUse = {
type: "tool_use",
name: "use_mcp_tool",
params: {
server_name: "test-server",
tool_name: "get_user_profile", // Model mangled hyphens to underscores
arguments: "{}",
},
partial: false,
}

mockAskApproval.mockResolvedValue(true)

await useMcpToolTool.handle(mockTask as Task, block as any, {
askApproval: mockAskApproval,
handleError: mockHandleError,
pushToolResult: mockPushToolResult,
removeClosingTag: mockRemoveClosingTag,
toolProtocol: "xml",
})

// Tool should be found and executed
expect(mockTask.consecutiveMistakeCount).toBe(0)
expect(mockTask.recordToolError).not.toHaveBeenCalled()
expect(mockTask.say).toHaveBeenCalledWith("mcp_server_request_started")

// The original tool name (with hyphens) should be passed to callTool
expect(callToolMock).toHaveBeenCalledWith("test-server", "get-user-profile", {})
})
})
})
18 changes: 16 additions & 2 deletions src/services/mcp/McpHub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { fileExistsAtPath } from "../../utils/fs"
import { arePathsEqual, getWorkspacePath } from "../../utils/path"
import { injectVariables } from "../../utils/config"
import { safeWriteJson } from "../../utils/safeWriteJson"
import { sanitizeMcpName } from "../../utils/mcp-name"
import { sanitizeMcpName, toolNamesMatch } from "../../utils/mcp-name"

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

return this.sanitizedNameRegistry.get(sanitizedServerName) ?? null
// Check the registry for sanitized name mapping
const registryMatch = this.sanitizedNameRegistry.get(sanitizedServerName)
if (registryMatch) {
return registryMatch
}

// Use fuzzy matching: treat hyphens and underscores as equivalent
const fuzzyMatch = this.connections.find((conn) => toolNamesMatch(conn.server.name, sanitizedServerName))
if (fuzzyMatch) {
return fuzzyMatch.server.name
}

return null
}

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