🐛 fix: MCP Name Normalization breaking User Provided Variables (#8644)

This commit is contained in:
Dustin Healy 2025-07-24 07:44:58 -07:00 committed by GitHub
parent 01470ef9fd
commit 1fe977e48f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 172 additions and 13 deletions

View file

@ -3,7 +3,6 @@ const { isEnabled, getUserMCPAuthMap } = require('@librechat/api');
const { CacheKeys, EModelEndpoint } = require('librechat-data-provider');
const { normalizeEndpointName } = require('~/server/utils');
const loadCustomConfig = require('./loadCustomConfig');
const { getCachedTools } = require('./getCachedTools');
const getLogStores = require('~/cache/getLogStores');
/**
@ -66,13 +65,9 @@ async function getMCPAuthMap({ userId, tools, findPluginAuthsByKeys }) {
if (!tools || tools.length === 0) {
return;
}
const appTools = await getCachedTools({
userId,
});
return await getUserMCPAuthMap({
tools,
userId,
appTools,
findPluginAuthsByKeys,
});
} catch (err) {

View file

@ -235,6 +235,7 @@ async function createMCPTool({ req, res, toolKey, provider: _provider }) {
responseFormat: AgentConstants.CONTENT_AND_ARTIFACT,
});
toolInstance.mcp = true;
toolInstance.mcpRawServerName = serverName;
return toolInstance;
}

View file

@ -0,0 +1,168 @@
import type { PluginAuthMethods } from '@librechat/data-schemas';
import type { GenericTool } from '@librechat/agents';
import { getPluginAuthMap } from '~/agents/auth';
import { getUserMCPAuthMap } from './auth';
jest.mock('~/agents/auth', () => ({
getPluginAuthMap: jest.fn(),
}));
const mockGetPluginAuthMap = getPluginAuthMap as jest.MockedFunction<typeof getPluginAuthMap>;
const createMockTool = (
name: string,
mcpRawServerName?: string,
mcp = true,
): GenericTool & { mcpRawServerName?: string; mcp?: boolean } =>
({
name,
mcpRawServerName,
mcp,
description: 'Mock tool',
}) as GenericTool & { mcpRawServerName?: string; mcp?: boolean };
const mockFindPluginAuthsByKeys: PluginAuthMethods['findPluginAuthsByKeys'] = jest.fn();
describe('getUserMCPAuthMap', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('Core Functionality', () => {
it('should handle server names with various special characters and spaces', async () => {
const testCases = [
{
originalName: 'Connector: Company',
normalizedToolName: 'tool_mcp_Connector__Company',
},
{
originalName: 'Server (Production) @ Company.com',
normalizedToolName: 'tool_mcp_Server__Production____Company.com',
},
{
originalName: '🌟 Testing Server™ (α-β) 测试服务器',
normalizedToolName: 'tool_mcp_____Testing_Server_________',
},
];
const tools = testCases.map((testCase) =>
createMockTool(testCase.normalizedToolName, testCase.originalName),
);
const expectedKeys = testCases.map((tc) => `mcp_${tc.originalName}`);
mockGetPluginAuthMap.mockResolvedValue({});
await getUserMCPAuthMap({
userId: 'user123',
tools,
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
});
expect(mockGetPluginAuthMap).toHaveBeenCalledWith({
userId: 'user123',
pluginKeys: expectedKeys,
throwError: false,
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
});
});
});
describe('Edge Cases', () => {
it('should return empty object when no tools have mcpRawServerName', async () => {
const tools = [
createMockTool('regular_tool', undefined, false),
createMockTool('another_tool', undefined, false),
createMockTool('test_mcp_Server_no_raw_name', undefined),
];
const result = await getUserMCPAuthMap({
userId: 'user123',
tools,
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
});
expect(result).toEqual({});
expect(mockGetPluginAuthMap).not.toHaveBeenCalled();
});
it('should handle empty or undefined tools array', async () => {
let result = await getUserMCPAuthMap({
userId: 'user123',
tools: [],
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
});
expect(result).toEqual({});
expect(mockGetPluginAuthMap).not.toHaveBeenCalled();
result = await getUserMCPAuthMap({
userId: 'user123',
tools: undefined,
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
});
expect(result).toEqual({});
expect(mockGetPluginAuthMap).not.toHaveBeenCalled();
});
it('should handle database errors gracefully', async () => {
const tools = [createMockTool('test_mcp_Server1', 'Server1')];
const dbError = new Error('Database connection failed');
mockGetPluginAuthMap.mockRejectedValue(dbError);
const result = await getUserMCPAuthMap({
userId: 'user123',
tools,
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
});
expect(result).toEqual({});
});
it('should handle non-Error exceptions gracefully', async () => {
const tools = [createMockTool('test_mcp_Server1', 'Server1')];
mockGetPluginAuthMap.mockRejectedValue('String error');
const result = await getUserMCPAuthMap({
userId: 'user123',
tools,
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
});
expect(result).toEqual({});
});
});
describe('Integration', () => {
it('should handle complete workflow with normalized tool names and original server names', async () => {
const originalServerName = 'Connector: Company';
const toolName = 'test_auth_mcp_Connector__Company';
const tools = [createMockTool(toolName, originalServerName)];
const mockCustomUserVars = {
'mcp_Connector: Company': {
API_KEY: 'test123',
SECRET_TOKEN: 'secret456',
},
};
mockGetPluginAuthMap.mockResolvedValue(mockCustomUserVars);
const result = await getUserMCPAuthMap({
userId: 'user123',
tools,
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
});
expect(mockGetPluginAuthMap).toHaveBeenCalledWith({
userId: 'user123',
pluginKeys: ['mcp_Connector: Company'],
throwError: false,
findPluginAuthsByKeys: mockFindPluginAuthsByKeys,
});
expect(result).toEqual(mockCustomUserVars);
});
});
});

View file

@ -3,17 +3,14 @@ import { Constants } from 'librechat-data-provider';
import type { PluginAuthMethods } from '@librechat/data-schemas';
import type { GenericTool } from '@librechat/agents';
import { getPluginAuthMap } from '~/agents/auth';
import { mcpToolPattern } from './utils';
export async function getUserMCPAuthMap({
userId,
tools,
appTools,
findPluginAuthsByKeys,
}: {
userId: string;
tools: GenericTool[] | undefined;
appTools: Record<string, unknown>;
findPluginAuthsByKeys: PluginAuthMethods['findPluginAuthsByKeys'];
}) {
if (!tools || tools.length === 0) {
@ -23,11 +20,9 @@ export async function getUserMCPAuthMap({
const uniqueMcpServers = new Set<string>();
for (const tool of tools) {
const toolKey = tool.name;
if (toolKey && appTools[toolKey] && mcpToolPattern.test(toolKey)) {
const parts = toolKey.split(Constants.mcp_delimiter);
const serverName = parts[parts.length - 1];
uniqueMcpServers.add(`${Constants.mcp_prefix}${serverName}`);
const mcpTool = tool as GenericTool & { mcpRawServerName?: string };
if (mcpTool.mcpRawServerName) {
uniqueMcpServers.add(`${Constants.mcp_prefix}${mcpTool.mcpRawServerName}`);
}
}