UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

411 lines (355 loc) 13.4 kB
import { LobeChatPluginApi, LobeChatPluginManifest, PluginSchema } from '@lobehub/chat-plugin-sdk'; import { DeploymentOption } from '@lobehub/market-sdk'; import { McpError } from '@modelcontextprotocol/sdk/types.js'; import { TRPCError } from '@trpc/server'; import debug from 'debug'; import { MCPClient, MCPClientParams, McpPrompt, McpResource, McpTool, StdioMCPParams, } from '@/libs/mcp'; import { mcpSystemDepsCheckService } from '@/server/services/mcp/deps'; import { CheckMcpInstallResult } from '@/types/plugins'; import { CustomPluginMetadata } from '@/types/tool/plugin'; import { safeParseJSON } from '@/utils/safeParseJSON'; const log = debug('lobe-mcp:service'); // Removed MCPConnection interface as it's no longer needed class MCPService { // Store instances of the custom MCPClient, keyed by serialized MCPClientParams private clients: Map<string, MCPClient> = new Map(); private sanitizeForLogging = <T extends Record<string, any>>(obj: T): Omit<T, 'env'> => { if (!obj) return obj; // eslint-disable-next-line @typescript-eslint/no-unused-vars const { env: _, ...rest } = obj; return rest as Omit<T, 'env'>; }; // --- MCP Interaction --- // listTools now accepts MCPClientParams async listTools(params: MCPClientParams): Promise<LobeChatPluginApi[]> { const client = await this.getClient(params); // Get client using params const loggableParams = this.sanitizeForLogging(params); log(`Listing tools using client for params: %O`, loggableParams); try { const result = await client.listTools(); log( `Tools listed successfully for params: %O, result count: %d`, loggableParams, result.length, ); return result.map<LobeChatPluginApi>((item) => ({ // Assuming identifier is the unique name/id description: item.description, name: item.name, parameters: item.inputSchema as PluginSchema, })); } catch (error) { console.error(`Error listing tools for params %O:`, loggableParams, error); // Propagate a TRPCError for better handling upstream throw new TRPCError({ cause: error, code: 'INTERNAL_SERVER_ERROR', message: `Error listing tools from MCP server: ${(error as Error).message}`, }); } } // listTools now accepts MCPClientParams async listRawTools(params: MCPClientParams): Promise<McpTool[]> { const client = await this.getClient(params); // Get client using params const loggableParams = this.sanitizeForLogging(params); log(`Listing tools using client for params: %O`, loggableParams); try { const result = await client.listTools(); log( `Tools listed successfully for params: %O, result count: %d`, loggableParams, result.length, ); return result; } catch (error) { console.error(`Error listing tools for params %O:`, loggableParams, error); // Propagate a TRPCError for better handling upstream throw new TRPCError({ cause: error, code: 'INTERNAL_SERVER_ERROR', message: `Error listing tools from MCP server: ${(error as Error).message}`, }); } } // listResources now accepts MCPClientParams async listResources(params: MCPClientParams): Promise<McpResource[]> { const client = await this.getClient(params); // Get client using params const loggableParams = this.sanitizeForLogging(params); log(`Listing resources using client for params: %O`, loggableParams); try { const result = await client.listResources(); log( `Resources listed successfully for params: %O, result count: %d`, loggableParams, result.length, ); return result; } catch (error) { console.error(`Error listing resources for params %O:`, loggableParams, error); // Propagate a TRPCError for better handling upstream throw new TRPCError({ cause: error, code: 'INTERNAL_SERVER_ERROR', message: `Error listing resources from MCP server: ${(error as Error).message}`, }); } } // listPrompts now accepts MCPClientParams async listPrompts(params: MCPClientParams): Promise<McpPrompt[]> { const client = await this.getClient(params); // Get client using params const loggableParams = this.sanitizeForLogging(params); log(`Listing prompts using client for params: %O`, loggableParams); try { const result = await client.listPrompts(); log( `Prompts listed successfully for params: %O, result count: %d`, loggableParams, result.length, ); return result; } catch (error) { console.error(`Error listing prompts for params %O:`, loggableParams, error); // Propagate a TRPCError for better handling upstream throw new TRPCError({ cause: error, code: 'INTERNAL_SERVER_ERROR', message: `Error listing prompts from MCP server: ${(error as Error).message}`, }); } } // callTool now accepts MCPClientParams, toolName, and args async callTool(params: MCPClientParams, toolName: string, argsStr: any): Promise<any> { const client = await this.getClient(params); // Get client using params const args = safeParseJSON(argsStr); const loggableParams = this.sanitizeForLogging(params); log( `Calling tool "${toolName}" using client for params: %O with args: %O`, loggableParams, args, ); try { // Delegate the call to the MCPClient instance const result = await client.callTool(toolName, args); // Pass args directly log( `Tool "${toolName}" called successfully for params: %O, result: %O`, loggableParams, result, ); const { content, isError } = result; if (isError) return result; const data = content as { text: string; type: 'text' }[]; const text = data?.[0]?.text; if (!text) return data; // try to get json object, which will be stringify in the client const json = safeParseJSON(text); if (json) return json; return text; } catch (error) { if (error instanceof McpError) { const mcpError = error as McpError; return mcpError.message; } console.error( `Error calling tool "${toolName}" for params %O:`, this.sanitizeForLogging(params), error, ); // Propagate a TRPCError throw new TRPCError({ cause: error, code: 'INTERNAL_SERVER_ERROR', message: `Error calling tool "${toolName}" on MCP server: ${(error as Error).message}`, }); } } // Private method to get or initialize a client based on parameters private async getClient(params: MCPClientParams): Promise<MCPClient> { const key = this.serializeParams(params); // Use custom serialization if (this.clients.has(key)) { return this.clients.get(key)!; } log(`No cached client found, Initializing new client.`); try { const client = new MCPClient(params); await client.initialize({ onProgress: (progress) => { log(`New client initializing... ${progress.progress}/${progress.total}`); }, }); // Initialization logic should be within MCPClient this.clients.set(key, client); log(`New client initialized and cached for key: ${key.slice(0, 20)}`); return client; } catch (error) { console.error(`Failed to initialize MCP client:`, error); // 保留完整的错误信息,特别是详细的 stderr 输出 const errorMessage = error instanceof Error ? error.message : String(error); if (typeof error === 'object' && !!error && 'data' in error) { throw new TRPCError({ cause: error, code: 'SERVICE_UNAVAILABLE', message: errorMessage, }); } // 记录详细的错误信息用于调试 log('Detailed initialization error: %O', { error: errorMessage, params: this.sanitizeForLogging(params), stack: error instanceof Error ? error.stack : undefined, }); throw new TRPCError({ cause: error, code: 'INTERNAL_SERVER_ERROR', message: errorMessage, // 直接使用完整的错误信息 }); } } // Custom serialization function to ensure consistent keys private serializeParams(params: MCPClientParams): string { const sortedKeys = Object.keys(params).sort(); const sortedParams: Record<string, any> = {}; for (const key of sortedKeys) { const value = (params as any)[key]; // Sort the 'args' array if it exists if (key === 'args' && Array.isArray(value)) { sortedParams[key] = JSON.stringify(key); } else { sortedParams[key] = value; } } return JSON.stringify(sortedParams); } async getStreamableMcpServerManifest( identifier: string, url: string, metadata?: CustomPluginMetadata, auth?: { accessToken?: string; token?: string; type: 'none' | 'bearer' | 'oauth2'; }, headers?: Record<string, string>, ): Promise<LobeChatPluginManifest> { const mcpParams = { name: identifier, type: 'http' as const, url }; // 如果有认证信息,添加到参数中 if (auth) { (mcpParams as any).auth = auth; } // 如果有 headers 信息,添加到参数中 if (headers) { (mcpParams as any).headers = headers; } const tools = await this.listTools(mcpParams); return { api: tools, identifier, meta: { avatar: metadata?.avatar || 'MCP_AVATAR', description: metadata?.description || `${identifier} MCP server has ${tools.length} tools, like "${tools[0]?.name}"`, title: identifier, }, // TODO: temporary type: 'mcp' as any, }; } async getStdioMcpServerManifest( params: Omit<StdioMCPParams, 'type'>, metadata?: CustomPluginMetadata, ): Promise<LobeChatPluginManifest> { const client = await this.getClient({ args: params.args, command: params.command, env: params.env, name: params.name, type: 'stdio', }); // Get client using params const manifest = await client.listManifests(); const identifier = params.name; return { api: manifest.tools ? this.transformMCPToolToLobeAPI(manifest.tools) : [], identifier, meta: { avatar: metadata?.avatar || 'MCP_AVATAR', description: metadata?.description || `${identifier} MCP server has ` + Object.entries(manifest) .filter(([key]) => ['tools', 'prompts', 'resources'].includes(key)) .map(([key, item]) => `${(item as Array<any>)?.length} ${key}`) .join(','), title: metadata?.name || identifier, }, ...manifest, // TODO: temporary type: 'mcp' as any, } as LobeChatPluginManifest; } /** * Check MCP plugin installation status */ async checkMcpInstall(input: { deploymentOptions: DeploymentOption[]; }): Promise<CheckMcpInstallResult> { try { const loggableInput = { deploymentOptions: input.deploymentOptions.map((o) => this.sanitizeForLogging(o)), }; log('Checking MCP plugin installation status: %O', loggableInput); const results = []; // 检查每个部署选项 for (const option of input.deploymentOptions) { // 使用系统依赖检查服务检查部署选项 const result = await mcpSystemDepsCheckService.checkDeployOption(option); results.push(result); } // 找出推荐的或第一个可安装的选项 const recommendedResult = results.find((r) => r.isRecommended && r.allDependenciesMet); const firstInstallableResult = results.find((r) => r.allDependenciesMet); // 返回推荐的结果,或第一个可安装的结果,或第一个结果 const bestResult = recommendedResult || firstInstallableResult || results[0]; log('Check completed, best result: %O', bestResult); // 构造返回结果,确保包含配置检查信息 const checkResult: CheckMcpInstallResult = { ...bestResult, allOptions: results, platform: process.platform, success: true, }; // 如果最佳结果需要配置,确保在顶层设置相关字段 if (bestResult?.needsConfig) { checkResult.needsConfig = true; checkResult.configSchema = bestResult.configSchema; log('Configuration required for best deployment option: %O', bestResult.configSchema); } return checkResult; } catch (error) { log('Check failed: %O', error); return { error: error instanceof Error ? error.message : 'Unknown error when checking MCP plugin installation status', platform: process.platform, success: false, }; } } private transformMCPToolToLobeAPI = (data: McpTool[]) => { return data.map<LobeChatPluginApi>((item) => ({ // Assuming identifier is the unique name/id description: item.description, name: item.name, parameters: item.inputSchema as PluginSchema, })); }; } // Export a singleton instance export const mcpService = new MCPService();