diff --git a/src/core/assistant-message/NativeToolCallParser.ts b/src/core/assistant-message/NativeToolCallParser.ts index f6eac36a9c1..0e28d14044c 100644 --- a/src/core/assistant-message/NativeToolCallParser.ts +++ b/src/core/assistant-message/NativeToolCallParser.ts @@ -16,7 +16,7 @@ import type { ApiStreamToolCallDeltaChunk, ApiStreamToolCallEndChunk, } from "../../api/transform/stream" -import { MCP_TOOL_PREFIX, MCP_TOOL_SEPARATOR, parseMcpToolName } from "../../utils/mcp-name" +import { isMcpTool, parseMcpToolName } from "../../utils/mcp-name" /** * Helper type to extract properly typed native arguments for a given tool. @@ -242,8 +242,7 @@ export class NativeToolCallParser { toolCall.argumentsAccumulator += chunk // For dynamic MCP tools, we don't return partial updates - wait for final - const mcpPrefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR - if (toolCall.name.startsWith(mcpPrefix)) { + if (isMcpTool(toolCall.name)) { return null } @@ -574,10 +573,8 @@ export class NativeToolCallParser { name: TName arguments: string }): ToolUse | McpToolUse | null { - // Check if this is a dynamic MCP tool (mcp--serverName--toolName) - const mcpPrefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR - - if (typeof toolCall.name === "string" && toolCall.name.startsWith(mcpPrefix)) { + // Check if this is a dynamic MCP tool (mcp--serverName--toolName or mcp__serverName__toolName) + if (typeof toolCall.name === "string" && isMcpTool(toolCall.name)) { return this.parseDynamicMcpTool(toolCall) } diff --git a/src/core/tools/UseMcpToolTool.ts b/src/core/tools/UseMcpToolTool.ts index e7ed744c78c..ca2f0614274 100644 --- a/src/core/tools/UseMcpToolTool.ts +++ b/src/core/tools/UseMcpToolTool.ts @@ -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 { findMatchingServerName, findMatchingToolName } from "../../utils/mcp-name" import { BaseTool, ToolCallbacks } from "./BaseTool" @@ -53,14 +54,18 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { return } + // Use resolved names (handles model-mangled names like underscores instead of hyphens) + const resolvedServerName = toolValidation.resolvedServerName ?? serverName + const resolvedToolName = toolValidation.resolvedToolName ?? toolName + // Reset mistake count on successful validation task.consecutiveMistakeCount = 0 - // Get user approval + // Get user approval (show resolved names to user) const completeMessage = JSON.stringify({ type: "use_mcp_tool", - serverName, - toolName, + serverName: resolvedServerName, + toolName: resolvedToolName, arguments: params.arguments ? JSON.stringify(params.arguments) : undefined, } satisfies ClineAskUseMcpServer) @@ -71,11 +76,11 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { return } - // Execute the tool and process results + // Execute the tool with resolved names await this.executeToolAndProcessResult( task, - serverName, - toolName, + resolvedServerName, + resolvedToolName, parsedArguments, executionId, pushToolResult, @@ -156,7 +161,12 @@ 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[] + resolvedServerName?: string + resolvedToolName?: string + }> { try { // Get the MCP hub to access server information const provider = task.providerRef.deref() @@ -168,12 +178,16 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { } // Get all servers to find the specific one + // Use fuzzy matching to handle model-mangled names (hyphens converted to underscores) const servers = mcpHub.getAllServers() - const server = servers.find((s) => s.name === serverName) + const availableServersArray = servers.map((s) => s.name) + + // Try fuzzy matching for server name + const matchedServerName = findMatchingServerName(serverName, availableServersArray) + const server = matchedServerName ? servers.find((s) => s.name === matchedServerName) : null - if (!server) { + if (!server || !matchedServerName) { // Fail fast when server is unknown - const availableServersArray = servers.map((s) => s.name) const availableServers = availableServersArray.length > 0 ? availableServersArray.join(", ") : "No servers available" @@ -186,6 +200,9 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { return { isValid: false, availableTools: [] } } + // At this point, matchedServerName is guaranteed to be non-null + const resolvedServerName = matchedServerName + // Check if the server has tools defined if (!server.tools || server.tools.length === 0) { // No tools available on this server @@ -195,39 +212,42 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { "error", t("mcp:errors.toolNotFound", { toolName, - serverName, + serverName: resolvedServerName, availableTools: "No tools available", }), ) task.didToolFailInCurrentTurn = true - pushToolResult(formatResponse.unknownMcpToolError(serverName, toolName, [])) + pushToolResult(formatResponse.unknownMcpToolError(resolvedServerName, toolName, [])) 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 + const availableToolNames = server.tools.map((tool) => tool.name) + const matchedToolName = findMatchingToolName(toolName, availableToolNames) + const tool = matchedToolName ? server.tools.find((t) => t.name === matchedToolName) : null - if (!tool) { + if (!tool || !matchedToolName) { // Tool not found - provide list of available tools - const availableToolNames = server.tools.map((tool) => tool.name) - task.consecutiveMistakeCount++ task.recordToolError("use_mcp_tool") await task.say( "error", t("mcp:errors.toolNotFound", { toolName, - serverName, + serverName: resolvedServerName, availableTools: availableToolNames.join(", "), }), ) task.didToolFailInCurrentTurn = true - pushToolResult(formatResponse.unknownMcpToolError(serverName, toolName, availableToolNames)) + pushToolResult(formatResponse.unknownMcpToolError(resolvedServerName, toolName, availableToolNames)) return { isValid: false, availableTools: availableToolNames } } + // At this point, matchedToolName is guaranteed to be non-null + const resolvedToolName = matchedToolName + // Check if the tool is disabled (enabledForPrompt is false) if (tool.enabledForPrompt === false) { // Tool is disabled - only show enabled tools @@ -239,20 +259,27 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { await task.say( "error", t("mcp:errors.toolDisabled", { - toolName, - serverName, + toolName: resolvedToolName, + serverName: resolvedServerName, availableTools: enabledToolNames.length > 0 ? enabledToolNames.join(", ") : "No enabled tools available", }), ) task.didToolFailInCurrentTurn = true - pushToolResult(formatResponse.unknownMcpToolError(serverName, toolName, enabledToolNames)) + pushToolResult( + formatResponse.unknownMcpToolError(resolvedServerName, resolvedToolName, enabledToolNames), + ) 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 resolved names for execution + return { + isValid: true, + availableTools: server.tools.map((tool) => tool.name), + resolvedServerName, + resolvedToolName, + } } 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 diff --git a/src/utils/__tests__/mcp-name.spec.ts b/src/utils/__tests__/mcp-name.spec.ts index 5511893f79e..01641e2605d 100644 --- a/src/utils/__tests__/mcp-name.spec.ts +++ b/src/utils/__tests__/mcp-name.spec.ts @@ -4,13 +4,18 @@ import { parseMcpToolName, isMcpTool, MCP_TOOL_SEPARATOR, + MCP_TOOL_SEPARATOR_MANGLED, MCP_TOOL_PREFIX, + generatePossibleOriginalNames, + findMatchingServerName, + findMatchingToolName, } from "../mcp-name" describe("mcp-name utilities", () => { describe("constants", () => { it("should have correct separator and prefix", () => { expect(MCP_TOOL_SEPARATOR).toBe("--") + expect(MCP_TOOL_SEPARATOR_MANGLED).toBe("__") expect(MCP_TOOL_PREFIX).toBe("mcp") }) }) @@ -21,6 +26,12 @@ describe("mcp-name utilities", () => { expect(isMcpTool("mcp--my_server--get_forecast")).toBe(true) }) + it("should return true for mangled MCP tool names (models convert -- to __)", () => { + expect(isMcpTool("mcp__server__tool")).toBe(true) + expect(isMcpTool("mcp__my_server__get_forecast")).toBe(true) + expect(isMcpTool("mcp__atlassian_jira__search")).toBe(true) + }) + it("should return false for non-MCP tool names", () => { expect(isMcpTool("server--tool")).toBe(false) expect(isMcpTool("tool")).toBe(false) @@ -28,7 +39,7 @@ describe("mcp-name utilities", () => { expect(isMcpTool("")).toBe(false) }) - it("should return false for old underscore format", () => { + it("should return false for old single underscore format", () => { expect(isMcpTool("mcp_server_tool")).toBe(false) }) @@ -128,10 +139,25 @@ describe("mcp-name utilities", () => { }) describe("parseMcpToolName", () => { - it("should parse valid mcp tool names", () => { + it("should parse valid mcp tool names with wasMangled=false", () => { expect(parseMcpToolName("mcp--server--tool")).toEqual({ serverName: "server", toolName: "tool", + wasMangled: false, + }) + }) + + it("should parse mangled mcp tool names (__ separator) with wasMangled=true", () => { + expect(parseMcpToolName("mcp__server__tool")).toEqual({ + serverName: "server", + toolName: "tool", + wasMangled: true, + }) + // This is the key case from issue #10642 - atlassian-jira gets mangled to atlassian_jira + expect(parseMcpToolName("mcp__atlassian_jira__search")).toEqual({ + serverName: "atlassian_jira", + toolName: "search", + wasMangled: true, }) }) @@ -140,7 +166,7 @@ describe("mcp-name utilities", () => { expect(parseMcpToolName("tool")).toBeNull() }) - it("should return null for old underscore format", () => { + it("should return null for old single underscore format", () => { expect(parseMcpToolName("mcp_server_tool")).toBeNull() }) @@ -148,6 +174,7 @@ describe("mcp-name utilities", () => { expect(parseMcpToolName("mcp--server--tool_name")).toEqual({ serverName: "server", toolName: "tool_name", + wasMangled: false, }) }) @@ -156,6 +183,7 @@ describe("mcp-name utilities", () => { expect(parseMcpToolName("mcp--my_server--tool")).toEqual({ serverName: "my_server", toolName: "tool", + wasMangled: false, }) }) @@ -163,12 +191,15 @@ describe("mcp-name utilities", () => { expect(parseMcpToolName("mcp--my_server--get_forecast")).toEqual({ serverName: "my_server", toolName: "get_forecast", + wasMangled: false, }) }) it("should return null for malformed names", () => { expect(parseMcpToolName("mcp--")).toBeNull() expect(parseMcpToolName("mcp--server")).toBeNull() + expect(parseMcpToolName("mcp__")).toBeNull() + expect(parseMcpToolName("mcp__server")).toBeNull() }) }) @@ -179,6 +210,7 @@ describe("mcp-name utilities", () => { expect(parsed).toEqual({ serverName: "server", toolName: "tool", + wasMangled: false, }) }) @@ -189,6 +221,7 @@ describe("mcp-name utilities", () => { expect(parsed).toEqual({ serverName: "my_server", toolName: "my_tool", + wasMangled: false, }) }) @@ -199,6 +232,7 @@ describe("mcp-name utilities", () => { expect(parsed).toEqual({ serverName: "my_server", toolName: "get_tool", + wasMangled: false, }) }) @@ -208,7 +242,205 @@ describe("mcp-name utilities", () => { expect(parsed).toEqual({ serverName: "Weather_API", toolName: "get_current_forecast", + wasMangled: false, + }) + }) + }) + + describe("generatePossibleOriginalNames", () => { + it("should include the original name in results", () => { + const results = generatePossibleOriginalNames("server") + expect(results).toContain("server") + }) + + it("should generate combinations with hyphens replacing underscores", () => { + const results = generatePossibleOriginalNames("my_server") + expect(results).toContain("my_server") + expect(results).toContain("my-server") + }) + + it("should generate all combinations for multiple underscores", () => { + // "a_b_c" has 2 underscores -> 2^2 = 4 combinations + const results = generatePossibleOriginalNames("a_b_c") + expect(results.length).toBe(4) + expect(results).toContain("a_b_c") + expect(results).toContain("a-b_c") + expect(results).toContain("a_b-c") + expect(results).toContain("a-b-c") + }) + + it("should generate 8 combinations for 3 underscores", () => { + // "a_b_c_d" has 3 underscores -> 2^3 = 8 combinations + const results = generatePossibleOriginalNames("a_b_c_d") + expect(results.length).toBe(8) + expect(results).toContain("a_b_c_d") + expect(results).toContain("a-b_c_d") + expect(results).toContain("a_b-c_d") + expect(results).toContain("a_b_c-d") + expect(results).toContain("a-b-c_d") + expect(results).toContain("a-b_c-d") + expect(results).toContain("a_b-c-d") + expect(results).toContain("a-b-c-d") + }) + + it("should handle the key issue #10642 case - atlassian_jira", () => { + const results = generatePossibleOriginalNames("atlassian_jira") + expect(results).toContain("atlassian_jira") + expect(results).toContain("atlassian-jira") // The original name + }) + + it("should handle names with no underscores", () => { + const results = generatePossibleOriginalNames("server") + expect(results).toEqual(["server"]) + }) + + it("should limit combinations for too many underscores (> 8)", () => { + const manyUnderscores = "a_b_c_d_e_f_g_h_i_j" // 9 underscores + const results = generatePossibleOriginalNames(manyUnderscores) + // Should only have 2 results: original and all-hyphens version + expect(results.length).toBe(2) + expect(results).toContain(manyUnderscores) + expect(results).toContain("a-b-c-d-e-f-g-h-i-j") + }) + + it("should handle exactly 8 underscores (256 combinations)", () => { + const eightUnderscores = "a_b_c_d_e_f_g_h_i" // 8 underscores + const results = generatePossibleOriginalNames(eightUnderscores) + // 2^8 = 256 combinations + expect(results.length).toBe(256) + }) + }) + + describe("findMatchingServerName", () => { + it("should return exact match first", () => { + const servers = ["my-server", "other-server"] + const result = findMatchingServerName("my-server", servers) + expect(result).toBe("my-server") + }) + + it("should find original hyphenated name from mangled name", () => { + const servers = ["atlassian-jira", "linear", "github"] + const result = findMatchingServerName("atlassian_jira", servers) + expect(result).toBe("atlassian-jira") + }) + + it("should return null if no match found", () => { + const servers = ["server1", "server2"] + const result = findMatchingServerName("unknown_server", servers) + expect(result).toBeNull() + }) + + it("should handle server names with multiple hyphens", () => { + const servers = ["my-cool-server", "another-server"] + const result = findMatchingServerName("my_cool_server", servers) + expect(result).toBe("my-cool-server") + }) + + it("should work with empty server list", () => { + const result = findMatchingServerName("server", []) + expect(result).toBeNull() + }) + + it("should match when original has underscores (not hyphens)", () => { + const servers = ["my_real_server", "other"] + const result = findMatchingServerName("my_real_server", servers) + expect(result).toBe("my_real_server") + }) + }) + + describe("findMatchingToolName", () => { + it("should return exact match first", () => { + const tools = ["get-data", "set-data"] + const result = findMatchingToolName("get-data", tools) + expect(result).toBe("get-data") + }) + + it("should find original hyphenated name from mangled name", () => { + const tools = ["get-user-info", "create-ticket", "search"] + const result = findMatchingToolName("get_user_info", tools) + expect(result).toBe("get-user-info") + }) + + it("should return null if no match found", () => { + const tools = ["tool1", "tool2"] + const result = findMatchingToolName("unknown_tool", tools) + expect(result).toBeNull() + }) + + it("should handle tool names with multiple hyphens", () => { + const tools = ["get-all-user-data", "search"] + const result = findMatchingToolName("get_all_user_data", tools) + expect(result).toBe("get-all-user-data") + }) + + it("should work with empty tool list", () => { + const result = findMatchingToolName("tool", []) + expect(result).toBeNull() + }) + + it("should match when original has underscores (not hyphens)", () => { + const tools = ["get_user", "search"] + const result = findMatchingToolName("get_user", tools) + expect(result).toBe("get_user") + }) + }) + + describe("issue #10642 - MCP tool names with hyphens fail", () => { + // End-to-end test for the specific bug reported in the issue + it("should correctly handle atlassian-jira being mangled to atlassian_jira", () => { + // The original MCP tool name as built by the system + const originalToolName = buildMcpToolName("atlassian-jira", "search") + expect(originalToolName).toBe("mcp--atlassian-jira--search") + + // What the model returns (hyphens converted to underscores) + const mangledToolName = "mcp__atlassian_jira__search" + + // isMcpTool should recognize both + expect(isMcpTool(originalToolName)).toBe(true) + expect(isMcpTool(mangledToolName)).toBe(true) + + // parseMcpToolName should parse both + const originalParsed = parseMcpToolName(originalToolName) + expect(originalParsed).toEqual({ + serverName: "atlassian-jira", + toolName: "search", + wasMangled: false, + }) + + const mangledParsed = parseMcpToolName(mangledToolName) + expect(mangledParsed).toEqual({ + serverName: "atlassian_jira", // Mangled name + toolName: "search", + wasMangled: true, + }) + + // findMatchingServerName should resolve mangled name back to original + const availableServers = ["atlassian-jira", "linear", "github"] + const resolvedServer = findMatchingServerName(mangledParsed!.serverName, availableServers) + expect(resolvedServer).toBe("atlassian-jira") + }) + + it("should handle litellm server with atlassian-jira tool", () => { + // From issue: mcp--litellm--atlassian-jira_search + const originalToolName = buildMcpToolName("litellm", "atlassian-jira_search") + expect(originalToolName).toBe("mcp--litellm--atlassian-jira_search") + + // Model might mangle it to: mcp__litellm__atlassian_jira_search + const mangledToolName = "mcp__litellm__atlassian_jira_search" + + expect(isMcpTool(mangledToolName)).toBe(true) + + const parsed = parseMcpToolName(mangledToolName) + expect(parsed).toEqual({ + serverName: "litellm", + toolName: "atlassian_jira_search", + wasMangled: true, }) + + // Find matching tool + const availableTools = ["atlassian-jira_search", "other_tool"] + const resolvedTool = findMatchingToolName(parsed!.toolName, availableTools) + expect(resolvedTool).toBe("atlassian-jira_search") }) }) }) diff --git a/src/utils/mcp-name.ts b/src/utils/mcp-name.ts index c81d5e770f0..0d6193ed33c 100644 --- a/src/utils/mcp-name.ts +++ b/src/utils/mcp-name.ts @@ -12,6 +12,13 @@ */ export const MCP_TOOL_SEPARATOR = "--" +/** + * Alternative separator that models may output. + * Some models (like Claude) convert hyphens to underscores in tool names, + * so "--" becomes "__". We need to recognize and handle this. + */ +export const MCP_TOOL_SEPARATOR_MANGLED = "__" + /** * Prefix for all MCP tool function names. */ @@ -19,12 +26,16 @@ export const MCP_TOOL_PREFIX = "mcp" /** * Check if a tool name is an MCP tool (starts with the MCP prefix and separator). + * Also recognizes mangled versions where models converted "--" to "__". * * @param toolName - The tool name to check - * @returns true if the tool name starts with "mcp--", false otherwise + * @returns true if the tool name starts with "mcp--" or "mcp__", false otherwise */ export function isMcpTool(toolName: string): boolean { - return toolName.startsWith(`${MCP_TOOL_PREFIX}${MCP_TOOL_SEPARATOR}`) + return ( + toolName.startsWith(`${MCP_TOOL_PREFIX}${MCP_TOOL_SEPARATOR}`) || + toolName.startsWith(`${MCP_TOOL_PREFIX}${MCP_TOOL_SEPARATOR_MANGLED}`) + ) } /** @@ -92,34 +103,143 @@ export function buildMcpToolName(serverName: string, toolName: string): string { /** * Parse an MCP tool function name back into server and tool names. * This handles sanitized names by splitting on the "--" separator. + * Also handles mangled names where models converted "--" to "__". * * Note: This returns the sanitized names, not the original names. * The original names cannot be recovered from the sanitized version. * - * @param mcpToolName - The full MCP tool name (e.g., "mcp--weather--get_forecast") - * @returns An object with serverName and toolName, or null if parsing fails + * @param mcpToolName - The full MCP tool name (e.g., "mcp--weather--get_forecast" or "mcp__weather__get_forecast") + * @returns An object with serverName, toolName, and wasMangled flag, or null if parsing fails */ -export function parseMcpToolName(mcpToolName: string): { serverName: string; toolName: string } | null { - const prefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR - if (!mcpToolName.startsWith(prefix)) { - return null +export function parseMcpToolName( + mcpToolName: string, +): { serverName: string; toolName: string; wasMangled: boolean } | null { + // Try canonical format first: mcp--server--tool + const canonicalPrefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR + if (mcpToolName.startsWith(canonicalPrefix)) { + const remainder = mcpToolName.slice(canonicalPrefix.length) + const separatorIndex = remainder.indexOf(MCP_TOOL_SEPARATOR) + if (separatorIndex !== -1) { + const serverName = remainder.slice(0, separatorIndex) + const toolName = remainder.slice(separatorIndex + MCP_TOOL_SEPARATOR.length) + if (serverName && toolName) { + return { serverName, toolName, wasMangled: false } + } + } + } + + // Try mangled format: mcp__server__tool (models may convert -- to __) + const mangledPrefix = MCP_TOOL_PREFIX + MCP_TOOL_SEPARATOR_MANGLED + if (mcpToolName.startsWith(mangledPrefix)) { + const remainder = mcpToolName.slice(mangledPrefix.length) + const separatorIndex = remainder.indexOf(MCP_TOOL_SEPARATOR_MANGLED) + if (separatorIndex !== -1) { + const serverName = remainder.slice(0, separatorIndex) + const toolName = remainder.slice(separatorIndex + MCP_TOOL_SEPARATOR_MANGLED.length) + if (serverName && toolName) { + return { serverName, toolName, wasMangled: true } + } + } + } + + return null +} + +/** + * Generate possible original names from a potentially mangled name. + * When models convert hyphens to underscores, we need to try matching + * the mangled name against servers/tools that may have had hyphens. + * + * Since we can't know which underscores were originally hyphens, we generate + * all possible combinations for fuzzy matching. + * + * For efficiency, we limit this to names with a reasonable number of underscores. + * + * @param mangledName - A name that may have had hyphens converted to underscores + * @returns An array of possible original names, including the input unchanged + */ +export function generatePossibleOriginalNames(mangledName: string): string[] { + const results: string[] = [mangledName] + + // Find positions of all underscores + const underscorePositions: number[] = [] + for (let i = 0; i < mangledName.length; i++) { + if (mangledName[i] === "_") { + underscorePositions.push(i) + } + } + + // Limit to prevent exponential explosion (2^n combinations) + // 8 underscores = 256 combinations, which is reasonable + if (underscorePositions.length > 8) { + // For too many underscores, just try the most common pattern: + // replace all underscores with hyphens + results.push(mangledName.replace(/_/g, "-")) + return results + } + + // Generate all combinations of replacing underscores with hyphens + const numCombinations = 1 << underscorePositions.length // 2^n + for (let mask = 1; mask < numCombinations; mask++) { + let variant = mangledName + for (let i = underscorePositions.length - 1; i >= 0; i--) { + if (mask & (1 << i)) { + const pos = underscorePositions[i] + variant = variant.slice(0, pos) + "-" + variant.slice(pos + 1) + } + } + results.push(variant) } - // Remove the "mcp--" prefix - const remainder = mcpToolName.slice(prefix.length) + return results +} - // Split on the separator to get server and tool names - const separatorIndex = remainder.indexOf(MCP_TOOL_SEPARATOR) - if (separatorIndex === -1) { - return null +/** + * Find a matching server name from a potentially mangled server name. + * Tries exact match first, then tries variations with underscores replaced by hyphens. + * + * @param mangledServerName - The server name from parsed MCP tool (may be mangled) + * @param availableServers - List of actual server names to match against + * @returns The matching server name, or null if no match found + */ +export function findMatchingServerName(mangledServerName: string, availableServers: string[]): string | null { + // Try exact match first + if (availableServers.includes(mangledServerName)) { + return mangledServerName } - const serverName = remainder.slice(0, separatorIndex) - const toolName = remainder.slice(separatorIndex + MCP_TOOL_SEPARATOR.length) + // Generate possible original names and try to find a match + const possibleNames = generatePossibleOriginalNames(mangledServerName) + for (const possibleName of possibleNames) { + if (availableServers.includes(possibleName)) { + return possibleName + } + } + + return null +} + +/** + * Find a matching tool name from a potentially mangled tool name. + * Tries exact match first, then tries variations with underscores replaced by hyphens. + * + * @param mangledToolName - The tool name from parsed MCP tool (may be mangled) + * @param availableTools - List of actual tool names to match against + * @returns The matching tool name, or null if no match found + */ +export function findMatchingToolName(mangledToolName: string, availableTools: string[]): string | null { + // Try exact match first + if (availableTools.includes(mangledToolName)) { + return mangledToolName + } - if (!serverName || !toolName) { - return null + // Generate possible original names and try to find a match + const possibleNames = generatePossibleOriginalNames(mangledToolName) + for (const possibleName of possibleNames) { + if (availableTools.includes(possibleName)) { + return possibleName + } } - return { serverName, toolName } + return null }