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