@xiaohui-wang/mcpadvisor
Version:
MCP Advisor & Installation - Find the right MCP server for your needs
456 lines (452 loc) • 19.5 kB
JavaScript
/**
* 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;
}
}
}