| | import pick from 'lodash/pick'; |
| | import { logger } from '@librechat/data-schemas'; |
| | import { CallToolResultSchema, ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js'; |
| | import type { RequestOptions } from '@modelcontextprotocol/sdk/shared/protocol.js'; |
| | import type { TokenMethods, IUser } from '@librechat/data-schemas'; |
| | import type { FlowStateManager } from '~/flow/manager'; |
| | import type { MCPOAuthTokens } from './oauth'; |
| | import type { RequestBody } from '~/types'; |
| | import type * as t from './types'; |
| | import { UserConnectionManager } from './UserConnectionManager'; |
| | import { ConnectionsRepository } from './ConnectionsRepository'; |
| | import { MCPServerInspector } from './registry/MCPServerInspector'; |
| | import { MCPServersInitializer } from './registry/MCPServersInitializer'; |
| | import { mcpServersRegistry as registry } from './registry/MCPServersRegistry'; |
| | import { formatToolContent } from './parsers'; |
| | import { MCPConnection } from './connection'; |
| | import { processMCPEnv } from '~/utils/env'; |
| |
|
| | |
| | |
| | |
| | |
| | export class MCPManager extends UserConnectionManager { |
| | private static instance: MCPManager | null; |
| |
|
| | |
| | public static async createInstance(configs: t.MCPServers): Promise<MCPManager> { |
| | if (MCPManager.instance) throw new Error('MCPManager has already been initialized.'); |
| | MCPManager.instance = new MCPManager(); |
| | await MCPManager.instance.initialize(configs); |
| | return MCPManager.instance; |
| | } |
| |
|
| | |
| | public static getInstance(): MCPManager { |
| | if (!MCPManager.instance) throw new Error('MCPManager has not been initialized.'); |
| | return MCPManager.instance; |
| | } |
| |
|
| | |
| | public async initialize(configs: t.MCPServers) { |
| | await MCPServersInitializer.initialize(configs); |
| | const appConfigs = await registry.sharedAppServers.getAll(); |
| | this.appConnections = new ConnectionsRepository(appConfigs); |
| | } |
| |
|
| | |
| | public async getConnection( |
| | args: { |
| | serverName: string; |
| | user?: IUser; |
| | forceNew?: boolean; |
| | flowManager?: FlowStateManager<MCPOAuthTokens | null>; |
| | } & Omit<t.OAuthConnectionOptions, 'useOAuth' | 'user' | 'flowManager'>, |
| | ): Promise<MCPConnection> { |
| | if (this.appConnections!.has(args.serverName)) { |
| | return this.appConnections!.get(args.serverName); |
| | } else if (args.user?.id) { |
| | return this.getUserConnection(args as Parameters<typeof this.getUserConnection>[0]); |
| | } else { |
| | throw new McpError( |
| | ErrorCode.InvalidRequest, |
| | `No connection found for server ${args.serverName}`, |
| | ); |
| | } |
| | } |
| |
|
| | |
| | public async getAppToolFunctions(): Promise<t.LCAvailableTools> { |
| | const toolFunctions: t.LCAvailableTools = {}; |
| | const configs = await registry.getAllServerConfigs(); |
| | for (const config of Object.values(configs)) { |
| | if (config.toolFunctions != null) { |
| | Object.assign(toolFunctions, config.toolFunctions); |
| | } |
| | } |
| | return toolFunctions; |
| | } |
| |
|
| | |
| | public async getServerToolFunctions( |
| | userId: string, |
| | serverName: string, |
| | ): Promise<t.LCAvailableTools | null> { |
| | try { |
| | if (this.appConnections?.has(serverName)) { |
| | return MCPServerInspector.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 MCPServerInspector.getToolFunctions(serverName, userConnections.get(serverName)!); |
| | } catch (error) { |
| | logger.warn( |
| | `[getServerToolFunctions] Error getting tool functions for server ${serverName}`, |
| | error, |
| | ); |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | private async getInstructions(serverNames?: string[]): Promise<Record<string, string>> { |
| | const instructions: Record<string, string> = {}; |
| | const configs = await registry.getAllServerConfigs(); |
| | for (const [serverName, config] of Object.entries(configs)) { |
| | if (config.serverInstructions != null) { |
| | instructions[serverName] = config.serverInstructions as string; |
| | } |
| | } |
| | if (!serverNames) return instructions; |
| | return pick(instructions, serverNames); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | public async formatInstructionsForContext(serverNames?: string[]): Promise<string> { |
| | |
| | const instructionsToInclude = await this.getInstructions(serverNames); |
| |
|
| | if (Object.keys(instructionsToInclude).length === 0) { |
| | return ''; |
| | } |
| |
|
| | |
| | const formattedInstructions = Object.entries(instructionsToInclude) |
| | .map(([serverName, instructions]) => { |
| | return `## ${serverName} MCP Server Instructions |
| | |
| | ${instructions}`; |
| | }) |
| | .join('\n\n'); |
| |
|
| | return `# MCP Server Instructions |
| | |
| | The following MCP servers are available with their specific instructions: |
| | |
| | ${formattedInstructions} |
| | |
| | Please follow these instructions when using tools from the respective MCP servers.`; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | async callTool({ |
| | user, |
| | serverName, |
| | toolName, |
| | provider, |
| | toolArguments, |
| | options, |
| | tokenMethods, |
| | requestBody, |
| | flowManager, |
| | oauthStart, |
| | oauthEnd, |
| | customUserVars, |
| | }: { |
| | user?: IUser; |
| | serverName: string; |
| | toolName: string; |
| | provider: t.Provider; |
| | toolArguments?: Record<string, unknown>; |
| | options?: RequestOptions; |
| | requestBody?: RequestBody; |
| | tokenMethods?: TokenMethods; |
| | customUserVars?: Record<string, string>; |
| | flowManager: FlowStateManager<MCPOAuthTokens | null>; |
| | oauthStart?: (authURL: string) => Promise<void>; |
| | oauthEnd?: () => Promise<void>; |
| | }): Promise<t.FormattedToolResponse> { |
| | |
| | let connection: MCPConnection | undefined; |
| | const userId = user?.id; |
| | const logPrefix = userId ? `[MCP][User: ${userId}][${serverName}]` : `[MCP][${serverName}]`; |
| |
|
| | try { |
| | if (userId && user) this.updateUserLastActivity(userId); |
| |
|
| | connection = await this.getConnection({ |
| | serverName, |
| | user, |
| | flowManager, |
| | tokenMethods, |
| | oauthStart, |
| | oauthEnd, |
| | signal: options?.signal, |
| | customUserVars, |
| | requestBody, |
| | }); |
| |
|
| | if (!(await connection.isConnected())) { |
| | |
| | throw new McpError( |
| | ErrorCode.InternalError, |
| | `${logPrefix} Connection is not active. Cannot execute tool ${toolName}.`, |
| | ); |
| | } |
| |
|
| | const rawConfig = (await registry.getServerConfig(serverName, userId)) as t.MCPOptions; |
| | const currentOptions = processMCPEnv({ |
| | user, |
| | options: rawConfig, |
| | customUserVars: customUserVars, |
| | body: requestBody, |
| | }); |
| | if ('headers' in currentOptions) { |
| | connection.setRequestHeaders(currentOptions.headers || {}); |
| | } |
| |
|
| | const result = await connection.client.request( |
| | { |
| | method: 'tools/call', |
| | params: { |
| | name: toolName, |
| | arguments: toolArguments, |
| | }, |
| | }, |
| | CallToolResultSchema, |
| | { |
| | timeout: connection.timeout, |
| | resetTimeoutOnProgress: true, |
| | ...options, |
| | }, |
| | ); |
| | if (userId) { |
| | this.updateUserLastActivity(userId); |
| | } |
| | this.checkIdleConnections(); |
| | return formatToolContent(result as t.MCPToolCallResponse, provider); |
| | } catch (error) { |
| | |
| | logger.error(`${logPrefix}[${toolName}] Tool call failed`, error); |
| | |
| | throw error; |
| | } |
| | } |
| | } |
| |
|