From a1cfe4cef33690e15acfbb54dc3bdf465567e46e Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 16 Jan 2026 08:31:29 +0000 Subject: [PATCH 1/4] fix: use hash suffix instead of truncation for long MCP tool names Addresses Issue #10766 where MCP tool names with hyphens fail when using native tool calling due to truncation cutting in the middle of hyphen encodings (___). Changes: - Add hash suffix (8 hex chars) when tool names exceed 64 characters instead of simple truncation - Register shortened names in a registry to allow lookup of original server/tool names - Parse function now checks registry first for shortened names - Add comprehensive tests for hash suffix behavior and roundtrip This ensures that long tool names with hyphens can be correctly resolved even after shortening, preserving the original tool identity. --- src/utils/__tests__/mcp-name.spec.ts | 175 ++++++++++++++++++++++++++- src/utils/mcp-name.ts | 80 +++++++++++- 2 files changed, 250 insertions(+), 5 deletions(-) diff --git a/src/utils/__tests__/mcp-name.spec.ts b/src/utils/__tests__/mcp-name.spec.ts index 0f3e37d5750..2764c077523 100644 --- a/src/utils/__tests__/mcp-name.spec.ts +++ b/src/utils/__tests__/mcp-name.spec.ts @@ -8,9 +8,19 @@ import { MCP_TOOL_SEPARATOR, MCP_TOOL_PREFIX, HYPHEN_ENCODING, + MAX_TOOL_NAME_LENGTH, + HASH_SUFFIX_LENGTH, + mcpToolNameRegistry, + clearMcpToolNameRegistry, + computeHashSuffix, } from "../mcp-name" describe("mcp-name utilities", () => { + // Clear the registry before each test to ensure isolation + beforeEach(() => { + clearMcpToolNameRegistry() + }) + describe("constants", () => { it("should have correct separator and prefix", () => { expect(MCP_TOOL_SEPARATOR).toBe("--") @@ -20,6 +30,14 @@ describe("mcp-name utilities", () => { it("should have correct hyphen encoding", () => { expect(HYPHEN_ENCODING).toBe("___") }) + + it("should have correct max tool name length", () => { + expect(MAX_TOOL_NAME_LENGTH).toBe(64) + }) + + it("should have correct hash suffix length", () => { + expect(HASH_SUFFIX_LENGTH).toBe(8) + }) }) describe("isMcpTool", () => { @@ -146,12 +164,52 @@ describe("mcp-name utilities", () => { expect(buildMcpToolName("server@name", "tool!name")).toBe("mcp--servername--toolname") }) - it("should truncate long names to 64 characters", () => { + it("should truncate long names to 64 characters with hash suffix", () => { const longServer = "a".repeat(50) const longTool = "b".repeat(50) const result = buildMcpToolName(longServer, longTool) expect(result.length).toBeLessThanOrEqual(64) + expect(result.length).toBe(64) expect(result.startsWith("mcp--")).toBe(true) + // Should end with underscore + 8 char hash suffix + expect(result).toMatch(/_[a-f0-9]{8}$/) + }) + + it("should use hash suffix for long names and register them", () => { + const longServer = "a".repeat(50) + const longTool = "b".repeat(50) + const result = buildMcpToolName(longServer, longTool) + + // The shortened name should be registered + expect(mcpToolNameRegistry.has(result)).toBe(true) + const registered = mcpToolNameRegistry.get(result) + expect(registered).toEqual({ + serverName: longServer, + toolName: longTool, + }) + }) + + it("should produce deterministic hash suffixes", () => { + const longServer = "a".repeat(50) + const longTool = "b".repeat(50) + // Build the same name twice + clearMcpToolNameRegistry() + const result1 = buildMcpToolName(longServer, longTool) + clearMcpToolNameRegistry() + const result2 = buildMcpToolName(longServer, longTool) + // Should produce identical results + expect(result1).toBe(result2) + }) + + it("should produce unique hash suffixes for different tools", () => { + const longServer = "a".repeat(50) + const result1 = buildMcpToolName(longServer, "tool1_" + "x".repeat(40)) + const result2 = buildMcpToolName(longServer, "tool2_" + "y".repeat(40)) + // Both should be truncated + expect(result1.length).toBe(64) + expect(result2.length).toBe(64) + // Should have different hash suffixes + expect(result1).not.toBe(result2) }) it("should handle names starting with numbers", () => { @@ -347,4 +405,119 @@ describe("mcp-name utilities", () => { }) }) }) + + describe("computeHashSuffix", () => { + it("should compute deterministic hash for the same inputs", () => { + const hash1 = computeHashSuffix("server", "tool") + const hash2 = computeHashSuffix("server", "tool") + expect(hash1).toBe(hash2) + }) + + it("should return 8-character hex string", () => { + const hash = computeHashSuffix("server", "tool") + expect(hash).toHaveLength(8) + expect(hash).toMatch(/^[a-f0-9]{8}$/) + }) + + it("should produce different hashes for different inputs", () => { + const hash1 = computeHashSuffix("server1", "tool") + const hash2 = computeHashSuffix("server2", "tool") + expect(hash1).not.toBe(hash2) + }) + }) + + describe("hash suffix roundtrip - fixes issue #10766", () => { + it("should preserve original names through roundtrip with long hyphenated tool names", () => { + // This is the exact scenario from issue #10766 + // Tool name with many hyphens that exceeds 64 chars when encoded + const serverName = "abcdefghij-kl-mnop-qrs-tuv" + const toolName = "wxyz-abcd-efghijk-lmno" + + // Build the tool name + const builtName = buildMcpToolName(serverName, toolName) + + // Should be truncated to 64 chars with hash suffix + expect(builtName.length).toBe(64) + expect(builtName).toMatch(/_[a-f0-9]{8}$/) + + // The critical fix: parsing should return the ORIGINAL names + const parsed = parseMcpToolName(builtName) + expect(parsed).toEqual({ + serverName: serverName, // Original with hyphens! + toolName: toolName, // Original with hyphens! + }) + }) + + it("should not corrupt hyphen encoding when truncation is needed", () => { + // Long server and tool names that would cause truncation mid-encoding + const serverName = "very-long-server-name-with-many-hyphens" + const toolName = "another-long-tool-name-with-hyphens" + + // Build the tool name + const builtName = buildMcpToolName(serverName, toolName) + + // Should be truncated to 64 chars + expect(builtName.length).toBe(64) + + // Parse should return original names (via registry lookup) + const parsed = parseMcpToolName(builtName) + expect(parsed).toEqual({ + serverName: serverName, + toolName: toolName, + }) + }) + + it("should work correctly when names do not need truncation", () => { + // Short names that don't need truncation + const serverName = "server" + const toolName = "get-data" + + const builtName = buildMcpToolName(serverName, toolName) + + // Should NOT have hash suffix + expect(builtName).toBe("mcp--server--get___data") + expect(builtName.length).toBeLessThan(64) + + // Normal decode path should work + const parsed = parseMcpToolName(builtName) + expect(parsed).toEqual({ + serverName: "server", + toolName: "get-data", // Hyphen decoded from ___ + }) + }) + + it("should handle the registry lookup for shortened names", () => { + const serverName = "a".repeat(30) + const toolName = "b".repeat(30) + "-hyphen" + + // Build registers the shortened name + const builtName = buildMcpToolName(serverName, toolName) + + // Verify it's in the registry + expect(mcpToolNameRegistry.has(builtName)).toBe(true) + + // Parse uses the registry to get original names + const parsed = parseMcpToolName(builtName) + expect(parsed).toEqual({ + serverName: serverName, + toolName: toolName, + }) + }) + }) + + describe("clearMcpToolNameRegistry", () => { + it("should clear all registered tool names", () => { + // Register some tool names via buildMcpToolName + const longServer = "a".repeat(50) + const longTool = "b".repeat(50) + buildMcpToolName(longServer, longTool) + + expect(mcpToolNameRegistry.size).toBeGreaterThan(0) + + // Clear the registry + clearMcpToolNameRegistry() + + expect(mcpToolNameRegistry.size).toBe(0) + }) + }) }) diff --git a/src/utils/mcp-name.ts b/src/utils/mcp-name.ts index 3e6d1ab8a47..47f9fa14e18 100644 --- a/src/utils/mcp-name.ts +++ b/src/utils/mcp-name.ts @@ -3,6 +3,8 @@ * API function name requirements across all providers. */ +import * as crypto from "crypto" + /** * Separator used between MCP prefix, server name, and tool name. * We use "--" (double hyphen) because: @@ -30,6 +32,50 @@ export const MCP_TOOL_PREFIX = "mcp" */ export const HYPHEN_ENCODING = "___" +/** + * Maximum length for tool names (Gemini's limit). + */ +export const MAX_TOOL_NAME_LENGTH = 64 + +/** + * Length of hash suffix used when truncation is needed. + * Using 8 characters from base36 gives us ~2.8 trillion combinations, + * which is more than enough to avoid collisions. + */ +export const HASH_SUFFIX_LENGTH = 8 + +/** + * Registry mapping shortened tool names (with hash suffix) to their original + * server and tool names. This is used to look up the original names when + * the model returns a shortened tool name. + * + * Key: shortened MCP tool name (e.g., "mcp--server--tool_a1b2c3d4") + * Value: { serverName, toolName } with original (decoded) names + */ +export const mcpToolNameRegistry = new Map() + +/** + * Clear the MCP tool name registry. + * Should be called when MCP servers are refreshed or disconnected. + */ +export function clearMcpToolNameRegistry(): void { + mcpToolNameRegistry.clear() +} + +/** + * Compute a deterministic hash suffix for a given server and tool name combination. + * Uses the original (not encoded) names to ensure consistency. + * + * @param serverName - The original server name (before sanitization) + * @param toolName - The original tool name (before sanitization) + * @returns An 8-character alphanumeric hash suffix + */ +export function computeHashSuffix(serverName: string, toolName: string): string { + const hash = crypto.createHash("sha256").update(`${serverName}:${toolName}`).digest("hex") + // Use first 8 hex characters for the suffix + return hash.slice(0, HASH_SUFFIX_LENGTH) +} + /** * Normalize an MCP tool name by converting underscore separators back to hyphens. * This handles the case where models (especially Claude) convert hyphens to underscores @@ -119,6 +165,8 @@ export function sanitizeMcpName(name: string): string { * The format is: mcp--{sanitized_server_name}--{sanitized_tool_name} * * The total length is capped at 64 characters to conform to API limits. + * When truncation is needed, a hash suffix is appended to preserve uniqueness + * and the mapping is stored in the registry for later lookup. * * @param serverName - The MCP server name * @param toolName - The tool name @@ -131,12 +179,26 @@ export function buildMcpToolName(serverName: string, toolName: string): string { // Build the full name: mcp--{server}--{tool} const fullName = `${MCP_TOOL_PREFIX}${MCP_TOOL_SEPARATOR}${sanitizedServer}${MCP_TOOL_SEPARATOR}${sanitizedTool}` - // Truncate if necessary (max 64 chars for Gemini) - if (fullName.length > 64) { - return fullName.slice(0, 64) + // If within limit, return as-is + if (fullName.length <= MAX_TOOL_NAME_LENGTH) { + return fullName } - return fullName + // Need to truncate: use hash suffix to preserve uniqueness + // Format: truncated_name_HASHSUFFIX (underscore + 8 hex chars = 9 chars for suffix) + const hashSuffix = computeHashSuffix(serverName, toolName) + const suffixWithSeparator = `_${hashSuffix}` // "_" + 8 chars = 9 chars + const maxTruncatedLength = MAX_TOOL_NAME_LENGTH - suffixWithSeparator.length // 64 - 9 = 55 + + // Truncate the full name and append hash suffix + const truncatedBase = fullName.slice(0, maxTruncatedLength) + const shortenedName = `${truncatedBase}${suffixWithSeparator}` + + // Register the mapping from shortened name to original names + // Store the original (decoded) names so parseMcpToolName can return them directly + mcpToolNameRegistry.set(shortenedName, { serverName, toolName }) + + return shortenedName } /** @@ -155,6 +217,9 @@ export function decodeMcpName(sanitizedName: string): string { * This handles sanitized names by splitting on the "--" separator * and decoding triple underscores back to hyphens. * + * For shortened names (those with hash suffixes), this first checks + * the registry for the original names. + * * @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 */ @@ -164,6 +229,13 @@ export function parseMcpToolName(mcpToolName: string): { serverName: string; too return null } + // First, check if this is a shortened name in the registry + // This handles names that were truncated with hash suffixes + const registeredName = mcpToolNameRegistry.get(mcpToolName) + if (registeredName) { + return registeredName + } + // Remove the "mcp--" prefix const remainder = mcpToolName.slice(prefix.length) From 865e9a361e75b6a54bb0ccdf813af202318dc209 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 16 Jan 2026 08:39:45 +0000 Subject: [PATCH 2/4] fix: increase MAX_TOOL_NAME_LENGTH from 64 to 128 per MCP spec --- src/utils/__tests__/mcp-name.spec.ts | 58 ++++++++++++++-------------- src/utils/mcp-name.ts | 9 +++-- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/src/utils/__tests__/mcp-name.spec.ts b/src/utils/__tests__/mcp-name.spec.ts index 2764c077523..c20e40a4fea 100644 --- a/src/utils/__tests__/mcp-name.spec.ts +++ b/src/utils/__tests__/mcp-name.spec.ts @@ -32,7 +32,7 @@ describe("mcp-name utilities", () => { }) it("should have correct max tool name length", () => { - expect(MAX_TOOL_NAME_LENGTH).toBe(64) + expect(MAX_TOOL_NAME_LENGTH).toBe(128) }) it("should have correct hash suffix length", () => { @@ -164,20 +164,20 @@ describe("mcp-name utilities", () => { expect(buildMcpToolName("server@name", "tool!name")).toBe("mcp--servername--toolname") }) - it("should truncate long names to 64 characters with hash suffix", () => { - const longServer = "a".repeat(50) - const longTool = "b".repeat(50) + it("should truncate long names to 128 characters with hash suffix", () => { + const longServer = "a".repeat(80) + const longTool = "b".repeat(80) const result = buildMcpToolName(longServer, longTool) - expect(result.length).toBeLessThanOrEqual(64) - expect(result.length).toBe(64) + expect(result.length).toBeLessThanOrEqual(128) + expect(result.length).toBe(128) expect(result.startsWith("mcp--")).toBe(true) // Should end with underscore + 8 char hash suffix expect(result).toMatch(/_[a-f0-9]{8}$/) }) it("should use hash suffix for long names and register them", () => { - const longServer = "a".repeat(50) - const longTool = "b".repeat(50) + const longServer = "a".repeat(80) + const longTool = "b".repeat(80) const result = buildMcpToolName(longServer, longTool) // The shortened name should be registered @@ -190,8 +190,8 @@ describe("mcp-name utilities", () => { }) it("should produce deterministic hash suffixes", () => { - const longServer = "a".repeat(50) - const longTool = "b".repeat(50) + const longServer = "a".repeat(80) + const longTool = "b".repeat(80) // Build the same name twice clearMcpToolNameRegistry() const result1 = buildMcpToolName(longServer, longTool) @@ -202,12 +202,12 @@ describe("mcp-name utilities", () => { }) it("should produce unique hash suffixes for different tools", () => { - const longServer = "a".repeat(50) - const result1 = buildMcpToolName(longServer, "tool1_" + "x".repeat(40)) - const result2 = buildMcpToolName(longServer, "tool2_" + "y".repeat(40)) + const longServer = "a".repeat(80) + const result1 = buildMcpToolName(longServer, "tool1_" + "x".repeat(70)) + const result2 = buildMcpToolName(longServer, "tool2_" + "y".repeat(70)) // Both should be truncated - expect(result1.length).toBe(64) - expect(result2.length).toBe(64) + expect(result1.length).toBe(128) + expect(result2.length).toBe(128) // Should have different hash suffixes expect(result1).not.toBe(result2) }) @@ -429,15 +429,15 @@ describe("mcp-name utilities", () => { describe("hash suffix roundtrip - fixes issue #10766", () => { it("should preserve original names through roundtrip with long hyphenated tool names", () => { // This is the exact scenario from issue #10766 - // Tool name with many hyphens that exceeds 64 chars when encoded - const serverName = "abcdefghij-kl-mnop-qrs-tuv" - const toolName = "wxyz-abcd-efghijk-lmno" + // Tool name with many hyphens that exceeds 128 chars when encoded + const serverName = "abcdefghij-kl-mnop-qrs-tuv-with-extra-long-suffix-to-exceed-limit" + const toolName = "wxyz-abcd-efghijk-lmno-plus-additional-long-suffix-here" // Build the tool name const builtName = buildMcpToolName(serverName, toolName) - // Should be truncated to 64 chars with hash suffix - expect(builtName.length).toBe(64) + // Should be truncated to 128 chars with hash suffix + expect(builtName.length).toBe(128) expect(builtName).toMatch(/_[a-f0-9]{8}$/) // The critical fix: parsing should return the ORIGINAL names @@ -450,14 +450,14 @@ describe("mcp-name utilities", () => { it("should not corrupt hyphen encoding when truncation is needed", () => { // Long server and tool names that would cause truncation mid-encoding - const serverName = "very-long-server-name-with-many-hyphens" - const toolName = "another-long-tool-name-with-hyphens" + const serverName = "very-long-server-name-with-many-hyphens-and-extra-content-to-exceed" + const toolName = "another-long-tool-name-with-hyphens-and-extra-content-to-exceed" // Build the tool name const builtName = buildMcpToolName(serverName, toolName) - // Should be truncated to 64 chars - expect(builtName.length).toBe(64) + // Should be truncated to 128 chars + expect(builtName.length).toBe(128) // Parse should return original names (via registry lookup) const parsed = parseMcpToolName(builtName) @@ -476,7 +476,7 @@ describe("mcp-name utilities", () => { // Should NOT have hash suffix expect(builtName).toBe("mcp--server--get___data") - expect(builtName.length).toBeLessThan(64) + expect(builtName.length).toBeLessThan(128) // Normal decode path should work const parsed = parseMcpToolName(builtName) @@ -487,8 +487,8 @@ describe("mcp-name utilities", () => { }) it("should handle the registry lookup for shortened names", () => { - const serverName = "a".repeat(30) - const toolName = "b".repeat(30) + "-hyphen" + const serverName = "a".repeat(80) + const toolName = "b".repeat(80) + "-hyphen" // Build registers the shortened name const builtName = buildMcpToolName(serverName, toolName) @@ -508,8 +508,8 @@ describe("mcp-name utilities", () => { describe("clearMcpToolNameRegistry", () => { it("should clear all registered tool names", () => { // Register some tool names via buildMcpToolName - const longServer = "a".repeat(50) - const longTool = "b".repeat(50) + const longServer = "a".repeat(80) + const longTool = "b".repeat(80) buildMcpToolName(longServer, longTool) expect(mcpToolNameRegistry.size).toBeGreaterThan(0) diff --git a/src/utils/mcp-name.ts b/src/utils/mcp-name.ts index 47f9fa14e18..25e90f6896b 100644 --- a/src/utils/mcp-name.ts +++ b/src/utils/mcp-name.ts @@ -33,9 +33,10 @@ export const MCP_TOOL_PREFIX = "mcp" export const HYPHEN_ENCODING = "___" /** - * Maximum length for tool names (Gemini's limit). + * Maximum length for tool names (MCP spec limit). + * See: https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names */ -export const MAX_TOOL_NAME_LENGTH = 64 +export const MAX_TOOL_NAME_LENGTH = 128 /** * Length of hash suffix used when truncation is needed. @@ -164,7 +165,7 @@ export function sanitizeMcpName(name: string): string { * Build a full MCP tool function name from server and tool names. * The format is: mcp--{sanitized_server_name}--{sanitized_tool_name} * - * The total length is capped at 64 characters to conform to API limits. + * The total length is capped at 128 characters per MCP spec. * When truncation is needed, a hash suffix is appended to preserve uniqueness * and the mapping is stored in the registry for later lookup. * @@ -188,7 +189,7 @@ export function buildMcpToolName(serverName: string, toolName: string): string { // Format: truncated_name_HASHSUFFIX (underscore + 8 hex chars = 9 chars for suffix) const hashSuffix = computeHashSuffix(serverName, toolName) const suffixWithSeparator = `_${hashSuffix}` // "_" + 8 chars = 9 chars - const maxTruncatedLength = MAX_TOOL_NAME_LENGTH - suffixWithSeparator.length // 64 - 9 = 55 + const maxTruncatedLength = MAX_TOOL_NAME_LENGTH - suffixWithSeparator.length // 128 - 9 = 119 // Truncate the full name and append hash suffix const truncatedBase = fullName.slice(0, maxTruncatedLength) From 7be724fdd12748ff604bc06a9b128e2ba53e7585 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 16 Jan 2026 08:53:12 +0000 Subject: [PATCH 3/4] fix: replace unbounded registry with LRU cache for MCP tool names - Remove mcpToolNameRegistry (unbounded Map) and clearMcpToolNameRegistry - Add LRU cache with size limit of 100 entries for encoded name computation - Add findToolByEncodedMcpName() to compare encoded names at lookup time - Add hasHashSuffix() helper for checking if a name needs lookup - Pass original encoded MCP name through the tool call stack - Update UseMcpToolTool.validateToolExists() to use encoded comparison - Update tests to cover new functions and LRU cache behavior This addresses the reviewer feedback to avoid memory leaks from the unbounded registry by comparing encoded tool names on both sides and using an LRU cache for performance. --- .../presentAssistantMessage.ts | 2 + src/core/tools/UseMcpToolTool.ts | 45 ++++- src/shared/tools.ts | 7 +- src/utils/__tests__/mcp-name.spec.ts | 182 +++++++++++------- src/utils/mcp-name.ts | 117 ++++++++--- 5 files changed, 245 insertions(+), 108 deletions(-) diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts index 693327a022e..96562187501 100644 --- a/src/core/assistant-message/presentAssistantMessage.ts +++ b/src/core/assistant-message/presentAssistantMessage.ts @@ -276,6 +276,7 @@ export async function presentAssistantMessage(cline: Task) { // Execute the MCP tool using the same handler as use_mcp_tool // Create a synthetic ToolUse block that the useMcpToolTool can handle + // Include the original encoded MCP name for lookup via encoded name comparison const syntheticToolUse: ToolUse<"use_mcp_tool"> = { type: "tool_use", id: mcpBlock.id, @@ -290,6 +291,7 @@ export async function presentAssistantMessage(cline: Task) { server_name: resolvedServerName, tool_name: mcpBlock.toolName, arguments: mcpBlock.arguments, + _encodedMcpName: mcpBlock.name, // Original encoded name for lookup via comparison }, } diff --git a/src/core/tools/UseMcpToolTool.ts b/src/core/tools/UseMcpToolTool.ts index e7ed744c78c..250778c9076 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 { findToolByEncodedMcpName, hasHashSuffix } from "../../utils/mcp-name" import { BaseTool, ToolCallbacks } from "./BaseTool" @@ -35,7 +36,11 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { } } - async execute(params: UseMcpToolParams, task: Task, callbacks: ToolCallbacks): Promise { + async execute( + params: UseMcpToolParams & { _encodedMcpName?: string }, + task: Task, + callbacks: ToolCallbacks, + ): Promise { const { askApproval, handleError, pushToolResult, toolProtocol } = callbacks try { @@ -48,11 +53,22 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { const { serverName, toolName, parsedArguments } = validation // Validate that the tool exists on the server - const toolValidation = await this.validateToolExists(task, serverName, toolName, pushToolResult) + // Pass the original encoded MCP name for lookup via comparison when direct lookup fails + const encodedMcpName = params._encodedMcpName + const toolValidation = await this.validateToolExists( + task, + serverName, + toolName, + pushToolResult, + encodedMcpName, + ) if (!toolValidation.isValid) { return } + // Use the resolved tool name (may differ from parsed name for shortened names) + const resolvedToolName = toolValidation.resolvedToolName || toolName + // Reset mistake count on successful validation task.consecutiveMistakeCount = 0 @@ -60,7 +76,7 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { const completeMessage = JSON.stringify({ type: "use_mcp_tool", serverName, - toolName, + toolName: resolvedToolName, arguments: params.arguments ? JSON.stringify(params.arguments) : undefined, } satisfies ClineAskUseMcpServer) @@ -75,7 +91,7 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { await this.executeToolAndProcessResult( task, serverName, - toolName, + resolvedToolName, parsedArguments, executionId, pushToolResult, @@ -156,7 +172,8 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { serverName: string, toolName: string, pushToolResult: (content: string) => void, - ): Promise<{ isValid: boolean; availableTools?: string[] }> { + encodedMcpName?: string, + ): Promise<{ isValid: boolean; availableTools?: string[]; resolvedToolName?: string }> { try { // Get the MCP hub to access server information const provider = task.providerRef.deref() @@ -205,8 +222,20 @@ 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 by direct name match + let tool = server.tools.find((tool) => tool.name === toolName) + let resolvedToolName = toolName + + // If direct lookup fails and we have an encoded MCP name with hash suffix, + // try to find the tool by comparing encoded names + if (!tool && encodedMcpName && hasHashSuffix(encodedMcpName)) { + const availableToolNames = server.tools.map((t) => t.name) + const matchedToolName = findToolByEncodedMcpName(serverName, encodedMcpName, availableToolNames) + if (matchedToolName) { + tool = server.tools.find((t) => t.name === matchedToolName) + resolvedToolName = matchedToolName + } + } if (!tool) { // Tool not found - provide list of available tools @@ -252,7 +281,7 @@ export class UseMcpToolTool extends BaseTool<"use_mcp_tool"> { } // Tool exists and is enabled - return { isValid: true, availableTools: server.tools.map((tool) => tool.name) } + return { isValid: true, availableTools: server.tools.map((tool) => tool.name), 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/shared/tools.ts b/src/shared/tools.ts index f893a3d332e..f4625919f73 100644 --- a/src/shared/tools.ts +++ b/src/shared/tools.ts @@ -108,7 +108,12 @@ export type NativeToolArgs = { search_files: { path: string; regex: string; file_pattern?: string | null } switch_mode: { mode_slug: string; reason: string } update_todo_list: { todos: string } - use_mcp_tool: { server_name: string; tool_name: string; arguments?: Record } + use_mcp_tool: { + server_name: string + tool_name: string + arguments?: Record + _encodedMcpName?: string + } write_to_file: { path: string; content: string } // Add more tools as they are migrated to native protocol } diff --git a/src/utils/__tests__/mcp-name.spec.ts b/src/utils/__tests__/mcp-name.spec.ts index c20e40a4fea..9b6eac6d0cd 100644 --- a/src/utils/__tests__/mcp-name.spec.ts +++ b/src/utils/__tests__/mcp-name.spec.ts @@ -10,15 +10,16 @@ import { HYPHEN_ENCODING, MAX_TOOL_NAME_LENGTH, HASH_SUFFIX_LENGTH, - mcpToolNameRegistry, - clearMcpToolNameRegistry, + clearEncodedNameCache, computeHashSuffix, + findToolByEncodedMcpName, + hasHashSuffix, } from "../mcp-name" describe("mcp-name utilities", () => { - // Clear the registry before each test to ensure isolation + // Clear the cache before each test to ensure isolation beforeEach(() => { - clearMcpToolNameRegistry() + clearEncodedNameCache() }) describe("constants", () => { @@ -175,27 +176,27 @@ describe("mcp-name utilities", () => { expect(result).toMatch(/_[a-f0-9]{8}$/) }) - it("should use hash suffix for long names and register them", () => { + it("should use hash suffix for long names and cache them", () => { const longServer = "a".repeat(80) const longTool = "b".repeat(80) const result = buildMcpToolName(longServer, longTool) - // The shortened name should be registered - expect(mcpToolNameRegistry.has(result)).toBe(true) - const registered = mcpToolNameRegistry.get(result) - expect(registered).toEqual({ - serverName: longServer, - toolName: longTool, - }) + // The shortened name should be deterministic + expect(result.length).toBe(128) + expect(result).toMatch(/_[a-f0-9]{8}$/) + + // Building again should return the same result (from cache) + const result2 = buildMcpToolName(longServer, longTool) + expect(result2).toBe(result) }) it("should produce deterministic hash suffixes", () => { const longServer = "a".repeat(80) const longTool = "b".repeat(80) - // Build the same name twice - clearMcpToolNameRegistry() + // Build the same name twice with cache cleared between + clearEncodedNameCache() const result1 = buildMcpToolName(longServer, longTool) - clearMcpToolNameRegistry() + clearEncodedNameCache() const result2 = buildMcpToolName(longServer, longTool) // Should produce identical results expect(result1).toBe(result2) @@ -427,97 +428,132 @@ describe("mcp-name utilities", () => { }) describe("hash suffix roundtrip - fixes issue #10766", () => { - it("should preserve original names through roundtrip with long hyphenated tool names", () => { - // This is the exact scenario from issue #10766 - // Tool name with many hyphens that exceeds 128 chars when encoded - const serverName = "abcdefghij-kl-mnop-qrs-tuv-with-extra-long-suffix-to-exceed-limit" - const toolName = "wxyz-abcd-efghijk-lmno-plus-additional-long-suffix-here" + it("should work correctly when names do not need truncation", () => { + // Short names that don't need truncation + const serverName = "server" + const toolName = "get-data" - // Build the tool name const builtName = buildMcpToolName(serverName, toolName) - // Should be truncated to 128 chars with hash suffix - expect(builtName.length).toBe(128) - expect(builtName).toMatch(/_[a-f0-9]{8}$/) + // Should NOT have hash suffix + expect(builtName).toBe("mcp--server--get___data") + expect(builtName.length).toBeLessThan(128) - // The critical fix: parsing should return the ORIGINAL names + // Normal decode path should work const parsed = parseMcpToolName(builtName) expect(parsed).toEqual({ - serverName: serverName, // Original with hyphens! - toolName: toolName, // Original with hyphens! + serverName: "server", + toolName: "get-data", // Hyphen decoded from ___ }) }) - it("should not corrupt hyphen encoding when truncation is needed", () => { - // Long server and tool names that would cause truncation mid-encoding - const serverName = "very-long-server-name-with-many-hyphens-and-extra-content-to-exceed" - const toolName = "another-long-tool-name-with-hyphens-and-extra-content-to-exceed" + it("should find tool by encoded name comparison for shortened names", () => { + // This is the new approach: instead of registry, compare encoded names + const serverName = "abcdefghij-kl-mnop-qrs-tuv-with-extra-long-suffix-to-exceed-limit" + const toolName = "wxyz-abcd-efghijk-lmno-plus-additional-long-suffix-here" - // Build the tool name - const builtName = buildMcpToolName(serverName, toolName) + // Build the encoded tool name + const encodedName = buildMcpToolName(serverName, toolName) - // Should be truncated to 128 chars - expect(builtName.length).toBe(128) + // Should be truncated to 128 chars with hash suffix + expect(encodedName.length).toBe(128) + expect(encodedName).toMatch(/_[a-f0-9]{8}$/) - // Parse should return original names (via registry lookup) - const parsed = parseMcpToolName(builtName) - expect(parsed).toEqual({ - serverName: serverName, - toolName: toolName, - }) + // The new approach: use findToolByEncodedMcpName to find the matching tool + const availableTools = [toolName, "other-tool", "another-tool"] + const foundTool = findToolByEncodedMcpName(serverName, encodedName, availableTools) + expect(foundTool).toBe(toolName) }) - it("should work correctly when names do not need truncation", () => { - // Short names that don't need truncation + it("should not find tool when none matches the encoded name", () => { const serverName = "server" - const toolName = "get-data" + const encodedName = "mcp--server--nonexistent_tool_a1b2c3d4" - const builtName = buildMcpToolName(serverName, toolName) + const availableTools = ["tool1", "tool2", "tool3"] + const foundTool = findToolByEncodedMcpName(serverName, encodedName, availableTools) + expect(foundTool).toBeNull() + }) + }) - // Should NOT have hash suffix - expect(builtName).toBe("mcp--server--get___data") - expect(builtName.length).toBeLessThan(128) + describe("findToolByEncodedMcpName", () => { + it("should find tool by exact encoded name match", () => { + const serverName = "myserver" + const toolName = "get-data" + const encodedName = buildMcpToolName(serverName, toolName) - // Normal decode path should work - const parsed = parseMcpToolName(builtName) - expect(parsed).toEqual({ - serverName: "server", - toolName: "get-data", // Hyphen decoded from ___ - }) + const availableTools = ["other-tool", "get-data", "another-tool"] + const foundTool = findToolByEncodedMcpName(serverName, encodedName, availableTools) + expect(foundTool).toBe("get-data") }) - it("should handle the registry lookup for shortened names", () => { + it("should find tool for shortened names with hash suffix", () => { const serverName = "a".repeat(80) const toolName = "b".repeat(80) + "-hyphen" + const encodedName = buildMcpToolName(serverName, toolName) - // Build registers the shortened name - const builtName = buildMcpToolName(serverName, toolName) + // The encoded name should have a hash suffix + expect(hasHashSuffix(encodedName)).toBe(true) - // Verify it's in the registry - expect(mcpToolNameRegistry.has(builtName)).toBe(true) + const availableTools = ["other-tool", toolName, "another-tool"] + const foundTool = findToolByEncodedMcpName(serverName, encodedName, availableTools) + expect(foundTool).toBe(toolName) + }) - // Parse uses the registry to get original names - const parsed = parseMcpToolName(builtName) - expect(parsed).toEqual({ - serverName: serverName, - toolName: toolName, - }) + it("should return null when no tool matches", () => { + const serverName = "server" + const encodedName = buildMcpToolName(serverName, "nonexistent") + + const availableTools = ["tool1", "tool2"] + const foundTool = findToolByEncodedMcpName(serverName, encodedName, availableTools) + expect(foundTool).toBeNull() + }) + + it("should handle empty available tools list", () => { + const serverName = "server" + const encodedName = buildMcpToolName(serverName, "tool") + + const foundTool = findToolByEncodedMcpName(serverName, encodedName, []) + expect(foundTool).toBeNull() }) }) - describe("clearMcpToolNameRegistry", () => { - it("should clear all registered tool names", () => { - // Register some tool names via buildMcpToolName + describe("hasHashSuffix", () => { + it("should return true for names with hash suffix", () => { + expect(hasHashSuffix("mcp--server--tool_a1b2c3d4")).toBe(true) + expect(hasHashSuffix("mcp--server--tool_12345678")).toBe(true) + expect(hasHashSuffix("mcp--server--tool_abcdef00")).toBe(true) + }) + + it("should return false for names without hash suffix", () => { + expect(hasHashSuffix("mcp--server--tool")).toBe(false) + expect(hasHashSuffix("mcp--server--tool_name")).toBe(false) + expect(hasHashSuffix("mcp--server--tool___name")).toBe(false) + }) + + it("should return false for names with partial hash patterns", () => { + // Not enough hex chars + expect(hasHashSuffix("mcp--server--tool_abc")).toBe(false) + // No underscore before hash + expect(hasHashSuffix("mcp--server--toola1b2c3d4")).toBe(false) + }) + }) + + describe("clearEncodedNameCache", () => { + it("should clear the encoded name cache", () => { + // Build some names to populate cache const longServer = "a".repeat(80) const longTool = "b".repeat(80) buildMcpToolName(longServer, longTool) + buildMcpToolName("server", "tool") - expect(mcpToolNameRegistry.size).toBeGreaterThan(0) - - // Clear the registry - clearMcpToolNameRegistry() + // Clear the cache + clearEncodedNameCache() - expect(mcpToolNameRegistry.size).toBe(0) + // Verify cache is cleared by checking that rebuilding takes the same path + // (we can't directly access the cache, but the function should work) + const result = buildMcpToolName(longServer, longTool) + expect(result.length).toBe(128) + expect(result).toMatch(/_[a-f0-9]{8}$/) }) }) }) diff --git a/src/utils/mcp-name.ts b/src/utils/mcp-name.ts index 25e90f6896b..57ff9756777 100644 --- a/src/utils/mcp-name.ts +++ b/src/utils/mcp-name.ts @@ -46,21 +46,48 @@ export const MAX_TOOL_NAME_LENGTH = 128 export const HASH_SUFFIX_LENGTH = 8 /** - * Registry mapping shortened tool names (with hash suffix) to their original - * server and tool names. This is used to look up the original names when - * the model returns a shortened tool name. - * - * Key: shortened MCP tool name (e.g., "mcp--server--tool_a1b2c3d4") - * Value: { serverName, toolName } with original (decoded) names + * LRU cache for encoded tool names. + * Maps "serverName:toolName" to the encoded MCP tool name. + * This avoids recomputing hash suffixes for frequently used tools. */ -export const mcpToolNameRegistry = new Map() +const ENCODED_NAME_CACHE_SIZE = 100 +const encodedNameCache = new Map() /** - * Clear the MCP tool name registry. - * Should be called when MCP servers are refreshed or disconnected. + * Get an encoded name from the LRU cache, updating recency. */ -export function clearMcpToolNameRegistry(): void { - mcpToolNameRegistry.clear() +function getCachedEncodedName(key: string): string | undefined { + const value = encodedNameCache.get(key) + if (value !== undefined) { + // Move to end (most recently used) by deleting and re-adding + encodedNameCache.delete(key) + encodedNameCache.set(key, value) + } + return value +} + +/** + * Set an encoded name in the LRU cache, evicting oldest if needed. + */ +function setCachedEncodedName(key: string, value: string): void { + if (encodedNameCache.has(key)) { + encodedNameCache.delete(key) + } else if (encodedNameCache.size >= ENCODED_NAME_CACHE_SIZE) { + // Evict the oldest entry (first in iteration order) + const oldestKey = encodedNameCache.keys().next().value + if (oldestKey) { + encodedNameCache.delete(oldestKey) + } + } + encodedNameCache.set(key, value) +} + +/** + * Clear the encoded name cache. + * Exported for testing purposes. + */ +export function clearEncodedNameCache(): void { + encodedNameCache.clear() } /** @@ -166,22 +193,30 @@ export function sanitizeMcpName(name: string): string { * The format is: mcp--{sanitized_server_name}--{sanitized_tool_name} * * The total length is capped at 128 characters per MCP spec. - * When truncation is needed, a hash suffix is appended to preserve uniqueness - * and the mapping is stored in the registry for later lookup. + * When truncation is needed, a hash suffix is appended to preserve uniqueness. + * The result is cached for efficient repeated lookups. * * @param serverName - The MCP server name * @param toolName - The tool name * @returns A sanitized function name in the format mcp--serverName--toolName */ export function buildMcpToolName(serverName: string, toolName: string): string { + // Check cache first + const cacheKey = `${serverName}:${toolName}` + const cached = getCachedEncodedName(cacheKey) + if (cached !== undefined) { + return cached + } + const sanitizedServer = sanitizeMcpName(serverName) const sanitizedTool = sanitizeMcpName(toolName) // Build the full name: mcp--{server}--{tool} const fullName = `${MCP_TOOL_PREFIX}${MCP_TOOL_SEPARATOR}${sanitizedServer}${MCP_TOOL_SEPARATOR}${sanitizedTool}` - // If within limit, return as-is + // If within limit, cache and return if (fullName.length <= MAX_TOOL_NAME_LENGTH) { + setCachedEncodedName(cacheKey, fullName) return fullName } @@ -195,9 +230,8 @@ export function buildMcpToolName(serverName: string, toolName: string): string { const truncatedBase = fullName.slice(0, maxTruncatedLength) const shortenedName = `${truncatedBase}${suffixWithSeparator}` - // Register the mapping from shortened name to original names - // Store the original (decoded) names so parseMcpToolName can return them directly - mcpToolNameRegistry.set(shortenedName, { serverName, toolName }) + // Cache the result + setCachedEncodedName(cacheKey, shortenedName) return shortenedName } @@ -218,8 +252,9 @@ export function decodeMcpName(sanitizedName: string): string { * This handles sanitized names by splitting on the "--" separator * and decoding triple underscores back to hyphens. * - * For shortened names (those with hash suffixes), this first checks - * the registry for the original names. + * Note: For shortened names (those with hash suffixes), this function + * returns the parsed names which may be truncated. Use findToolByEncodedMcpName() + * to find the correct original tool name by comparing encoded names. * * @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 @@ -230,13 +265,6 @@ export function parseMcpToolName(mcpToolName: string): { serverName: string; too return null } - // First, check if this is a shortened name in the registry - // This handles names that were truncated with hash suffixes - const registeredName = mcpToolNameRegistry.get(mcpToolName) - if (registeredName) { - return registeredName - } - // Remove the "mcp--" prefix const remainder = mcpToolName.slice(prefix.length) @@ -259,3 +287,40 @@ export function parseMcpToolName(mcpToolName: string): { serverName: string; too toolName: decodeMcpName(toolName), } } + +/** + * Find a tool by comparing encoded MCP names. + * This is used when the model returns a shortened name with a hash suffix. + * Instead of using a registry, we compare by encoding each available tool name + * and checking if it matches the encoded name from the model. + * + * @param serverName - The original server name (decoded) + * @param encodedMcpName - The full encoded MCP tool name returned by the model + * @param availableToolNames - List of available tool names on the server + * @returns The matching original tool name, or null if not found + */ +export function findToolByEncodedMcpName( + serverName: string, + encodedMcpName: string, + availableToolNames: string[], +): string | null { + for (const toolName of availableToolNames) { + const encoded = buildMcpToolName(serverName, toolName) + if (encoded === encodedMcpName) { + return toolName + } + } + return null +} + +/** + * Check if an MCP tool name appears to be a shortened name with a hash suffix. + * Shortened names have the pattern: ...._XXXXXXXX where X is a hex character. + * + * @param mcpToolName - The MCP tool name to check + * @returns true if the name appears to have a hash suffix + */ +export function hasHashSuffix(mcpToolName: string): boolean { + // Check for pattern: ends with underscore + 8 hex characters + return /_.{8}$/.test(mcpToolName) && /[a-f0-9]{8}$/.test(mcpToolName) +} From ef21f947f27bd7f1919af73505b937ad12fd1879 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Fri, 16 Jan 2026 14:30:45 +0000 Subject: [PATCH 4/4] fix: revert MAX_TOOL_NAME_LENGTH to 64 for Gemini compatibility The MCP spec recommends 128 characters, but Gemini enforces a 64-character limit for function names. Since buildMcpToolName() is provider-agnostic (called without provider context), we must use the lowest common denominator. This ensures compatibility with all supported API providers. --- src/utils/__tests__/mcp-name.spec.ts | 46 ++++++++++++++-------------- src/utils/mcp-name.ts | 10 +++--- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/utils/__tests__/mcp-name.spec.ts b/src/utils/__tests__/mcp-name.spec.ts index 9b6eac6d0cd..cb25e33e344 100644 --- a/src/utils/__tests__/mcp-name.spec.ts +++ b/src/utils/__tests__/mcp-name.spec.ts @@ -33,7 +33,7 @@ describe("mcp-name utilities", () => { }) it("should have correct max tool name length", () => { - expect(MAX_TOOL_NAME_LENGTH).toBe(128) + expect(MAX_TOOL_NAME_LENGTH).toBe(64) }) it("should have correct hash suffix length", () => { @@ -165,24 +165,24 @@ describe("mcp-name utilities", () => { expect(buildMcpToolName("server@name", "tool!name")).toBe("mcp--servername--toolname") }) - it("should truncate long names to 128 characters with hash suffix", () => { - const longServer = "a".repeat(80) - const longTool = "b".repeat(80) + it("should truncate long names to 64 characters with hash suffix", () => { + const longServer = "a".repeat(50) + const longTool = "b".repeat(50) const result = buildMcpToolName(longServer, longTool) - expect(result.length).toBeLessThanOrEqual(128) - expect(result.length).toBe(128) + expect(result.length).toBeLessThanOrEqual(64) + expect(result.length).toBe(64) expect(result.startsWith("mcp--")).toBe(true) // Should end with underscore + 8 char hash suffix expect(result).toMatch(/_[a-f0-9]{8}$/) }) it("should use hash suffix for long names and cache them", () => { - const longServer = "a".repeat(80) - const longTool = "b".repeat(80) + const longServer = "a".repeat(50) + const longTool = "b".repeat(50) const result = buildMcpToolName(longServer, longTool) // The shortened name should be deterministic - expect(result.length).toBe(128) + expect(result.length).toBe(64) expect(result).toMatch(/_[a-f0-9]{8}$/) // Building again should return the same result (from cache) @@ -191,8 +191,8 @@ describe("mcp-name utilities", () => { }) it("should produce deterministic hash suffixes", () => { - const longServer = "a".repeat(80) - const longTool = "b".repeat(80) + const longServer = "a".repeat(50) + const longTool = "b".repeat(50) // Build the same name twice with cache cleared between clearEncodedNameCache() const result1 = buildMcpToolName(longServer, longTool) @@ -203,12 +203,12 @@ describe("mcp-name utilities", () => { }) it("should produce unique hash suffixes for different tools", () => { - const longServer = "a".repeat(80) - const result1 = buildMcpToolName(longServer, "tool1_" + "x".repeat(70)) - const result2 = buildMcpToolName(longServer, "tool2_" + "y".repeat(70)) + const longServer = "a".repeat(50) + const result1 = buildMcpToolName(longServer, "tool1_" + "x".repeat(40)) + const result2 = buildMcpToolName(longServer, "tool2_" + "y".repeat(40)) // Both should be truncated - expect(result1.length).toBe(128) - expect(result2.length).toBe(128) + expect(result1.length).toBe(64) + expect(result2.length).toBe(64) // Should have different hash suffixes expect(result1).not.toBe(result2) }) @@ -437,7 +437,7 @@ describe("mcp-name utilities", () => { // Should NOT have hash suffix expect(builtName).toBe("mcp--server--get___data") - expect(builtName.length).toBeLessThan(128) + expect(builtName.length).toBeLessThan(64) // Normal decode path should work const parsed = parseMcpToolName(builtName) @@ -456,7 +456,7 @@ describe("mcp-name utilities", () => { const encodedName = buildMcpToolName(serverName, toolName) // Should be truncated to 128 chars with hash suffix - expect(encodedName.length).toBe(128) + expect(encodedName.length).toBe(64) expect(encodedName).toMatch(/_[a-f0-9]{8}$/) // The new approach: use findToolByEncodedMcpName to find the matching tool @@ -487,8 +487,8 @@ describe("mcp-name utilities", () => { }) it("should find tool for shortened names with hash suffix", () => { - const serverName = "a".repeat(80) - const toolName = "b".repeat(80) + "-hyphen" + const serverName = "a".repeat(50) + const toolName = "b".repeat(50) + "-hyphen" const encodedName = buildMcpToolName(serverName, toolName) // The encoded name should have a hash suffix @@ -541,8 +541,8 @@ describe("mcp-name utilities", () => { describe("clearEncodedNameCache", () => { it("should clear the encoded name cache", () => { // Build some names to populate cache - const longServer = "a".repeat(80) - const longTool = "b".repeat(80) + const longServer = "a".repeat(50) + const longTool = "b".repeat(50) buildMcpToolName(longServer, longTool) buildMcpToolName("server", "tool") @@ -552,7 +552,7 @@ describe("mcp-name utilities", () => { // Verify cache is cleared by checking that rebuilding takes the same path // (we can't directly access the cache, but the function should work) const result = buildMcpToolName(longServer, longTool) - expect(result.length).toBe(128) + expect(result.length).toBe(64) expect(result).toMatch(/_[a-f0-9]{8}$/) }) }) diff --git a/src/utils/mcp-name.ts b/src/utils/mcp-name.ts index 57ff9756777..cd1d7823134 100644 --- a/src/utils/mcp-name.ts +++ b/src/utils/mcp-name.ts @@ -33,10 +33,10 @@ export const MCP_TOOL_PREFIX = "mcp" export const HYPHEN_ENCODING = "___" /** - * Maximum length for tool names (MCP spec limit). - * See: https://modelcontextprotocol.io/specification/2025-11-25/server/tools#tool-names + * Maximum length for tool names (Gemini's function name limit). + * * The MCP spec recommends 128, but Gemini enforces 64 characters. */ -export const MAX_TOOL_NAME_LENGTH = 128 +export const MAX_TOOL_NAME_LENGTH = 64 /** * Length of hash suffix used when truncation is needed. @@ -192,7 +192,7 @@ export function sanitizeMcpName(name: string): string { * Build a full MCP tool function name from server and tool names. * The format is: mcp--{sanitized_server_name}--{sanitized_tool_name} * - * The total length is capped at 128 characters per MCP spec. + * The total length is capped at 64 characters for Gemini compatibility. * When truncation is needed, a hash suffix is appended to preserve uniqueness. * The result is cached for efficient repeated lookups. * @@ -224,7 +224,7 @@ export function buildMcpToolName(serverName: string, toolName: string): string { // Format: truncated_name_HASHSUFFIX (underscore + 8 hex chars = 9 chars for suffix) const hashSuffix = computeHashSuffix(serverName, toolName) const suffixWithSeparator = `_${hashSuffix}` // "_" + 8 chars = 9 chars - const maxTruncatedLength = MAX_TOOL_NAME_LENGTH - suffixWithSeparator.length // 128 - 9 = 119 + const maxTruncatedLength = MAX_TOOL_NAME_LENGTH - suffixWithSeparator.length // 64 - 9 = 55 // Truncate the full name and append hash suffix const truncatedBase = fullName.slice(0, maxTruncatedLength)