Skip to content

Commit 07eda96

Browse files
committed
✨ feat: Add MCP Reinitialization to MCPPanel
- Refactored tool caching to include user-specific tools in various service files. - Refactored MCPManager class for clarity - Added a new endpoint for reinitializing MCP servers, allowing for dynamic updates of server configurations. - Enhanced the MCPPanel component to support server reinitialization with user feedback.
1 parent 170cc34 commit 07eda96

File tree

14 files changed

+511
-189
lines changed

14 files changed

+511
-189
lines changed

api/app/clients/tools/util/handleTools.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ const loadTools = async ({
235235

236236
/** @type {Record<string, string>} */
237237
const toolContextMap = {};
238-
const appTools = (await getCachedTools({ includeGlobal: true })) ?? {};
238+
const cachedTools = (await getCachedTools({ userId: user, includeGlobal: true })) ?? {};
239239

240240
for (const tool of tools) {
241241
if (tool === Tools.execute_code) {
@@ -303,7 +303,7 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
303303
});
304304
};
305305
continue;
306-
} else if (tool && appTools[tool] && mcpToolPattern.test(tool)) {
306+
} else if (tool && cachedTools && mcpToolPattern.test(tool)) {
307307
requestedTools[tool] = async () =>
308308
createMCPTool({
309309
req: options.req,

api/models/Agent.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ const getAgent = async (searchParameter) => await Agent.findOne(searchParameter)
6161
const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _m }) => {
6262
const { model, ...model_parameters } = _m;
6363
/** @type {Record<string, FunctionTool>} */
64-
const availableTools = await getCachedTools({ includeGlobal: true });
64+
const availableTools = await getCachedTools({ userId: req.user.id, includeGlobal: true });
6565
/** @type {TEphemeralAgent | null} */
6666
const ephemeralAgent = req.body.ephemeralAgent;
6767
const mcpServers = new Set(ephemeralAgent?.mcp);

api/server/controllers/PluginController.js

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ const getAvailableTools = async (req, res) => {
145145
return;
146146
}
147147

148+
// If not in cache, build from manifest
148149
let pluginManifest = availableTools;
149150
const customConfig = await getCustomConfig();
150151
if (customConfig?.mcpServers != null) {
@@ -218,16 +219,73 @@ const getAvailableTools = async (req, res) => {
218219
toolsOutput.push(toolToAdd);
219220
}
220221

221-
const finalTools = filterUniquePlugins(toolsOutput);
222+
const userId = req.user?.id;
223+
const cachedUserTools = await getCachedTools({ userId });
224+
const userPlugins = convertMCPToolsToPlugins(cachedUserTools, customConfig);
225+
const finalTools = filterUniquePlugins([...userPlugins, ...toolsOutput]);
222226
await cache.set(CacheKeys.TOOLS, finalTools);
227+
223228
res.status(200).json(finalTools);
224229
} catch (error) {
225230
logger.error('[getAvailableTools]', error);
226231
res.status(500).json({ message: error.message });
227232
}
228233
};
229234

235+
/**
236+
* Converts MCP function format tools to plugin format
237+
* @param {Object} functionTools - Object with function format tools
238+
* @param {Object} customConfig - Custom configuration for MCP servers
239+
* @returns {Array} Array of plugin objects
240+
*/
241+
function convertMCPToolsToPlugins(functionTools, customConfig) {
242+
const plugins = [];
243+
244+
for (const [toolKey, toolData] of Object.entries(functionTools)) {
245+
if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) {
246+
continue;
247+
}
248+
249+
const functionData = toolData.function;
250+
const parts = toolKey.split(Constants.mcp_delimiter);
251+
const serverName = parts[parts.length - 1];
252+
253+
const plugin = {
254+
name: parts[0], // Use the tool name without server suffix
255+
pluginKey: toolKey,
256+
description: functionData.description || '',
257+
authenticated: true,
258+
icon: undefined,
259+
};
260+
261+
// Build authConfig for MCP tools
262+
const serverConfig = customConfig?.mcpServers?.[serverName];
263+
if (!serverConfig?.customUserVars) {
264+
plugin.authConfig = [];
265+
plugins.push(plugin);
266+
continue;
267+
}
268+
269+
const customVarKeys = Object.keys(serverConfig.customUserVars);
270+
if (customVarKeys.length === 0) {
271+
plugin.authConfig = [];
272+
} else {
273+
plugin.authConfig = Object.entries(serverConfig.customUserVars).map(([key, value]) => ({
274+
authField: key,
275+
label: value.title || key,
276+
description: value.description || '',
277+
}));
278+
}
279+
280+
plugins.push(plugin);
281+
}
282+
283+
return plugins;
284+
}
285+
230286
module.exports = {
231287
getAvailableTools,
232288
getAvailablePluginsController,
289+
convertMCPToolsToPlugins,
290+
filterUniquePlugins,
233291
};

