mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 08:12:00 +02:00
✨ feat: Add MCP Reinitialization to MCPPanel (#8418)
* ✨ 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. * 🔃 refactor: Simplify Plugin Deduplication and Clear Cache Post-MCP Initialization - Replaced manual deduplication of tools with the dedicated `filterUniquePlugins` function for improved readability. - Added back cache clearing for tools after MCP initialization to ensure fresh data is used. - Removed unused exports from `PluginController.js` to clean up the codebase.
This commit is contained in:
parent
14660d75ae
commit
faaba30af1
14 changed files with 499 additions and 193 deletions
|
@ -230,7 +230,7 @@ const loadTools = async ({
|
||||||
|
|
||||||
/** @type {Record<string, string>} */
|
/** @type {Record<string, string>} */
|
||||||
const toolContextMap = {};
|
const toolContextMap = {};
|
||||||
const appTools = (await getCachedTools({ includeGlobal: true })) ?? {};
|
const cachedTools = (await getCachedTools({ userId: user, includeGlobal: true })) ?? {};
|
||||||
|
|
||||||
for (const tool of tools) {
|
for (const tool of tools) {
|
||||||
if (tool === Tools.execute_code) {
|
if (tool === Tools.execute_code) {
|
||||||
|
@ -298,7 +298,7 @@ Current Date & Time: ${replaceSpecialVars({ text: '{{iso_datetime}}' })}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
continue;
|
continue;
|
||||||
} else if (tool && appTools[tool] && mcpToolPattern.test(tool)) {
|
} else if (tool && cachedTools && mcpToolPattern.test(tool)) {
|
||||||
requestedTools[tool] = async () =>
|
requestedTools[tool] = async () =>
|
||||||
createMCPTool({
|
createMCPTool({
|
||||||
req: options.req,
|
req: options.req,
|
||||||
|
|
|
@ -61,7 +61,7 @@ const getAgent = async (searchParameter) => await Agent.findOne(searchParameter)
|
||||||
const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _m }) => {
|
const loadEphemeralAgent = async ({ req, agent_id, endpoint, model_parameters: _m }) => {
|
||||||
const { model, ...model_parameters } = _m;
|
const { model, ...model_parameters } = _m;
|
||||||
/** @type {Record<string, FunctionTool>} */
|
/** @type {Record<string, FunctionTool>} */
|
||||||
const availableTools = await getCachedTools({ includeGlobal: true });
|
const availableTools = await getCachedTools({ userId: req.user.id, includeGlobal: true });
|
||||||
/** @type {TEphemeralAgent | null} */
|
/** @type {TEphemeralAgent | null} */
|
||||||
const ephemeralAgent = req.body.ephemeralAgent;
|
const ephemeralAgent = req.body.ephemeralAgent;
|
||||||
const mcpServers = new Set(ephemeralAgent?.mcp);
|
const mcpServers = new Set(ephemeralAgent?.mcp);
|
||||||
|
|
|
@ -138,15 +138,21 @@ function createGetServerTools() {
|
||||||
*/
|
*/
|
||||||
const getAvailableTools = async (req, res) => {
|
const getAvailableTools = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
const userId = req.user?.id;
|
||||||
|
const customConfig = await getCustomConfig();
|
||||||
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);
|
||||||
if (cachedToolsArray) {
|
const cachedUserTools = await getCachedTools({ userId });
|
||||||
res.status(200).json(cachedToolsArray);
|
const userPlugins = convertMCPToolsToPlugins(cachedUserTools, customConfig);
|
||||||
|
|
||||||
|
if (cachedToolsArray && userPlugins) {
|
||||||
|
const dedupedTools = filterUniquePlugins([...userPlugins, ...cachedToolsArray]);
|
||||||
|
res.status(200).json(dedupedTools);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If not in cache, build from manifest
|
||||||
let pluginManifest = availableTools;
|
let pluginManifest = availableTools;
|
||||||
const customConfig = await getCustomConfig();
|
|
||||||
if (customConfig?.mcpServers != null) {
|
if (customConfig?.mcpServers != null) {
|
||||||
const mcpManager = getMCPManager();
|
const mcpManager = getMCPManager();
|
||||||
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
||||||
|
@ -217,16 +223,69 @@ const getAvailableTools = async (req, res) => {
|
||||||
|
|
||||||
toolsOutput.push(toolToAdd);
|
toolsOutput.push(toolToAdd);
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalTools = filterUniquePlugins(toolsOutput);
|
const finalTools = filterUniquePlugins(toolsOutput);
|
||||||
await cache.set(CacheKeys.TOOLS, finalTools);
|
await cache.set(CacheKeys.TOOLS, finalTools);
|
||||||
res.status(200).json(finalTools);
|
|
||||||
|
const dedupedTools = filterUniquePlugins([...userPlugins, ...finalTools]);
|
||||||
|
|
||||||
|
res.status(200).json(dedupedTools);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('[getAvailableTools]', error);
|
logger.error('[getAvailableTools]', error);
|
||||||
res.status(500).json({ message: error.message });
|
res.status(500).json({ message: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts MCP function format tools to plugin format
|
||||||
|
* @param {Object} functionTools - Object with function format tools
|
||||||
|
* @param {Object} customConfig - Custom configuration for MCP servers
|
||||||
|
* @returns {Array} Array of plugin objects
|
||||||
|
*/
|
||||||
|
function convertMCPToolsToPlugins(functionTools, customConfig) {
|
||||||
|
const plugins = [];
|
||||||
|
|
||||||
|
for (const [toolKey, toolData] of Object.entries(functionTools)) {
|
||||||
|
if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const functionData = toolData.function;
|
||||||
|
const parts = toolKey.split(Constants.mcp_delimiter);
|
||||||
|
const serverName = parts[parts.length - 1];
|
||||||
|
|
||||||
|
const plugin = {
|
||||||
|
name: parts[0], // Use the tool name without server suffix
|
||||||
|
pluginKey: toolKey,
|
||||||
|
description: functionData.description || '',
|
||||||
|
authenticated: true,
|
||||||
|
icon: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build authConfig for MCP tools
|
||||||
|
const serverConfig = customConfig?.mcpServers?.[serverName];
|
||||||
|
if (!serverConfig?.customUserVars) {
|
||||||
|
plugin.authConfig = [];
|
||||||
|
plugins.push(plugin);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins.push(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
return plugins;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
getAvailableTools,
|
getAvailableTools,
|
||||||
getAvailablePluginsController,
|
getAvailablePluginsController,
|
||||||
|
|
|
@ -16,7 +16,7 @@ const { connectDb, indexSync } = require('~/db');
|
||||||
const validateImageRequest = require('./middleware/validateImageRequest');
|
const validateImageRequest = require('./middleware/validateImageRequest');
|
||||||
const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies');
|
const { jwtLogin, ldapLogin, passportLogin } = require('~/strategies');
|
||||||
const errorController = require('./controllers/ErrorController');
|
const errorController = require('./controllers/ErrorController');
|
||||||
const initializeMCP = require('./services/initializeMCP');
|
const initializeMCPs = require('./services/initializeMCPs');
|
||||||
const configureSocialLogins = require('./socialLogins');
|
const configureSocialLogins = require('./socialLogins');
|
||||||
const AppService = require('./services/AppService');
|
const AppService = require('./services/AppService');
|
||||||
const staticCache = require('./utils/staticCache');
|
const staticCache = require('./utils/staticCache');
|
||||||
|
@ -146,7 +146,7 @@ const startServer = async () => {
|
||||||
logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
|
logger.info(`Server listening at http://${host == '0.0.0.0' ? 'localhost' : host}:${port}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeMCP(app);
|
initializeMCPs(app);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
const { Router } = require('express');
|
const { Router } = require('express');
|
||||||
const { MCPOAuthHandler } = require('@librechat/api');
|
|
||||||
const { logger } = require('@librechat/data-schemas');
|
const { logger } = require('@librechat/data-schemas');
|
||||||
const { CacheKeys } = require('librechat-data-provider');
|
const { MCPOAuthHandler } = require('@librechat/api');
|
||||||
|
const { CacheKeys, Constants } = require('librechat-data-provider');
|
||||||
|
const { findToken, updateToken, createToken, deleteTokens } = require('~/models');
|
||||||
|
const { setCachedTools, getCachedTools, loadCustomConfig } = require('~/server/services/Config');
|
||||||
|
const { getUserPluginAuthValue } = require('~/server/services/PluginService');
|
||||||
|
const { getMCPManager, getFlowStateManager } = require('~/config');
|
||||||
const { requireJwtAuth } = require('~/server/middleware');
|
const { requireJwtAuth } = require('~/server/middleware');
|
||||||
const { getFlowStateManager } = require('~/config');
|
|
||||||
const { getLogStores } = require('~/cache');
|
const { getLogStores } = require('~/cache');
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
@ -202,4 +205,106 @@ router.get('/oauth/status/:flowId', async (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reinitialize MCP server
|
||||||
|
* This endpoint allows reinitializing a specific MCP server
|
||||||
|
*/
|
||||||
|
router.post('/:serverName/reinitialize', requireJwtAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { serverName } = req.params;
|
||||||
|
const user = req.user;
|
||||||
|
|
||||||
|
if (!user?.id) {
|
||||||
|
return res.status(401).json({ error: 'User not authenticated' });
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[MCP Reinitialize] Reinitializing server: ${serverName}`);
|
||||||
|
|
||||||
|
const config = await loadCustomConfig();
|
||||||
|
if (!config || !config.mcpServers || !config.mcpServers[serverName]) {
|
||||||
|
return res.status(404).json({
|
||||||
|
error: `MCP server '${serverName}' not found in configuration`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const flowsCache = getLogStores(CacheKeys.FLOWS);
|
||||||
|
const flowManager = getFlowStateManager(flowsCache);
|
||||||
|
const mcpManager = getMCPManager();
|
||||||
|
|
||||||
|
await mcpManager.disconnectServer(serverName);
|
||||||
|
logger.info(`[MCP Reinitialize] Disconnected existing server: ${serverName}`);
|
||||||
|
|
||||||
|
const serverConfig = config.mcpServers[serverName];
|
||||||
|
mcpManager.mcpConfigs[serverName] = serverConfig;
|
||||||
|
let customUserVars = {};
|
||||||
|
if (serverConfig.customUserVars && typeof serverConfig.customUserVars === 'object') {
|
||||||
|
for (const varName of Object.keys(serverConfig.customUserVars)) {
|
||||||
|
try {
|
||||||
|
const value = await getUserPluginAuthValue(user.id, varName, false);
|
||||||
|
if (value) {
|
||||||
|
customUserVars[varName] = value;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`[MCP Reinitialize] Error fetching ${varName} for user ${user.id}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let userConnection = null;
|
||||||
|
try {
|
||||||
|
userConnection = await mcpManager.getUserConnection({
|
||||||
|
user,
|
||||||
|
serverName,
|
||||||
|
flowManager,
|
||||||
|
customUserVars,
|
||||||
|
tokenMethods: {
|
||||||
|
findToken,
|
||||||
|
updateToken,
|
||||||
|
createToken,
|
||||||
|
deleteTokens,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`[MCP Reinitialize] Error initializing MCP server ${serverName} for user:`, err);
|
||||||
|
return res.status(500).json({ error: 'Failed to reinitialize MCP server for user' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userTools = (await getCachedTools({ userId: user.id })) || {};
|
||||||
|
|
||||||
|
// Remove any old tools from this server in the user's cache
|
||||||
|
const mcpDelimiter = Constants.mcp_delimiter;
|
||||||
|
for (const key of Object.keys(userTools)) {
|
||||||
|
if (key.endsWith(`${mcpDelimiter}${serverName}`)) {
|
||||||
|
delete userTools[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the new tools from this server
|
||||||
|
const tools = await userConnection.fetchTools();
|
||||||
|
for (const tool of tools) {
|
||||||
|
const name = `${tool.name}${Constants.mcp_delimiter}${serverName}`;
|
||||||
|
userTools[name] = {
|
||||||
|
type: 'function',
|
||||||
|
['function']: {
|
||||||
|
name,
|
||||||
|
description: tool.description,
|
||||||
|
parameters: tool.inputSchema,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the updated user tool cache
|
||||||
|
await setCachedTools(userTools, { userId: user.id });
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: `MCP server '${serverName}' reinitialized successfully`,
|
||||||
|
serverName,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('[MCP Reinitialize] Unexpected error', error);
|
||||||
|
res.status(500).json({ error: 'Internal server error' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
@ -104,7 +104,7 @@ function createAbortHandler({ userId, serverName, toolName, flowManager }) {
|
||||||
* @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
|
* @returns { Promise<typeof tool | { _call: (toolInput: Object | string) => unknown}> } An object with `_call` method to execute the tool input.
|
||||||
*/
|
*/
|
||||||
async function createMCPTool({ req, res, toolKey, provider: _provider }) {
|
async function createMCPTool({ req, res, toolKey, provider: _provider }) {
|
||||||
const availableTools = await getCachedTools({ includeGlobal: true });
|
const availableTools = await getCachedTools({ userId: req.user?.id, includeGlobal: true });
|
||||||
const toolDefinition = availableTools?.[toolKey]?.function;
|
const toolDefinition = availableTools?.[toolKey]?.function;
|
||||||
if (!toolDefinition) {
|
if (!toolDefinition) {
|
||||||
logger.error(`Tool ${toolKey} not found in available tools`);
|
logger.error(`Tool ${toolKey} not found in available tools`);
|
||||||
|
|
|
@ -226,7 +226,7 @@ async function processRequiredActions(client, requiredActions) {
|
||||||
`[required actions] user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`,
|
`[required actions] user: ${client.req.user.id} | thread_id: ${requiredActions[0].thread_id} | run_id: ${requiredActions[0].run_id}`,
|
||||||
requiredActions,
|
requiredActions,
|
||||||
);
|
);
|
||||||
const toolDefinitions = await getCachedTools({ includeGlobal: true });
|
const toolDefinitions = await getCachedTools({ userId: client.req.user.id, includeGlobal: true });
|
||||||
const seenToolkits = new Set();
|
const seenToolkits = new Set();
|
||||||
const tools = requiredActions
|
const tools = requiredActions
|
||||||
.map((action) => {
|
.map((action) => {
|
||||||
|
|
|
@ -9,20 +9,35 @@ const { getLogStores } = require('~/cache');
|
||||||
* Initialize MCP servers
|
* Initialize MCP servers
|
||||||
* @param {import('express').Application} app - Express app instance
|
* @param {import('express').Application} app - Express app instance
|
||||||
*/
|
*/
|
||||||
async function initializeMCP(app) {
|
async function initializeMCPs(app) {
|
||||||
const mcpServers = app.locals.mcpConfig;
|
const mcpServers = app.locals.mcpConfig;
|
||||||
if (!mcpServers) {
|
if (!mcpServers) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter out servers with startup: false
|
||||||
|
const filteredServers = {};
|
||||||
|
for (const [name, config] of Object.entries(mcpServers)) {
|
||||||
|
if (config.startup === false) {
|
||||||
|
logger.info(`Skipping MCP server '${name}' due to startup: false`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
filteredServers[name] = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(filteredServers).length === 0) {
|
||||||
|
logger.info('[MCP] No MCP servers to initialize (all skipped or none configured)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.info('Initializing MCP servers...');
|
logger.info('Initializing MCP servers...');
|
||||||
const mcpManager = getMCPManager();
|
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;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await mcpManager.initializeMCP({
|
await mcpManager.initializeMCPs({
|
||||||
mcpServers,
|
mcpServers: filteredServers,
|
||||||
flowManager,
|
flowManager,
|
||||||
tokenMethods: {
|
tokenMethods: {
|
||||||
findToken,
|
findToken,
|
||||||
|
@ -47,10 +62,11 @@ async function initializeMCP(app) {
|
||||||
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
const cache = getLogStores(CacheKeys.CONFIG_STORE);
|
||||||
await cache.delete(CacheKeys.TOOLS);
|
await cache.delete(CacheKeys.TOOLS);
|
||||||
logger.debug('Cleared tools array cache after MCP initialization');
|
logger.debug('Cleared tools array cache after MCP initialization');
|
||||||
|
|
||||||
logger.info('MCP servers initialized successfully');
|
logger.info('MCP servers initialized successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Failed to initialize MCP servers:', error);
|
logger.error('Failed to initialize MCP servers:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = initializeMCP;
|
module.exports = initializeMCPs;
|
|
@ -1,8 +1,11 @@
|
||||||
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
|
||||||
import { ChevronLeft } from 'lucide-react';
|
|
||||||
import { Constants } from 'librechat-data-provider';
|
import { Constants } from 'librechat-data-provider';
|
||||||
|
import { ChevronLeft, RefreshCw } from 'lucide-react';
|
||||||
import { useForm, Controller } from 'react-hook-form';
|
import { useForm, Controller } from 'react-hook-form';
|
||||||
import { useUpdateUserPluginsMutation } from 'librechat-data-provider/react-query';
|
import React, { useState, useCallback, useMemo, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
useUpdateUserPluginsMutation,
|
||||||
|
useReinitializeMCPServerMutation,
|
||||||
|
} from 'librechat-data-provider/react-query';
|
||||||
import type { TUpdateUserPlugins } from 'librechat-data-provider';
|
import type { TUpdateUserPlugins } from 'librechat-data-provider';
|
||||||
import { Button, Input, Label } from '~/components/ui';
|
import { Button, Input, Label } from '~/components/ui';
|
||||||
import { useGetStartupConfig } from '~/data-provider';
|
import { useGetStartupConfig } from '~/data-provider';
|
||||||
|
@ -24,6 +27,8 @@ export default function MCPPanel() {
|
||||||
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
const [selectedServerNameForEditing, setSelectedServerNameForEditing] = useState<string | null>(
|
||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
|
const [rotatingServers, setRotatingServers] = useState<Set<string>>(new Set());
|
||||||
|
const reinitializeMCPMutation = useReinitializeMCPServerMutation();
|
||||||
|
|
||||||
const mcpServerDefinitions = useMemo(() => {
|
const mcpServerDefinitions = useMemo(() => {
|
||||||
if (!startupConfig?.mcpServers) {
|
if (!startupConfig?.mcpServers) {
|
||||||
|
@ -89,6 +94,32 @@ export default function MCPPanel() {
|
||||||
setSelectedServerNameForEditing(null);
|
setSelectedServerNameForEditing(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReinitializeServer = useCallback(
|
||||||
|
async (serverName: string) => {
|
||||||
|
setRotatingServers((prev) => new Set(prev).add(serverName));
|
||||||
|
try {
|
||||||
|
await reinitializeMCPMutation.mutateAsync(serverName);
|
||||||
|
showToast({
|
||||||
|
message: `MCP server '${serverName}' reinitialized successfully`,
|
||||||
|
status: 'success',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reinitializing MCP server:', error);
|
||||||
|
showToast({
|
||||||
|
message: 'Failed to reinitialize MCP server',
|
||||||
|
status: 'error',
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setRotatingServers((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
next.delete(serverName);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[showToast, reinitializeMCPMutation],
|
||||||
|
);
|
||||||
|
|
||||||
if (startupConfigLoading) {
|
if (startupConfigLoading) {
|
||||||
return <MCPPanelSkeleton />;
|
return <MCPPanelSkeleton />;
|
||||||
}
|
}
|
||||||
|
@ -144,14 +175,27 @@ export default function MCPPanel() {
|
||||||
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
<div className="h-auto max-w-full overflow-x-hidden p-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{mcpServerDefinitions.map((server) => (
|
{mcpServerDefinitions.map((server) => (
|
||||||
<Button
|
<div key={server.serverName} className="flex items-center gap-2">
|
||||||
key={server.serverName}
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="w-full justify-start dark:hover:bg-gray-700"
|
className="flex-1 justify-start dark:hover:bg-gray-700"
|
||||||
onClick={() => handleServerClickToEdit(server.serverName)}
|
onClick={() => handleServerClickToEdit(server.serverName)}
|
||||||
>
|
>
|
||||||
{server.serverName}
|
{server.serverName}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handleReinitializeServer(server.serverName)}
|
||||||
|
className="px-2 py-1"
|
||||||
|
title="Reinitialize MCP server"
|
||||||
|
disabled={reinitializeMCPMutation.isLoading}
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={`h-4 w-4 ${rotatingServers.has(server.serverName) ? 'animate-spin' : ''}`}
|
||||||
|
/>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,7 +39,7 @@ export class MCPManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stores configs and initializes app-level connections */
|
/** Stores configs and initializes app-level connections */
|
||||||
public async initializeMCP({
|
public async initializeMCPs({
|
||||||
mcpServers,
|
mcpServers,
|
||||||
flowManager,
|
flowManager,
|
||||||
tokenMethods,
|
tokenMethods,
|
||||||
|
@ -60,173 +60,17 @@ export class MCPManager {
|
||||||
const entries = Object.entries(mcpServers);
|
const entries = Object.entries(mcpServers);
|
||||||
const initializedServers = new Set();
|
const initializedServers = new Set();
|
||||||
const connectionResults = await Promise.allSettled(
|
const connectionResults = await Promise.allSettled(
|
||||||
entries.map(async ([serverName, _config], i) => {
|
entries.map(async ([serverName, config], i) => {
|
||||||
/** Process env for app-level connections */
|
|
||||||
const config = processMCPEnv(_config);
|
|
||||||
|
|
||||||
/** Existing tokens for system-level connections */
|
|
||||||
let tokens: MCPOAuthTokens | null = null;
|
|
||||||
if (tokenMethods?.findToken) {
|
|
||||||
try {
|
|
||||||
/** Refresh function for app-level connections */
|
|
||||||
const refreshTokensFunction = async (
|
|
||||||
refreshToken: string,
|
|
||||||
metadata: {
|
|
||||||
userId: string;
|
|
||||||
serverName: string;
|
|
||||||
identifier: string;
|
|
||||||
clientInfo?: OAuthClientInformation;
|
|
||||||
},
|
|
||||||
) => {
|
|
||||||
/** URL from config if available */
|
|
||||||
const serverUrl = (config as t.SSEOptions | t.StreamableHTTPOptions).url;
|
|
||||||
return await MCPOAuthHandler.refreshOAuthTokens(
|
|
||||||
refreshToken,
|
|
||||||
{
|
|
||||||
serverName: metadata.serverName,
|
|
||||||
serverUrl,
|
|
||||||
clientInfo: metadata.clientInfo,
|
|
||||||
},
|
|
||||||
config.oauth,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Flow state to prevent concurrent token operations */
|
|
||||||
const tokenFlowId = `tokens:${CONSTANTS.SYSTEM_USER_ID}:${serverName}`;
|
|
||||||
tokens = await flowManager.createFlowWithHandler(
|
|
||||||
tokenFlowId,
|
|
||||||
'mcp_get_tokens',
|
|
||||||
async () => {
|
|
||||||
return await MCPTokenStorage.getTokens({
|
|
||||||
userId: CONSTANTS.SYSTEM_USER_ID,
|
|
||||||
serverName,
|
|
||||||
findToken: tokenMethods.findToken,
|
|
||||||
refreshTokens: refreshTokensFunction,
|
|
||||||
createToken: tokenMethods.createToken,
|
|
||||||
updateToken: tokenMethods.updateToken,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
logger.debug(`[MCP][${serverName}] No existing tokens found`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tokens) {
|
|
||||||
logger.info(`[MCP][${serverName}] Loaded OAuth tokens`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const connection = new MCPConnection(serverName, config, undefined, tokens);
|
|
||||||
|
|
||||||
/** Listen for OAuth requirements */
|
|
||||||
logger.info(`[MCP][${serverName}] Setting up OAuth event listener`);
|
|
||||||
connection.on('oauthRequired', async (data) => {
|
|
||||||
logger.debug(`[MCP][${serverName}] oauthRequired event received`);
|
|
||||||
const result = await this.handleOAuthRequired({
|
|
||||||
...data,
|
|
||||||
flowManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result?.tokens && tokenMethods?.createToken) {
|
|
||||||
try {
|
|
||||||
connection.setOAuthTokens(result.tokens);
|
|
||||||
await MCPTokenStorage.storeTokens({
|
|
||||||
userId: CONSTANTS.SYSTEM_USER_ID,
|
|
||||||
serverName,
|
|
||||||
tokens: result.tokens,
|
|
||||||
createToken: tokenMethods.createToken,
|
|
||||||
updateToken: tokenMethods.updateToken,
|
|
||||||
findToken: tokenMethods.findToken,
|
|
||||||
clientInfo: result.clientInfo,
|
|
||||||
});
|
|
||||||
logger.info(`[MCP][${serverName}] OAuth tokens saved to storage`);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`[MCP][${serverName}] Failed to save OAuth tokens to storage`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only emit oauthHandled if we actually got tokens (OAuth succeeded)
|
|
||||||
if (result?.tokens) {
|
|
||||||
connection.emit('oauthHandled');
|
|
||||||
} else {
|
|
||||||
// OAuth failed, emit oauthFailed to properly reject the promise
|
|
||||||
logger.warn(`[MCP][${serverName}] OAuth failed, emitting oauthFailed event`);
|
|
||||||
connection.emit('oauthFailed', new Error('OAuth authentication failed'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const connectTimeout = config.initTimeout ?? 30000;
|
await this.initializeMCP({
|
||||||
const connectionTimeout = new Promise<void>((_, reject) =>
|
serverName,
|
||||||
setTimeout(
|
config,
|
||||||
() => reject(new Error(`Connection timeout after ${connectTimeout}ms`)),
|
|
||||||
connectTimeout,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const connectionAttempt = this.initializeServer({
|
|
||||||
connection,
|
|
||||||
logPrefix: `[MCP][${serverName}]`,
|
|
||||||
flowManager,
|
flowManager,
|
||||||
handleOAuth: false,
|
tokenMethods,
|
||||||
});
|
});
|
||||||
await Promise.race([connectionAttempt, connectionTimeout]);
|
initializedServers.add(i);
|
||||||
|
|
||||||
if (await connection.isConnected()) {
|
|
||||||
initializedServers.add(i);
|
|
||||||
this.connections.set(serverName, connection);
|
|
||||||
|
|
||||||
/** Unified `serverInstructions` configuration */
|
|
||||||
const configInstructions = config.serverInstructions;
|
|
||||||
|
|
||||||
if (configInstructions !== undefined) {
|
|
||||||
if (typeof configInstructions === 'string') {
|
|
||||||
this.serverInstructions.set(serverName, configInstructions);
|
|
||||||
logger.info(
|
|
||||||
`[MCP][${serverName}] Custom instructions stored for context inclusion: ${configInstructions}`,
|
|
||||||
);
|
|
||||||
} else if (configInstructions === true) {
|
|
||||||
/** Server-provided instructions */
|
|
||||||
const serverInstructions = connection.client.getInstructions();
|
|
||||||
|
|
||||||
if (serverInstructions) {
|
|
||||||
this.serverInstructions.set(serverName, serverInstructions);
|
|
||||||
logger.info(
|
|
||||||
`[MCP][${serverName}] Server instructions stored for context inclusion: ${serverInstructions}`,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logger.info(
|
|
||||||
`[MCP][${serverName}] serverInstructions=true but no server instructions available`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.info(
|
|
||||||
`[MCP][${serverName}] Instructions explicitly disabled (serverInstructions=false)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.info(
|
|
||||||
`[MCP][${serverName}] Instructions not included (serverInstructions not configured)`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const serverCapabilities = connection.client.getServerCapabilities();
|
|
||||||
logger.info(`[MCP][${serverName}] Capabilities: ${JSON.stringify(serverCapabilities)}`);
|
|
||||||
|
|
||||||
if (serverCapabilities?.tools) {
|
|
||||||
const tools = await connection.client.listTools();
|
|
||||||
if (tools.tools.length) {
|
|
||||||
logger.info(
|
|
||||||
`[MCP][${serverName}] Available tools: ${tools.tools
|
|
||||||
.map((tool) => tool.name)
|
|
||||||
.join(', ')}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[MCP][${serverName}] Initialization failed`, error);
|
logger.error(`[MCP][${serverName}] Initialization failed`, error);
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -260,6 +104,176 @@ export class MCPManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Initializes a single MCP server connection (app-level) */
|
||||||
|
public async initializeMCP({
|
||||||
|
serverName,
|
||||||
|
config,
|
||||||
|
flowManager,
|
||||||
|
tokenMethods,
|
||||||
|
}: {
|
||||||
|
serverName: string;
|
||||||
|
config: t.MCPOptions;
|
||||||
|
flowManager: FlowStateManager<MCPOAuthTokens | null>;
|
||||||
|
tokenMethods?: TokenMethods;
|
||||||
|
}): Promise<void> {
|
||||||
|
const processedConfig = processMCPEnv(config);
|
||||||
|
let tokens: MCPOAuthTokens | null = null;
|
||||||
|
if (tokenMethods?.findToken) {
|
||||||
|
try {
|
||||||
|
/** Refresh function for app-level connections */
|
||||||
|
const refreshTokensFunction = async (
|
||||||
|
refreshToken: string,
|
||||||
|
metadata: {
|
||||||
|
userId: string;
|
||||||
|
serverName: string;
|
||||||
|
identifier: string;
|
||||||
|
clientInfo?: OAuthClientInformation;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const serverUrl = (processedConfig as t.SSEOptions | t.StreamableHTTPOptions).url;
|
||||||
|
return await MCPOAuthHandler.refreshOAuthTokens(
|
||||||
|
refreshToken,
|
||||||
|
{
|
||||||
|
serverName: metadata.serverName,
|
||||||
|
serverUrl,
|
||||||
|
clientInfo: metadata.clientInfo,
|
||||||
|
},
|
||||||
|
processedConfig.oauth,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Flow state to prevent concurrent token operations */
|
||||||
|
const tokenFlowId = `tokens:${CONSTANTS.SYSTEM_USER_ID}:${serverName}`;
|
||||||
|
tokens = await flowManager.createFlowWithHandler(
|
||||||
|
tokenFlowId,
|
||||||
|
'mcp_get_tokens',
|
||||||
|
async () => {
|
||||||
|
return await MCPTokenStorage.getTokens({
|
||||||
|
userId: CONSTANTS.SYSTEM_USER_ID,
|
||||||
|
serverName,
|
||||||
|
findToken: tokenMethods.findToken,
|
||||||
|
refreshTokens: refreshTokensFunction,
|
||||||
|
createToken: tokenMethods.createToken,
|
||||||
|
updateToken: tokenMethods.updateToken,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
logger.debug(`[MCP][${serverName}] No existing tokens found`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tokens) {
|
||||||
|
logger.info(`[MCP][${serverName}] Loaded OAuth tokens`);
|
||||||
|
}
|
||||||
|
const connection = new MCPConnection(serverName, processedConfig, undefined, tokens);
|
||||||
|
logger.info(`[MCP][${serverName}] Setting up OAuth event listener`);
|
||||||
|
connection.on('oauthRequired', async (data) => {
|
||||||
|
logger.debug(`[MCP][${serverName}] oauthRequired event received`);
|
||||||
|
const result = await this.handleOAuthRequired({
|
||||||
|
...data,
|
||||||
|
flowManager,
|
||||||
|
});
|
||||||
|
if (result?.tokens && tokenMethods?.createToken) {
|
||||||
|
try {
|
||||||
|
connection.setOAuthTokens(result.tokens);
|
||||||
|
await MCPTokenStorage.storeTokens({
|
||||||
|
userId: CONSTANTS.SYSTEM_USER_ID,
|
||||||
|
serverName,
|
||||||
|
tokens: result.tokens,
|
||||||
|
createToken: tokenMethods.createToken,
|
||||||
|
updateToken: tokenMethods.updateToken,
|
||||||
|
findToken: tokenMethods.findToken,
|
||||||
|
clientInfo: result.clientInfo,
|
||||||
|
});
|
||||||
|
logger.info(`[MCP][${serverName}] OAuth tokens saved to storage`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[MCP][${serverName}] Failed to save OAuth tokens to storage`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only emit oauthHandled if we actually got tokens (OAuth succeeded)
|
||||||
|
if (result?.tokens) {
|
||||||
|
connection.emit('oauthHandled');
|
||||||
|
} else {
|
||||||
|
// OAuth failed, emit oauthFailed to properly reject the promise
|
||||||
|
logger.warn(`[MCP][${serverName}] OAuth failed, emitting oauthFailed event`);
|
||||||
|
connection.emit('oauthFailed', new Error('OAuth authentication failed'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const connectTimeout = processedConfig.initTimeout ?? 30000;
|
||||||
|
const connectionTimeout = new Promise<void>((_, reject) =>
|
||||||
|
setTimeout(
|
||||||
|
() => reject(new Error(`Connection timeout after ${connectTimeout}ms`)),
|
||||||
|
connectTimeout,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const connectionAttempt = this.initializeServer({
|
||||||
|
connection,
|
||||||
|
logPrefix: `[MCP][${serverName}]`,
|
||||||
|
flowManager,
|
||||||
|
handleOAuth: false,
|
||||||
|
});
|
||||||
|
await Promise.race([connectionAttempt, connectionTimeout]);
|
||||||
|
if (await connection.isConnected()) {
|
||||||
|
this.connections.set(serverName, connection);
|
||||||
|
|
||||||
|
/** Unified `serverInstructions` configuration */
|
||||||
|
const configInstructions = processedConfig.serverInstructions;
|
||||||
|
if (configInstructions !== undefined) {
|
||||||
|
if (typeof configInstructions === 'string') {
|
||||||
|
this.serverInstructions.set(serverName, configInstructions);
|
||||||
|
logger.info(
|
||||||
|
`[MCP][${serverName}] Custom instructions stored for context inclusion: ${configInstructions}`,
|
||||||
|
);
|
||||||
|
} else if (configInstructions === true) {
|
||||||
|
/** Server-provided instructions */
|
||||||
|
const serverInstructions = connection.client.getInstructions();
|
||||||
|
|
||||||
|
if (serverInstructions) {
|
||||||
|
this.serverInstructions.set(serverName, serverInstructions);
|
||||||
|
logger.info(
|
||||||
|
`[MCP][${serverName}] Server instructions stored for context inclusion: ${serverInstructions}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`[MCP][${serverName}] serverInstructions=true but no server instructions available`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`[MCP][${serverName}] Instructions explicitly disabled (serverInstructions=false)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
`[MCP][${serverName}] Instructions not included (serverInstructions not configured)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverCapabilities = connection.client.getServerCapabilities();
|
||||||
|
logger.info(`[MCP][${serverName}] Capabilities: ${JSON.stringify(serverCapabilities)}`);
|
||||||
|
|
||||||
|
if (serverCapabilities?.tools) {
|
||||||
|
const tools = await connection.client.listTools();
|
||||||
|
if (tools.tools.length) {
|
||||||
|
logger.info(
|
||||||
|
`[MCP][${serverName}] Available tools: ${tools.tools
|
||||||
|
.map((tool) => tool.name)
|
||||||
|
.join(', ')}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info(`[MCP][${serverName}] ✓ Initialized`);
|
||||||
|
} else {
|
||||||
|
logger.info(`[MCP][${serverName}] ✗ Failed`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`[MCP][${serverName}] Initialization failed`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Generic server initialization logic */
|
/** Generic server initialization logic */
|
||||||
private async initializeServer({
|
private async initializeServer({
|
||||||
connection,
|
connection,
|
||||||
|
|
|
@ -708,5 +708,53 @@ describe('Environment Variable Extraction (MCP)', () => {
|
||||||
SYSTEM_PATH: process.env.PATH, // Actual value of PATH from the test environment
|
SYSTEM_PATH: process.env.PATH, // Actual value of PATH from the test environment
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should process GitHub MCP server configuration with PAT_TOKEN placeholder', () => {
|
||||||
|
const user = createTestUser({ id: 'github-user-123', email: 'user@example.com' });
|
||||||
|
const customUserVars = {
|
||||||
|
PAT_TOKEN: 'ghp_1234567890abcdef1234567890abcdef12345678', // GitHub Personal Access Token
|
||||||
|
};
|
||||||
|
|
||||||
|
// Simulate the GitHub MCP server configuration from librechat.yaml
|
||||||
|
const obj: MCPOptions = {
|
||||||
|
type: 'streamable-http',
|
||||||
|
url: 'https://api.githubcopilot.com/mcp/',
|
||||||
|
headers: {
|
||||||
|
Authorization: '{{PAT_TOKEN}}',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'LibreChat-MCP-Client',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = processMCPEnv(obj, user, customUserVars);
|
||||||
|
|
||||||
|
expect('headers' in result && result.headers).toEqual({
|
||||||
|
Authorization: 'ghp_1234567890abcdef1234567890abcdef12345678',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'User-Agent': 'LibreChat-MCP-Client',
|
||||||
|
});
|
||||||
|
expect('url' in result && result.url).toBe('https://api.githubcopilot.com/mcp/');
|
||||||
|
expect(result.type).toBe('streamable-http');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle GitHub MCP server configuration without PAT_TOKEN (placeholder remains)', () => {
|
||||||
|
const user = createTestUser({ id: 'github-user-123' });
|
||||||
|
// No customUserVars provided - PAT_TOKEN should remain as placeholder
|
||||||
|
const obj: MCPOptions = {
|
||||||
|
type: 'streamable-http',
|
||||||
|
url: 'https://api.githubcopilot.com/mcp/',
|
||||||
|
headers: {
|
||||||
|
Authorization: '{{PAT_TOKEN}}',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = processMCPEnv(obj, user);
|
||||||
|
|
||||||
|
expect('headers' in result && result.headers).toEqual({
|
||||||
|
Authorization: '{{PAT_TOKEN}}', // Should remain unchanged since no customUserVars provided
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -132,6 +132,8 @@ export const resendVerificationEmail = () => '/api/user/verify/resend';
|
||||||
|
|
||||||
export const plugins = () => '/api/plugins';
|
export const plugins = () => '/api/plugins';
|
||||||
|
|
||||||
|
export const mcpReinitialize = (serverName: string) => `/api/mcp/${serverName}/reinitialize`;
|
||||||
|
|
||||||
export const config = () => '/api/config';
|
export const config = () => '/api/config';
|
||||||
|
|
||||||
export const prompts = () => '/api/prompts';
|
export const prompts = () => '/api/prompts';
|
||||||
|
|
|
@ -141,6 +141,10 @@ export const updateUserPlugins = (payload: t.TUpdateUserPlugins) => {
|
||||||
return request.post(endpoints.userPlugins(), payload);
|
return request.post(endpoints.userPlugins(), payload);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const reinitializeMCPServer = (serverName: string) => {
|
||||||
|
return request.post(endpoints.mcpReinitialize(serverName));
|
||||||
|
};
|
||||||
|
|
||||||
/* Config */
|
/* Config */
|
||||||
|
|
||||||
export const getStartupConfig = (): Promise<
|
export const getStartupConfig = (): Promise<
|
||||||
|
|
|
@ -316,6 +316,20 @@ export const useUpdateUserPluginsMutation = (
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useReinitializeMCPServerMutation = (): UseMutationResult<
|
||||||
|
{ success: boolean; message: string; serverName: string },
|
||||||
|
unknown,
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
> => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation((serverName: string) => dataService.reinitializeMCPServer(serverName), {
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.refetchQueries([QueryKeys.tools]);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetCustomConfigSpeechQuery = (
|
export const useGetCustomConfigSpeechQuery = (
|
||||||
config?: UseQueryOptions<t.TCustomConfigSpeechResponse>,
|
config?: UseQueryOptions<t.TCustomConfigSpeechResponse>,
|
||||||
): QueryObserverResult<t.TCustomConfigSpeechResponse> => {
|
): QueryObserverResult<t.TCustomConfigSpeechResponse> => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue