From 2e535072c14a2372b7a367b2a35e2e1868d7bdf9 Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Sun, 22 Feb 2026 19:34:18 +0300 Subject: [PATCH 1/2] fix: stdio session Signed-off-by: Alexander Ivanov --- app/native-server/src/mcp/mcp-server-stdio.ts | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/app/native-server/src/mcp/mcp-server-stdio.ts b/app/native-server/src/mcp/mcp-server-stdio.ts index b7bb1e83..64c70bc8 100644 --- a/app/native-server/src/mcp/mcp-server-stdio.ts +++ b/app/native-server/src/mcp/mcp-server-stdio.ts @@ -17,6 +17,7 @@ import * as path from 'path'; let stdioMcpServer: Server | null = null; let mcpClient: Client | null = null; +let sessionId: string | undefined = undefined; // Read configuration from stdio-config.json const loadConfig = () => { @@ -65,14 +66,38 @@ 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 () => { + console.error('[stdio-mcp] Closing session...'); + if (sessionId) { + try { + const config = loadConfig(); + await fetch(config.url, { + method: 'DELETE', + headers: { 'Mcp-Session-Id': sessionId! }, + }); + console.error('[stdio-mcp] Session terminated'); + } catch (e: any) { + console.error('[stdio-mcp] Failed to terminate session:', e.message); + } + 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 +142,22 @@ 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 { + clearInterval(parentCheck); + cleanup(); + } + }, 10000); + parentCheck.unref(); } main().catch((error) => { From 7ffe7ecd4b322c492acff509d817e00a619be115 Mon Sep 17 00:00:00 2001 From: Alexander Ivanov Date: Sun, 22 Feb 2026 22:07:33 +0300 Subject: [PATCH 2/2] fix: address code review comments - Remove redundant non-null assertion (sessionId!) - Add isCleaningUp flag to prevent concurrent cleanup calls - Add AbortController with 3s timeout for DELETE request - Check ESRCH error code in parent PID watchdog Co-authored-by: Qwen-Coder --- app/native-server/src/mcp/mcp-server-stdio.ts | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/app/native-server/src/mcp/mcp-server-stdio.ts b/app/native-server/src/mcp/mcp-server-stdio.ts index 64c70bc8..fc9582d2 100644 --- a/app/native-server/src/mcp/mcp-server-stdio.ts +++ b/app/native-server/src/mcp/mcp-server-stdio.ts @@ -18,6 +18,7 @@ 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 = () => { @@ -79,17 +80,29 @@ export const ensureMcpClient = async () => { // 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! }, + 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; } @@ -152,9 +165,13 @@ async function main() { const parentCheck = setInterval(() => { try { process.kill(parentPid, 0); - } catch { - clearInterval(parentCheck); - cleanup(); + } 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();