diff --git a/public/components/DeleteDialog.jsx b/public/components/DeleteDialog.jsx
index b8df23d..232217c 100644
--- a/public/components/DeleteDialog.jsx
+++ b/public/components/DeleteDialog.jsx
@@ -1,4 +1,5 @@
-function DeleteDialog({ isOpen, onClose, onConfirm, serverName }) {
+// 定义DeleteDialog组件并将其暴露为全局变量
+window.DeleteDialog = function DeleteDialog({ isOpen, onClose, onConfirm, serverName }) {
return (
@@ -28,5 +29,3 @@ function DeleteDialog({ isOpen, onClose, onConfirm, serverName }) {
);
}
-
-module.exports = DeleteDialog;
diff --git a/public/js/app.js b/public/js/app.js
index 6409d54..71348e2 100644
--- a/public/js/app.js
+++ b/public/js/app.js
@@ -421,14 +421,38 @@ function App() {
useEffect(() => {
fetch('/api/servers')
.then((response) => response.json())
- .then((data) => setServers(data))
+ .then((data) => {
+ // 处理API响应中的包装对象,提取data字段
+ if (data && data.success && Array.isArray(data.data)) {
+ setServers(data.data);
+ } else if (data && Array.isArray(data)) {
+ // 兼容性处理,如果API直接返回数组
+ setServers(data);
+ } else {
+ // 如果数据格式不符合预期,设置为空数组
+ console.error('Invalid server data format:', data);
+ setServers([]);
+ }
+ })
.catch((err) => setError(err.message));
// Poll for updates every 5 seconds
const interval = setInterval(() => {
fetch('/api/servers')
.then((response) => response.json())
- .then((data) => setServers(data))
+ .then((data) => {
+ // 处理API响应中的包装对象,提取data字段
+ if (data && data.success && Array.isArray(data.data)) {
+ setServers(data.data);
+ } else if (data && Array.isArray(data)) {
+ // 兼容性处理,如果API直接返回数组
+ setServers(data);
+ } else {
+ // 如果数据格式不符合预期,设置为空数组
+ console.error('Invalid server data format:', data);
+ setServers([]);
+ }
+ })
.catch((err) => setError(err.message));
}, 5000);
diff --git a/src/config/index.ts b/src/config/index.ts
new file mode 100644
index 0000000..8fc3dfe
--- /dev/null
+++ b/src/config/index.ts
@@ -0,0 +1,44 @@
+import dotenv from 'dotenv';
+import path from 'path';
+import fs from 'fs';
+import { McpSettings } from '../types/index.js';
+
+dotenv.config();
+
+const defaultConfig = {
+ port: process.env.PORT || 3000,
+ mcpHubName: 'mcphub',
+ mcpHubVersion: '0.0.1',
+};
+
+export const getSettingsPath = (): string => {
+ return path.resolve(process.cwd(), 'mcp_settings.json');
+};
+
+export const loadSettings = (): McpSettings => {
+ const settingsPath = getSettingsPath();
+ try {
+ const settingsData = fs.readFileSync(settingsPath, 'utf8');
+ return JSON.parse(settingsData);
+ } catch (error) {
+ console.error(`Failed to load settings from ${settingsPath}:`, error);
+ return { mcpServers: {} };
+ }
+};
+
+export const saveSettings = (settings: McpSettings): boolean => {
+ const settingsPath = getSettingsPath();
+ try {
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
+ return true;
+ } catch (error) {
+ console.error(`Failed to save settings to ${settingsPath}:`, error);
+ return false;
+ }
+};
+
+export const expandEnvVars = (value: string): string => {
+ return value.replace(/\$\{([^}]+)\}/g, (_, key) => process.env[key] || '');
+};
+
+export default defaultConfig;
\ No newline at end of file
diff --git a/src/controllers/serverController.ts b/src/controllers/serverController.ts
new file mode 100644
index 0000000..f9d0a1a
--- /dev/null
+++ b/src/controllers/serverController.ts
@@ -0,0 +1,124 @@
+import { Request, Response } from 'express';
+import { ApiResponse, AddServerRequest } from '../types/index.js';
+import { getServersInfo, addServer, removeServer } from '../services/mcpService.js';
+import { loadSettings } from '../config/index.js';
+
+let mcpServerInstance: any;
+
+export const setMcpServerInstance = (server: any): void => {
+ mcpServerInstance = server;
+};
+
+export const getAllServers = (_: Request, res: Response): void => {
+ try {
+ const serversInfo = getServersInfo();
+ const response: ApiResponse = {
+ success: true,
+ data: serversInfo
+ };
+ res.json(response);
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ message: 'Failed to get servers information'
+ });
+ }
+};
+
+export const getAllSettings = (_: Request, res: Response): void => {
+ try {
+ const settings = loadSettings();
+ const response: ApiResponse = {
+ success: true,
+ data: settings
+ };
+ res.json(response);
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ message: 'Failed to get server settings'
+ });
+ }
+};
+
+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,
+ message: 'Server name is required'
+ });
+ return;
+ }
+
+ if (!config || typeof config !== 'object') {
+ res.status(400).json({
+ success: false,
+ message: 'Server configuration is required'
+ });
+ return;
+ }
+
+ if (!config.url && (!config.command || !config.args)) {
+ res.status(400).json({
+ success: false,
+ message: 'Server configuration must include either a URL or command with arguments'
+ });
+ return;
+ }
+
+ const result = await addServer(mcpServerInstance, name, config);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Server added successfully'
+ });
+ } else {
+ res.status(400).json({
+ success: false,
+ message: result.message || 'Failed to add server'
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ message: 'Internal server error'
+ });
+ }
+};
+
+export const deleteServer = async (req: Request, res: Response): Promise => {
+ try {
+ const { name } = req.params;
+
+ if (!name) {
+ res.status(400).json({
+ success: false,
+ message: 'Server name is required'
+ });
+ return;
+ }
+
+ const result = removeServer(name);
+
+ if (result.success) {
+ res.json({
+ success: true,
+ message: 'Server removed successfully'
+ });
+ } else {
+ res.status(404).json({
+ success: false,
+ message: result.message || 'Server not found or failed to remove'
+ });
+ }
+ } catch (error) {
+ res.status(500).json({
+ success: false,
+ message: 'Internal server error'
+ });
+ }
+};
\ No newline at end of file
diff --git a/src/index.ts b/src/index.ts
index 3149efc..4c152a7 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,134 +1,17 @@
-import express, { Request, Response } from 'express';
-import dotenv from 'dotenv';
-import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
-import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
-import {
- registerAllTools,
- getServersInfo,
- getServersSettings,
- addServer,
- removeServer,
-} from './server.js';
-import path from 'path';
+import AppServer from './server.js';
-dotenv.config();
+const appServer = new AppServer();
-let server = new McpServer({
- name: 'mcphub',
- version: '0.0.1',
-});
-
-// Register all MCP tools from the modular structure
-registerAllTools(server);
-
-const app = express();
-const PORT = process.env.PORT || 3000;
-
-// Serve static files from the public directory
-app.use(express.static('public'));
-
-// Add conditional JSON parsing middleware
-app.use((req, res, next) => {
- if (req.path !== '/sse' && req.path !== '/messages') {
- express.json()(req, res, next);
- } else {
- next();
+async function boot() {
+ try {
+ await appServer.initialize();
+ appServer.start();
+ } catch (error) {
+ console.error('Failed to start application:', error);
+ process.exit(1);
}
-});
+}
-// to support multiple simultaneous connections we have a lookup object from sessionId to transport
-const transports: { [sessionId: string]: SSEServerTransport } = {};
+boot();
-// API endpoint to get server and tools information
-app.get('/api/servers', (req: Request, res: Response) => {
- const serversInfo = getServersInfo();
- res.json(serversInfo);
-});
-
-// API endpoint to get all server settings
-app.get('/api/settings', (req: Request, res: Response) => {
- const settings = getServersSettings();
- res.json(settings);
-});
-
-// API endpoint to add a new server
-app.post('/api/servers', async (req: Request, res: Response) => {
- const { name, config } = req.body;
-
- if (!name || typeof name !== 'string') {
- return res.status(400).json({ success: false, message: 'Server name is required' });
- }
-
- if (!config || typeof config !== 'object') {
- return res.status(400).json({ success: false, message: 'Server configuration is required' });
- }
-
- // Validate config has either url or command+args
- if (!config.url && (!config.command || !config.args)) {
- return res.status(400).json({
- success: false,
- message: 'Server configuration must include either a URL or command with arguments',
- });
- }
-
- const { success, message } = await addServer(server, name, config);
- if (success) {
- res.json({ success: true, message: 'Server added successfully' });
- } else {
- res.status(400).json({ success: false, message: message || 'Failed to add server' });
- }
-});
-
-// API endpoint to remove a server
-app.delete('/api/servers/:name', async (req: Request, res: Response) => {
- const { name } = req.params;
-
- if (!name) {
- return res.status(400).json({ success: false, message: 'Server name is required' });
- }
-
- const result = removeServer(name);
- if (result.success) {
- server = new McpServer({
- name: 'mcphub',
- version: '0.0.1',
- });
-
- registerAllTools(server);
-
- res.json({ success: true, message: 'Server removed successfully' });
- } else {
- res.status(404).json({ success: false, message: 'Server not found or failed to remove' });
- }
-});
-
-app.get('/sse', async (_: Request, res: Response) => {
- const transport = new SSEServerTransport('/messages', res);
- transports[transport.sessionId] = transport;
- res.on('close', () => {
- delete transports[transport.sessionId];
- });
- await server.connect(transport);
-});
-
-// Serve index.html for the root route
-app.get('/', (req: Request, res: Response) => {
- res.sendFile(path.join(process.cwd(), 'public', 'index.html'));
-});
-
-app.post('/messages', async (req: Request, res: Response) => {
- const sessionId = req.query.sessionId as string;
- const transport = transports[sessionId];
- if (transport) {
- await transport.handlePostMessage(req, res);
- } else {
- console.error(`No transport found for sessionId: ${sessionId}`);
- res.status(400).send('No transport found for sessionId');
- }
-});
-
-app.listen(PORT, () => {
- console.log(`Server is running on port ${PORT}`);
-});
-
-export default app;
+export default appServer.getApp();
diff --git a/src/middlewares/index.ts b/src/middlewares/index.ts
new file mode 100644
index 0000000..326cdd9
--- /dev/null
+++ b/src/middlewares/index.ts
@@ -0,0 +1,33 @@
+import express, { Request, Response, NextFunction } from 'express';
+import path from 'path';
+
+export const errorHandler = (
+ err: Error,
+ _req: Request,
+ res: Response,
+ _next: NextFunction
+): void => {
+ console.error('Unhandled error:', err);
+ res.status(500).json({
+ success: false,
+ message: 'Internal server error'
+ });
+};
+
+export const initMiddlewares = (app: express.Application): void => {
+ app.use(express.static('public'));
+
+ app.use((req, res, next) => {
+ if (req.path !== '/sse' && req.path !== '/messages') {
+ express.json()(req, res, next);
+ } else {
+ next();
+ }
+ });
+
+ app.get('/', (_req: Request, res: Response) => {
+ res.sendFile(path.join(process.cwd(), 'public', 'index.html'));
+ });
+
+ app.use(errorHandler);
+};
\ No newline at end of file
diff --git a/src/routes/index.ts b/src/routes/index.ts
new file mode 100644
index 0000000..9348676
--- /dev/null
+++ b/src/routes/index.ts
@@ -0,0 +1,24 @@
+import express from 'express';
+import {
+ getAllServers,
+ getAllSettings,
+ createServer,
+ 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);
+
+ router.get('/servers', getAllServers);
+ router.get('/settings', getAllSettings);
+ router.post('/servers', createServer);
+ router.delete('/servers/:name', deleteServer);
+
+ app.use('/api', router);
+};
+
+export default router;
\ No newline at end of file
diff --git a/src/server.ts b/src/server.ts
index ee03cfe..d16e4af 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -1,300 +1,49 @@
+import express from 'express';
+import config from './config/index.js';
+import { createMcpServer, 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';
-import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
-import { Client } from '@modelcontextprotocol/sdk/client/index.js';
-import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
-import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
-import * as z from 'zod';
-import { ZodType, ZodRawShape } from 'zod';
-import fs from 'fs';
-import path from 'path';
-// Define settings interface
-interface McpSettings {
- mcpServers: {
- [key: string]: {
- url?: string;
- command?: string;
- args?: string[];
- env?: Record;
- };
- };
-}
+export class AppServer {
+ private app: express.Application;
+ private mcpServer: McpServer;
+ private port: number | string;
-// Add type definitions for API responses
-export interface ServerInfo {
- name: string;
- status: 'connected' | 'connecting' | 'disconnected';
- tools: ToolInfo[];
- client?: Client;
- transport?: SSEClientTransport | StdioClientTransport;
-}
-
-export interface ToolInfo {
- name: string;
- description: string;
- inputSchema: Record;
-}
-
-// Function to read and parse the settings file
-function loadSettings(): McpSettings {
- const settingsPath = path.resolve(process.cwd(), 'mcp_settings.json');
- try {
- const settingsData = fs.readFileSync(settingsPath, 'utf8');
- return JSON.parse(settingsData);
- } catch (error) {
- console.error(`Failed to load settings from ${settingsPath}:`, error);
- return { mcpServers: {} };
- }
-}
-
-// Function to save settings to file
-export function saveSettings(settings: McpSettings): boolean {
- const settingsPath = path.resolve(process.cwd(), 'mcp_settings.json');
- try {
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), 'utf8');
- return true;
- } catch (error) {
- console.error(`Failed to save settings to ${settingsPath}:`, error);
- return false;
- }
-}
-
-// Initialize clients and transports from settings
-function initializeClientsFromSettings(): ServerInfo[] {
- const settings = loadSettings();
-
- function expandEnvVars(value: string) {
- return value.replace(/\$\{([^}]+)\}/g, (_, key) => process.env[key] || '');
+ constructor() {
+ this.app = express();
+ this.port = config.port;
+ this.mcpServer = createMcpServer(config.mcpHubName, config.mcpHubVersion);
}
- for (const [name, config] of Object.entries(settings.mcpServers)) {
- const serverInfo = serverInfos.find((info) => info.name === name);
- if (serverInfo && serverInfo.status === 'connected') {
- console.log(`Server '${name}' is already connected.`);
- continue;
- }
-
- let transport;
-
- if (config.url) {
- transport = new SSEClientTransport(new URL(config.url));
- } else if (config.command && config.args) {
- const rawEnv = { ...process.env, ...(config.env || {}) };
- const env: Record = {};
- for (const key in rawEnv) {
- if (typeof rawEnv[key] === 'string') {
- env[key] = expandEnvVars(rawEnv[key] as string);
- }
- }
-
- transport = new StdioClientTransport({
- command: config.command,
- args: config.args,
- env: env,
- });
- } else {
- console.warn(`Skipping server '${name}': missing required configuration`);
- serverInfos.push({
- name,
- status: 'disconnected',
- tools: [],
- });
- continue;
- }
-
- const client = new Client(
- {
- name: `mcp-client-${name}`,
- version: '1.0.0',
- },
- {
- capabilities: {
- prompts: {},
- resources: {},
- tools: {},
- },
- },
- );
-
- serverInfos.push({
- name,
- status: 'connecting', // Set to connecting when client exists
- tools: [],
- client,
- transport,
- });
-
- console.log(`Initialized client for server: ${name}`);
- }
-
- return serverInfos;
-}
-
-// Initialize server info
-let serverInfos: ServerInfo[] = [];
-serverInfos = initializeClientsFromSettings();
-
-// Export the registerAllTools function
-export const registerAllTools = async (server: McpServer) => {
- for (const serverInfo of serverInfos) {
- if (serverInfo.status === 'connected') continue;
- if (!serverInfo.client || !serverInfo.transport) continue;
-
+ async initialize(): Promise {
try {
- serverInfo.status = 'connecting';
- console.log(`Connecting to server: ${serverInfo.name}...`);
-
- await serverInfo.client.connect(serverInfo.transport);
- const tools = await serverInfo.client.listTools();
-
- serverInfo.tools = tools.tools.map((tool) => ({
- name: tool.name,
- description: tool.description || '',
- inputSchema: tool.inputSchema.properties || {},
- }));
-
- serverInfo.status = 'connected';
- console.log(`Successfully connected to server: ${serverInfo.name}`);
-
- for (const tool of tools.tools) {
- console.log(`Registering tool: ${JSON.stringify(tool)}`);
- await server.tool(
- tool.name,
- tool.description || '',
- cast(tool.inputSchema.properties),
- async (params: Record) => {
- console.log(`Calling tool: ${tool.name} with params: ${JSON.stringify(params)}`);
- const result = await serverInfo.client!.callTool({
- name: tool.name,
- arguments: params,
- });
- console.log(`Tool result: ${JSON.stringify(result)}`);
- return result as CallToolResult;
- },
- );
- }
+ await registerAllTools(this.mcpServer);
+ initMiddlewares(this.app);
+ initRoutes(this.app, this.mcpServer);
+ this.app.get('/sse', (req, res) => handleSseConnection(req, res, this.mcpServer));
+ this.app.post('/messages', handleSseMessage);
+ console.log('Server initialized successfully');
} catch (error) {
- console.error(
- `Failed to connect to server for client: ${serverInfo.name} by error: ${error}`,
- );
- serverInfo.status = 'disconnected';
+ console.error('Error initializing server:', error);
+ throw error;
}
}
-};
-// Add function to get current server status
-export function getServersInfo(): ServerInfo[] {
- return serverInfos.map(({ name, status, tools }) => ({
- name,
- status,
- tools,
- }));
-}
+ start(): void {
+ this.app.listen(this.port, () => {
+ console.log(`Server is running on port ${this.port}`);
+ });
+ }
-// Add function to get all server settings
-export function getServersSettings(): McpSettings {
- return loadSettings();
-}
+ getApp(): express.Application {
+ return this.app;
+ }
-// Add function to add a new server
-export async function addServer(
- mcpServer: McpServer,
- name: string,
- config: { url?: string; command?: string; args?: string[]; env?: Record },
-): 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' };
- }
-
- serverInfos = initializeClientsFromSettings();
- registerAllTools(mcpServer);
-
- return { success: true, message: 'Server added successfully' };
- } catch (error) {
- console.error(`Failed to add server: ${name}`, error);
- return { success: false, message: 'Failed to add server' };
+ getMcpServer(): McpServer {
+ return this.mcpServer;
}
}
-export function removeServer(name: string): {
- success: boolean;
-} {
- try {
- const settings = loadSettings();
-
- if (!settings.mcpServers[name]) {
- return { success: false };
- }
-
- delete settings.mcpServers[name];
-
- if (!saveSettings(settings)) {
- return { success: false };
- }
-
- const serverInfo = serverInfos.find((serverInfo) => serverInfo.name === name);
- if (serverInfo && serverInfo.client) {
- serverInfo.client.close();
- serverInfo.transport?.close();
- }
- serverInfos = serverInfos.filter((serverInfo) => serverInfo.name !== name);
- return { success: true };
- } catch (error) {
- console.error(`Failed to remove server: ${name}`, error);
- return { success: false };
- }
-}
-
-function cast(inputSchema: unknown): ZodRawShape {
- if (typeof inputSchema !== 'object' || inputSchema === null) {
- throw new Error('Invalid input schema');
- }
-
- const properties = inputSchema as Record;
- const processedSchema: ZodRawShape = {};
- for (const key in properties) {
- const prop = properties[key];
- if (prop instanceof ZodType) {
- processedSchema[key] = prop.optional();
- } else if (typeof prop === 'object' && prop !== null) {
- let zodType: ZodType;
- switch (prop.type) {
- case 'string':
- zodType = z.string();
- break;
- case 'number':
- zodType = z.number();
- break;
- case 'boolean':
- zodType = z.boolean();
- break;
- case 'integer':
- zodType = z.number().int();
- break;
- case 'array':
- zodType = z.array(z.any());
- break;
- case 'object':
- zodType = z.record(z.any());
- break;
- default:
- zodType = z.any();
- }
-
- if (prop.description) {
- zodType = zodType.describe(prop.description);
- }
-
- processedSchema[key] = zodType.optional();
- }
- }
- return processedSchema;
-}
+export default AppServer;
diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts
new file mode 100644
index 0000000..b3098c6
--- /dev/null
+++ b/src/services/mcpService.ts
@@ -0,0 +1,258 @@
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
+import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
+import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
+import * as z from 'zod';
+import { ZodType, ZodRawShape } from 'zod';
+import { ServerInfo, ServerConfig, ToolInfo } from '../types/index.js';
+import { loadSettings, saveSettings, expandEnvVars } from '../config/index.js';
+
+// Store all server information
+let serverInfos: ServerInfo[] = [];
+
+// Initialize MCP server clients
+export const initializeClientsFromSettings = (): ServerInfo[] => {
+ const settings = loadSettings();
+ const existingServerInfos = serverInfos;
+ serverInfos = [];
+
+ for (const [name, config] of Object.entries(settings.mcpServers)) {
+ // Check if server is already connected
+ const existingServer = existingServerInfos.find(s => s.name === name && s.status === 'connected');
+ if (existingServer) {
+ serverInfos.push(existingServer);
+ console.log(`Server '${name}' is already connected.`);
+ continue;
+ }
+
+ let transport;
+ if (config.url) {
+ transport = new SSEClientTransport(new URL(config.url));
+ } else if (config.command && config.args) {
+ const rawEnv = { ...process.env, ...(config.env || {}) };
+ const env: Record = {};
+
+ for (const key in rawEnv) {
+ if (typeof rawEnv[key] === 'string') {
+ env[key] = expandEnvVars(rawEnv[key] as string);
+ }
+ }
+
+ transport = new StdioClientTransport({
+ command: config.command,
+ args: config.args,
+ env: env,
+ });
+ } else {
+ console.warn(`Skipping server '${name}': missing required configuration`);
+ serverInfos.push({
+ name,
+ status: 'disconnected',
+ tools: [],
+ });
+ continue;
+ }
+
+ const client = new Client(
+ {
+ name: `mcp-client-${name}`,
+ version: '1.0.0',
+ },
+ {
+ capabilities: {
+ prompts: {},
+ resources: {},
+ tools: {},
+ },
+ },
+ );
+
+ serverInfos.push({
+ name,
+ status: 'connecting',
+ tools: [],
+ client,
+ transport,
+ });
+ console.log(`Initialized client for server: ${name}`);
+ }
+
+ return serverInfos;
+};
+
+// Register all MCP tools
+export const registerAllTools = async (server: McpServer): Promise => {
+ for (const serverInfo of serverInfos) {
+ if (serverInfo.status === 'connected') continue;
+ if (!serverInfo.client || !serverInfo.transport) continue;
+
+ try {
+ serverInfo.status = 'connecting';
+ console.log(`Connecting to server: ${serverInfo.name}...`);
+
+ await serverInfo.client.connect(serverInfo.transport);
+ const tools = await serverInfo.client.listTools();
+
+ serverInfo.tools = tools.tools.map((tool) => ({
+ name: tool.name,
+ description: tool.description || '',
+ inputSchema: tool.inputSchema.properties || {},
+ }));
+
+ serverInfo.status = 'connected';
+ console.log(`Successfully connected to server: ${serverInfo.name}`);
+
+ for (const tool of tools.tools) {
+ console.log(`Registering tool: ${JSON.stringify(tool)}`);
+
+ await server.tool(
+ tool.name,
+ tool.description || '',
+ cast(tool.inputSchema.properties),
+ async (params: Record) => {
+ console.log(`Calling tool: ${tool.name} with params: ${JSON.stringify(params)}`);
+
+ const result = await serverInfo.client!.callTool({
+ name: tool.name,
+ arguments: params,
+ });
+
+ console.log(`Tool result: ${JSON.stringify(result)}`);
+ return result as CallToolResult;
+ },
+ );
+ }
+ } catch (error) {
+ console.error(`Failed to connect to server for client: ${serverInfo.name} by error: ${error}`);
+ serverInfo.status = 'disconnected';
+ }
+ }
+};
+
+// Get all server information
+export const getServersInfo = (): Omit[] => {
+ return serverInfos.map(({ name, status, tools }) => ({
+ name,
+ status,
+ tools,
+ }));
+};
+
+// 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' };
+ }
+
+ // Reinitialize clients and register tools
+ serverInfos = initializeClientsFromSettings();
+ await registerAllTools(mcpServer);
+
+ return { success: true, message: 'Server added successfully' };
+ } catch (error) {
+ console.error(`Failed to add server: ${name}`, error);
+ return { success: false, message: 'Failed to add server' };
+ }
+};
+
+// Remove server
+export const removeServer = (name: string): { success: boolean; message?: string } => {
+ try {
+ const settings = loadSettings();
+
+ if (!settings.mcpServers[name]) {
+ return { success: false, message: 'Server not found' };
+ }
+
+ delete settings.mcpServers[name];
+
+ if (!saveSettings(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);
+
+ return { success: true, message: 'Server removed successfully' };
+ } catch (error) {
+ console.error(`Failed to remove server: ${name}`, error);
+ return { success: false, message: `Failed to remove server: ${error}` };
+ }
+};
+
+// Create McpServer instance
+export const createMcpServer = (name: string, version: string): McpServer => {
+ return new McpServer({ name, version });
+};
+
+// Helper function: Convert JSON Schema to Zod Schema
+function cast(inputSchema: unknown): ZodRawShape {
+ if (typeof inputSchema !== 'object' || inputSchema === null) {
+ throw new Error('Invalid input schema');
+ }
+
+ const properties = inputSchema as Record;
+ const processedSchema: ZodRawShape = {};
+
+ for (const key in properties) {
+ const prop = properties[key];
+
+ if (prop instanceof ZodType) {
+ processedSchema[key] = prop.optional();
+ } else if (typeof prop === 'object' && prop !== null) {
+ let zodType: ZodType;
+
+ switch (prop.type) {
+ case 'string':
+ zodType = z.string();
+ break;
+ case 'number':
+ zodType = z.number();
+ break;
+ case 'boolean':
+ zodType = z.boolean();
+ break;
+ case 'integer':
+ zodType = z.number().int();
+ break;
+ case 'array':
+ zodType = z.array(z.any());
+ break;
+ case 'object':
+ zodType = z.record(z.any());
+ break;
+ default:
+ zodType = z.any();
+ }
+
+ if (prop.description) {
+ zodType = zodType.describe(prop.description);
+ }
+
+ processedSchema[key] = zodType.optional();
+ }
+ }
+
+ return processedSchema;
+}
\ No newline at end of file
diff --git a/src/services/sseService.ts b/src/services/sseService.ts
new file mode 100644
index 0000000..1f9fedb
--- /dev/null
+++ b/src/services/sseService.ts
@@ -0,0 +1,41 @@
+import { Request, Response } from 'express';
+import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
+import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
+
+const transports: { [sessionId: string]: SSEServerTransport } = {};
+
+export const handleSseConnection = async (
+ req: Request,
+ res: Response,
+ server: McpServer
+): 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);
+};
+
+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 {
+ console.error(`No transport found for sessionId: ${sessionId}`);
+ res.status(400).send('No transport found for sessionId');
+ }
+};
+
+export const getConnectionCount = (): number => {
+ return Object.keys(transports).length;
+};
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..c5ec73d
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,41 @@
+import { Client } from '@modelcontextprotocol/sdk/client/index.js';
+import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
+import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
+
+export interface McpSettings {
+ mcpServers: {
+ [key: string]: ServerConfig;
+ };
+}
+
+export interface ServerConfig {
+ url?: string;
+ command?: string;
+ args?: string[];
+ env?: Record;
+}
+
+export interface ServerInfo {
+ name: string;
+ status: 'connected' | 'connecting' | 'disconnected';
+ tools: ToolInfo[];
+ client?: Client;
+ transport?: SSEClientTransport | StdioClientTransport;
+}
+
+export interface ToolInfo {
+ name: string;
+ description: string;
+ inputSchema: Record;
+}
+
+export interface ApiResponse {
+ success: boolean;
+ message?: string;
+ data?: T;
+}
+
+export interface AddServerRequest {
+ name: string;
+ config: ServerConfig;
+}
\ No newline at end of file