diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index ba9a1d0..31ddd81 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -6,9 +6,7 @@ import tailwindcss from '@tailwindcss/vite'; import { readFileSync } from 'fs'; // Get package.json version -const packageJson = JSON.parse( - readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8') -); +const packageJson = JSON.parse(readFileSync(path.resolve(__dirname, '../package.json'), 'utf-8')); // https://vitejs.dev/config/ export default defineConfig({ @@ -22,6 +20,9 @@ export default defineConfig({ // Make package version available as global variable 'import.meta.env.PACKAGE_VERSION': JSON.stringify(packageJson.version), }, + build: { + sourcemap: true, // Enable source maps for production build + }, server: { proxy: { '/api': { diff --git a/package.json b/package.json index 57d5fb0..344c810 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "backend:build": "tsc", "start": "node dist/index.js", "backend:dev": "tsx watch src/index.ts", + "backend:debug": "tsx watch src/index.ts --inspect", "lint": "eslint . --ext .ts", "format": "prettier --write \"src/**/*.ts\"", "test": "jest", @@ -28,6 +29,7 @@ "frontend:build": "cd frontend && vite build", "frontend:preview": "cd frontend && vite preview", "dev": "concurrently \"pnpm backend:dev\" \"pnpm frontend:dev\"", + "debug": "concurrently \"pnpm backend:debug\" \"pnpm frontend:dev\"", "prepublishOnly": "npm run build && node scripts/verify-dist.js" }, "keywords": [ @@ -39,7 +41,7 @@ "author": "", "license": "ISC", "dependencies": { - "@modelcontextprotocol/sdk": "^1.10.2", + "@modelcontextprotocol/sdk": "^1.11.1", "bcryptjs": "^3.0.2", "dotenv": "^16.3.1", "express": "^4.18.2", @@ -94,4 +96,4 @@ "node": ">=16.0.0" }, "packageManager": "pnpm@10.10.0+sha256.fa0f513aa8191764d2b6b432420788c270f07b4f999099b65bb2010eec702a30" -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 184be77..c5ce18a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@modelcontextprotocol/sdk': - specifier: ^1.10.2 - version: 1.10.2 + specifier: ^1.11.1 + version: 1.11.1 bcryptjs: specifier: ^3.0.2 version: 3.0.2 @@ -867,8 +867,8 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} - '@modelcontextprotocol/sdk@1.10.2': - resolution: {integrity: sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==} + '@modelcontextprotocol/sdk@1.11.1': + resolution: {integrity: sha512-9LfmxKTb1v+vUS1/emSk1f5ePmTLkb9Le9AxOB5T0XM59EUumwcS45z05h7aiZx3GI0Bl7mjb3FMEglYj+acuQ==} engines: {node: '>=18'} '@next/env@15.2.4': @@ -4263,7 +4263,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 - '@modelcontextprotocol/sdk@1.10.2': + '@modelcontextprotocol/sdk@1.11.1': dependencies: content-type: 1.0.5 cors: 2.8.5 @@ -6781,7 +6781,7 @@ snapshots: send@1.2.0: dependencies: - debug: 4.3.6 + debug: 4.4.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 diff --git a/src/services/mcpService.ts b/src/services/mcpService.ts index 811cd62..85a3195 100644 --- a/src/services/mcpService.ts +++ b/src/services/mcpService.ts @@ -9,31 +9,36 @@ import config from '../config/index.js'; import { getGroup } from './sseService.js'; import { getServersInGroup } from './groupService.js'; -let currentServer: Server; +const servers: { [sessionId: string]: Server } = {}; export const initMcpServer = async (name: string, version: string): Promise => { - currentServer = createMcpServer(name, version); - await registerAllTools(currentServer, true, true); + await registerAllTools(true); }; -export const setMcpServer = (server: Server): void => { - currentServer = server; +export const getMcpServer = (sessionId: string): Server => { + if (!servers[sessionId]) { + const server = createMcpServer(config.mcpHubName, config.mcpHubVersion); + servers[sessionId] = server; + } + return servers[sessionId]; }; -export const getMcpServer = (): Server => { - return currentServer; +export const deleteMcpServer = (sessionId: string): void => { + delete servers[sessionId]; }; export const notifyToolChanged = async () => { - await registerAllTools(currentServer, true, false); - currentServer - .sendToolListChanged() - .catch((error) => { - console.warn('Failed to send tool list changed notification:', error.message); - }) - .then(() => { - console.log('Tool list changed notification sent successfully'); - }); + await registerAllTools(false); + Object.values(servers).forEach((server) => { + server + .sendToolListChanged() + .catch((error) => { + console.warn('Failed to send tool list changed notification:', error.message); + }) + .then(() => { + console.log('Tool list changed notification sent successfully'); + }); + }); }; // Store all server information @@ -79,13 +84,13 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] => } else if (conf.command && conf.args) { const env: Record = conf.env || {}; env['PATH'] = expandEnvVars(process.env.PATH as string) || ''; - + // Add UV_DEFAULT_INDEX from settings if available (for Python packages) const settings = loadSettings(); if (settings.systemConfig?.install?.pythonIndexUrl && conf.command === 'uvx') { env['UV_DEFAULT_INDEX'] = settings.systemConfig.install.pythonIndexUrl; } - + transport = new StdioClientTransport({ command: conf.command, args: conf.args, @@ -180,11 +185,7 @@ export const initializeClientsFromSettings = (isInit: boolean): ServerInfo[] => }; // Register all MCP tools -export const registerAllTools = async ( - server: Server, - forceInit: boolean, - isInit: boolean, -): Promise => { +export const registerAllTools = async (isInit: boolean): Promise => { initializeClientsFromSettings(isInit); }; @@ -236,7 +237,7 @@ export const addServer = async ( return { success: false, message: 'Failed to save settings' }; } - registerAllTools(currentServer, false, false); + registerAllTools(false); return { success: true, message: 'Server added successfully' }; } catch (error) { console.error(`Failed to add server: ${name}`, error); @@ -343,59 +344,62 @@ export const toggleServerStatus = async ( } }; +const handleListToolsRequest = async (_: any, extra: any) => { + const sessionId = extra.sessionId || ''; + const group = getGroup(sessionId); + console.log(`Handling ListToolsRequest for group: ${group}`); + const allServerInfos = serverInfos.filter((serverInfo) => { + if (serverInfo.enabled === false) return false; + if (!group) return true; + const serversInGroup = getServersInGroup(group); + if (!serversInGroup || serversInGroup.length === 0) return serverInfo.name === group; + return serversInGroup.includes(serverInfo.name); + }); + + const allTools = []; + for (const serverInfo of allServerInfos) { + if (serverInfo.tools && serverInfo.tools.length > 0) { + allTools.push(...serverInfo.tools); + } + } + + return { + tools: allTools, + }; +}; + +const handleCallToolRequest = async (request: any, extra: any) => { + console.log(`Handling CallToolRequest for tool: ${request.params.name}`); + try { + const serverInfo = getServerByTool(request.params.name); + if (!serverInfo) { + throw new Error(`Server not found: ${request.params.name}`); + } + const client = serverInfo.client; + if (!client) { + throw new Error(`Client not found for server: ${request.params.name}`); + } + const result = await client.callTool(request.params); + console.log(`Tool call result: ${JSON.stringify(result)}`); + return result; + } catch (error) { + console.error(`Error handling CallToolRequest: ${error}`); + return { + content: [ + { + type: 'text', + text: `Error: ${error}`, + }, + ], + isError: true, + }; + } +}; + // Create McpServer instance export const createMcpServer = (name: string, version: string): Server => { const server = new Server({ name, version }, { capabilities: { tools: {} } }); - server.setRequestHandler(ListToolsRequestSchema, async (_, extra) => { - const sessionId = extra.sessionId || ''; - const group = getGroup(sessionId); - console.log(`Handling ListToolsRequest for group: ${group}`); - const allServerInfos = serverInfos.filter((serverInfo) => { - if (serverInfo.enabled === false) return false; - if (!group) return true; - const serversInGroup = getServersInGroup(group); - if (!serversInGroup || serversInGroup.length === 0) return serverInfo.name === group; - return serversInGroup.includes(serverInfo.name); - }); - - const allTools = []; - for (const serverInfo of allServerInfos) { - if (serverInfo.tools && serverInfo.tools.length > 0) { - allTools.push(...serverInfo.tools); - } - } - - return { - tools: allTools, - }; - }); - - server.setRequestHandler(CallToolRequestSchema, async (request, _) => { - console.log(`Handling CallToolRequest for tool: ${request.params.name}`); - try { - const serverInfo = getServerByTool(request.params.name); - if (!serverInfo) { - throw new Error(`Server not found: ${request.params.name}`); - } - const client = serverInfo.client; - if (!client) { - throw new Error(`Client not found for server: ${request.params.name}`); - } - const result = await client.callTool(request.params); - console.log(`Tool call result: ${JSON.stringify(result)}`); - return result; - } catch (error) { - console.error(`Error handling CallToolRequest: ${error}`); - return { - content: [ - { - type: 'text', - text: `Error: ${error}`, - }, - ], - isError: true, - }; - } - }); + server.setRequestHandler(ListToolsRequestSchema, handleListToolsRequest); + server.setRequestHandler(CallToolRequestSchema, handleCallToolRequest); return server; }; diff --git a/src/services/sseService.ts b/src/services/sseService.ts index a887b44..c18aa15 100644 --- a/src/services/sseService.ts +++ b/src/services/sseService.ts @@ -4,7 +4,7 @@ import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'; import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js'; -import { getMcpServer } from './mcpService.js'; +import { deleteMcpServer, getMcpServer } from './mcpService.js'; import { loadSettings } from '../config/index.js'; const transports: { [sessionId: string]: { transport: Transport; group: string } } = {}; @@ -32,13 +32,14 @@ export const handleSseConnection = async (req: Request, res: Response): Promise< res.on('close', () => { delete transports[transport.sessionId]; + deleteMcpServer(transport.sessionId); console.log(`SSE connection closed: ${transport.sessionId}`); }); console.log( `New SSE connection established: ${transport.sessionId} with group: ${group || 'global'}`, ); - await getMcpServer().connect(transport); + await getMcpServer(transport.sessionId).connect(transport); }; export const handleSseMessage = async (req: Request, res: Response): Promise => { @@ -56,9 +57,9 @@ export const handleSseMessage = async (req: Request, res: Response): Promise => { - console.log('Handling MCP post request'); const sessionId = req.headers['mcp-session-id'] as string | undefined; const group = req.params.group; + console.log(`Handling MCP post request for sessionId: ${sessionId} and group: ${group}`); const settings = loadSettings(); const routingConfig = settings.systemConfig?.routing || { enableGlobalRoute: true, @@ -71,6 +72,7 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise let transport: StreamableHTTPServerTransport; if (sessionId && transports[sessionId]) { + console.log(`Reusing existing transport for sessionId: ${sessionId}`); transport = transports[sessionId].transport as StreamableHTTPServerTransport; } else if (!sessionId && isInitializeRequest(req.body)) { transport = new StreamableHTTPServerTransport({ @@ -83,10 +85,13 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise transport.onclose = () => { if (transport.sessionId) { delete transports[transport.sessionId]; + deleteMcpServer(transport.sessionId); + console.log(`MCP connection closed: ${transport.sessionId}`); } }; - await getMcpServer().connect(transport); + console.log(`MCP connection established: ${transport.sessionId}`); + await getMcpServer(transport.sessionId || 'mcp').connect(transport); } else { res.status(400).json({ jsonrpc: '2.0', @@ -99,6 +104,7 @@ export const handleMcpPostRequest = async (req: Request, res: Response): Promise return; } + console.log(`Handling request using transport with type ${transport.constructor.name}`); await transport.handleRequest(req, res, req.body); };