mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +01:00
🧬 refactor: Optimize MCP Tool Queries with Server-Centric Architecture
🧬 refactor: Optimize MCP Tool Queries with Server-Centric Architecture
refactor: optimize mcp tool queries by removing redundancy, making server-centric structure, enabling query only when expected, minimize looping/transforming query data, eliminating unused/compute-heavy methods
ci: MCP Server Tools Mocking in Agent Tests
This commit is contained in:
parent
5b1a31ef4d
commit
f0599ad36c
19 changed files with 235 additions and 1104 deletions
|
|
@ -8,6 +8,7 @@ process.env.CREDS_IV = '0123456789abcdef';
|
||||||
|
|
||||||
jest.mock('~/server/services/Config', () => ({
|
jest.mock('~/server/services/Config', () => ({
|
||||||
getCachedTools: jest.fn(),
|
getCachedTools: jest.fn(),
|
||||||
|
getMCPServerTools: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const mongoose = require('mongoose');
|
const mongoose = require('mongoose');
|
||||||
|
|
@ -30,7 +31,7 @@ const {
|
||||||
generateActionMetadataHash,
|
generateActionMetadataHash,
|
||||||
} = require('./Agent');
|
} = require('./Agent');
|
||||||
const permissionService = require('~/server/services/PermissionService');
|
const permissionService = require('~/server/services/PermissionService');
|
||||||
const { getCachedTools } = require('~/server/services/Config');
|
const { getCachedTools, getMCPServerTools } = require('~/server/services/Config');
|
||||||
const { AclEntry } = require('~/db/models');
|
const { AclEntry } = require('~/db/models');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1929,6 +1930,16 @@ describe('models/Agent', () => {
|
||||||
another_tool: {},
|
another_tool: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mock getMCPServerTools to return tools for each server
|
||||||
|
getMCPServerTools.mockImplementation(async (server) => {
|
||||||
|
if (server === 'server1') {
|
||||||
|
return { tool1_mcp_server1: {} };
|
||||||
|
} else if (server === 'server2') {
|
||||||
|
return { tool2_mcp_server2: {} };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { id: 'user123' },
|
user: { id: 'user123' },
|
||||||
body: {
|
body: {
|
||||||
|
|
@ -2113,6 +2124,14 @@ describe('models/Agent', () => {
|
||||||
|
|
||||||
getCachedTools.mockResolvedValue(availableTools);
|
getCachedTools.mockResolvedValue(availableTools);
|
||||||
|
|
||||||
|
// Mock getMCPServerTools to return all tools for server1
|
||||||
|
getMCPServerTools.mockImplementation(async (server) => {
|
||||||
|
if (server === 'server1') {
|
||||||
|
return availableTools; // All 100 tools belong to server1
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { id: 'user123' },
|
user: { id: 'user123' },
|
||||||
body: {
|
body: {
|
||||||
|
|
@ -2654,6 +2673,17 @@ describe('models/Agent', () => {
|
||||||
tool_mcp_server2: {}, // Different server
|
tool_mcp_server2: {}, // Different server
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mock getMCPServerTools to return only tools matching the server
|
||||||
|
getMCPServerTools.mockImplementation(async (server) => {
|
||||||
|
if (server === 'server1') {
|
||||||
|
// Only return tool that correctly matches server1 format
|
||||||
|
return { tool_mcp_server1: {} };
|
||||||
|
} else if (server === 'server2') {
|
||||||
|
return { tool_mcp_server2: {} };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
const mockReq = {
|
const mockReq = {
|
||||||
user: { id: 'user123' },
|
user: { id: 'user123' },
|
||||||
body: {
|
body: {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@
|
||||||
*/
|
*/
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { Constants } = require('librechat-data-provider');
|
const { Constants } = require('librechat-data-provider');
|
||||||
const { convertMCPToolToPlugin } = require('@librechat/api');
|
|
||||||
const {
|
const {
|
||||||
cacheMCPServerTools,
|
cacheMCPServerTools,
|
||||||
getMCPServerTools,
|
getMCPServerTools,
|
||||||
|
|
@ -14,7 +13,6 @@ const { getMCPManager } = require('~/config');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all MCP tools available to the user
|
* Get all MCP tools available to the user
|
||||||
* Returns only MCP tools, not regular LibreChat tools
|
|
||||||
*/
|
*/
|
||||||
const getMCPTools = async (req, res) => {
|
const getMCPTools = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|
@ -26,77 +24,97 @@ const getMCPTools = async (req, res) => {
|
||||||
|
|
||||||
const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
|
const appConfig = req.config ?? (await getAppConfig({ role: req.user?.role }));
|
||||||
if (!appConfig?.mcpConfig) {
|
if (!appConfig?.mcpConfig) {
|
||||||
return res.status(200).json([]);
|
return res.status(200).json({ servers: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
const mcpManager = getMCPManager();
|
const mcpManager = getMCPManager();
|
||||||
const configuredServers = Object.keys(appConfig.mcpConfig);
|
const configuredServers = Object.keys(appConfig.mcpConfig);
|
||||||
const mcpTools = [];
|
const mcpServers = {};
|
||||||
|
|
||||||
// Fetch tools from each configured server
|
const cachePromises = configuredServers.map((serverName) =>
|
||||||
|
getMCPServerTools(serverName).then((tools) => ({ serverName, tools })),
|
||||||
|
);
|
||||||
|
const cacheResults = await Promise.all(cachePromises);
|
||||||
|
|
||||||
|
const serverToolsMap = new Map();
|
||||||
|
for (const { serverName, tools } of cacheResults) {
|
||||||
|
if (tools) {
|
||||||
|
serverToolsMap.set(serverName, tools);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverTools = await mcpManager.getServerToolFunctions(userId, serverName);
|
||||||
|
if (!serverTools) {
|
||||||
|
logger.debug(`[getMCPTools] No tools found for server ${serverName}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
serverToolsMap.set(serverName, serverTools);
|
||||||
|
|
||||||
|
if (Object.keys(serverTools).length > 0) {
|
||||||
|
// Cache asynchronously without blocking
|
||||||
|
cacheMCPServerTools({ serverName, serverTools }).catch((err) =>
|
||||||
|
logger.error(`[getMCPTools] Failed to cache tools for ${serverName}:`, err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process each configured server
|
||||||
for (const serverName of configuredServers) {
|
for (const serverName of configuredServers) {
|
||||||
try {
|
try {
|
||||||
// First check server-specific cache
|
const serverTools = serverToolsMap.get(serverName);
|
||||||
let serverTools = await getMCPServerTools(serverName);
|
|
||||||
|
|
||||||
if (!serverTools) {
|
// Get server config once
|
||||||
// If not cached, fetch from MCP manager
|
const serverConfig = appConfig.mcpConfig[serverName];
|
||||||
const allTools = await mcpManager.getAllToolFunctions(userId);
|
const rawServerConfig = mcpManager.getRawConfig(serverName);
|
||||||
serverTools = {};
|
|
||||||
|
|
||||||
// Filter tools for this specific server
|
// Initialize server object with all server-level data
|
||||||
for (const [toolKey, toolData] of Object.entries(allTools)) {
|
const server = {
|
||||||
if (toolKey.endsWith(`${Constants.mcp_delimiter}${serverName}`)) {
|
name: serverName,
|
||||||
serverTools[toolKey] = toolData;
|
icon: rawServerConfig?.iconPath || '',
|
||||||
}
|
authenticated: true,
|
||||||
}
|
authConfig: [],
|
||||||
|
tools: [],
|
||||||
|
};
|
||||||
|
|
||||||
// Cache server tools if found
|
// Set authentication config once for the server
|
||||||
if (Object.keys(serverTools).length > 0) {
|
if (serverConfig?.customUserVars) {
|
||||||
await cacheMCPServerTools({ serverName, serverTools });
|
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
||||||
|
if (customVarKeys.length > 0) {
|
||||||
|
server.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
|
||||||
|
authField: key,
|
||||||
|
label: value.title || key,
|
||||||
|
description: value.description || '',
|
||||||
|
}));
|
||||||
|
server.authenticated = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert to plugin format
|
// Process tools efficiently - no need for convertMCPToolToPlugin
|
||||||
for (const [toolKey, toolData] of Object.entries(serverTools)) {
|
if (serverTools) {
|
||||||
const plugin = convertMCPToolToPlugin({
|
for (const [toolKey, toolData] of Object.entries(serverTools)) {
|
||||||
toolKey,
|
if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) {
|
||||||
toolData,
|
continue;
|
||||||
mcpManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (plugin) {
|
|
||||||
// Add authentication config from server config
|
|
||||||
const serverConfig = appConfig.mcpConfig[serverName];
|
|
||||||
if (serverConfig?.customUserVars) {
|
|
||||||
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
|
||||||
if (customVarKeys.length === 0) {
|
|
||||||
plugin.authConfig = [];
|
|
||||||
plugin.authenticated = true;
|
|
||||||
} else {
|
|
||||||
plugin.authConfig = Object.entries(serverConfig.customUserVars).map(
|
|
||||||
([key, value]) => ({
|
|
||||||
authField: key,
|
|
||||||
label: value.title || key,
|
|
||||||
description: value.description || '',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
plugin.authenticated = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
plugin.authConfig = [];
|
|
||||||
plugin.authenticated = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mcpTools.push(plugin);
|
const toolName = toolKey.split(Constants.mcp_delimiter)[0];
|
||||||
|
server.tools.push({
|
||||||
|
name: toolName,
|
||||||
|
pluginKey: toolKey,
|
||||||
|
description: toolData.function.description || '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only add server if it has tools or is configured
|
||||||
|
if (server.tools.length > 0 || serverConfig) {
|
||||||
|
mcpServers[serverName] = server;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[getMCPTools] Error loading tools for server ${serverName}:`, error);
|
logger.error(`[getMCPTools] Error loading tools for server ${serverName}:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json(mcpTools);
|
res.status(200).json({ servers: mcpServers });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[getMCPTools]', error);
|
logger.error('[getMCPTools]', error);
|
||||||
res.status(500).json({ message: error.message });
|
res.status(500).json({ message: error.message });
|
||||||
|
|
|
||||||
|
|
@ -1,561 +0,0 @@
|
||||||
const { getMCPTools } = require('./mcp');
|
|
||||||
const { getAppConfig, getMCPServerTools } = require('~/server/services/Config');
|
|
||||||
const { getMCPManager } = require('~/config');
|
|
||||||
const { convertMCPToolToPlugin } = require('@librechat/api');
|
|
||||||
|
|
||||||
jest.mock('@librechat/data-schemas', () => ({
|
|
||||||
logger: {
|
|
||||||
debug: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('librechat-data-provider', () => ({
|
|
||||||
Constants: {
|
|
||||||
mcp_delimiter: '~~~',
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('@librechat/api', () => ({
|
|
||||||
convertMCPToolToPlugin: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('~/server/services/Config', () => ({
|
|
||||||
getAppConfig: jest.fn(),
|
|
||||||
getMCPServerTools: jest.fn(),
|
|
||||||
cacheMCPServerTools: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('~/config', () => ({
|
|
||||||
getMCPManager: jest.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('MCP Controller', () => {
|
|
||||||
let mockReq, mockRes, mockMCPManager;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.clearAllMocks();
|
|
||||||
|
|
||||||
mockReq = {
|
|
||||||
user: { id: 'test-user-id', role: 'user' },
|
|
||||||
config: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
mockRes = {
|
|
||||||
status: jest.fn().mockReturnThis(),
|
|
||||||
json: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
mockMCPManager = {
|
|
||||||
getAllToolFunctions: jest.fn().mockResolvedValue({}),
|
|
||||||
};
|
|
||||||
|
|
||||||
getMCPManager.mockReturnValue(mockMCPManager);
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {},
|
|
||||||
});
|
|
||||||
getMCPServerTools.mockResolvedValue(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getMCPTools', () => {
|
|
||||||
it('should return 401 when user ID is not found', async () => {
|
|
||||||
mockReq.user = null;
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(401);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Unauthorized' });
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
expect(logger.warn).toHaveBeenCalledWith('[getMCPTools] User ID not found in request');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty array when no mcpConfig exists', async () => {
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
// No mcpConfig
|
|
||||||
});
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use cached server tools when available', async () => {
|
|
||||||
const cachedTools = {
|
|
||||||
'tool1~~~server1': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
getMCPServerTools.mockResolvedValue(cachedTools);
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockPlugin = {
|
|
||||||
name: 'Tool 1',
|
|
||||||
pluginKey: 'tool1~~~server1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
};
|
|
||||||
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
expect(getMCPServerTools).toHaveBeenCalledWith('server1');
|
|
||||||
expect(mockMCPManager.getAllToolFunctions).not.toHaveBeenCalled();
|
|
||||||
expect(convertMCPToolToPlugin).toHaveBeenCalledWith({
|
|
||||||
toolKey: 'tool1~~~server1',
|
|
||||||
toolData: cachedTools['tool1~~~server1'],
|
|
||||||
mcpManager: mockMCPManager,
|
|
||||||
});
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([
|
|
||||||
{
|
|
||||||
...mockPlugin,
|
|
||||||
authConfig: [],
|
|
||||||
authenticated: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fetch from MCP manager when cache is empty', async () => {
|
|
||||||
getMCPServerTools.mockResolvedValue(null);
|
|
||||||
|
|
||||||
const allTools = {
|
|
||||||
'tool1~~~server1': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'tool2~~~server2': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool2',
|
|
||||||
description: 'Tool 2',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mockMCPManager.getAllToolFunctions.mockResolvedValue(allTools);
|
|
||||||
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockPlugin = {
|
|
||||||
name: 'Tool 1',
|
|
||||||
pluginKey: 'tool1~~~server1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
};
|
|
||||||
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
expect(getMCPServerTools).toHaveBeenCalledWith('server1');
|
|
||||||
expect(mockMCPManager.getAllToolFunctions).toHaveBeenCalledWith('test-user-id');
|
|
||||||
|
|
||||||
// Should cache the server tools
|
|
||||||
const { cacheMCPServerTools } = require('~/server/services/Config');
|
|
||||||
expect(cacheMCPServerTools).toHaveBeenCalledWith({
|
|
||||||
serverName: 'server1',
|
|
||||||
serverTools: {
|
|
||||||
'tool1~~~server1': allTools['tool1~~~server1'],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([
|
|
||||||
{
|
|
||||||
...mockPlugin,
|
|
||||||
authConfig: [],
|
|
||||||
authenticated: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle custom user variables in server config', async () => {
|
|
||||||
getMCPServerTools.mockResolvedValue({
|
|
||||||
'tool1~~~server1': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {
|
|
||||||
customUserVars: {
|
|
||||||
API_KEY: {
|
|
||||||
title: 'API Key',
|
|
||||||
description: 'Your API key',
|
|
||||||
},
|
|
||||||
SECRET: {
|
|
||||||
title: 'Secret Token',
|
|
||||||
description: 'Your secret token',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockPlugin = {
|
|
||||||
name: 'Tool 1',
|
|
||||||
pluginKey: 'tool1~~~server1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
};
|
|
||||||
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([
|
|
||||||
{
|
|
||||||
...mockPlugin,
|
|
||||||
authConfig: [
|
|
||||||
{
|
|
||||||
authField: 'API_KEY',
|
|
||||||
label: 'API Key',
|
|
||||||
description: 'Your API key',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
authField: 'SECRET',
|
|
||||||
label: 'Secret Token',
|
|
||||||
description: 'Your secret token',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
authenticated: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty custom user variables', async () => {
|
|
||||||
getMCPServerTools.mockResolvedValue({
|
|
||||||
'tool1~~~server1': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {
|
|
||||||
customUserVars: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockPlugin = {
|
|
||||||
name: 'Tool 1',
|
|
||||||
pluginKey: 'tool1~~~server1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
};
|
|
||||||
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([
|
|
||||||
{
|
|
||||||
...mockPlugin,
|
|
||||||
authConfig: [],
|
|
||||||
authenticated: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle multiple servers', async () => {
|
|
||||||
getMCPServerTools.mockResolvedValue(null);
|
|
||||||
|
|
||||||
const allTools = {
|
|
||||||
'tool1~~~server1': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'tool2~~~server2': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool2',
|
|
||||||
description: 'Tool 2',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mockMCPManager.getAllToolFunctions.mockResolvedValue(allTools);
|
|
||||||
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {},
|
|
||||||
server2: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockPlugin1 = {
|
|
||||||
name: 'Tool 1',
|
|
||||||
pluginKey: 'tool1~~~server1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
};
|
|
||||||
const mockPlugin2 = {
|
|
||||||
name: 'Tool 2',
|
|
||||||
pluginKey: 'tool2~~~server2',
|
|
||||||
description: 'Tool 2',
|
|
||||||
};
|
|
||||||
|
|
||||||
convertMCPToolToPlugin.mockReturnValueOnce(mockPlugin1).mockReturnValueOnce(mockPlugin2);
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
expect(getMCPServerTools).toHaveBeenCalledWith('server1');
|
|
||||||
expect(getMCPServerTools).toHaveBeenCalledWith('server2');
|
|
||||||
expect(mockMCPManager.getAllToolFunctions).toHaveBeenCalledTimes(2);
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([
|
|
||||||
{
|
|
||||||
...mockPlugin1,
|
|
||||||
authConfig: [],
|
|
||||||
authenticated: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
...mockPlugin2,
|
|
||||||
authConfig: [],
|
|
||||||
authenticated: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle server-specific errors gracefully', async () => {
|
|
||||||
getMCPServerTools.mockResolvedValue(null);
|
|
||||||
mockMCPManager.getAllToolFunctions.mockRejectedValue(new Error('Server connection failed'));
|
|
||||||
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {},
|
|
||||||
server2: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
|
||||||
'[getMCPTools] Error loading tools for server server1:',
|
|
||||||
expect.any(Error),
|
|
||||||
);
|
|
||||||
expect(logger.error).toHaveBeenCalledWith(
|
|
||||||
'[getMCPTools] Error loading tools for server server2:',
|
|
||||||
expect.any(Error),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Should still return 200 with empty array
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip tools when convertMCPToolToPlugin returns null', async () => {
|
|
||||||
getMCPServerTools.mockResolvedValue({
|
|
||||||
'tool1~~~server1': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'tool2~~~server1': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool2',
|
|
||||||
description: 'Tool 2',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockPlugin = {
|
|
||||||
name: 'Tool 1',
|
|
||||||
pluginKey: 'tool1~~~server1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
};
|
|
||||||
|
|
||||||
// First tool returns plugin, second returns null
|
|
||||||
convertMCPToolToPlugin.mockReturnValueOnce(mockPlugin).mockReturnValueOnce(null);
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([
|
|
||||||
{
|
|
||||||
...mockPlugin,
|
|
||||||
authConfig: [],
|
|
||||||
authenticated: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use req.config when available', async () => {
|
|
||||||
const reqConfig = {
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
mockReq.config = reqConfig;
|
|
||||||
|
|
||||||
getMCPServerTools.mockResolvedValue({
|
|
||||||
'tool1~~~server1': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockPlugin = {
|
|
||||||
name: 'Tool 1',
|
|
||||||
pluginKey: 'tool1~~~server1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
};
|
|
||||||
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
// Should not call getAppConfig when req.config is available
|
|
||||||
expect(getAppConfig).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([
|
|
||||||
{
|
|
||||||
...mockPlugin,
|
|
||||||
authConfig: [],
|
|
||||||
authenticated: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle general error in getMCPTools', async () => {
|
|
||||||
const error = new Error('Unexpected error');
|
|
||||||
getAppConfig.mockRejectedValue(error);
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
|
||||||
expect(logger.error).toHaveBeenCalledWith('[getMCPTools]', error);
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith({ message: 'Unexpected error' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle custom user variables without title or description', async () => {
|
|
||||||
getMCPServerTools.mockResolvedValue({
|
|
||||||
'tool1~~~server1': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {
|
|
||||||
customUserVars: {
|
|
||||||
MY_VAR: {
|
|
||||||
// No title or description
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockPlugin = {
|
|
||||||
name: 'Tool 1',
|
|
||||||
pluginKey: 'tool1~~~server1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
};
|
|
||||||
convertMCPToolToPlugin.mockReturnValue(mockPlugin);
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([
|
|
||||||
{
|
|
||||||
...mockPlugin,
|
|
||||||
authConfig: [
|
|
||||||
{
|
|
||||||
authField: 'MY_VAR',
|
|
||||||
label: 'MY_VAR', // Falls back to key
|
|
||||||
description: '', // Empty string
|
|
||||||
},
|
|
||||||
],
|
|
||||||
authenticated: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not cache when no tools are found for a server', async () => {
|
|
||||||
getMCPServerTools.mockResolvedValue(null);
|
|
||||||
|
|
||||||
const allTools = {
|
|
||||||
'tool1~~~otherserver': {
|
|
||||||
type: 'function',
|
|
||||||
function: {
|
|
||||||
name: 'tool1',
|
|
||||||
description: 'Tool 1',
|
|
||||||
parameters: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
mockMCPManager.getAllToolFunctions.mockResolvedValue(allTools);
|
|
||||||
|
|
||||||
getAppConfig.mockResolvedValue({
|
|
||||||
mcpConfig: {
|
|
||||||
server1: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await getMCPTools(mockReq, mockRes);
|
|
||||||
|
|
||||||
const { cacheMCPServerTools } = require('~/server/services/Config');
|
|
||||||
expect(cacheMCPServerTools).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
expect(mockRes.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(mockRes.json).toHaveBeenCalledWith([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { createContext, useContext, useState, useMemo } from 'react';
|
import React, { createContext, useContext, useState, useMemo } from 'react';
|
||||||
import { Constants, EModelEndpoint } from 'librechat-data-provider';
|
import { EModelEndpoint } from 'librechat-data-provider';
|
||||||
import type { MCP, Action, TPlugin } from 'librechat-data-provider';
|
import type { MCP, Action, TPlugin } from 'librechat-data-provider';
|
||||||
import type { AgentPanelContextType, MCPServerInfo } from '~/common';
|
import type { AgentPanelContextType, MCPServerInfo } from '~/common';
|
||||||
import {
|
import {
|
||||||
|
|
@ -30,6 +30,7 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
||||||
const [activePanel, setActivePanel] = useState<Panel>(Panel.builder);
|
const [activePanel, setActivePanel] = useState<Panel>(Panel.builder);
|
||||||
const [agent_id, setCurrentAgentId] = useState<string | undefined>(undefined);
|
const [agent_id, setCurrentAgentId] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
const { data: actions } = useGetActionsQuery(EModelEndpoint.agents, {
|
const { data: actions } = useGetActionsQuery(EModelEndpoint.agents, {
|
||||||
enabled: !!agent_id,
|
enabled: !!agent_id,
|
||||||
});
|
});
|
||||||
|
|
@ -38,11 +39,10 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
||||||
enabled: !!agent_id,
|
enabled: !!agent_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: mcpTools } = useMCPToolsQuery({
|
const { data: mcpData } = useMCPToolsQuery({
|
||||||
enabled: !!agent_id,
|
enabled: !!agent_id && startupConfig?.mcpServers != null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
|
||||||
const { agentsConfig, endpointsConfig } = useGetAgentsConfig();
|
const { agentsConfig, endpointsConfig } = useGetAgentsConfig();
|
||||||
const mcpServerNames = useMemo(
|
const mcpServerNames = useMemo(
|
||||||
() => Object.keys(startupConfig?.mcpServers ?? {}),
|
() => Object.keys(startupConfig?.mcpServers ?? {}),
|
||||||
|
|
@ -57,33 +57,34 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
||||||
const configuredServers = new Set(mcpServerNames);
|
const configuredServers = new Set(mcpServerNames);
|
||||||
const serversMap = new Map<string, MCPServerInfo>();
|
const serversMap = new Map<string, MCPServerInfo>();
|
||||||
|
|
||||||
if (mcpTools) {
|
if (mcpData?.servers) {
|
||||||
for (const pluginTool of mcpTools) {
|
for (const [serverName, serverData] of Object.entries(mcpData.servers)) {
|
||||||
if (pluginTool.pluginKey.includes(Constants.mcp_delimiter)) {
|
const metadata = {
|
||||||
const [_toolName, serverName] = pluginTool.pluginKey.split(Constants.mcp_delimiter);
|
name: serverName,
|
||||||
|
pluginKey: serverName,
|
||||||
|
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
||||||
|
icon: serverData.icon || '',
|
||||||
|
authConfig: serverData.authConfig,
|
||||||
|
authenticated: serverData.authenticated,
|
||||||
|
} as TPlugin;
|
||||||
|
|
||||||
if (!serversMap.has(serverName)) {
|
const tools = serverData.tools.map((tool) => ({
|
||||||
const metadata = {
|
tool_id: tool.pluginKey,
|
||||||
name: serverName,
|
metadata: {
|
||||||
pluginKey: serverName,
|
...tool,
|
||||||
description: `${localize('com_ui_tool_collection_prefix')} ${serverName}`,
|
icon: serverData.icon,
|
||||||
icon: pluginTool.icon || '',
|
authConfig: serverData.authConfig,
|
||||||
} as TPlugin;
|
authenticated: serverData.authenticated,
|
||||||
|
} as TPlugin,
|
||||||
|
}));
|
||||||
|
|
||||||
serversMap.set(serverName, {
|
serversMap.set(serverName, {
|
||||||
serverName,
|
serverName,
|
||||||
tools: [],
|
tools,
|
||||||
isConfigured: configuredServers.has(serverName),
|
isConfigured: configuredServers.has(serverName),
|
||||||
isConnected: connectionStatus?.[serverName]?.connectionState === 'connected',
|
isConnected: connectionStatus?.[serverName]?.connectionState === 'connected',
|
||||||
metadata,
|
metadata,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
serversMap.get(serverName)!.tools.push({
|
|
||||||
tool_id: pluginTool.pluginKey,
|
|
||||||
metadata: pluginTool as TPlugin,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,7 +110,7 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
||||||
}
|
}
|
||||||
|
|
||||||
return serversMap;
|
return serversMap;
|
||||||
}, [mcpTools, localize, mcpServerNames, connectionStatus]);
|
}, [mcpData, localize, mcpServerNames, connectionStatus]);
|
||||||
|
|
||||||
const value: AgentPanelContextType = {
|
const value: AgentPanelContextType = {
|
||||||
mcp,
|
mcp,
|
||||||
|
|
@ -120,7 +121,6 @@ export function AgentPanelProvider({ children }: { children: React.ReactNode })
|
||||||
setMcps,
|
setMcps,
|
||||||
agent_id,
|
agent_id,
|
||||||
setAction,
|
setAction,
|
||||||
mcpTools,
|
|
||||||
activePanel,
|
activePanel,
|
||||||
regularTools,
|
regularTools,
|
||||||
agentsConfig,
|
agentsConfig,
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,6 @@ export type AgentPanelContextType = {
|
||||||
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
setMcps: React.Dispatch<React.SetStateAction<t.MCP[] | undefined>>;
|
||||||
activePanel?: string;
|
activePanel?: string;
|
||||||
regularTools?: t.TPlugin[];
|
regularTools?: t.TPlugin[];
|
||||||
mcpTools?: t.TPlugin[];
|
|
||||||
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
setActivePanel: React.Dispatch<React.SetStateAction<Panel>>;
|
||||||
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
setCurrentAgentId: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
agent_id?: string;
|
agent_id?: string;
|
||||||
|
|
|
||||||
|
|
@ -473,13 +473,15 @@ export default function AgentConfig({ createMutation }: Pick<AgentPanelProps, 'c
|
||||||
setIsOpen={setShowToolDialog}
|
setIsOpen={setShowToolDialog}
|
||||||
endpoint={EModelEndpoint.agents}
|
endpoint={EModelEndpoint.agents}
|
||||||
/>
|
/>
|
||||||
<MCPToolSelectDialog
|
{startupConfig?.mcpServers != null && (
|
||||||
agentId={agent_id}
|
<MCPToolSelectDialog
|
||||||
isOpen={showMCPToolDialog}
|
agentId={agent_id}
|
||||||
mcpServerNames={mcpServerNames}
|
isOpen={showMCPToolDialog}
|
||||||
setIsOpen={setShowMCPToolDialog}
|
mcpServerNames={mcpServerNames}
|
||||||
endpoint={EModelEndpoint.agents}
|
setIsOpen={setShowMCPToolDialog}
|
||||||
/>
|
endpoint={EModelEndpoint.agents}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ import type { TError, AgentToolType } from 'librechat-data-provider';
|
||||||
import type { AgentForm, TPluginStoreDialogProps } from '~/common';
|
import type { AgentForm, TPluginStoreDialogProps } from '~/common';
|
||||||
import { useLocalize, usePluginDialogHelpers, useMCPServerManager } from '~/hooks';
|
import { useLocalize, usePluginDialogHelpers, useMCPServerManager } from '~/hooks';
|
||||||
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
|
import CustomUserVarsSection from '~/components/MCP/CustomUserVarsSection';
|
||||||
import { useGetStartupConfig, useMCPToolsQuery } from '~/data-provider';
|
|
||||||
import { PluginPagination } from '~/components/Plugins/Store';
|
import { PluginPagination } from '~/components/Plugins/Store';
|
||||||
import { useAgentPanelContext } from '~/Providers';
|
import { useAgentPanelContext } from '~/Providers';
|
||||||
|
import { useMCPToolsQuery } from '~/data-provider';
|
||||||
import MCPToolItem from './MCPToolItem';
|
import MCPToolItem from './MCPToolItem';
|
||||||
|
|
||||||
function MCPToolSelectDialog({
|
function MCPToolSelectDialog({
|
||||||
|
|
@ -24,11 +24,12 @@ function MCPToolSelectDialog({
|
||||||
endpoint: EModelEndpoint.agents;
|
endpoint: EModelEndpoint.agents;
|
||||||
}) {
|
}) {
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const { mcpServersMap } = useAgentPanelContext();
|
|
||||||
const { initializeServer } = useMCPServerManager();
|
const { initializeServer } = useMCPServerManager();
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
|
||||||
const { refetch: refetchMCPTools } = useMCPToolsQuery();
|
|
||||||
const { getValues, setValue } = useFormContext<AgentForm>();
|
const { getValues, setValue } = useFormContext<AgentForm>();
|
||||||
|
const { mcpServersMap, startupConfig } = useAgentPanelContext();
|
||||||
|
const { refetch: refetchMCPTools } = useMCPToolsQuery({
|
||||||
|
enabled: mcpServersMap.size > 0,
|
||||||
|
});
|
||||||
|
|
||||||
const [isInitializing, setIsInitializing] = useState<string | null>(null);
|
const [isInitializing, setIsInitializing] = useState<string | null>(null);
|
||||||
const [configuringServer, setConfiguringServer] = useState<string | null>(null);
|
const [configuringServer, setConfiguringServer] = useState<string | null>(null);
|
||||||
|
|
@ -90,18 +91,17 @@ function MCPToolSelectDialog({
|
||||||
setIsInitializing(null);
|
setIsInitializing(null);
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
const { data: updatedMCPTools } = await refetchMCPTools();
|
const { data: updatedMCPData } = await refetchMCPTools();
|
||||||
|
|
||||||
const currentTools = getValues('tools') || [];
|
const currentTools = getValues('tools') || [];
|
||||||
const toolsToAdd: string[] = [
|
const toolsToAdd: string[] = [
|
||||||
`${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`,
|
`${Constants.mcp_server}${Constants.mcp_delimiter}${serverName}`,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (updatedMCPTools) {
|
if (updatedMCPData?.servers?.[serverName]) {
|
||||||
updatedMCPTools.forEach((tool) => {
|
const serverData = updatedMCPData.servers[serverName];
|
||||||
if (tool.pluginKey.endsWith(`${Constants.mcp_delimiter}${serverName}`)) {
|
serverData.tools.forEach((tool) => {
|
||||||
toolsToAdd.push(tool.pluginKey);
|
toolsToAdd.push(tool.pluginKey);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,17 +5,17 @@
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { QueryKeys, dataService } from 'librechat-data-provider';
|
import { QueryKeys, dataService } from 'librechat-data-provider';
|
||||||
import type { UseQueryOptions, QueryObserverResult } from '@tanstack/react-query';
|
import type { UseQueryOptions, QueryObserverResult } from '@tanstack/react-query';
|
||||||
import type { TPlugin } from 'librechat-data-provider';
|
import type { MCPServersResponse } from 'librechat-data-provider';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hook for fetching MCP-specific tools
|
* Hook for fetching MCP-specific tools
|
||||||
* @param config - React Query configuration
|
* @param config - React Query configuration
|
||||||
* @returns MCP tools grouped by server
|
* @returns MCP servers with their tools
|
||||||
*/
|
*/
|
||||||
export const useMCPToolsQuery = <TData = TPlugin[]>(
|
export const useMCPToolsQuery = <TData = MCPServersResponse>(
|
||||||
config?: UseQueryOptions<TPlugin[], unknown, TData>,
|
config?: UseQueryOptions<MCPServersResponse, unknown, TData>,
|
||||||
): QueryObserverResult<TData> => {
|
): QueryObserverResult<TData> => {
|
||||||
return useQuery<TPlugin[], unknown, TData>(
|
return useQuery<MCPServersResponse, unknown, TData>(
|
||||||
[QueryKeys.mcpTools],
|
[QueryKeys.mcpTools],
|
||||||
() => dataService.getMCPTools(),
|
() => dataService.getMCPTools(),
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import type { TStartupConfig, TPlugin, TUser } from 'librechat-data-provider';
|
||||||
import { mapPlugins, selectPlugins, processPlugins } from '~/utils';
|
import { mapPlugins, selectPlugins, processPlugins } from '~/utils';
|
||||||
import { cleanupTimestampedStorage } from '~/utils/timestamps';
|
import { cleanupTimestampedStorage } from '~/utils/timestamps';
|
||||||
import useSpeechSettingsInit from './useSpeechSettingsInit';
|
import useSpeechSettingsInit from './useSpeechSettingsInit';
|
||||||
|
import { useMCPToolsQuery } from '~/data-provider';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const pluginStore: TPlugin = {
|
const pluginStore: TPlugin = {
|
||||||
|
|
@ -35,6 +36,10 @@ export default function useAppStartup({
|
||||||
|
|
||||||
useSpeechSettingsInit(!!user);
|
useSpeechSettingsInit(!!user);
|
||||||
|
|
||||||
|
useMCPToolsQuery({
|
||||||
|
enabled: !!startupConfig?.mcpServers && !!user,
|
||||||
|
});
|
||||||
|
|
||||||
/** Clean up old localStorage entries on startup */
|
/** Clean up old localStorage entries on startup */
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
cleanupTimestampedStorage();
|
cleanupTimestampedStorage();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
export * from './useGetMCPTools';
|
|
||||||
export * from './useMCPConnectionStatus';
|
export * from './useMCPConnectionStatus';
|
||||||
export * from './useMCPSelect';
|
export * from './useMCPSelect';
|
||||||
export * from './useVisibleTools';
|
export * from './useVisibleTools';
|
||||||
|
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { Constants } from 'librechat-data-provider';
|
|
||||||
import type { TPlugin } from 'librechat-data-provider';
|
|
||||||
import { useMCPToolsQuery, useGetStartupConfig } from '~/data-provider';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook for fetching and filtering MCP tools based on server configuration
|
|
||||||
* Uses the dedicated MCP tools query instead of filtering from general tools
|
|
||||||
*/
|
|
||||||
export function useGetMCPTools() {
|
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
|
||||||
|
|
||||||
// Use dedicated MCP tools query
|
|
||||||
const { data: rawMcpTools } = useMCPToolsQuery({
|
|
||||||
select: (data: TPlugin[]) => {
|
|
||||||
// Group tools by server for easier management
|
|
||||||
const mcpToolsMap = new Map<string, TPlugin>();
|
|
||||||
data.forEach((tool) => {
|
|
||||||
const parts = tool.pluginKey.split(Constants.mcp_delimiter);
|
|
||||||
const serverName = parts[parts.length - 1];
|
|
||||||
if (!mcpToolsMap.has(serverName)) {
|
|
||||||
mcpToolsMap.set(serverName, {
|
|
||||||
name: serverName,
|
|
||||||
pluginKey: tool.pluginKey,
|
|
||||||
authConfig: tool.authConfig,
|
|
||||||
authenticated: tool.authenticated,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Array.from(mcpToolsMap.values());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Filter out servers that have chatMenu disabled
|
|
||||||
const mcpToolDetails = useMemo(() => {
|
|
||||||
if (!rawMcpTools || !startupConfig?.mcpServers) {
|
|
||||||
return rawMcpTools;
|
|
||||||
}
|
|
||||||
return rawMcpTools.filter((tool) => {
|
|
||||||
const serverConfig = startupConfig?.mcpServers?.[tool.name];
|
|
||||||
return serverConfig?.chatMenu !== false;
|
|
||||||
});
|
|
||||||
}, [rawMcpTools, startupConfig?.mcpServers]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
mcpToolDetails,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -7,9 +7,9 @@ import {
|
||||||
useUpdateUserPluginsMutation,
|
useUpdateUserPluginsMutation,
|
||||||
useReinitializeMCPServerMutation,
|
useReinitializeMCPServerMutation,
|
||||||
} from 'librechat-data-provider/react-query';
|
} from 'librechat-data-provider/react-query';
|
||||||
import type { TUpdateUserPlugins, TPlugin } from 'librechat-data-provider';
|
import type { TUpdateUserPlugins, TPlugin, MCPServersResponse } from 'librechat-data-provider';
|
||||||
import type { ConfigFieldDetail } from '~/common';
|
import type { ConfigFieldDetail } from '~/common';
|
||||||
import { useLocalize, useMCPSelect, useGetMCPTools, useMCPConnectionStatus } from '~/hooks';
|
import { useLocalize, useMCPSelect, useMCPConnectionStatus } from '~/hooks';
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
|
|
||||||
interface ServerState {
|
interface ServerState {
|
||||||
|
|
@ -24,7 +24,6 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
const localize = useLocalize();
|
const localize = useLocalize();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { showToast } = useToastContext();
|
const { showToast } = useToastContext();
|
||||||
const { mcpToolDetails } = useGetMCPTools();
|
|
||||||
const { data: startupConfig } = useGetStartupConfig();
|
const { data: startupConfig } = useGetStartupConfig();
|
||||||
const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({ conversationId });
|
const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({ conversationId });
|
||||||
|
|
||||||
|
|
@ -448,7 +447,10 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
|
|
||||||
const getServerStatusIconProps = useCallback(
|
const getServerStatusIconProps = useCallback(
|
||||||
(serverName: string) => {
|
(serverName: string) => {
|
||||||
const tool = mcpToolDetails?.find((t) => t.name === serverName);
|
const mcpData = queryClient.getQueryData<MCPServersResponse | undefined>([
|
||||||
|
QueryKeys.mcpTools,
|
||||||
|
]);
|
||||||
|
const serverData = mcpData?.servers?.[serverName];
|
||||||
const serverStatus = connectionStatus?.[serverName];
|
const serverStatus = connectionStatus?.[serverName];
|
||||||
const serverConfig = startupConfig?.mcpServers?.[serverName];
|
const serverConfig = startupConfig?.mcpServers?.[serverName];
|
||||||
|
|
||||||
|
|
@ -458,17 +460,20 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
|
|
||||||
previousFocusRef.current = document.activeElement as HTMLElement;
|
previousFocusRef.current = document.activeElement as HTMLElement;
|
||||||
|
|
||||||
const configTool = tool || {
|
/** Minimal TPlugin object for the config dialog */
|
||||||
|
const configTool: TPlugin = {
|
||||||
name: serverName,
|
name: serverName,
|
||||||
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
authConfig: serverConfig?.customUserVars
|
authConfig:
|
||||||
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
|
serverData?.authConfig ||
|
||||||
authField: key,
|
(serverConfig?.customUserVars
|
||||||
label: config.title,
|
? Object.entries(serverConfig.customUserVars).map(([key, config]) => ({
|
||||||
description: config.description,
|
authField: key,
|
||||||
}))
|
label: config.title,
|
||||||
: [],
|
description: config.description,
|
||||||
authenticated: false,
|
}))
|
||||||
|
: []),
|
||||||
|
authenticated: serverData?.authenticated ?? false,
|
||||||
};
|
};
|
||||||
setSelectedToolForConfig(configTool);
|
setSelectedToolForConfig(configTool);
|
||||||
setIsConfigModalOpen(true);
|
setIsConfigModalOpen(true);
|
||||||
|
|
@ -486,7 +491,14 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
return {
|
return {
|
||||||
serverName,
|
serverName,
|
||||||
serverStatus,
|
serverStatus,
|
||||||
tool,
|
tool: serverData
|
||||||
|
? ({
|
||||||
|
name: serverName,
|
||||||
|
pluginKey: `${Constants.mcp_prefix}${serverName}`,
|
||||||
|
icon: serverData.icon,
|
||||||
|
authenticated: serverData.authenticated,
|
||||||
|
} as TPlugin)
|
||||||
|
: undefined,
|
||||||
onConfigClick: handleConfigClick,
|
onConfigClick: handleConfigClick,
|
||||||
isInitializing: isInitializing(serverName),
|
isInitializing: isInitializing(serverName),
|
||||||
canCancel: isCancellable(serverName),
|
canCancel: isCancellable(serverName),
|
||||||
|
|
@ -495,8 +507,8 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
queryClient,
|
||||||
isCancellable,
|
isCancellable,
|
||||||
mcpToolDetails,
|
|
||||||
isInitializing,
|
isInitializing,
|
||||||
cancelOAuthFlow,
|
cancelOAuthFlow,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import { ConnectionsRepository } from '~/mcp/ConnectionsRepository';
|
||||||
import { formatToolContent } from './parsers';
|
import { formatToolContent } from './parsers';
|
||||||
import { MCPConnection } from './connection';
|
import { MCPConnection } from './connection';
|
||||||
import { processMCPEnv } from '~/utils/env';
|
import { processMCPEnv } from '~/utils/env';
|
||||||
import { CONSTANTS } from './enum';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Centralized manager for MCP server connections and tool execution.
|
* Centralized manager for MCP server connections and tool execution.
|
||||||
|
|
@ -78,6 +77,28 @@ export class MCPManager extends UserConnectionManager {
|
||||||
|
|
||||||
return allToolFunctions;
|
return allToolFunctions;
|
||||||
}
|
}
|
||||||
|
/** Returns all available tool functions from all connections available to user */
|
||||||
|
public async getServerToolFunctions(
|
||||||
|
userId: string,
|
||||||
|
serverName: string,
|
||||||
|
): Promise<t.LCAvailableTools | null> {
|
||||||
|
if (this.appConnections?.has(serverName)) {
|
||||||
|
return this.serversRegistry.getToolFunctions(
|
||||||
|
serverName,
|
||||||
|
await this.appConnections.get(serverName),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userConnections = this.getUserConnections(userId);
|
||||||
|
if (!userConnections || userConnections.size === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!userConnections.has(serverName)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.serversRegistry.getToolFunctions(serverName, userConnections.get(serverName)!);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get instructions for MCP servers
|
* Get instructions for MCP servers
|
||||||
|
|
@ -121,72 +142,6 @@ ${formattedInstructions}
|
||||||
Please follow these instructions when using tools from the respective MCP servers.`;
|
Please follow these instructions when using tools from the respective MCP servers.`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async loadAppManifestTools(): Promise<t.LCManifestTool[]> {
|
|
||||||
const connections = await this.appConnections!.getAll();
|
|
||||||
return await this.loadManifestTools(connections);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async loadUserManifestTools(userId: string): Promise<t.LCManifestTool[]> {
|
|
||||||
const connections = this.getUserConnections(userId);
|
|
||||||
return await this.loadManifestTools(connections);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async loadAllManifestTools(userId: string): Promise<t.LCManifestTool[]> {
|
|
||||||
const appTools = await this.loadAppManifestTools();
|
|
||||||
const userTools = await this.loadUserManifestTools(userId);
|
|
||||||
return [...appTools, ...userTools];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Loads tools from all app-level connections into the manifest. */
|
|
||||||
private async loadManifestTools(
|
|
||||||
connections?: Map<string, MCPConnection> | null,
|
|
||||||
): Promise<t.LCToolManifest> {
|
|
||||||
const mcpTools: t.LCManifestTool[] = [];
|
|
||||||
if (!connections || connections.size === 0) {
|
|
||||||
return mcpTools;
|
|
||||||
}
|
|
||||||
for (const [serverName, connection] of connections.entries()) {
|
|
||||||
try {
|
|
||||||
if (!(await connection.isConnected())) {
|
|
||||||
logger.warn(
|
|
||||||
`[MCP][${serverName}] Connection not available for ${serverName} manifest tools.`,
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tools = await connection.fetchTools();
|
|
||||||
const serverTools: t.LCManifestTool[] = [];
|
|
||||||
for (const tool of tools) {
|
|
||||||
const pluginKey = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`;
|
|
||||||
|
|
||||||
const config = this.serversRegistry.parsedConfigs[serverName];
|
|
||||||
const manifestTool: t.LCManifestTool = {
|
|
||||||
name: tool.name,
|
|
||||||
pluginKey,
|
|
||||||
description: tool.description ?? '',
|
|
||||||
icon: connection.iconPath,
|
|
||||||
authConfig: config?.customUserVars
|
|
||||||
? Object.entries(config.customUserVars).map(([key, value]) => ({
|
|
||||||
authField: key,
|
|
||||||
label: value.title || key,
|
|
||||||
description: value.description || '',
|
|
||||||
}))
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
if (config?.chatMenu === false) {
|
|
||||||
manifestTool.chatMenu = false;
|
|
||||||
}
|
|
||||||
mcpTools.push(manifestTool);
|
|
||||||
serverTools.push(manifestTool);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[MCP][${serverName}] Error fetching tools for manifest:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mcpTools;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls a tool on an MCP server, using either a user-specific connection
|
* Calls a tool on an MCP server, using either a user-specific connection
|
||||||
* (if userId is provided) or an app-level connection. Updates the last activity timestamp
|
* (if userId is provided) or an app-level connection. Updates the last activity timestamp
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import pick from 'lodash/pick';
|
||||||
import pickBy from 'lodash/pickBy';
|
import pickBy from 'lodash/pickBy';
|
||||||
import mapValues from 'lodash/mapValues';
|
import mapValues from 'lodash/mapValues';
|
||||||
import { logger } from '@librechat/data-schemas';
|
import { logger } from '@librechat/data-schemas';
|
||||||
|
import { Constants } from 'librechat-data-provider';
|
||||||
import type { MCPConnection } from '~/mcp/connection';
|
import type { MCPConnection } from '~/mcp/connection';
|
||||||
import type { JsonSchemaType } from '~/types';
|
import type { JsonSchemaType } from '~/types';
|
||||||
import type * as t from '~/mcp/types';
|
import type * as t from '~/mcp/types';
|
||||||
|
|
@ -9,7 +10,6 @@ import { ConnectionsRepository } from '~/mcp/ConnectionsRepository';
|
||||||
import { detectOAuthRequirement } from '~/mcp/oauth';
|
import { detectOAuthRequirement } from '~/mcp/oauth';
|
||||||
import { sanitizeUrlForLogging } from '~/mcp/utils';
|
import { sanitizeUrlForLogging } from '~/mcp/utils';
|
||||||
import { processMCPEnv, isEnabled } from '~/utils';
|
import { processMCPEnv, isEnabled } from '~/utils';
|
||||||
import { CONSTANTS } from '~/mcp/enum';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages MCP server configurations and metadata discovery.
|
* Manages MCP server configurations and metadata discovery.
|
||||||
|
|
@ -127,7 +127,7 @@ export class MCPServersRegistry {
|
||||||
|
|
||||||
const toolFunctions: t.LCAvailableTools = {};
|
const toolFunctions: t.LCAvailableTools = {};
|
||||||
tools.forEach((tool) => {
|
tools.forEach((tool) => {
|
||||||
const name = `${tool.name}${CONSTANTS.mcp_delimiter}${serverName}`;
|
const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`;
|
||||||
toolFunctions[name] = {
|
toolFunctions[name] = {
|
||||||
type: 'function',
|
type: 'function',
|
||||||
['function']: {
|
['function']: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
export enum CONSTANTS {
|
export enum CONSTANTS {
|
||||||
mcp_delimiter = '_mcp_',
|
|
||||||
/** System user ID for app-level OAuth tokens (all zeros ObjectId) */
|
/** System user ID for app-level OAuth tokens (all zeros ObjectId) */
|
||||||
SYSTEM_USER_ID = '000000000000000000000000',
|
SYSTEM_USER_ID = '000000000000000000000000',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,6 @@
|
||||||
import { AuthType, Constants, EToolResources } from 'librechat-data-provider';
|
import { AuthType, EToolResources } from 'librechat-data-provider';
|
||||||
import type { TPlugin, FunctionTool } from 'librechat-data-provider';
|
import type { TPlugin } from 'librechat-data-provider';
|
||||||
import type { MCPManager } from '~/mcp/MCPManager';
|
import { filterUniquePlugins, checkPluginAuth, getToolkitKey } from './format';
|
||||||
import {
|
|
||||||
convertMCPToolsToPlugins,
|
|
||||||
filterUniquePlugins,
|
|
||||||
checkPluginAuth,
|
|
||||||
getToolkitKey,
|
|
||||||
} from './format';
|
|
||||||
|
|
||||||
describe('format.ts helper functions', () => {
|
describe('format.ts helper functions', () => {
|
||||||
describe('filterUniquePlugins', () => {
|
describe('filterUniquePlugins', () => {
|
||||||
|
|
@ -197,212 +191,6 @@ describe('format.ts helper functions', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('convertMCPToolsToPlugins', () => {
|
|
||||||
it('should return undefined when functionTools is undefined', () => {
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools: undefined });
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when functionTools is not an object', () => {
|
|
||||||
const result = convertMCPToolsToPlugins({
|
|
||||||
functionTools: 'not-an-object' as unknown as Record<string, FunctionTool>,
|
|
||||||
});
|
|
||||||
expect(result).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty array when functionTools is empty object', () => {
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools: {} });
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip entries without function property', () => {
|
|
||||||
const functionTools: Record<string, FunctionTool> = {
|
|
||||||
tool1: { type: 'function' } as FunctionTool,
|
|
||||||
tool2: { function: { name: 'tool2', description: 'Tool 2' } } as FunctionTool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools });
|
|
||||||
expect(result).toHaveLength(0); // tool2 doesn't have mcp_delimiter in key
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip entries without mcp_delimiter in key', () => {
|
|
||||||
const functionTools: Record<string, FunctionTool> = {
|
|
||||||
'regular-tool': {
|
|
||||||
type: 'function',
|
|
||||||
function: { name: 'regular-tool', description: 'Regular tool' },
|
|
||||||
} as FunctionTool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools });
|
|
||||||
expect(result).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should convert MCP tools to plugins correctly', () => {
|
|
||||||
const functionTools: Record<string, FunctionTool> = {
|
|
||||||
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
||||||
type: 'function',
|
|
||||||
function: { name: 'tool1', description: 'Tool 1 description' },
|
|
||||||
} as FunctionTool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools });
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result![0]).toEqual({
|
|
||||||
name: 'tool1',
|
|
||||||
pluginKey: `tool1${Constants.mcp_delimiter}server1`,
|
|
||||||
description: 'Tool 1 description',
|
|
||||||
authenticated: true,
|
|
||||||
icon: undefined,
|
|
||||||
authConfig: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle missing description', () => {
|
|
||||||
const functionTools: Record<string, FunctionTool> = {
|
|
||||||
[`tool1${Constants.mcp_delimiter}server1`]: {
|
|
||||||
type: 'function',
|
|
||||||
function: { name: 'tool1' },
|
|
||||||
} as FunctionTool,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools });
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result![0].description).toBe('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add icon from server config', () => {
|
|
||||||
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({
|
|
||||||
command: 'test',
|
|
||||||
args: [],
|
|
||||||
iconPath: '/path/to/icon.png',
|
|
||||||
}),
|
|
||||||
} as unknown as MCPManager;
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result![0].icon).toBe('/path/to/icon.png');
|
|
||||||
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle customUserVars in server config', () => {
|
|
||||||
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({
|
|
||||||
command: 'test',
|
|
||||||
args: [],
|
|
||||||
customUserVars: {
|
|
||||||
API_KEY: { title: 'API Key', description: 'Your API key' },
|
|
||||||
SECRET: { title: 'Secret', description: 'Your secret' },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
} as unknown as MCPManager;
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result![0].authConfig).toHaveLength(2);
|
|
||||||
expect(result![0].authConfig).toEqual([
|
|
||||||
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
|
||||||
{ authField: 'SECRET', label: 'Secret', description: 'Your secret' },
|
|
||||||
]);
|
|
||||||
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should use key as label when title is missing in customUserVars', () => {
|
|
||||||
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({
|
|
||||||
command: 'test',
|
|
||||||
args: [],
|
|
||||||
customUserVars: {
|
|
||||||
API_KEY: { title: 'API Key', description: 'Your API key' },
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
} as unknown as MCPManager;
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
expect(result![0].authConfig).toEqual([
|
|
||||||
{ authField: 'API_KEY', label: 'API Key', description: 'Your API key' },
|
|
||||||
]);
|
|
||||||
expect(mockMcpManager.getRawConfig).toHaveBeenCalledWith('server1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle empty customUserVars', () => {
|
|
||||||
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({
|
|
||||||
command: 'test',
|
|
||||||
args: [],
|
|
||||||
customUserVars: {},
|
|
||||||
}),
|
|
||||||
} as unknown as MCPManager;
|
|
||||||
|
|
||||||
const result = convertMCPToolsToPlugins({ functionTools, mcpManager: mockMcpManager });
|
|
||||||
expect(result).toHaveLength(1);
|
|
||||||
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');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getToolkitKey', () => {
|
describe('getToolkitKey', () => {
|
||||||
it('should return undefined when toolName is undefined', () => {
|
it('should return undefined when toolName is undefined', () => {
|
||||||
const toolkits: TPlugin[] = [
|
const toolkits: TPlugin[] = [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { AuthType, Constants, EToolResources } from 'librechat-data-provider';
|
import { AuthType, EToolResources } from 'librechat-data-provider';
|
||||||
import type { TPlugin } from 'librechat-data-provider';
|
import type { TPlugin } from 'librechat-data-provider';
|
||||||
import type { MCPManager } from '~/mcp/MCPManager';
|
|
||||||
import { LCAvailableTools, LCFunctionTool } from '~/mcp/types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Filters out duplicate plugins from the list of plugins.
|
* Filters out duplicate plugins from the list of plugins.
|
||||||
|
|
@ -48,90 +46,6 @@ export const checkPluginAuth = (plugin?: TPlugin): boolean => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts MCP function format tool to plugin format
|
|
||||||
* @param params
|
|
||||||
* @param params.toolKey
|
|
||||||
* @param params.toolData
|
|
||||||
* @param params.customConfig
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export function convertMCPToolToPlugin({
|
|
||||||
toolKey,
|
|
||||||
toolData,
|
|
||||||
mcpManager,
|
|
||||||
}: {
|
|
||||||
toolKey: string;
|
|
||||||
toolData: LCFunctionTool;
|
|
||||||
mcpManager?: MCPManager;
|
|
||||||
}): TPlugin | undefined {
|
|
||||||
if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const functionData = toolData.function;
|
|
||||||
const parts = toolKey.split(Constants.mcp_delimiter);
|
|
||||||
const serverName = parts[parts.length - 1];
|
|
||||||
|
|
||||||
const serverConfig = mcpManager?.getRawConfig(serverName);
|
|
||||||
|
|
||||||
const plugin: TPlugin = {
|
|
||||||
/** Tool name without server suffix */
|
|
||||||
name: parts[0],
|
|
||||||
pluginKey: toolKey,
|
|
||||||
description: functionData.description || '',
|
|
||||||
authenticated: true,
|
|
||||||
icon: serverConfig?.iconPath,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!serverConfig?.customUserVars) {
|
|
||||||
/** `authConfig` for MCP tools */
|
|
||||||
plugin.authConfig = [];
|
|
||||||
return plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customVarKeys = Object.keys(serverConfig.customUserVars);
|
|
||||||
if (customVarKeys.length === 0) {
|
|
||||||
plugin.authConfig = [];
|
|
||||||
} else {
|
|
||||||
plugin.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
|
|
||||||
authField: key,
|
|
||||||
label: value.title || key,
|
|
||||||
description: value.description || '',
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
return plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts MCP function format tools to plugin format
|
|
||||||
* @param functionTools - Object with function format tools
|
|
||||||
* @param customConfig - Custom configuration for MCP servers
|
|
||||||
* @returns Array of plugin objects
|
|
||||||
*/
|
|
||||||
export function convertMCPToolsToPlugins({
|
|
||||||
functionTools,
|
|
||||||
mcpManager,
|
|
||||||
}: {
|
|
||||||
functionTools?: LCAvailableTools;
|
|
||||||
mcpManager?: MCPManager;
|
|
||||||
}): TPlugin[] | undefined {
|
|
||||||
if (!functionTools || typeof functionTools !== 'object') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const plugins: TPlugin[] = [];
|
|
||||||
for (const [toolKey, toolData] of Object.entries(functionTools)) {
|
|
||||||
const plugin = convertMCPToolToPlugin({ toolKey, toolData, mcpManager });
|
|
||||||
if (plugin) {
|
|
||||||
plugins.push(plugin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return plugins;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param toolkits
|
* @param toolkits
|
||||||
* @param toolName
|
* @param toolName
|
||||||
|
|
|
||||||
|
|
@ -299,7 +299,7 @@ export const getAvailableTools = (
|
||||||
|
|
||||||
/* MCP Tools - Decoupled from regular tools */
|
/* MCP Tools - Decoupled from regular tools */
|
||||||
|
|
||||||
export const getMCPTools = (): Promise<s.TPlugin[]> => {
|
export const getMCPTools = (): Promise<q.MCPServersResponse> => {
|
||||||
return request.get(endpoints.mcp.tools);
|
return request.get(endpoints.mcp.tools);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,25 @@ export type AllPromptGroupsResponse = t.TPromptGroup[];
|
||||||
|
|
||||||
export type ConversationTagsResponse = s.TConversationTag[];
|
export type ConversationTagsResponse = s.TConversationTag[];
|
||||||
|
|
||||||
|
/* MCP Types */
|
||||||
|
export type MCPTool = {
|
||||||
|
name: string;
|
||||||
|
pluginKey: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MCPServer = {
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
authenticated: boolean;
|
||||||
|
authConfig: s.TPluginAuthConfig[];
|
||||||
|
tools: MCPTool[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MCPServersResponse = {
|
||||||
|
servers: Record<string, MCPServer>;
|
||||||
|
};
|
||||||
|
|
||||||
export type VerifyToolAuthParams = { toolId: string };
|
export type VerifyToolAuthParams = { toolId: string };
|
||||||
export type VerifyToolAuthResponse = {
|
export type VerifyToolAuthResponse = {
|
||||||
authenticated: boolean;
|
authenticated: boolean;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue