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']: {