From b1443787ca83ee306e263fa90b3169346453c527 Mon Sep 17 00:00:00 2001 From: samanhappy Date: Mon, 7 Apr 2025 16:18:11 +0800 Subject: [PATCH] feat: update README files for clarity and consistency; refactor server management logic --- README.md | 2 +- README.zh.md | 2 +- src/controllers/serverController.ts | 42 +++------------- src/routes/index.ts | 16 +++--- src/server.ts | 16 ++---- src/services/mcpService.ts | 75 ++++++++++++----------------- src/services/sseService.ts | 23 +++------ 7 files changed, 57 insertions(+), 119 deletions(-) diff --git a/README.md b/README.md index d63d926..a8e13fd 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ MCPHub is a unified hub server that consolidates multiple MCP (Model Context Pro ## Features -- **Built-in MCP Servers**: Comes with featured MCP servers like `amap-maps`, `github`, `slack`, and more. +- **Built-in featured MCP Servers**: Comes with featured MCP servers like `amap-maps`, `playwright`, `slack`, and more. - **Centralized Management**: Oversee multiple MCP servers from one convenient hub. - **Broad Protocol Support**: Works seamlessly with both stdio and SSE MCP protocols. - **Intuitive Dashboard UI**: Monitor server status and manage servers dynamically via a web interface. diff --git a/README.zh.md b/README.zh.md index ecd762c..09bcfad 100644 --- a/README.zh.md +++ b/README.zh.md @@ -8,7 +8,7 @@ MCPHub 是一款统一的中心服务,可以将多个 MCP(Model Context Prot ## 功能 -- **内置 MCP 服务**:提供 `amap-maps`、`github`、`slack` 等热门服务。 +- **内置精选 MCP 服务**:默认安装 `amap-maps`、`playwright`、`slack` 等热门服务,开箱即用。 - **集中管理**:通过单一中心轻松管理多个 MCP 服务。 - **协议兼容**:同时支持 stdio 与 SSE MCP 协议,确保无缝对接。 - **直观仪表盘**:通过 Web 界面实时监控服务状态,并动态管理服务。 diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts index 9e4f901..8dd5f69 100644 --- a/src/controllers/serverController.ts +++ b/src/controllers/serverController.ts @@ -1,39 +1,13 @@ import { Request, Response } from 'express'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { ApiResponse, AddServerRequest } from '../types/index.js'; import { getServersInfo, addServer, removeServer, - createMcpServer, - registerAllTools, updateMcpServer, + recreateMcpServer, } from '../services/mcpService.js'; import { loadSettings } from '../config/index.js'; -import config from '../config/index.js'; - -let mcpServerInstance: McpServer; - -export const setMcpServerInstance = (server: McpServer): void => { - mcpServerInstance = server; -}; - -// 重新创建 McpServer 实例 -export const recreateMcpServerInstance = async (): Promise => { - console.log('Re-creating McpServer instance'); - - // 创建新的 McpServer 实例 - const newServer = createMcpServer(config.mcpHubName, config.mcpHubVersion); - - // 重新注册所有工具 - await registerAllTools(newServer); - - // 更新全局实例 - mcpServerInstance.close(); - mcpServerInstance = newServer; - console.log('McpServer instance successfully re-created'); - return mcpServerInstance; -}; export const getAllServers = (_: Request, res: Response): void => { try { @@ -70,7 +44,6 @@ export const getAllSettings = (_: Request, res: Response): void => { export const createServer = async (req: Request, res: Response): Promise => { try { const { name, config } = req.body as AddServerRequest; - if (!name || typeof name !== 'string') { res.status(400).json({ success: false, @@ -95,9 +68,9 @@ export const createServer = async (req: Request, res: Response): Promise = return; } - const result = await addServer(mcpServerInstance, name, config); - + const result = await addServer(name, config); if (result.success) { + recreateMcpServer(); res.json({ success: true, message: 'Server added successfully', @@ -128,12 +101,10 @@ export const deleteServer = async (req: Request, res: Response): Promise = return; } - // 先删除服务器 const result = removeServer(name); if (result.success) { - // 重新创建 McpServer 实例 - recreateMcpServerInstance(); + recreateMcpServer(); res.json({ success: true, message: 'Server removed successfully', @@ -181,10 +152,9 @@ export const updateServer = async (req: Request, res: Response): Promise = return; } - const result = await updateMcpServer(mcpServerInstance, name, config); - + const result = await updateMcpServer(name, config); if (result.success) { - recreateMcpServerInstance(); + recreateMcpServer(); res.json({ success: true, message: 'Server updated successfully', diff --git a/src/routes/index.ts b/src/routes/index.ts index 3c26dd4..c48fbc9 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -1,19 +1,15 @@ import express from 'express'; -import { - getAllServers, - getAllSettings, - createServer, +import { + getAllServers, + getAllSettings, + createServer, updateServer, deleteServer, - setMcpServerInstance } from '../controllers/serverController.js'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; const router = express.Router(); -export const initRoutes = (app: express.Application, server: McpServer): void => { - setMcpServerInstance(server); - +export const initRoutes = (app: express.Application): void => { router.get('/servers', getAllServers); router.get('/settings', getAllSettings); router.post('/servers', createServer); @@ -23,4 +19,4 @@ export const initRoutes = (app: express.Application, server: McpServer): void => app.use('/api', router); }; -export default router; \ No newline at end of file +export default router; diff --git a/src/server.ts b/src/server.ts index d16e4af..3d71db8 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,28 +1,26 @@ import express from 'express'; import config from './config/index.js'; -import { createMcpServer, registerAllTools } from './services/mcpService.js'; +import { initMcpServer, registerAllTools } from './services/mcpService.js'; import { initMiddlewares } from './middlewares/index.js'; import { initRoutes } from './routes/index.js'; import { handleSseConnection, handleSseMessage } from './services/sseService.js'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; export class AppServer { private app: express.Application; - private mcpServer: McpServer; private port: number | string; constructor() { this.app = express(); this.port = config.port; - this.mcpServer = createMcpServer(config.mcpHubName, config.mcpHubVersion); } async initialize(): Promise { try { - await registerAllTools(this.mcpServer); + const mcpServer = await initMcpServer(config.mcpHubName, config.mcpHubVersion); + await registerAllTools(mcpServer, true); initMiddlewares(this.app); - initRoutes(this.app, this.mcpServer); - this.app.get('/sse', (req, res) => handleSseConnection(req, res, this.mcpServer)); + initRoutes(this.app); + this.app.get('/sse', (req, res) => handleSseConnection(req, res)); this.app.post('/messages', handleSseMessage); console.log('Server initialized successfully'); } catch (error) { @@ -40,10 +38,6 @@ export class AppServer { getApp(): express.Application { return this.app; } - - getMcpServer(): McpServer { - return this.mcpServer; - } } export default AppServer; diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 4651564..2bafbf1 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -8,6 +8,32 @@ import { ZodType, ZodRawShape } from 'zod'; import { ServerInfo, ServerConfig } from '../types/index.js'; import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js'; import { exec } from 'child_process'; +import config from '../config/index.js'; + +let mcpServer: McpServer; + +export const initMcpServer = (name: string, version: string): McpServer => { + mcpServer = new McpServer({ name, version }); + return mcpServer; +}; + +export const setMcpServer = (server: McpServer): void => { + mcpServer = server; +}; + +export const getMcpServer = (): McpServer => { + return mcpServer; +}; + +export const recreateMcpServer = async () => { + console.log('Re-creating McpServer instance'); + const newServer = createMcpServer(config.mcpHubName, config.mcpHubVersion); + await registerAllTools(newServer, true); + let oldServer = getMcpServer(); + setMcpServer(newServer); + oldServer.close(); + console.log('McpServer instance successfully re-created'); +}; // Store all server information let serverInfos: ServerInfo[] = []; @@ -86,10 +112,10 @@ export const initializeClientsFromSettings = (): ServerInfo[] => { }; // Register all MCP tools -export const registerAllTools = async (server: McpServer): Promise => { +export const registerAllTools = async (server: McpServer, forceInit: boolean): Promise => { initializeClientsFromSettings(); for (const serverInfo of serverInfos) { - if (serverInfo.status === 'connected') continue; + if (serverInfo.status === 'connected' && !forceInit) continue; if (!serverInfo.client || !serverInfo.transport) continue; try { @@ -108,7 +134,6 @@ export const registerAllTools = async (server: McpServer): Promise => { for (const tool of tools.tools) { console.log(`Registering tool: ${JSON.stringify(tool)}`); - await server.tool( tool.name, tool.description || '', @@ -151,25 +176,21 @@ const getServerInfoByName = (name: string): ServerInfo | undefined => { // Add new server export const addServer = async ( - mcpServer: McpServer, name: string, config: ServerConfig, ): Promise<{ success: boolean; message?: string }> => { try { const settings = loadSettings(); - if (settings.mcpServers[name]) { return { success: false, message: 'Server name already exists' }; } settings.mcpServers[name] = config; - if (!saveSettings(settings)) { return { success: false, message: 'Failed to save settings' }; } - registerAllTools(mcpServer); - + registerAllTools(mcpServer, false); return { success: true, message: 'Server added successfully' }; } catch (error) { console.error(`Failed to add server: ${name}`, error); @@ -178,10 +199,7 @@ export const addServer = async ( }; // Remove server -export const removeServer = ( - name: string, - mcpServer?: McpServer, -): { success: boolean; message?: string } => { +export const removeServer = (name: string): { success: boolean; message?: string } => { try { const settings = loadSettings(); @@ -195,24 +213,7 @@ export const removeServer = ( return { success: false, message: 'Failed to save settings' }; } - // Close existing connections - const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name); - if (serverInfo && serverInfo.client) { - serverInfo.client.close(); - serverInfo.transport?.close(); - } - - // Remove from list serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name); - - // Re-create and initialize the McpServer if provided - if (mcpServer) { - console.log(`Re-initializing McpServer after removing ${name}`); - registerAllTools(mcpServer).catch((error) => { - console.error(`Error re-initializing McpServer after removing ${name}:`, error); - }); - } - return { success: true, message: 'Server removed successfully' }; } catch (error) { console.error(`Failed to remove server: ${name}`, error); @@ -222,41 +223,27 @@ export const removeServer = ( // Update existing server export const updateMcpServer = async ( - mcpServer: McpServer, name: string, config: ServerConfig, ): Promise<{ success: boolean; message?: string }> => { try { const settings = loadSettings(); - if (!settings.mcpServers[name]) { return { success: false, message: 'Server not found' }; } - // Update server configuration settings.mcpServers[name] = config; - if (!saveSettings(settings)) { return { success: false, message: 'Failed to save settings' }; } - // Close existing connections if any const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name); if (serverInfo && serverInfo.client) { - serverInfo.transport?.close(); - // serverInfo.transport = undefined; - serverInfo.client.close(); - // serverInfo.client = undefined; - console.log(`Closed existing connection for server: ${name}`); - // kill process // await killProcess(serverInfo); } - // Remove from list serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name); - console.log(`Server Infos after removing: ${JSON.stringify(serverInfos)}`); - return { success: true, message: 'Server updated successfully' }; } catch (error) { console.error(`Failed to update server: ${name}`, error); @@ -291,8 +278,6 @@ export const createMcpServer = (name: string, version: string): McpServer => { return new McpServer({ name, version }); }; -// Optimized comments to focus on key details and removed redundant explanations - // Helper function: Convert JSON Schema to Zod Schema function cast(inputSchema: unknown): ZodRawShape { if (typeof inputSchema !== 'object' || inputSchema === null) { diff --git a/src/services/sseService.ts b/src/services/sseService.ts index 1f9fedb..fb10230 100644 --- a/src/services/sseService.ts +++ b/src/services/sseService.ts @@ -1,33 +1,26 @@ import { Request, Response } from 'express'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import { getMcpServer } from './mcpService.js'; const transports: { [sessionId: string]: SSEServerTransport } = {}; -export const handleSseConnection = async ( - req: Request, - res: Response, - server: McpServer -): Promise => { +export const handleSseConnection = async (req: Request, res: Response): Promise => { const transport = new SSEServerTransport('/messages', res); transports[transport.sessionId] = transport; - + res.on('close', () => { delete transports[transport.sessionId]; console.log(`SSE connection closed: ${transport.sessionId}`); }); - + console.log(`New SSE connection established: ${transport.sessionId}`); - await server.connect(transport); + await getMcpServer().connect(transport); }; -export const handleSseMessage = async ( - req: Request, - res: Response -): Promise => { +export const handleSseMessage = async (req: Request, res: Response): Promise => { const sessionId = req.query.sessionId as string; const transport = transports[sessionId]; - + if (transport) { await transport.handlePostMessage(req, res); } else { @@ -38,4 +31,4 @@ export const handleSseMessage = async ( export const getConnectionCount = (): number => { return Object.keys(transports).length; -}; \ No newline at end of file +};