From b9c46a0e126e1f8674c4de52edb9aadfdfb2faf2 Mon Sep 17 00:00:00 2001 From: Dylan Vos Date: Tue, 22 Sep 2026 20:08:58 +0200 Subject: [PATCH 1/2] fix(paseo-omp): Better child process closing based in `stdin.end()`, with prevention for starting childprocess on Windows to kill process --- paseo-omp/server/provider/mcp-transport.ts | 60 +++++++++++++++---- .../server/provider/omp-rpc-transport.ts | 44 ++++++++++++-- 2 files changed, 85 insertions(+), 19 deletions(-) diff --git a/paseo-omp/server/provider/mcp-transport.ts b/paseo-omp/server/provider/mcp-transport.ts index efd73407..1f38a8e4 100644 --- a/paseo-omp/server/provider/mcp-transport.ts +++ b/paseo-omp/server/provider/mcp-transport.ts @@ -7,7 +7,8 @@ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/ import { deserializeMessage, serializeMessage } from "@modelcontextprotocol/sdk/shared/stdio.js"; import type { FetchLike, Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"; -import { terminateSpawnedProcessTree } from "./omp-rpc-process"; +import { terminateSpawnedProcessTree, PROCESS_STOP_TIMEOUT_MS } from "./omp-rpc-process"; +import type { ProcessTreeCleanup } from "./omp-rpc-process"; import { OmpCleanupFailure } from "./security"; const MAX_MCP_TRANSPORT_FRAME_BYTES = 1024 * 1024; @@ -224,21 +225,54 @@ export class SupervisedStdioClientTransport implements Transport { this.notifyClose(); return; } - const treeCleanup = this.startTreeCleanup(); - if (!this.exited) { + + if (this.exited) return; + + class TimeOutError extends Error {} + + const endStdinPromise = new Promise((resolve, reject) => { + if (this.exited) { + resolve("verified"); + return; + } + + let timeout: number; + this.child.once("close", () => { + clearTimeout(timeout); + resolve("verified"); + }); + this.child.once("error", (error: Error) => { + clearTimeout(timeout); + reject(error); + }); + + timeout = setTimeout(() => reject(new TimeOutError("MCP process did not close in time")), PROCESS_STOP_TIMEOUT_MS); + try { - child.stdin.end(); - } catch { - // Process-tree cleanup remains authoritative when stdin is already closed. + this.child.stdin.end(); + } catch (error) { + reject(error); } + }); + + try { + await endStdinPromise; + } catch (error) { + if (error instanceof TimeOutError) { + console.warn(`MCP stdio process did not close in time (${PROCESS_STOP_TIMEOUT_MS}ms), starting tree cleanup`); + let terminated = await this.startTreeCleanup(); + + if (!terminated) throw new Error("MCP stdio process tree cleanup failed"); + } + } + + if ( + !this.spawnFailedWithoutProcess && + !this.exited && + !(await this.waitForExit(PROCESS_STOP_TIMEOUT_MS)) + ) { + throw new Error("MCP stdio process did not close after tree cleanup"); } - const terminated = await treeCleanup; - const exited = - this.spawnFailedWithoutProcess || - this.exited || - (await waitForExit(this.exit.promise, PROCESS_EXIT_TIMEOUT_MS)); - this.notifyClose(); - if (!terminated || !exited) throw new Error("MCP stdio process tree cleanup failed"); } } diff --git a/paseo-omp/server/provider/omp-rpc-transport.ts b/paseo-omp/server/provider/omp-rpc-transport.ts index 34d9c120..cc24aa3f 100644 --- a/paseo-omp/server/provider/omp-rpc-transport.ts +++ b/paseo-omp/server/provider/omp-rpc-transport.ts @@ -473,16 +473,48 @@ export class OmpRpcProcess { this.flushProtocolViolations(); this.clearChunk(); this.failPending(new Error("OMP RPC process was closed")); - const cleanupPromise = this.startTreeCleanup(); - if (!this.exited) { + + if (this.exited) return; + + class TimeOutError extends Error {} + + const endStdinPromise = new Promise((resolve, reject) => { + if (this.exited) { + resolve("verified"); + return; + } + + let timeout: number; + this.child.once("close", () => { + clearTimeout(timeout); + resolve("verified"); + }); + this.child.once("error", (error: Error) => { + clearTimeout(timeout); + reject(error); + }); + + timeout = setTimeout(() => reject(new TimeOutError("OMP RPC process did not close in time")), PROCESS_STOP_TIMEOUT_MS); + try { this.child.stdin.end(); - } catch { - // Continue waiting for process-tree cleanup when the input channel is already closed. + } catch (error) { + reject(error); + } + }); + + try { + await endStdinPromise; + } catch (error) { + if (error instanceof TimeOutError) { + console.warn(`OMP RPC process did not close in time (${PROCESS_STOP_TIMEOUT_MS}ms), starting tree cleanup`); + let cleanup = await this.startTreeCleanup(); + + if (cleanup === "uncertain" && this.exited) cleanup = "verified"; + if (cleanup !== "verified") throw new Error("OMP RPC process tree cleanup failed"); } } - const cleanup = await cleanupPromise; - if (cleanup !== "verified") throw new Error("OMP RPC process tree cleanup failed"); + if ( !this.spawnFailedWithoutProcess && !this.exited && From b260587acde1cb90fbd96ed19f5cddd33074b1fb Mon Sep 17 00:00:00 2001 From: Dylan Vos Date: Tue, 22 Sep 2026 21:42:08 +0200 Subject: [PATCH 2/2] fix(paseo-omp): Fixed typings --- paseo-omp/server/provider/mcp-transport.ts | 2 +- paseo-omp/server/provider/omp-rpc-transport.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/paseo-omp/server/provider/mcp-transport.ts b/paseo-omp/server/provider/mcp-transport.ts index 1f38a8e4..74b6930d 100644 --- a/paseo-omp/server/provider/mcp-transport.ts +++ b/paseo-omp/server/provider/mcp-transport.ts @@ -236,7 +236,7 @@ export class SupervisedStdioClientTransport implements Transport { return; } - let timeout: number; + let timeout: TimerHandle; this.child.once("close", () => { clearTimeout(timeout); resolve("verified"); diff --git a/paseo-omp/server/provider/omp-rpc-transport.ts b/paseo-omp/server/provider/omp-rpc-transport.ts index cc24aa3f..313bf82c 100644 --- a/paseo-omp/server/provider/omp-rpc-transport.ts +++ b/paseo-omp/server/provider/omp-rpc-transport.ts @@ -484,7 +484,7 @@ export class OmpRpcProcess { return; } - let timeout: number; + let timeout: TimerHandle; this.child.once("close", () => { clearTimeout(timeout); resolve("verified");