mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-22 19:30:15 +01:00
refactor: Update convertMCPToolsToPlugins to use mcpManager for server configuration and adjust related tests
This commit is contained in:
parent
b2b2aee945
commit
e7af3bdaed
4 changed files with 110 additions and 66 deletions
|
|
@ -106,7 +106,9 @@ const getAvailableTools = async (req, res) => {
|
||||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||||
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
|
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
|
||||||
const cachedUserTools = await getCachedTools({ userId });
|
const cachedUserTools = await getCachedTools({ userId });
|
||||||
const userPlugins = convertMCPToolsToPlugins({ functionTools: cachedUserTools, customConfig });
|
|
||||||
|
const mcpManager = getMCPManager();
|
||||||
|
const userPlugins = convertMCPToolsToPlugins({ functionTools: cachedUserTools, mcpManager });
|
||||||
|
|
||||||
if (cachedToolsArray != null && userPlugins != null) {
|
if (cachedToolsArray != null && userPlugins != null) {
|
||||||
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
|
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
|
||||||
|
|
@ -117,7 +119,6 @@ const getAvailableTools = async (req, res) => {
|
||||||
let pluginManifest = availableTools;
|
let pluginManifest = availableTools;
|
||||||
if (customConfig?.mcpServers != null) {
|
if (customConfig?.mcpServers != null) {
|
||||||
try {
|
try {
|
||||||
const mcpManager = getMCPManager();
|
|
||||||
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
||||||
const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null;
|
const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null;
|
||||||
const serverToolsCallback = createServerToolsCallback();
|
const serverToolsCallback = createServerToolsCallback();
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ jest.mock('~/server/services/ToolService', () => ({
|
||||||
jest.mock('~/config', () => ({
|
jest.mock('~/config', () => ({
|
||||||
getMCPManager: jest.fn(() => ({
|
getMCPManager: jest.fn(() => ({
|
||||||
loadManifestTools: jest.fn().mockResolvedValue([]),
|
loadManifestTools: jest.fn().mockResolvedValue([]),
|
||||||
|
getRawConfig: jest.fn(),
|
||||||
})),
|
})),
|
||||||
getFlowStateManager: jest.fn(),
|
getFlowStateManager: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
@ -167,7 +168,7 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||||
functionTools: mockUserTools,
|
functionTools: mockUserTools,
|
||||||
customConfig: null,
|
mcpManager: expect.any(Object),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -240,9 +241,9 @@ describe('PluginController', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('plugin.icon behavior', () => {
|
describe('plugin.icon behavior', () => {
|
||||||
const callGetAvailableToolsWithMCPServer = async (mcpServers) => {
|
const callGetAvailableToolsWithMCPServer = async (serverConfig) => {
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCustomConfig.mockResolvedValue({ mcpServers });
|
getCustomConfig.mockResolvedValue(null);
|
||||||
|
|
||||||
const functionTools = {
|
const functionTools = {
|
||||||
[`test-tool${Constants.mcp_delimiter}test-server`]: {
|
[`test-tool${Constants.mcp_delimiter}test-server`]: {
|
||||||
|
|
@ -254,11 +255,18 @@ describe('PluginController', () => {
|
||||||
name: 'test-tool',
|
name: 'test-tool',
|
||||||
pluginKey: `test-tool${Constants.mcp_delimiter}test-server`,
|
pluginKey: `test-tool${Constants.mcp_delimiter}test-server`,
|
||||||
description: 'A test tool',
|
description: 'A test tool',
|
||||||
icon: mcpServers['test-server']?.iconPath,
|
icon: serverConfig?.iconPath,
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
authConfig: [],
|
authConfig: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock the MCP manager to return the server config
|
||||||
|
const mockMCPManager = {
|
||||||
|
loadManifestTools: jest.fn().mockResolvedValue([]),
|
||||||
|
getRawConfig: jest.fn().mockReturnValue(serverConfig),
|
||||||
|
};
|
||||||
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
getCachedTools.mockResolvedValueOnce(functionTools);
|
getCachedTools.mockResolvedValueOnce(functionTools);
|
||||||
convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]);
|
convertMCPToolsToPlugins.mockReturnValue([mockConvertedPlugin]);
|
||||||
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
filterUniquePlugins.mockImplementation((plugins) => plugins);
|
||||||
|
|
@ -275,20 +283,16 @@ describe('PluginController', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
it('should set plugin.icon when iconPath is defined', async () => {
|
it('should set plugin.icon when iconPath is defined', async () => {
|
||||||
const mcpServers = {
|
const serverConfig = {
|
||||||
'test-server': {
|
iconPath: '/path/to/icon.png',
|
||||||
iconPath: '/path/to/icon.png',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const testTool = await callGetAvailableToolsWithMCPServer(mcpServers);
|
const testTool = await callGetAvailableToolsWithMCPServer(serverConfig);
|
||||||
expect(testTool.icon).toBe('/path/to/icon.png');
|
expect(testTool.icon).toBe('/path/to/icon.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set plugin.icon to undefined when iconPath is not defined', async () => {
|
it('should set plugin.icon to undefined when iconPath is not defined', async () => {
|
||||||
const mcpServers = {
|
const serverConfig = {};
|
||||||
'test-server': {},
|
const testTool = await callGetAvailableToolsWithMCPServer(serverConfig);
|
||||||
};
|
|
||||||
const testTool = await callGetAvailableToolsWithMCPServer(mcpServers);
|
|
||||||
expect(testTool.icon).toBeUndefined();
|
expect(testTool.icon).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -318,6 +322,7 @@ describe('PluginController', () => {
|
||||||
// Mock the MCP manager to return tools
|
// Mock the MCP manager to return tools
|
||||||
const mockMCPManager = {
|
const mockMCPManager = {
|
||||||
loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools),
|
loadManifestTools: jest.fn().mockResolvedValue(mcpManagerTools),
|
||||||
|
getRawConfig: jest.fn(),
|
||||||
};
|
};
|
||||||
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
|
|
@ -388,7 +393,7 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||||
functionTools: null,
|
functionTools: null,
|
||||||
customConfig: null,
|
mcpManager: expect.any(Object),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -408,7 +413,7 @@ describe('PluginController', () => {
|
||||||
|
|
||||||
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
expect(convertMCPToolsToPlugins).toHaveBeenCalledWith({
|
||||||
functionTools: undefined,
|
functionTools: undefined,
|
||||||
customConfig: null,
|
mcpManager: expect.any(Object),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -459,6 +464,13 @@ describe('PluginController', () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Mock the MCP manager to return server config without customUserVars
|
||||||
|
const mockMCPManager = {
|
||||||
|
loadManifestTools: jest.fn().mockResolvedValue([]),
|
||||||
|
getRawConfig: jest.fn(),
|
||||||
|
};
|
||||||
|
require('~/config').getMCPManager.mockReturnValue(mockMCPManager);
|
||||||
|
|
||||||
mockCache.get.mockResolvedValue(null);
|
mockCache.get.mockResolvedValue(null);
|
||||||
getCustomConfig.mockResolvedValue(customConfig);
|
getCustomConfig.mockResolvedValue(customConfig);
|
||||||
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
getCachedTools.mockResolvedValueOnce(mockUserTools);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { AuthType, Constants, EToolResources } from 'librechat-data-provider';
|
import { AuthType, Constants, EToolResources } from 'librechat-data-provider';
|
||||||
import type { TPlugin, FunctionTool, TCustomConfig } from 'librechat-data-provider';
|
import type { TPlugin, FunctionTool } from 'librechat-data-provider';
|
||||||
|
import type { MCPManager } from '~/mcp/MCPManager';
|
||||||
import {
|
import {
|
||||||
convertMCPToolsToPlugins,
|
convertMCPToolsToPlugins,
|
||||||
filterUniquePlugins,
|
filterUniquePlugins,
|
||||||
|
|
@ -277,19 +278,18 @@ describe('format.ts helper functions', () => {
|
||||||
} as FunctionTool,
|
} as FunctionTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const customConfig: Partial<TCustomConfig> = {
|
const mockMcpManager = {
|
||||||
mcpServers: {
|
getRawConfig: jest.fn().mockReturnValue({
|
||||||
server1: {
|
command: 'test',
|
||||||
command: 'test',
|
args: [],
|
||||||
args: [],
|
iconPath: '/path/to/icon.png',
|
||||||
iconPath: '/path/to/icon.png',
|
}),
|
||||||
},
|
} as unknown as MCPManager;
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools, customConfig });
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result![0].icon).toBe('/path/to/icon.png');
|
expect(result![0].icon).toBe('/path/to/icon.png');
|
||||||
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle customUserVars in server config', () => {
|
it('should handle customUserVars in server config', () => {
|
||||||
|
|
@ -300,26 +300,25 @@ describe('format.ts helper functions', () => {
|
||||||
} as FunctionTool,
|
} as FunctionTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const customConfig: Partial<TCustomConfig> = {
|
const mockMcpManager = {
|
||||||
mcpServers: {
|
getRawConfig: jest.fn().mockReturnValue({
|
||||||
server1: {
|
command: 'test',
|
||||||
command: 'test',
|
args: [],
|
||||||
args: [],
|
customUserVars: {
|
||||||
customUserVars: {
|
API_KEY: { title: 'API Key', description: 'Your API key' },
|
||||||
API_KEY: { title: 'API Key', description: 'Your API key' },
|
SECRET: { title: 'Secret', description: 'Your secret' },
|
||||||
SECRET: { title: 'Secret', description: 'Your secret' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
};
|
} as unknown as MCPManager;
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools, customConfig });
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result![0].authConfig).toHaveLength(2);
|
expect(result![0].authConfig).toHaveLength(2);
|
||||||
expect(result![0].authConfig).toEqual([
|
expect(result![0].authConfig).toEqual([
|
||||||
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
||||||
{ authField: 'SECRET', label: 'Secret', description: 'Your secret' },
|
{ authField: 'SECRET', label: 'Secret', description: 'Your secret' },
|
||||||
]);
|
]);
|
||||||
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use key as label when title is missing in customUserVars', () => {
|
it('should use key as label when title is missing in customUserVars', () => {
|
||||||
|
|
@ -330,23 +329,22 @@ describe('format.ts helper functions', () => {
|
||||||
} as FunctionTool,
|
} as FunctionTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const customConfig: Partial<TCustomConfig> = {
|
const mockMcpManager = {
|
||||||
mcpServers: {
|
getRawConfig: jest.fn().mockReturnValue({
|
||||||
server1: {
|
command: 'test',
|
||||||
command: 'test',
|
args: [],
|
||||||
args: [],
|
customUserVars: {
|
||||||
customUserVars: {
|
API_KEY: { title: 'API Key', description: 'Your API key' },
|
||||||
API_KEY: { title: 'API Key', description: 'Your API key' },
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
};
|
} as unknown as MCPManager;
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools, customConfig });
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result![0].authConfig).toEqual([
|
expect(result![0].authConfig).toEqual([
|
||||||
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
||||||
]);
|
]);
|
||||||
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle empty customUserVars', () => {
|
it('should handle empty customUserVars', () => {
|
||||||
|
|
@ -357,19 +355,51 @@ describe('format.ts helper functions', () => {
|
||||||
} as FunctionTool,
|
} as FunctionTool,
|
||||||
};
|
};
|
||||||
|
|
||||||
const customConfig: Partial<TCustomConfig> = {
|
const mockMcpManager = {
|
||||||
mcpServers: {
|
getRawConfig: jest.fn().mockReturnValue({
|
||||||
server1: {
|
command: 'test',
|
||||||
command: 'test',
|
args: [],
|
||||||
args: [],
|
customUserVars: {},
|
||||||
customUserVars: {},
|
}),
|
||||||
},
|
} as unknown as MCPManager;
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools, customConfig });
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
||||||
expect(result).toHaveLength(1);
|
expect(result).toHaveLength(1);
|
||||||
expect(result![0].authConfig).toEqual([]);
|
expect(result![0].authConfig).toEqual([]);
|
||||||
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing mcpManager', () => {
|
||||||
|
const functionTools: Record<string, FunctionTool> = {
|
||||||
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'tool1', description: 'Tool 1' },
|
||||||
|
} as FunctionTool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = convertMCPToolsToPlugins({ functionTools });
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result![0].icon).toBeUndefined();
|
||||||
|
expect(result![0].authConfig).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle when getRawConfig returns undefined', () => {
|
||||||
|
const functionTools: Record<string, FunctionTool> = {
|
||||||
|
[`tool1${Constants.mcp_delimiter}server1`]: {
|
||||||
|
type: 'function',
|
||||||
|
function: { name: 'tool1', description: 'Tool 1' },
|
||||||
|
} as FunctionTool,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockMcpManager = {
|
||||||
|
getRawConfig: jest.fn().mockReturnValue(undefined),
|
||||||
|
} as unknown as MCPManager;
|
||||||
|
|
||||||
|
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result![0].icon).toBeUndefined();
|
||||||
|
expect(result![0].authConfig).toEqual([]);
|
||||||
|
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { AuthType, Constants, EToolResources } from 'librechat-data-provider';
|
import { AuthType, Constants, EToolResources } from 'librechat-data-provider';
|
||||||
import type { TCustomConfig, TPlugin, FunctionTool } from 'librechat-data-provider';
|
import type { TPlugin, FunctionTool } from 'librechat-data-provider';
|
||||||
|
import type { MCPManager } from '~/mcp/MCPManager';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters out duplicate plugins from the list of plugins.
|
* Filters out duplicate plugins from the list of plugins.
|
||||||
|
|
@ -49,15 +50,15 @@ export const checkPluginAuth = (plugin?: TPlugin): boolean => {
|
||||||
/**
|
/**
|
||||||
* Converts MCP function format tools to plugin format
|
* Converts MCP function format tools to plugin format
|
||||||
* @param functionTools - Object with function format tools
|
* @param functionTools - Object with function format tools
|
||||||
* @param customConfig - Custom configuration for MCP servers
|
* @param mcpManager - MCP manager instance for server configuration access
|
||||||
* @returns Array of plugin objects
|
* @returns Array of plugin objects
|
||||||
*/
|
*/
|
||||||
export function convertMCPToolsToPlugins({
|
export function convertMCPToolsToPlugins({
|
||||||
functionTools,
|
functionTools,
|
||||||
customConfig,
|
mcpManager,
|
||||||
}: {
|
}: {
|
||||||
functionTools?: Record<string, FunctionTool>;
|
functionTools?: Record<string, FunctionTool>;
|
||||||
customConfig?: Partial<TCustomConfig> | null;
|
mcpManager?: MCPManager;
|
||||||
}): TPlugin[] | undefined {
|
}): TPlugin[] | undefined {
|
||||||
if (!functionTools || typeof functionTools !== 'object') {
|
if (!functionTools || typeof functionTools !== 'object') {
|
||||||
return;
|
return;
|
||||||
|
|
@ -73,7 +74,7 @@ export function convertMCPToolsToPlugins({
|
||||||
const parts = toolKey.split(Constants.mcp_delimiter);
|
const parts = toolKey.split(Constants.mcp_delimiter);
|
||||||
const serverName = parts[parts.length - 1];
|
const serverName = parts[parts.length - 1];
|
||||||
|
|
||||||
const serverConfig = customConfig?.mcpServers?.[serverName];
|
const serverConfig = mcpManager?.getRawConfig(serverName);
|
||||||
|
|
||||||
const plugin: TPlugin = {
|
const plugin: TPlugin = {
|
||||||
/** Tool name without server suffix */
|
/** Tool name without server suffix */
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue