diff --git a/api/server/services/MCP.js b/api/server/services/MCP.js index 1d4fc5112c..b9baef462e 100644 --- a/api/server/services/MCP.js +++ b/api/server/services/MCP.js @@ -1,5 +1,6 @@ const { z } = require('zod'); const { tool } = require('@langchain/core/tools'); +const { normalizeServerName } = require('librechat-mcp'); const { Constants: AgentConstants, Providers } = require('@librechat/agents'); const { Constants, @@ -38,6 +39,7 @@ async function createMCPTool({ req, toolKey, provider: _provider }) { } const [toolName, serverName] = toolKey.split(Constants.mcp_delimiter); + const normalizedToolKey = `${toolName}${Constants.mcp_delimiter}${normalizeServerName(serverName)}`; if (!req.user?.id) { logger.error( @@ -83,7 +85,7 @@ async function createMCPTool({ req, toolKey, provider: _provider }) { const toolInstance = tool(_call, { schema, - name: toolKey, + name: normalizedToolKey, description: description || '', responseFormat: AgentConstants.CONTENT_AND_ARTIFACT, }); diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts index ba1dcdf821..ba063ef1e3 100644 --- a/packages/mcp/src/index.ts +++ b/packages/mcp/src/index.ts @@ -1,5 +1,7 @@ /* MCP */ export * from './manager'; +/* Utilities */ +export * from './utils'; /* Flow */ export * from './flow/manager'; /* types */ diff --git a/packages/mcp/src/utils.test.ts b/packages/mcp/src/utils.test.ts new file mode 100644 index 0000000000..bc5d0ba7d4 --- /dev/null +++ b/packages/mcp/src/utils.test.ts @@ -0,0 +1,28 @@ +import { normalizeServerName } from './utils'; + +describe('normalizeServerName', () => { + it('should not modify server names that already match the pattern', () => { + const result = normalizeServerName('valid-server_name.123'); + expect(result).toBe('valid-server_name.123'); + }); + + it('should normalize server names with non-ASCII characters', () => { + const result = normalizeServerName('我的服务'); + // Should generate a fallback name with a hash + expect(result).toMatch(/^server_\d+$/); + expect(result).toMatch(/^[a-zA-Z0-9_.-]+$/); + }); + + it('should normalize server names with special characters', () => { + const result = normalizeServerName('server@name!'); + // The actual result doesn't have the trailing underscore after trimming + expect(result).toBe('server_name'); + expect(result).toMatch(/^[a-zA-Z0-9_.-]+$/); + }); + + it('should trim leading and trailing underscores', () => { + const result = normalizeServerName('!server-name!'); + expect(result).toBe('server-name'); + expect(result).toMatch(/^[a-zA-Z0-9_.-]+$/); + }); +}); diff --git a/packages/mcp/src/utils.ts b/packages/mcp/src/utils.ts new file mode 100644 index 0000000000..f315976fcf --- /dev/null +++ b/packages/mcp/src/utils.ts @@ -0,0 +1,30 @@ +/** + * Normalizes a server name to match the pattern ^[a-zA-Z0-9_.-]+$ + * This is required for Azure OpenAI models with Tool Calling + */ +export function normalizeServerName(serverName: string): string { + // Check if the server name already matches the pattern + if (/^[a-zA-Z0-9_.-]+$/.test(serverName)) { + return serverName; + } + + /** Replace non-matching characters with underscores. + This preserves the general structure while ensuring compatibility. + Trims leading/trailing underscores + */ + const normalized = serverName.replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/^_+|_+$/g, ''); + + // If the result is empty (e.g., all characters were non-ASCII and got trimmed), + // generate a fallback name to ensure we always have a valid function name + if (!normalized) { + /** Hash of the original name to ensure uniqueness */ + let hash = 0; + for (let i = 0; i < serverName.length; i++) { + hash = (hash << 5) - hash + serverName.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + return `server_${Math.abs(hash)}`; + } + + return normalized; +}