feat: update README files for clarity and consistency; refactor server management logic

This commit is contained in:
samanhappy
2025-04-07 16:18:11 +08:00
parent 3afb0b9dc1
commit b1443787ca
7 changed files with 57 additions and 119 deletions

View File

@@ -8,7 +8,7 @@ MCPHub is a unified hub server that consolidates multiple MCP (Model Context Pro
## Features ## 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. - **Centralized Management**: Oversee multiple MCP servers from one convenient hub.
- **Broad Protocol Support**: Works seamlessly with both stdio and SSE MCP protocols. - **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. - **Intuitive Dashboard UI**: Monitor server status and manage servers dynamically via a web interface.

View File

@@ -8,7 +8,7 @@ MCPHub 是一款统一的中心服务,可以将多个 MCPModel Context Prot
## 功能 ## 功能
- **内置 MCP 服务**提供 `amap-maps``github``slack` 等热门服务。 - **内置精选 MCP 服务**默认安装 `amap-maps``playwright``slack` 等热门服务,开箱即用
- **集中管理**:通过单一中心轻松管理多个 MCP 服务。 - **集中管理**:通过单一中心轻松管理多个 MCP 服务。
- **协议兼容**:同时支持 stdio 与 SSE MCP 协议,确保无缝对接。 - **协议兼容**:同时支持 stdio 与 SSE MCP 协议,确保无缝对接。
- **直观仪表盘**:通过 Web 界面实时监控服务状态,并动态管理服务。 - **直观仪表盘**:通过 Web 界面实时监控服务状态,并动态管理服务。

View File

@@ -1,39 +1,13 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ApiResponse, AddServerRequest } from '../types/index.js'; import { ApiResponse, AddServerRequest } from '../types/index.js';
import { import {
getServersInfo, getServersInfo,
addServer, addServer,
removeServer, removeServer,
createMcpServer,
registerAllTools,
updateMcpServer, updateMcpServer,
recreateMcpServer,
} from '../services/mcpService.js'; } from '../services/mcpService.js';
import { loadSettings } from '../config/index.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<McpServer> => {
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 => { export const getAllServers = (_: Request, res: Response): void => {
try { try {
@@ -70,7 +44,6 @@ export const getAllSettings = (_: Request, res: Response): void => {
export const createServer = async (req: Request, res: Response): Promise<void> => { export const createServer = async (req: Request, res: Response): Promise<void> => {
try { try {
const { name, config } = req.body as AddServerRequest; const { name, config } = req.body as AddServerRequest;
if (!name || typeof name !== 'string') { if (!name || typeof name !== 'string') {
res.status(400).json({ res.status(400).json({
success: false, success: false,
@@ -95,9 +68,9 @@ export const createServer = async (req: Request, res: Response): Promise<void> =
return; return;
} }
const result = await addServer(mcpServerInstance, name, config); const result = await addServer(name, config);
if (result.success) { if (result.success) {
recreateMcpServer();
res.json({ res.json({
success: true, success: true,
message: 'Server added successfully', message: 'Server added successfully',
@@ -128,12 +101,10 @@ export const deleteServer = async (req: Request, res: Response): Promise<void> =
return; return;
} }
// 先删除服务器
const result = removeServer(name); const result = removeServer(name);
if (result.success) { if (result.success) {
// 重新创建 McpServer 实例 recreateMcpServer();
recreateMcpServerInstance();
res.json({ res.json({
success: true, success: true,
message: 'Server removed successfully', message: 'Server removed successfully',
@@ -181,10 +152,9 @@ export const updateServer = async (req: Request, res: Response): Promise<void> =
return; return;
} }
const result = await updateMcpServer(mcpServerInstance, name, config); const result = await updateMcpServer(name, config);
if (result.success) { if (result.success) {
recreateMcpServerInstance(); recreateMcpServer();
res.json({ res.json({
success: true, success: true,
message: 'Server updated successfully', message: 'Server updated successfully',

View File

@@ -1,19 +1,15 @@
import express from 'express'; import express from 'express';
import { import {
getAllServers, getAllServers,
getAllSettings, getAllSettings,
createServer, createServer,
updateServer, updateServer,
deleteServer, deleteServer,
setMcpServerInstance
} from '../controllers/serverController.js'; } from '../controllers/serverController.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
const router = express.Router(); const router = express.Router();
export const initRoutes = (app: express.Application, server: McpServer): void => { export const initRoutes = (app: express.Application): void => {
setMcpServerInstance(server);
router.get('/servers', getAllServers); router.get('/servers', getAllServers);
router.get('/settings', getAllSettings); router.get('/settings', getAllSettings);
router.post('/servers', createServer); router.post('/servers', createServer);
@@ -23,4 +19,4 @@ export const initRoutes = (app: express.Application, server: McpServer): void =>
app.use('/api', router); app.use('/api', router);
}; };
export default router; export default router;

View File

@@ -1,28 +1,26 @@
import express from 'express'; import express from 'express';
import config from './config/index.js'; 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 { initMiddlewares } from './middlewares/index.js';
import { initRoutes } from './routes/index.js'; import { initRoutes } from './routes/index.js';
import { handleSseConnection, handleSseMessage } from './services/sseService.js'; import { handleSseConnection, handleSseMessage } from './services/sseService.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
export class AppServer { export class AppServer {
private app: express.Application; private app: express.Application;
private mcpServer: McpServer;
private port: number | string; private port: number | string;
constructor() { constructor() {
this.app = express(); this.app = express();
this.port = config.port; this.port = config.port;
this.mcpServer = createMcpServer(config.mcpHubName, config.mcpHubVersion);
} }
async initialize(): Promise<void> { async initialize(): Promise<void> {
try { try {
await registerAllTools(this.mcpServer); const mcpServer = await initMcpServer(config.mcpHubName, config.mcpHubVersion);
await registerAllTools(mcpServer, true);
initMiddlewares(this.app); initMiddlewares(this.app);
initRoutes(this.app, this.mcpServer); initRoutes(this.app);
this.app.get('/sse', (req, res) => handleSseConnection(req, res, this.mcpServer)); this.app.get('/sse', (req, res) => handleSseConnection(req, res));
this.app.post('/messages', handleSseMessage); this.app.post('/messages', handleSseMessage);
console.log('Server initialized successfully'); console.log('Server initialized successfully');
} catch (error) { } catch (error) {
@@ -40,10 +38,6 @@ export class AppServer {
getApp(): express.Application { getApp(): express.Application {
return this.app; return this.app;
} }
getMcpServer(): McpServer {
return this.mcpServer;
}
} }
export default AppServer; export default AppServer;

View File

@@ -8,6 +8,32 @@ import { ZodType, ZodRawShape } from 'zod';
import { ServerInfo, ServerConfig } from '../types/index.js'; import { ServerInfo, ServerConfig } from '../types/index.js';
import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js'; import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js';
import { exec } from 'child_process'; 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 // Store all server information
let serverInfos: ServerInfo[] = []; let serverInfos: ServerInfo[] = [];
@@ -86,10 +112,10 @@ export const initializeClientsFromSettings = (): ServerInfo[] => {
}; };
// Register all MCP tools // Register all MCP tools
export const registerAllTools = async (server: McpServer): Promise<void> => { export const registerAllTools = async (server: McpServer, forceInit: boolean): Promise<void> => {
initializeClientsFromSettings(); initializeClientsFromSettings();
for (const serverInfo of serverInfos) { for (const serverInfo of serverInfos) {
if (serverInfo.status === 'connected') continue; if (serverInfo.status === 'connected' && !forceInit) continue;
if (!serverInfo.client || !serverInfo.transport) continue; if (!serverInfo.client || !serverInfo.transport) continue;
try { try {
@@ -108,7 +134,6 @@ export const registerAllTools = async (server: McpServer): Promise<void> => {
for (const tool of tools.tools) { for (const tool of tools.tools) {
console.log(`Registering tool: ${JSON.stringify(tool)}`); console.log(`Registering tool: ${JSON.stringify(tool)}`);
await server.tool( await server.tool(
tool.name, tool.name,
tool.description || '', tool.description || '',
@@ -151,25 +176,21 @@ const getServerInfoByName = (name: string): ServerInfo | undefined => {
// Add new server // Add new server
export const addServer = async ( export const addServer = async (
mcpServer: McpServer,
name: string, name: string,
config: ServerConfig, config: ServerConfig,
): Promise<{ success: boolean; message?: string }> => { ): Promise<{ success: boolean; message?: string }> => {
try { try {
const settings = loadSettings(); const settings = loadSettings();
if (settings.mcpServers[name]) { if (settings.mcpServers[name]) {
return { success: false, message: 'Server name already exists' }; return { success: false, message: 'Server name already exists' };
} }
settings.mcpServers[name] = config; settings.mcpServers[name] = config;
if (!saveSettings(settings)) { if (!saveSettings(settings)) {
return { success: false, message: 'Failed to save settings' }; return { success: false, message: 'Failed to save settings' };
} }
registerAllTools(mcpServer); registerAllTools(mcpServer, false);
return { success: true, message: 'Server added successfully' }; return { success: true, message: 'Server added successfully' };
} catch (error) { } catch (error) {
console.error(`Failed to add server: ${name}`, error); console.error(`Failed to add server: ${name}`, error);
@@ -178,10 +199,7 @@ export const addServer = async (
}; };
// Remove server // Remove server
export const removeServer = ( export const removeServer = (name: string): { success: boolean; message?: string } => {
name: string,
mcpServer?: McpServer,
): { success: boolean; message?: string } => {
try { try {
const settings = loadSettings(); const settings = loadSettings();
@@ -195,24 +213,7 @@ export const removeServer = (
return { success: false, message: 'Failed to save settings' }; 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); 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' }; return { success: true, message: 'Server removed successfully' };
} catch (error) { } catch (error) {
console.error(`Failed to remove server: ${name}`, error); console.error(`Failed to remove server: ${name}`, error);
@@ -222,41 +223,27 @@ export const removeServer = (
// Update existing server // Update existing server
export const updateMcpServer = async ( export const updateMcpServer = async (
mcpServer: McpServer,
name: string, name: string,
config: ServerConfig, config: ServerConfig,
): Promise<{ success: boolean; message?: string }> => { ): Promise<{ success: boolean; message?: string }> => {
try { try {
const settings = loadSettings(); const settings = loadSettings();
if (!settings.mcpServers[name]) { if (!settings.mcpServers[name]) {
return { success: false, message: 'Server not found' }; return { success: false, message: 'Server not found' };
} }
// Update server configuration
settings.mcpServers[name] = config; settings.mcpServers[name] = config;
if (!saveSettings(settings)) { if (!saveSettings(settings)) {
return { success: false, message: 'Failed to save settings' }; return { success: false, message: 'Failed to save settings' };
} }
// Close existing connections if any
const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name); const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
if (serverInfo && serverInfo.client) { 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 // kill process
// await killProcess(serverInfo); // await killProcess(serverInfo);
} }
// Remove from list
serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name); serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name);
console.log(`Server Infos after removing: ${JSON.stringify(serverInfos)}`);
return { success: true, message: 'Server updated successfully' }; return { success: true, message: 'Server updated successfully' };
} catch (error) { } catch (error) {
console.error(`Failed to update server: ${name}`, 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 }); return new McpServer({ name, version });
}; };
// Optimized comments to focus on key details and removed redundant explanations
// Helper function: Convert JSON Schema to Zod Schema // Helper function: Convert JSON Schema to Zod Schema
function cast(inputSchema: unknown): ZodRawShape { function cast(inputSchema: unknown): ZodRawShape {
if (typeof inputSchema !== 'object' || inputSchema === null) { if (typeof inputSchema !== 'object' || inputSchema === null) {

View File

@@ -1,33 +1,26 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; 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 } = {}; const transports: { [sessionId: string]: SSEServerTransport } = {};
export const handleSseConnection = async ( export const handleSseConnection = async (req: Request, res: Response): Promise<void> => {
req: Request,
res: Response,
server: McpServer
): Promise<void> => {
const transport = new SSEServerTransport('/messages', res); const transport = new SSEServerTransport('/messages', res);
transports[transport.sessionId] = transport; transports[transport.sessionId] = transport;
res.on('close', () => { res.on('close', () => {
delete transports[transport.sessionId]; delete transports[transport.sessionId];
console.log(`SSE connection closed: ${transport.sessionId}`); console.log(`SSE connection closed: ${transport.sessionId}`);
}); });
console.log(`New SSE connection established: ${transport.sessionId}`); console.log(`New SSE connection established: ${transport.sessionId}`);
await server.connect(transport); await getMcpServer().connect(transport);
}; };
export const handleSseMessage = async ( export const handleSseMessage = async (req: Request, res: Response): Promise<void> => {
req: Request,
res: Response
): Promise<void> => {
const sessionId = req.query.sessionId as string; const sessionId = req.query.sessionId as string;
const transport = transports[sessionId]; const transport = transports[sessionId];
if (transport) { if (transport) {
await transport.handlePostMessage(req, res); await transport.handlePostMessage(req, res);
} else { } else {
@@ -38,4 +31,4 @@ export const handleSseMessage = async (
export const getConnectionCount = (): number => { export const getConnectionCount = (): number => {
return Object.keys(transports).length; return Object.keys(transports).length;
}; };