From 48887a743de1d58639ca78b9774c55f3f1d1b333 Mon Sep 17 00:00:00 2001 From: Gopal Bagaswar Date: Thu, 30 Apr 2026 09:40:41 +0530 Subject: [PATCH] fix(mcp): warn when composed MCP tool name exceeds OpenAI 64-char limit When an MCP server name and tool name combine such that \`${tool.name}${mcp_delimiter}${serverName}\` exceeds 64 characters, the resulting Chat Completions / Responses API request is rejected by OpenAI-compatible providers with HTTP 400 ("string too long. Expected a string with maximum length 64"). Today this only surfaces at request time inside the agent runtime, where the cause is hard to identify because the end user only sees the truncated provider error. This commit emits an actionable \`logger.warn\` at tool-cache build time identifying the offending server name, tool name, and combined length so operators can shorten one of the two before the next request fails. No behavior change for tool names that already fit the limit. Closes #7435. Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/api/src/mcp/tools.spec.ts | 53 ++++++++++++++++++++++++++++++ packages/api/src/mcp/tools.ts | 22 +++++++++++++ 2 files changed, 75 insertions(+) diff --git a/packages/api/src/mcp/tools.spec.ts b/packages/api/src/mcp/tools.spec.ts index 2a5d201df8..edae0be446 100644 --- a/packages/api/src/mcp/tools.spec.ts +++ b/packages/api/src/mcp/tools.spec.ts @@ -3,6 +3,16 @@ import { createMCPToolCacheService } from './tools'; import type { LCAvailableTools } from './types'; import type { MCPToolInput, MCPToolCacheDeps } from './tools'; +jest.mock('@librechat/data-schemas', () => ({ + ...jest.requireActual('@librechat/data-schemas'), + logger: { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + function createMockDeps(overrides: Partial = {}): MCPToolCacheDeps { return { getCachedTools: jest.fn().mockResolvedValue(null), @@ -68,6 +78,49 @@ describe('createMCPToolCacheService', () => { updateMCPServerTools({ userId: 'u1', serverName: 'srv', tools }), ).rejects.toThrow('Redis down'); }); + + it('warns when the composed tool name exceeds the OpenAI 64-char limit', async () => { + const { logger } = jest.requireMock('@librechat/data-schemas') as { + logger: { warn: jest.Mock; debug: jest.Mock; error: jest.Mock }; + }; + logger.warn.mockClear(); + + const deps = createMockDeps(); + const { updateMCPServerTools } = createMCPToolCacheService(deps); + + // 25 + 5 (delimiter "_mcp_") + 40 = 70 > 64 + const longServerName = 'my-very-long-mcp-server-1'; + const longToolName = 'execute_extremely_descriptive_action_name'; + const tools: MCPToolInput[] = [{ name: longToolName }]; + + await updateMCPServerTools({ + userId: 'u1', + serverName: longServerName, + tools, + }); + + expect(logger.warn).toHaveBeenCalledTimes(1); + const message = logger.warn.mock.calls[0][0] as string; + expect(message).toContain(longServerName); + expect(message).toContain(longToolName); + expect(message).toContain('exceeds'); + expect(message).toContain('64'); + }); + + it('does not warn for tool names within the OpenAI 64-char limit', async () => { + const { logger } = jest.requireMock('@librechat/data-schemas') as { + logger: { warn: jest.Mock; debug: jest.Mock; error: jest.Mock }; + }; + logger.warn.mockClear(); + + const deps = createMockDeps(); + const { updateMCPServerTools } = createMCPToolCacheService(deps); + + const tools: MCPToolInput[] = [{ name: 'search' }]; + await updateMCPServerTools({ userId: 'u1', serverName: 'brave', tools }); + + expect(logger.warn).not.toHaveBeenCalled(); + }); }); describe('mergeAppTools', () => { diff --git a/packages/api/src/mcp/tools.ts b/packages/api/src/mcp/tools.ts index d539cc5bd0..d5a2c5419e 100644 --- a/packages/api/src/mcp/tools.ts +++ b/packages/api/src/mcp/tools.ts @@ -3,6 +3,18 @@ import { Constants } from 'librechat-data-provider'; import type { JsonSchemaType } from '@librechat/agents'; import type { LCAvailableTools, LCFunctionTool } from './types'; +/** + * Maximum allowed length for tool function names accepted by OpenAI-compatible + * Chat Completions / Responses APIs. Tool calls whose names exceed this limit + * are rejected with HTTP 400 ("string too long. Expected ... maximum length 64"). + * MCP tool names are constructed as `${toolName}${mcp_delimiter}${serverName}`, + * so callers must keep the combined length within this bound. + * + * Refs: https://platform.openai.com/docs/api-reference/chat/create + * https://github.com/danny-avila/LibreChat/issues/7435 + */ +const MAX_OPENAI_TOOL_NAME_LENGTH = 64; + export interface MCPToolInput { name: string; description?: string; @@ -40,6 +52,16 @@ export function createMCPToolCacheService(deps: MCPToolCacheDeps) { for (const tool of tools) { const name = `${tool.name}${mcpDelimiter}${serverName}`; + if (name.length > MAX_OPENAI_TOOL_NAME_LENGTH) { + logger.warn( + `[MCP Cache] Tool name "${name}" (${name.length} chars) exceeds the ` + + `${MAX_OPENAI_TOOL_NAME_LENGTH}-char limit enforced by OpenAI-compatible APIs ` + + `(server: "${serverName}", tool: "${tool.name}", delimiter: "${mcpDelimiter}"). ` + + `Calls including this tool will be rejected with HTTP 400. ` + + `Shorten the MCP server name or the tool name to fit within ` + + `${MAX_OPENAI_TOOL_NAME_LENGTH - mcpDelimiter.length} characters combined.`, + ); + } const entry: LCFunctionTool = { type: 'function', ['function']: {