mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-06-10 10:05:18 +00:00
Some checks failed
Docker Dev Images Build / build (Dockerfile, librechat-dev, node) (push) Waiting to run
Docker Dev Images Build / build (Dockerfile.multi, librechat-dev-api, api-build) (push) Waiting to run
GitNexus Index / index (push) Waiting to run
GitNexus Index / post-index (push) Blocked by required conditions
Sync Locize Translations & Create Translation PR / Sync Translation Keys with Locize (push) Waiting to run
Sync Locize Translations & Create Translation PR / Create Translation PR on Version Published (push) Blocked by required conditions
Sync Helm Chart Tags / Ignore non-main push (push) Waiting to run
Sync Helm Chart Tags / Sync chart tags (push) Waiting to run
Publish `librechat-data-provider` to NPM / pack (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / pack (push) Has been cancelled
Publish `librechat-data-provider` to NPM / publish-npm (push) Has been cancelled
Publish `@librechat/data-schemas` to NPM / publish-npm (push) Has been cancelled
* Add OBO (On-Behalf-Of) token exchange support for MCP server connections Enables transparent authentication to Entra ID-backed MCP servers using the logged-in user's federated token via the OAuth 2.0 jwt-bearer grant. Configured via obo.scopes in librechat.yaml server config. - Extract generic OboTokenService from GraphTokenService (jwt-bearer grant + cache) - Refactor GraphTokenService to thin wrapper delegating to OboTokenService - Add obo schema field to BaseOptionsSchema in data-provider - Add resolveOboToken in packages/api/src/mcp/oauth/obo.ts (validates federated token, calls resolver, returns MCPOAuthTokens) - Wire oboTokenResolver through MCPConnectionFactory, MCPManager, UserConnectionManager - OBO tokens injected via request headers (not OAuth transport), refreshed on each tool call - Explicit error on OBO failure (no fallthrough to standard OAuth redirect) - Add unit tests for both resolveOboToken (9 tests) and exchangeOboToken (14 tests) * Add OBO authentication option to MCP server UI configuration Enable users to configure On-Behalf-Of (OBO) token exchange for MCP servers created via the UI (MongoDB-stored), in addition to the existing YAML-based configuration. - Add "On-Behalf-Of (OBO)" radio option to MCP server auth section with scopes input field - Remove obo from omitServerManagedFields so the field passes UI schema validation - Add OBO to AuthTypeEnum, obo_scopes to AuthConfig, and OBO handling in form defaults and submission - Add .min(1) validation on obo.scopes to reject empty strings - Add English localization keys: com_ui_obo, com_ui_obo_scopes, com_ui_obo_scopes_description - Add 5 schema validation tests for OBO field acceptance, transport compatibility, and edge cases * 🧊 fix: Add obo to safe properties in redactServerSecrets. Fixes the OBO configuration not showing up in the MCP UI after app restart * Address linter errors * 🧊 fix: fail closed on OBO refresh errors and retry transient token exchange failures - stop tool calls from falling back to stale Authorization headers when per-call OBO refresh fails - add one-time retry for transient Entra OBO exchange failures (network/429/5xx) - preserve structured OBO failure reasons and retryability in resolveOboToken - improve OBO auth error messaging for connection setup and tool execution - add tests for transient vs permanent OBO failure paths * Addressing linting errors / warnings * 🧊 fix: isolate OBO MCP auth to user-scoped connections - block OBO-enabled servers from app-level shared MCP connections - bypass shared connection lookup for OBO servers in MCPManager.getConnection - add regressions covering OBO connection scoping and preserve non-OBO app connection reuse * 🛠️ refactor: centralize MCP user-scoped connection policy - add shared requiresUserScopedConnection helper for OAuth, OBO, and customUserVars - use the shared predicate in MCPManager and ConnectionsRepository - add utils coverage for user-scoped connection policy * 🧊 fix: restrict MCP OBO config to header-capable transports - Move OBO configuration out of the shared MCP base options schema and allow it only on SSE and streamable-http transports, where request headers are applied. - Explicitly reject OBO on stdio and websocket configs to avoid accepted-but- nonfunctional server definitions. Add schema coverage for admin/config parsing and user-input websocket validation. * 🧊 fix: single-flight concurrent OBO token exchanges Concurrent tool calls that arrive on a cache miss were each issuing their own jwt-bearer request to the IdP. Under that fan-out, Entra intermittently returned errors that the retry classifier saw as non-retryable, surfacing as: "The identity provider rejected the OBO token exchange. Cannot execute tool <name>. Re-authenticate the user or verify the configured OBO scopes and retry." A user retry then hit the populated cache and succeeded, which matches the observed flakiness — the cache was empty at the moment of fan-out but populated by the time the user clicked retry. - Coalesce concurrent exchanges in `OboTokenService.exchangeOboToken` keyed by `${openidId}:${scopes}`. Callers that arrive while an exchange is in flight share the same upstream request and receive the same result. `fromCache=false` continues to force a fresh, independent exchange (and is not joined by `fromCache=true` callers). The IdP call, single-retry path, and cache write are unchanged — they were moved into a `performOboExchange` helper so the coalescing wrapper stays small. - Tests cover: coalescing on the same key, isolation between different keys, cleanup on success, cleanup on failure, and the `fromCache=false` bypass. * 🔒 feat: gate MCP OBO config behind MCP_SERVERS.CONFIGURE_OBO permission OBO silently mints per-user delegated tokens from the caller's federated access token and forwards them to whatever URL the server config points at. Previously, anyone with MCP_SERVERS.CREATE could configure obo.scopes — so if server creation is ever delegated beyond admins, a user could stand up an attacker-controlled server, attach it to a shared agent, and exfiltrate other users' downstream tokens on tool invocation. Add a dedicated MCP_SERVERS.CONFIGURE_OBO permission (ADMIN: true, USER: false by default) and enforce it at three layers so the safety property no longer depends on CREATE staying admin-only: - Create/update: POST/PATCH /api/mcp/servers returns 403 when the body carries `obo` and the caller's role lacks the permission. - Runtime fail-closed: for DB-sourced configs, MCPConnectionFactory and MCPManager.callTool re-check the original author's role before each OBO exchange. If the author has been downgraded, the exchange is skipped (factory) or refused (callTool) — retained configs lose their privileges automatically. - UI: the OBO option is hidden in the MCP server dialog for users without the permission; a CONFIGURE_OBO toggle is exposed in the MCP admin role editor. Existing role docs receive the new sub-key via the permission backfill in updateInterfacePermissions on next startup, preserving any operator-set values. YAML/Config-sourced server configs are unaffected since they're admin-controlled at the deployment level. * 🧊 fix: wire OBO machinery for servers with requiresOAuth: false The discovery and user-connection paths gated OAuth wiring (flow manager, token methods, oboTokenResolver, oboTrustChecker) behind isOAuthServer(), which only considers requiresOAuth/oauth fields. A DB-stored OBO server with requiresOAuth: false therefore landed in the non-OAuth branch, never received an oboTokenResolver, and the factory's usesObo getter evaluated to false — sending a bare request that the upstream rejected with invalid_token. Add requiresOAuthMachinery() (OAuth OR OBO) and use it at those two gates. isOAuthServer remains for the OAuth-handshake-only check (shouldInitiateOAuthBeforeConnect), where OBO must not initiate a handshake. Plumb the OBO resolver/trust-checker through ToolDiscoveryOptions so reinitMCPServer can pass them on the discovery path. * 🧊 fix: lock all OBO-target fields (URL, proxy, headers, auth) without CONFIGURE_OBO The CONFIGURE_OBO permission was meant to gate control of the endpoint that receives OBO-minted per-user delegated tokens and the scopes that are requested. The previous frontend lock + backend gate only covered obo.scopes and the auth section, leaving url/proxy/headers/etc. editable by anyone with UPDATE — meaning a non-permission user could still redirect an existing OBO server's token flow to an attacker endpoint. Switch to an allowlist policy: when editing an OBO server without CONFIGURE_OBO, only title/description/iconPath are mutable. Backend rejects any other field change with 403; frontend disables the non-allowlist sections (URL, transport, auth, trust) via fieldset. The comparison surface (MCP_USER_INPUT_FIELDS) is derived from MCPServerUserInputSchema's union members so it stays in sync with the schema. New schema fields land in the locked set by default — adding to the allowlist is the only way to unlock them, which preserves the security-review boundary. * 🧊 fix: skip unauthenticated MCP inspection for OBO-only servers MCPServerInspector.inspectServer() ran an unauthenticated temp connection unless the config had requiresOAuth or customUserVars set. For OBO-only servers without standard MCP OAuth advertisement, this caused MCPConnectionFactory.create to attempt the connection without a user or oboTokenResolver — failing on servers that reject the MCP initialize handshake without a valid bearer token, which surfaced as MCP_INSPECTION_FAILED on create/update. Add `obo` to the skip list alongside requiresOAuth and customUserVars, matching the existing pattern for user-scoped auth modes. * Addressed linting error: watchedTitle is declared but never referenced (the auto-fill logic at line 156 uses getValues('title') instead). Deleted constant.
432 lines
14 KiB
JavaScript
432 lines
14 KiB
JavaScript
/**
|
|
* MCP Tools Controller
|
|
* Handles MCP-specific tool endpoints, decoupled from regular LibreChat tools
|
|
*
|
|
* @import { MCPServerRegistry } from '@librechat/api'
|
|
* @import { MCPServerDocument } from 'librechat-data-provider'
|
|
*/
|
|
const { logger } = require('@librechat/data-schemas');
|
|
const {
|
|
checkAccess,
|
|
MCPErrorCodes,
|
|
redactServerSecrets,
|
|
redactAllServerSecrets,
|
|
isMCPDomainNotAllowedError,
|
|
isMCPInspectionFailedError,
|
|
} = require('@librechat/api');
|
|
const {
|
|
Constants,
|
|
Permissions,
|
|
PermissionTypes,
|
|
MCPServerUserInputSchema,
|
|
MCP_USER_INPUT_FIELDS,
|
|
} = require('librechat-data-provider');
|
|
const {
|
|
resolveConfigServers,
|
|
resolveMcpConfigNames,
|
|
resolveAllMcpConfigs,
|
|
} = require('~/server/services/MCP');
|
|
const { cacheMCPServerTools, getMCPServerTools } = require('~/server/services/Config');
|
|
const { getMCPManager, getMCPServersRegistry } = require('~/config');
|
|
const db = require('~/models');
|
|
|
|
/**
|
|
* Handles MCP-specific errors and sends appropriate HTTP responses.
|
|
* @param {Error} error - The error to handle
|
|
* @param {import('express').Response} res - Express response object
|
|
* @returns {import('express').Response | null} Response if handled, null if not an MCP error
|
|
*/
|
|
function handleMCPError(error, res) {
|
|
if (isMCPDomainNotAllowedError(error)) {
|
|
return res.status(error.statusCode).json({
|
|
error: error.code,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
if (isMCPInspectionFailedError(error)) {
|
|
return res.status(error.statusCode).json({
|
|
error: error.code,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
// Fallback for legacy string-based error handling (backwards compatibility)
|
|
if (error.message?.startsWith(MCPErrorCodes.DOMAIN_NOT_ALLOWED)) {
|
|
return res.status(403).json({
|
|
error: MCPErrorCodes.DOMAIN_NOT_ALLOWED,
|
|
message: error.message.replace(/^MCP_DOMAIN_NOT_ALLOWED\s*:\s*/i, ''),
|
|
});
|
|
}
|
|
|
|
if (error.message?.startsWith(MCPErrorCodes.INSPECTION_FAILED)) {
|
|
return res.status(400).json({
|
|
error: MCPErrorCodes.INSPECTION_FAILED,
|
|
message: error.message,
|
|
});
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get all MCP tools available to the user.
|
|
*/
|
|
const getMCPTools = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
logger.warn('[getMCPTools] User ID not found in request');
|
|
return res.status(401).json({ message: 'Unauthorized' });
|
|
}
|
|
|
|
const mcpConfig = await resolveAllMcpConfigs(userId, req.user);
|
|
const configuredServers = Object.keys(mcpConfig);
|
|
|
|
if (!configuredServers.length) {
|
|
return res.status(200).json({ servers: {} });
|
|
}
|
|
|
|
const mcpManager = getMCPManager();
|
|
const mcpServers = {};
|
|
|
|
const serverToolsMap = new Map();
|
|
const cacheResults = await Promise.all(
|
|
configuredServers.map(async (serverName) => {
|
|
try {
|
|
return {
|
|
serverName,
|
|
tools: await getMCPServerTools(userId, serverName),
|
|
};
|
|
} catch (error) {
|
|
logger.error(`[getMCPTools] Error fetching cached tools for ${serverName}:`, error);
|
|
return { serverName, tools: null };
|
|
}
|
|
}),
|
|
);
|
|
for (const { serverName, tools } of cacheResults) {
|
|
if (tools) {
|
|
serverToolsMap.set(serverName, tools);
|
|
continue;
|
|
}
|
|
|
|
let serverTools;
|
|
try {
|
|
serverTools = await mcpManager.getServerToolFunctions(userId, serverName);
|
|
} catch (error) {
|
|
logger.error(`[getMCPTools] Error fetching tools for server ${serverName}:`, error);
|
|
continue;
|
|
}
|
|
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({ userId, serverName, serverTools }).catch((err) =>
|
|
logger.error(`[getMCPTools] Failed to cache tools for ${serverName}:`, err),
|
|
);
|
|
}
|
|
}
|
|
|
|
// Process each configured server
|
|
for (const serverName of configuredServers) {
|
|
try {
|
|
const serverTools = serverToolsMap.get(serverName);
|
|
|
|
const serverConfig = mcpConfig[serverName];
|
|
|
|
const server = {
|
|
name: serverName,
|
|
icon: serverConfig?.iconPath || '',
|
|
authenticated: true,
|
|
authConfig: [],
|
|
tools: [],
|
|
};
|
|
|
|
// Set authentication config once for the server
|
|
if (serverConfig?.customUserVars) {
|
|
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 || '',
|
|
sensitive: value.sensitive,
|
|
}));
|
|
server.authenticated = false;
|
|
}
|
|
}
|
|
|
|
// Process tools efficiently - no need for convertMCPToolToPlugin
|
|
if (serverTools) {
|
|
for (const [toolKey, toolData] of Object.entries(serverTools)) {
|
|
if (!toolData.function || !toolKey.includes(Constants.mcp_delimiter)) {
|
|
continue;
|
|
}
|
|
|
|
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) {
|
|
logger.error(`[getMCPTools] Error loading tools for server ${serverName}:`, error);
|
|
}
|
|
}
|
|
|
|
res.status(200).json({ servers: mcpServers });
|
|
} catch (error) {
|
|
logger.error('[getMCPTools]', error);
|
|
res.status(500).json({ message: error.message });
|
|
}
|
|
};
|
|
/**
|
|
* Get all MCP servers with permissions
|
|
* @route GET /api/mcp/servers
|
|
*/
|
|
const getMCPServersList = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
if (!userId) {
|
|
return res.status(401).json({ message: 'Unauthorized' });
|
|
}
|
|
|
|
const serverConfigs = await resolveAllMcpConfigs(userId, req.user);
|
|
return res.json(redactAllServerSecrets(serverConfigs));
|
|
} catch (error) {
|
|
logger.error('[getMCPServersList]', error);
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns true when the request body's parsed config configures OBO. We block
|
|
* non-permission holders from creating or updating any DB-stored MCP server
|
|
* that mints per-user delegated tokens.
|
|
*/
|
|
function configHasObo(parsedConfig) {
|
|
return (
|
|
!!parsedConfig &&
|
|
typeof parsedConfig === 'object' &&
|
|
'obo' in parsedConfig &&
|
|
parsedConfig.obo != null
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Fields a user without `CONFIGURE_OBO` may modify on an OBO server (allowlist).
|
|
* Any field not on this list is locked: changes to it (add, modify, or remove)
|
|
* require the permission. Allowlisting is fail-closed — when upstream introduces
|
|
* a new MCP server config field, it lands in the locked set by default until
|
|
* explicitly opted in here. Anything that could redirect the OBO token flow
|
|
* (`url`, `proxy`, `headers`), change scopes (`obo`), or reroute auth (`oauth`,
|
|
* `apiKey`, `customUserVars`) MUST stay locked.
|
|
*/
|
|
const OBO_USER_EDITABLE_FIELDS = new Set(['title', 'description', 'iconPath']);
|
|
|
|
/**
|
|
* Returns true when any non-allowlisted user-input field differs between the
|
|
* existing server config and the new payload. Treats add, remove, and modify
|
|
* as changes (stable JSON compare, with absence on either side counting as a
|
|
* change unless both sides are absent). The comparison surface is
|
|
* `MCP_USER_INPUT_FIELDS` (schema-derived from `MCPServerUserInputSchema`),
|
|
* so new fields on the schema are picked up automatically and stay locked
|
|
* by default until added to the allowlist above.
|
|
*/
|
|
function violatesOboLockdown(existingConfig, newConfig) {
|
|
for (const field of MCP_USER_INPUT_FIELDS) {
|
|
if (OBO_USER_EDITABLE_FIELDS.has(field)) continue;
|
|
const existing = existingConfig?.[field];
|
|
const next = newConfig?.[field];
|
|
if (existing === undefined && next === undefined) continue;
|
|
if (JSON.stringify(existing) !== JSON.stringify(next)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
async function callerCanConfigureObo(req) {
|
|
return checkAccess({
|
|
req,
|
|
user: req.user,
|
|
permissionType: PermissionTypes.MCP_SERVERS,
|
|
permissions: [Permissions.CONFIGURE_OBO],
|
|
getRoleByName: db.getRoleByName,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create MCP server
|
|
* @route POST /api/mcp/servers
|
|
*/
|
|
const createMCPServerController = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
const { config } = req.body;
|
|
|
|
const validation = MCPServerUserInputSchema.safeParse(config);
|
|
if (!validation.success) {
|
|
return res.status(400).json({
|
|
message: 'Invalid configuration',
|
|
errors: validation.error.errors,
|
|
});
|
|
}
|
|
if (configHasObo(validation.data) && !(await callerCanConfigureObo(req))) {
|
|
logger.warn(
|
|
`[createMCPServer] User ${userId} attempted to configure OBO without ${Permissions.CONFIGURE_OBO} permission`,
|
|
);
|
|
return res
|
|
.status(403)
|
|
.json({ message: 'Forbidden: Insufficient permissions to configure OBO' });
|
|
}
|
|
const reservedServerNames = await resolveMcpConfigNames(req);
|
|
const result = await getMCPServersRegistry().addServer(
|
|
'temp_server_name',
|
|
validation.data,
|
|
'DB',
|
|
userId,
|
|
reservedServerNames,
|
|
);
|
|
res.status(201).json({
|
|
serverName: result.serverName,
|
|
...redactServerSecrets(result.config),
|
|
});
|
|
} catch (error) {
|
|
logger.error('[createMCPServer]', error);
|
|
const mcpErrorResponse = handleMCPError(error, res);
|
|
if (mcpErrorResponse) {
|
|
return mcpErrorResponse;
|
|
}
|
|
res.status(500).json({ message: error.message });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get MCP server by ID
|
|
*/
|
|
const getMCPServerById = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
const { serverName } = req.params;
|
|
if (!serverName) {
|
|
return res.status(400).json({ message: 'Server name is required' });
|
|
}
|
|
const configServers = await resolveConfigServers(req);
|
|
const parsedConfig = await getMCPServersRegistry().getServerConfig(
|
|
serverName,
|
|
userId,
|
|
configServers,
|
|
);
|
|
|
|
if (!parsedConfig) {
|
|
return res.status(404).json({ message: 'MCP server not found' });
|
|
}
|
|
|
|
res.status(200).json(redactServerSecrets(parsedConfig));
|
|
} catch (error) {
|
|
logger.error('[getMCPServerById]', error);
|
|
res.status(500).json({ message: error.message });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update MCP server
|
|
* @route PATCH /api/mcp/servers/:serverName
|
|
*/
|
|
const updateMCPServerController = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
const { serverName } = req.params;
|
|
const { config } = req.body;
|
|
|
|
const validation = MCPServerUserInputSchema.safeParse(config);
|
|
if (!validation.success) {
|
|
return res.status(400).json({
|
|
message: 'Invalid configuration',
|
|
errors: validation.error.errors,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* On an existing OBO server, lock down every user-input field except the
|
|
* cosmetic allowlist (title, description, iconPath) for callers without
|
|
* CONFIGURE_OBO. This closes the OBO redirect vector — without it, a user
|
|
* with UPDATE could change `url` (or `proxy`/`headers`/`customUserVars`)
|
|
* to point OBO-minted tokens at an attacker-controlled endpoint. Adds,
|
|
* modifies, and removes are all caught.
|
|
*/
|
|
const existingConfig = await getMCPServersRegistry().getServerConfig(serverName, userId);
|
|
if (configHasObo(existingConfig) && !(await callerCanConfigureObo(req))) {
|
|
if (violatesOboLockdown(existingConfig, validation.data)) {
|
|
logger.warn(
|
|
`[updateMCPServer] User ${userId} attempted to modify a locked field on OBO server '${serverName}' without ${Permissions.CONFIGURE_OBO} permission`,
|
|
);
|
|
return res
|
|
.status(403)
|
|
.json({ message: 'Forbidden: Insufficient permissions to configure OBO' });
|
|
}
|
|
} else if (configHasObo(validation.data) && !(await callerCanConfigureObo(req))) {
|
|
// Adding OBO to a non-OBO server (or first-time configuration) still
|
|
// requires the permission, even if existing has no OBO.
|
|
logger.warn(
|
|
`[updateMCPServer] User ${userId} attempted to add OBO to '${serverName}' without ${Permissions.CONFIGURE_OBO} permission`,
|
|
);
|
|
return res
|
|
.status(403)
|
|
.json({ message: 'Forbidden: Insufficient permissions to configure OBO' });
|
|
}
|
|
|
|
const parsedConfig = await getMCPServersRegistry().updateServer(
|
|
serverName,
|
|
validation.data,
|
|
'DB',
|
|
userId,
|
|
);
|
|
|
|
res.status(200).json(redactServerSecrets(parsedConfig));
|
|
} catch (error) {
|
|
logger.error('[updateMCPServer]', error);
|
|
const mcpErrorResponse = handleMCPError(error, res);
|
|
if (mcpErrorResponse) {
|
|
return mcpErrorResponse;
|
|
}
|
|
res.status(500).json({ message: error.message });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Delete MCP server
|
|
* @route DELETE /api/mcp/servers/:serverName
|
|
*/
|
|
const deleteMCPServerController = async (req, res) => {
|
|
try {
|
|
const userId = req.user?.id;
|
|
const { serverName } = req.params;
|
|
await getMCPServersRegistry().removeServer(serverName, 'DB', userId);
|
|
res.status(200).json({ message: 'MCP server deleted successfully' });
|
|
} catch (error) {
|
|
logger.error('[deleteMCPServer]', error);
|
|
res.status(500).json({ message: error.message });
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
getMCPTools,
|
|
getMCPServersList,
|
|
createMCPServerController,
|
|
getMCPServerById,
|
|
updateMCPServerController,
|
|
deleteMCPServerController,
|
|
};
|