api/server/index.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const { connectDb, indexSync } = require('~/db');
1616
const validateImageRequest = require('./middleware/validateImageRequest');
1717
const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies');
1818
const errorController = require('./controllers/ErrorController');
19-
const initializeMCP = require('./services/initializeMCP');
19+
const initializeMCPs = require('./services/initializeMCPs');
2020
const configureSocialLogins = require('./socialLogins');
2121
const AppService = require('./services/AppService');
2222
const staticCache = require('./utils/staticCache');
@@ -146,7 +146,7 @@ const startServer = async () => {
146146
logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
147147
}
148148

149-
initializeMCP(app);
149+
initializeMCPs(app);
150150
});
151151
};
152152

api/server/routes/mcp.js

Lines changed: 125 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
const { Router } = require('express');
2-
const { MCPOAuthHandler } = require('@librechat/api');
32
const { logger } = require('@librechat/data-schemas');
4-
const { CacheKeys } = require('librechat-data-provider');
3+
const { MCPOAuthHandler } = require('@librechat/api');
4+
const { CacheKeys, Constants } = require('librechat-data-provider');
5+
const {
6+
convertMCPToolsToPlugins,
7+
filterUniquePlugins,
8+
} = require('~/server/controllers/PluginController');
9+
const { setCachedTools, getCachedTools, loadCustomConfig } = require('~/server/services/Config');
10+
const { findToken, updateToken, createToken, deleteTokens } = require('~/models');
11+
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
12+
const { getMCPManager, getFlowStateManager } = require('~/config');
513
const { requireJwtAuth } = require('~/server/middleware');
6-
const { getFlowStateManager } = require('~/config');
714
const { getLogStores } = require('~/cache');
815

916
const router = Router();
@@ -202,4 +209,119 @@ router.get('/oauth/status/:flowId', async (req, res) => {
202209
}
203210
});
204211

212+
/**
213+
* Reinitialize MCP server
214+
* This endpoint allows reinitializing a specific MCP server
215+
*/
216+
router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
217+
try {
218+
const { serverName } = req.params;
219+
const user = req.user;
220+
221+
if (!user?.id) {
222+
return res.status(401).json({ error: 'User not authenticated' });
223+
}
224+
225+
logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`);
226+
227+
const config = await loadCustomConfig();
228+
if (!config || !config.mcpServers || !config.mcpServers[serverName]) {
229+
return res.status(404).json({
230+
error: `MCP server '${serverName}' not found in configuration`,
231+
});
232+
}
233+
234+
const flowsCache = getLogStores(CacheKeys.FLOWS);
235+
const flowManager = getFlowStateManager(flowsCache);
236+
const mcpManager = getMCPManager();
237+
238+
await mcpManager.disconnectServer(serverName);
239+
logger.info(`[MCP Reinitialize] Disconnected existing server: ${serverName}`);
240+
241+
const serverConfig = config.mcpServers[serverName];
242+
mcpManager.mcpConfigs[serverName] = serverConfig;
243+
let customUserVars = {};
244+
if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') {
245+
for (const varName of Object.keys(serverConfig.customUserVars)) {
246+
try {
247+
const value = await getUserPluginAuthValue(user.id, varName, false);
248+
if (value) {
249+
customUserVars[varName] = value;
250+
}
251+
} catch (err) {
252+
logger.error(`[MCP Reinitialize] Error fetching ${varName} for user ${user.id}:`, err);
253+
}
254+
}
255+
}
256+
257+
let userConnection = null;
258+
try {
259+
userConnection = await mcpManager.getUserConnection({
260+
user,
261+
serverName,
262+
flowManager,
263+
customUserVars,
264+
tokenMethods: {
265+
findToken,
266+
updateToken,
267+
createToken,
268+
deleteTokens,
269+
},
270+
});
271+
} catch (err) {
272+
logger.error(`[MCP Reinitialize] Error initializing MCP server ${serverName} for user:`, err);
273+
return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' });
274+
}
275+
276+
const userTools = (await getCachedTools({ userId: user.id })) || {};
277+
278+
// Remove any old tools from this server in the user's cache
279+
const mcpDelimiter = Constants.mcp_delimiter;
280+
for (const key of Object.keys(userTools)) {
281+
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
282+
delete userTools[key];
283+
}
284+
}
285+
286+
// Add the new tools from this server
287+
const tools = await userConnection.fetchTools();
288+
for (const tool of tools) {
289+
const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`;
290+
userTools[name] = {
291+
type: 'function',
292+
['function']: {
293+
name,
294+
description: tool.description,
295+
parameters: tool.inputSchema,
296+
},
297+
};
298+
}
299+
300+
// Save the updated user tool cache
301+
await setCachedTools(userTools, { userId: user.id });
302+
303+
// Get the current global tools cache
304+
const cache = getLogStores(CacheKeys.CONFIG_STORE);
305+
const cachedToolsArray = await cache.get(CacheKeys.TOOLS);
306+
307+
// Convert user MCP tools to plugin format
308+
const userPlugins = convertMCPToolsToPlugins(userTools, config);
309+
310+
// Merge user plugins with global tools and deduplicate
311+
const mergedTools = filterUniquePlugins([...userPlugins, ...(cachedToolsArray || [])]);
312+
313+
// Update the global tools cache
314+
await cache.set(CacheKeys.TOOLS, mergedTools);
315+
316+
res.json({
317+
success: true,
318+
message: `MCP server '${serverName}' reinitialized successfully`,
319+
serverName,
320+
});
321+
} catch (error) {
322+
logger.error('[MCP Reinitialize] Unexpected error', error);
323+
res.status(500).json({ error: 'Internal server error' });
324+
}
325+
});
326+
205327
module.exports = router;

