mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-10 10:02:36 +01:00
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run
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
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
* feat: MCP server reinitialization recovery mechanism - Added functionality to store a stub configuration for MCP servers that fail inspection at startup, allowing for recovery via reinitialization. - Introduced `reinspectServer` method in `MCPServersRegistry` to handle reinspection of previously failed servers. - Enhanced `MCPServersInitializer` to log and manage server initialization failures, ensuring proper handling of inspection failures. - Added integration tests to verify the recovery process for unreachable MCP servers, ensuring that stub configurations are stored and can be reinitialized successfully. - Updated type definitions to include `inspectionFailed` flag in server configurations for better state management. * fix: MCP server handling for inspection failures - Updated `reinitMCPServer` to return a structured response when the server is unreachable, providing clearer feedback on the failure. - Modified `ConnectionsRepository` to prevent connections to servers marked as inspection failed, improving error handling. - Adjusted `MCPServersRegistry` methods to ensure proper management of server states, including throwing errors for non-failed servers during reinspection. - Enhanced integration tests to validate the behavior of the system when dealing with unreachable MCP servers and inspection failures, ensuring robust recovery mechanisms. * fix: Clear all cached server configurations in MCPServersRegistry - Added a comment to clarify the necessity of clearing all cached server configurations when updating a server's configuration, as the cache is keyed by userId without a reverse index for enumeration. * fix: Update integration test for file_tools_server inspection handling - Modified the test to verify that the `file_tools_server` is stored as a stub when inspection fails, ensuring it can be reinitialized correctly. - Adjusted expectations to confirm that the `inspectionFailed` flag is set to true for the stub configuration, enhancing the robustness of the recovery mechanism. * test: Add unit tests for reinspecting servers in MCPServersRegistry - Introduced tests for the `reinspectServer` method to validate error handling when called on a healthy server and when the server does not exist. - Ensured that appropriate exceptions are thrown for both scenarios, enhancing the robustness of server state management. * test: Add integration test for concurrent reinspectServer calls - Introduced a new test to validate that multiple concurrent calls to reinspectServer do not crash or corrupt the server state. - Ensured that at least one call succeeds and any failures are due to the server not being in a failed state, enhancing the reliability of the reinitialization process. * test: Enhance integration test for concurrent MCP server reinitialization - Added a new test to validate that concurrent calls to reinitialize the MCP server do not crash or corrupt the server state. - Ensured that at least one call succeeds and that failures are handled gracefully, improving the reliability of the reinitialization process. - Reset MCPManager instance after each test to maintain a clean state for subsequent tests.
210 lines
6.8 KiB
JavaScript
210 lines
6.8 KiB
JavaScript
const { logger } = require('@librechat/data-schemas');
|
|
const { CacheKeys, Constants } = require('librechat-data-provider');
|
|
const { getMCPManager, getMCPServersRegistry, getFlowStateManager } = require('~/config');
|
|
const { findToken, createToken, updateToken, deleteTokens } = require('~/models');
|
|
const { updateMCPServerTools } = require('~/server/services/Config');
|
|
const { getLogStores } = require('~/cache');
|
|
|
|
/**
|
|
* Reinitializes an MCP server connection and discovers available tools.
|
|
* When OAuth is required, uses discovery mode to list tools without full authentication
|
|
* (per MCP spec, tool listing should be possible without auth).
|
|
* @param {Object} params
|
|
* @param {IUser} params.user - The user from the request object.
|
|
* @param {string} params.serverName - The name of the MCP server
|
|
* @param {boolean} params.returnOnOAuth - Whether to initiate OAuth and return, or wait for OAuth flow to finish
|
|
* @param {AbortSignal} [params.signal] - The abort signal to handle cancellation.
|
|
* @param {boolean} [params.forceNew]
|
|
* @param {number} [params.connectionTimeout]
|
|
* @param {FlowStateManager<any>} [params.flowManager]
|
|
* @param {(authURL: string) => Promise<void>} [params.oauthStart]
|
|
* @param {Record<string, Record<string, string>>} [params.userMCPAuthMap]
|
|
*/
|
|
async function reinitMCPServer({
|
|
user,
|
|
signal,
|
|
forceNew,
|
|
serverName,
|
|
userMCPAuthMap,
|
|
connectionTimeout,
|
|
returnOnOAuth = true,
|
|
oauthStart: _oauthStart,
|
|
flowManager: _flowManager,
|
|
}) {
|
|
/** @type {MCPConnection | null} */
|
|
let connection = null;
|
|
/** @type {LCAvailableTools | null} */
|
|
let availableTools = null;
|
|
/** @type {ReturnType<MCPConnection['fetchTools']> | null} */
|
|
let tools = null;
|
|
let oauthRequired = false;
|
|
let oauthUrl = null;
|
|
|
|
try {
|
|
const registry = getMCPServersRegistry();
|
|
const serverConfig = await registry.getServerConfig(serverName, user?.id);
|
|
if (serverConfig?.inspectionFailed) {
|
|
logger.info(
|
|
`[MCP Reinitialize] Server ${serverName} had failed inspection, attempting reinspection`,
|
|
);
|
|
try {
|
|
const storageLocation = serverConfig.dbId ? 'DB' : 'CACHE';
|
|
await registry.reinspectServer(serverName, storageLocation, user?.id);
|
|
logger.info(`[MCP Reinitialize] Reinspection succeeded for server: ${serverName}`);
|
|
} catch (reinspectError) {
|
|
logger.error(
|
|
`[MCP Reinitialize] Reinspection failed for server ${serverName}:`,
|
|
reinspectError,
|
|
);
|
|
return {
|
|
availableTools: null,
|
|
success: false,
|
|
message: `MCP server '${serverName}' is still unreachable`,
|
|
oauthRequired: false,
|
|
serverName,
|
|
oauthUrl: null,
|
|
tools: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
const customUserVars = userMCPAuthMap?.[`${Constants.mcp_prefix}${serverName}`];
|
|
const flowManager = _flowManager ?? getFlowStateManager(getLogStores(CacheKeys.FLOWS));
|
|
const mcpManager = getMCPManager();
|
|
const tokenMethods = { findToken, updateToken, createToken, deleteTokens };
|
|
|
|
const oauthStart =
|
|
_oauthStart ??
|
|
(async (authURL) => {
|
|
logger.info(`[MCP Reinitialize] OAuth URL received for ${serverName}`);
|
|
oauthUrl = authURL;
|
|
oauthRequired = true;
|
|
});
|
|
|
|
try {
|
|
connection = await mcpManager.getConnection({
|
|
user,
|
|
signal,
|
|
forceNew,
|
|
oauthStart,
|
|
serverName,
|
|
flowManager,
|
|
tokenMethods,
|
|
returnOnOAuth,
|
|
customUserVars,
|
|
connectionTimeout,
|
|
});
|
|
|
|
logger.info(`[MCP Reinitialize] Successfully established connection for ${serverName}`);
|
|
} catch (err) {
|
|
logger.info(`[MCP Reinitialize] getConnection threw error: ${err.message}`);
|
|
logger.info(
|
|
`[MCP Reinitialize] OAuth state - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
|
|
);
|
|
|
|
const isOAuthError =
|
|
err.message?.includes('OAuth') ||
|
|
err.message?.includes('authentication') ||
|
|
err.message?.includes('401');
|
|
|
|
const isOAuthFlowInitiated = err.message === 'OAuth flow initiated - return early';
|
|
|
|
if (isOAuthError || oauthRequired || isOAuthFlowInitiated) {
|
|
logger.info(
|
|
`[MCP Reinitialize] OAuth required for ${serverName}, attempting tool discovery without auth`,
|
|
);
|
|
oauthRequired = true;
|
|
|
|
try {
|
|
const discoveryResult = await mcpManager.discoverServerTools({
|
|
user,
|
|
signal,
|
|
serverName,
|
|
flowManager,
|
|
tokenMethods,
|
|
oauthStart,
|
|
customUserVars,
|
|
connectionTimeout,
|
|
});
|
|
|
|
if (discoveryResult.tools && discoveryResult.tools.length > 0) {
|
|
tools = discoveryResult.tools;
|
|
logger.info(
|
|
`[MCP Reinitialize] Discovered ${tools.length} tools for ${serverName} without full auth`,
|
|
);
|
|
}
|
|
} catch (discoveryErr) {
|
|
logger.debug(
|
|
`[MCP Reinitialize] Tool discovery failed for ${serverName}: ${discoveryErr?.message ?? String(discoveryErr)}`,
|
|
);
|
|
}
|
|
} else {
|
|
logger.error(
|
|
`[MCP Reinitialize] Error initializing MCP server ${serverName} for user:`,
|
|
err,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (connection && !oauthRequired) {
|
|
tools = await connection.fetchTools();
|
|
}
|
|
|
|
if (tools && tools.length > 0) {
|
|
availableTools = await updateMCPServerTools({
|
|
userId: user.id,
|
|
serverName,
|
|
tools,
|
|
});
|
|
}
|
|
|
|
logger.debug(
|
|
`[MCP Reinitialize] Sending response for ${serverName} - oauthRequired: ${oauthRequired}, oauthUrl: ${oauthUrl ? 'present' : 'null'}`,
|
|
);
|
|
|
|
const getResponseMessage = () => {
|
|
if (oauthRequired && tools && tools.length > 0) {
|
|
return `MCP server '${serverName}' tools discovered, OAuth required for execution`;
|
|
}
|
|
if (oauthRequired) {
|
|
return `MCP server '${serverName}' ready for OAuth authentication`;
|
|
}
|
|
if (connection) {
|
|
return `MCP server '${serverName}' reinitialized successfully`;
|
|
}
|
|
return `Failed to reinitialize MCP server '${serverName}'`;
|
|
};
|
|
|
|
const result = {
|
|
availableTools,
|
|
success: Boolean(
|
|
(connection && !oauthRequired) ||
|
|
(oauthRequired && oauthUrl) ||
|
|
(tools && tools.length > 0),
|
|
),
|
|
message: getResponseMessage(),
|
|
oauthRequired,
|
|
serverName,
|
|
oauthUrl,
|
|
tools,
|
|
};
|
|
|
|
logger.debug(`[MCP Reinitialize] Response for ${serverName}:`, {
|
|
success: result.success,
|
|
oauthRequired: result.oauthRequired,
|
|
oauthUrl: result.oauthUrl ? 'present' : null,
|
|
toolsCount: tools?.length ?? 0,
|
|
});
|
|
|
|
return result;
|
|
} catch (error) {
|
|
logger.error(
|
|
'[MCP Reinitialize] Error loading MCP Tools, servers may still be initializing:',
|
|
error,
|
|
);
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
reinitMCPServer,
|
|
};
|