UNPKG

@xiaohui-wang/mcpadvisor

Version:

MCP Advisor & Installation - Find the right MCP server for your needs

456 lines (452 loc) 19.5 kB
/** * MCP Server service * Handles server setup and tool registration */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js'; import { z } from 'zod'; import express from 'express'; import cors from 'cors'; import { SERVER_NAME, SERVER_VERSION } from '../config/constants.js'; import { formatServersToMCPContent } from '../utils/formatter.js'; import logger from '../utils/logger.js'; import { RestServerTransport } from '@chatmcp/sdk/server/rest.js'; import { InstallationGuideService } from './installation/installationGuideService.js'; import { ConfigurationGuideService } from './installation/configurationGuideService.js'; import { addAdditionalSources } from './loadService.js'; // Define Zod schemas for validation const GeneralArgumentsSchema = z .object({ // New search params taskDescription: z.string().min(1).optional(), keywords: z.union([z.string().array(), z.string()]).optional().default([]), capabilities: z.union([z.string().array(), z.string()]).optional().default([]), // Legacy query param (kept for backward compatibility) query: z.string().min(1).optional(), // Other params mcpName: z.string().min(1).optional(), githubUrl: z.string().url().optional(), mcpClient: z.string().optional(), }) .refine(data => // Ensure at least one search parameter or install parameters are provided !!(data.taskDescription || data.query || (data.mcpName && data.githubUrl)), { message: 'At least taskDescription/query or both mcpName and githubUrl must be provided', }) .transform(data => { // Transform data to ensure consistent format const transformed = { ...data }; // Handle legacy query parameter if (data.query && !data.taskDescription) { transformed.taskDescription = data.query; } // Ensure arrays are properly formatted if (transformed.keywords && !Array.isArray(transformed.keywords)) { transformed.keywords = [transformed.keywords].filter(Boolean); } if (transformed.capabilities && !Array.isArray(transformed.capabilities)) { transformed.capabilities = [transformed.capabilities].filter(Boolean); } return transformed; }); // Schema for additional sources const SourcesSchema = z.object({ remote_urls: z.array(z.string()).optional(), local_files: z.array(z.string()).optional(), field_map: z.record(z.string(), z.array(z.string())).optional(), }); /** * Transport type enum */ export var TransportType; (function (TransportType) { TransportType["STDIO"] = "stdio"; TransportType["SSE"] = "sse"; TransportType["REST"] = "rest"; })(TransportType || (TransportType = {})); /** * MCP Server service */ export class ServerService { server; searchService; sseTransports = {}; expressApp; /** * Create a new server service * @param searchService - The search service to use for queries */ constructor(searchService) { this.searchService = searchService; logger.info(`Initializing ServerService`, 'Server', { name: SERVER_NAME, version: SERVER_VERSION, }); // Create server instance this.server = new Server({ name: SERVER_NAME, version: SERVER_VERSION, }, { capabilities: { tools: {}, }, }); this.registerHandlers(); logger.info('Server handlers registered'); } /** * Register request handlers for the server */ registerHandlers() { // List available tools this.server.setRequestHandler(ListToolsRequestSchema, async () => { logger.debug('Handling ListTools request'); return { tools: [this.recommendMcpServerTool(), this.installMcpServerTool()], }; }); // Handle tool execution this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; logger.info(`Handling tool call: ${name}`); try { if (name === 'recommend-mcp-servers') { const parsedArgs = GeneralArgumentsSchema.parse(args); const { taskDescription, keywords = [], capabilities = [] } = parsedArgs; if (!taskDescription) { return { content: [ { type: 'text', text: 'Error: taskDescription parameter is required for recommend-mcp-servers tool', }, ], isError: true, }; } const searchParams = { taskDescription, keywords: Array.isArray(keywords) ? keywords : [keywords].filter(Boolean), capabilities: Array.isArray(capabilities) ? capabilities : [capabilities].filter(Boolean), }; logger.info(`Processing recommend-mcp-servers request`, 'Search', { taskDescription, keywords, capabilities, }); const servers = await this.searchService.search(searchParams); logger.debug(`Found servers matching query`, 'Search', { count: servers.length, taskDescription, }); return { content: formatServersToMCPContent(servers), isError: false, }; } else if (name === 'install-mcp-server') { const parsedArgs = GeneralArgumentsSchema.parse(args); const mcpName = parsedArgs.mcpName; const githubUrl = parsedArgs.githubUrl; const mcpClient = parsedArgs.mcpClient || ''; if (!mcpName || !githubUrl) { return { content: [ { type: 'text', text: 'Error: Both mcpName and githubUrl parameters are required for install-mcp-server tool', }, ], isError: true, }; } logger.info(`Processing install-mcp-server request`, 'Installation', { mcpName, githubUrl, mcpClient, }); // 获取 GitHub README 内容 const installationGuideService = new InstallationGuideService(); const installationGuide = await installationGuideService.generateInstallationGuide(githubUrl, mcpName); // 生成客户端特定的配置指南 const configurationGuideService = new ConfigurationGuideService(); const configGuide = configurationGuideService.generateConfigurationGuide(mcpName, mcpClient); // 合并安装指南和配置指南 const completeGuide = `${installationGuide}\n\n${configGuide}`; return { content: [ { type: 'text', text: completeGuide, }, ], isError: false, }; } else { const errorMsg = `Unknown tool: ${name}`; logger.error(errorMsg); return { content: [ { type: 'text', text: errorMsg, }, ], isError: true, }; } } catch (error) { logger.error(`Error handling request: ${error instanceof Error ? error.message : String(error)}`); return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : String(error)}`, }, ], isError: true, }; } }); } recommendMcpServerTool() { return { name: 'recommend-mcp-servers', description: ` 此工具用于寻找合适且专业MCP服务器。 基于您的具体需求,从互联网资源库以及内部MCP库中筛选并推荐最适合的MCP服务器解决方案。 返回结果包含服务器名称、功能描述、所属类别,为您的业务成功提供精准技术支持。 `, inputSchema: { type: 'object', properties: { taskDescription: { type: 'string', description: ` 请提供所需MCP服务器的精确任务描述。 有效查询示例: - '用于风控策略部署的MCP服务器' - '保险产品精算定价的MCP服务器' 无效查询示例: - '保险MCP服务器'(过于宽泛) - '风控系统'(缺乏具体保险场景) - '精算工具'(未指明具体功能需求) 查询应明确指定: 1. 业务流程(如产品定价、核保、理赔、准备金计算等) 2. 具体功能需求(如风险分析、策略部署、策略研发、特征研发等) `, }, keywords: { type: 'array', items: { type: 'string' }, description: '当前任务对应的搜索关键词列表,当提供关键词会优先对 MCP Server 筛选', default: [], }, capabilities: { type: 'array', items: { type: 'string' }, description: '当前任务所需功能列表,当提供功能列表会综合任务描述和功能列表对 MCP Server 筛选', default: [], }, }, required: ['taskDescription'], }, }; } installMcpServerTool() { return { name: 'install-mcp-server', description: ` 此工具用于安装MCP服务器。 请告诉我您想要安装哪个 MCP 以及其 githubUrl,我将会告诉您如何安装对应的 MCP, 并指导您在不同AI助手环境中正确配置MCP服务器。 `, inputSchema: { type: 'object', properties: { mcpName: { type: 'string', description: `请输入您想要安装的MCP名称。`, }, githubUrl: { type: 'string', description: `请输入您想要安装的MCP的githubUrl。`, }, mcpClient: { type: 'string', description: `可选,请指定您使用的MCP客户端(如Claude Desktop、Windsurf、Cursor、Cline等)。不同客户端的配置方式可能不同。`, }, }, required: ['mcpName', 'githubUrl'], }, }; } /** * @param config - SSE configuration */ setupExpressServer(config) { const { port, host = 'localhost', path = '/sse', messagePath = '/messages', } = config; this.expressApp = express(); // 如果使用 express.json(),会报错 InternalServerError: stream is not readable // this.expressApp.use(express.json()); this.expressApp.use(cors()); // SSE endpoint this.expressApp.get(path, async (req, res) => { const transport = new SSEServerTransport(messagePath, res); this.sseTransports[transport.sessionId] = transport; res.on('close', () => { logger.info(`SSE connection closed for session ${transport.sessionId}`); delete this.sseTransports[transport.sessionId]; }); try { logger.info(`New SSE connection started on ${path}`); await this.server.connect(transport); logger.info(`SSE connection established for session ${transport.sessionId}`); } catch (error) { logger.error(`Error connecting transport: ${error instanceof Error ? error.message : String(error)}`); res.status(500).end(); } }); // Message handling endpoint this.expressApp.post(messagePath, async (req, res) => { const sessionId = req.query.sessionId; logger.info(`Received message for session ${sessionId}`); const transport = this.sseTransports[sessionId]; if (transport) { try { await transport.handlePostMessage(req, res); } catch (error) { logger.error(`Error handling message: ${error instanceof Error ? error.message : String(error)}`); res.status(500).json({ error: 'Internal server error' }); } } else { logger.warn(`No transport found for session ${sessionId}, sseTransports length: ${Object.keys(this.sseTransports).length}, first: ${this.sseTransports[Object.keys(this.sseTransports)[0]].sessionId}`); res.status(400).json({ error: 'No transport found for sessionId' }); } }); // Health check endpoint this.expressApp.get('/health', (_, res) => { res.status(200).json({ status: 'ok', server: SERVER_NAME, version: SERVER_VERSION, connections: Object.keys(this.sseTransports).length, }); }); } /** * Start the server with stdio transport */ async startWithStdio() { try { logger.info('Starting server with stdio transport'); const transport = new StdioServerTransport(); await this.server.connect(transport); logger.info(`${SERVER_NAME} Server running on stdio`); } catch (error) { logger.error(`Failed to start server with stdio: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Start the server with SSE transport * @param config - SSE configuration */ async startWithSSE(config) { try { const { port, host = 'localhost' } = config; logger.info(`Starting server with SSE transport`, 'Server', { host, port, }); this.setupExpressServer(config); if (!this.expressApp) { throw new Error('Express app not initialized'); } // Add endpoint for adding sources dynamically this.expressApp.post('/api/sources', express.json(), async (req, res) => { try { const { remote_urls, local_files, field_map } = SourcesSchema.parse(req.body); const sources = {}; if (remote_urls?.length) sources.remote_urls = remote_urls; if (local_files?.length) sources.local_files = local_files; const items = await addAdditionalSources(sources, field_map); res.status(200).json({ success: true, message: 'Sources added successfully', itemCount: items.length, }); } catch (error) { logger.error(`Error adding sources: ${error instanceof Error ? error.message : String(error)}`); res.status(400).json({ success: false, error: error instanceof Error ? error.message : 'Invalid request', }); } }); // Start the HTTP server this.expressApp.listen(port, host, () => { logger.info(`${SERVER_NAME} Server running on http://${host}:${port}`); logger.info(`SSE endpoint available at http://${host}:${port}${config.path || '/sse'}`); logger.info(`Messages endpoint available at http://${host}:${port}${config.messagePath || '/messages'}`); }); } catch (error) { logger.error(`Failed to start server with SSE: ${error instanceof Error ? error.message : String(error)}`); throw error; } } /** * Start the server with REST transport */ async startWithRest(config) { logger.info('Starting server with REST transport'); const transport = new RestServerTransport({ port: config.port, endpoint: config.endpoint, }); await this.server.connect(transport); await transport.startServer(); } /** * Start the server with the specified transport * @param transportType - Type of transport to use * @param transportConfig - SSE configuration (required if transportType is SSE) */ async start(transportType = TransportType.STDIO, transportConfig) { try { if (transportType === TransportType.SSE) { if (!transportConfig) { throw new Error('SSE configuration required for SSE transport'); } await this.startWithSSE(transportConfig); } else if (transportType === TransportType.REST) { if (!transportConfig) { throw new Error('SSE configuration required for REST transport'); } await this.startWithRest(transportConfig); } else { await this.startWithStdio(); } } catch (error) { logger.error(`Failed to start server: ${error instanceof Error ? error.message : String(error)}`); throw error; } } }