api/server/services/MCP.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ function createAbortHandler({ userId, serverName, toolName, flowManager }) {
104104
* @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
105105
*/
106106
async function createMCPTool({ req, res, toolKey, provider: _provider }) {
107-
const availableTools = await getCachedTools({ includeGlobal: true });
107+
const availableTools = await getCachedTools({ userId: req.user?.id, includeGlobal: true });
108108
const toolDefinition = availableTools?.[toolKey]?.function;
109109
if (!toolDefinition) {
110110
logger.error(`Tool ${toolKey} not found in available tools`);

api/server/services/ToolService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ async function processRequiredActions(client, requiredActions) {
226226
`[required actions] user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`,
227227
requiredActions,
228228
);
229-
const toolDefinitions = await getCachedTools({ includeGlobal: true });
229+
const toolDefinitions = await getCachedTools({ userId: client.req.user.id, includeGlobal: true });
230230
const seenToolkits = new Set();
231231
const tools = requiredActions
232232
.map((action) => {

api/server/services/initializeMCP.js renamed to api/server/services/initializeMCPs.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,35 @@ const { getLogStores } = require('~/cache');
99
* Initialize MCP servers
1010
* @param {import('express').Application} app - Express app instance
1111
*/
12-
async function initializeMCP(app) {
12+
async function initializeMCPs(app) {
1313
const mcpServers = app.locals.mcpConfig;
1414
if (!mcpServers) {
1515
return;
1616
}
1717

18+
// Filter out servers with startup: false
19+
const filteredServers = {};
20+
for (const [name, config] of Object.entries(mcpServers)) {
21+
if (config.startup === false) {
22+
logger.info(`Skipping MCP server '${name}' due to startup: false`);
23+
continue;
24+
}
25+
filteredServers[name] = config;
26+
}
27+
28+
if (Object.keys(filteredServers).length === 0) {
29+
logger.info('[MCP] No MCP servers to initialize (all skipped or none configured)');
30+
return;
31+
}
32+
1833
logger.info('Initializing MCP servers...');
1934
const mcpManager = getMCPManager();
2035
const flowsCache = getLogStores(CacheKeys.FLOWS);
2136
const flowManager = flowsCache ? getFlowStateManager(flowsCache) : null;
2237

2338
try {
24-
await mcpManager.initializeMCP({
25-
mcpServers,
39+
await mcpManager.initializeMCPs({
40+
mcpServers: filteredServers,
2641
flowManager,
2742
tokenMethods: {
2843
findToken,
@@ -47,10 +62,11 @@ async function initializeMCP(app) {
4762
const cache = getLogStores(CacheKeys.CONFIG_STORE);
4863
await cache.delete(CacheKeys.TOOLS);
4964
logger.debug('Cleared tools array cache after MCP initialization');
65+
5066
logger.info('MCP servers initialized successfully');
5167
} catch (error) {
5268
logger.error('Failed to initialize MCP servers:', error);
5369
}
5470
}
5571

56-
module.exports = initializeMCP;
72+
module.exports = initializeMCPs;

0 commit comments

Comments
 (0)