diff --git a/app/native-server/src/mcp/mcp-server-stdio.ts b/app/native-server/src/mcp/mcp-server-stdio.ts index b7bb1e83..fc9582d2 100644 --- a/app/native-server/src/mcp/mcp-server-stdio.ts +++ b/app/native-server/src/mcp/mcp-server-stdio.ts @@ -17,6 +17,8 @@ import * as path from 'path'; let stdioMcpServer: Server | null = null; let mcpClient: Client | null = null; +let sessionId: string | undefined = undefined; +let isCleaningUp = false; // Read configuration from stdio-config.json const loadConfig = () => { @@ -65,14 +67,50 @@ export const ensureMcpClient = async () => { mcpClient = new Client({ name: 'Mcp Chrome Proxy', version: '1.0.0' }, { capabilities: {} }); const transport = new StreamableHTTPClientTransport(new URL(config.url), {}); await mcpClient.connect(transport); + // Save session ID for cleanup on exit + sessionId = transport.sessionId; return mcpClient; } catch (error) { mcpClient?.close(); mcpClient = null; + sessionId = undefined; console.error('Failed to connect to MCP server:', error); } }; +// Cleanup function to close session on exit +const cleanup = async () => { + // Prevent concurrent cleanup calls + if (isCleaningUp) return; + isCleaningUp = true; + + console.error('[stdio-mcp] Closing session...'); + if (sessionId) { + const abortController = new AbortController(); + const timeoutId = setTimeout(() => { + abortController.abort(); + }, 3000); + + try { + const config = loadConfig(); + await fetch(config.url, { + method: 'DELETE', + headers: { 'Mcp-Session-Id': sessionId }, + signal: abortController.signal, + }); + console.error('[stdio-mcp] Session terminated'); + } catch (e: any) { + console.error('[stdio-mcp] Failed to terminate session:', e.message); + } finally { + clearTimeout(timeoutId); + } + sessionId = undefined; + } + mcpClient?.close(); + stdioMcpServer?.close(); + process.exit(0); +}; + export const setupTools = (server: Server) => { // List tools handler server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOL_SCHEMAS })); @@ -117,6 +155,26 @@ const handleToolCall = async (name: string, args: any): Promise async function main() { const transport = new StdioServerTransport(); await getStdioMcpServer().connect(transport); + + // Setup stdin handlers to cleanup session on exit + process.stdin.on('end', cleanup); + process.stdin.on('close', cleanup); + + // Watchdog for parent PID (backup mechanism) + const parentPid = process.ppid; + const parentCheck = setInterval(() => { + try { + process.kill(parentPid, 0); + } catch (error: any) { + // Only treat ESRCH ("No such process") as a terminated parent. + // Other errors like EPERM mean the process may still exist. + if (error && error.code === 'ESRCH') { + clearInterval(parentCheck); + cleanup(); + } + } + }, 10000); + parentCheck.unref(); } main().catch((error) => {