Summary
Running several mcpx cf-api execute invocations concurrently can leave Bun processes alive after the caller has completed. Each orphaned process spins at roughly one full CPU core indefinitely.
Observed incident
Environment:
- mcpx:
0.9.15
- repository revision:
b297d683a33235872b79a4929896f5e16974da53
- Bun:
1.3.14
- macOS:
26.5.2 (25F84), arm64
- server:
cf-api
Five independent zone queries were launched concurrently. Four bun .../mcpx cf-api execute children remained alive after the orchestration call had returned. More than two hours later each process still consumed 96–97% CPU:
PID %CPU ELAPSED STATE
34999 96.6 02:23:22 Rs
35000 96.4 02:23:22 Rs
35875 96.3 02:20:49 Rs
35877 96.3 02:20:49 Rs
Two surviving processes used structured --input; two used the equivalent explicit form:
mcpx cf-api execute --raw \
--account_id ACCOUNT_ID \
--code 'async () => { const r = await cloudflare.request({ method: "GET", path: "/zones", query: { name: "usemarkd.app", "account.id": accountId } }); return { success: r.success, zones: r.result }; }'
The same request executed sequentially completed normally in about one second. This points to a concurrent lifecycle/cleanup race rather than a permanently pending Cloudflare request.
All four spinning processes exited immediately after SIGTERM; SIGKILL was not required.
Expected behavior
Every mcpx ... execute invocation must terminate after producing its result or error, including when multiple CLI processes run concurrently. No timer, transport, stdio handle, worker, or cleanup loop should keep the Bun process alive or busy-spin after completion.
Suggested regression coverage
Add a process-level test that launches several execute commands concurrently against a deterministic test MCP server, waits for all outputs, and asserts:
- every child exits within a bounded deadline;
- exit codes and outputs are preserved;
- no child remains alive after the parent/orchestrator closes its pipes;
- error and cancellation paths also release transports, timers, workers, and signal handlers.
A CPU-spin detector or bounded post-result process-exit assertion would expose this class of leak directly.
Summary
Running several
mcpx cf-api executeinvocations concurrently can leave Bun processes alive after the caller has completed. Each orphaned process spins at roughly one full CPU core indefinitely.Observed incident
Environment:
0.9.15b297d683a33235872b79a4929896f5e16974da531.3.1426.5.2(25F84), arm64cf-apiFive independent zone queries were launched concurrently. Four
bun .../mcpx cf-api executechildren remained alive after the orchestration call had returned. More than two hours later each process still consumed 96–97% CPU:Two surviving processes used structured
--input; two used the equivalent explicit form:mcpx cf-api execute --raw \ --account_id ACCOUNT_ID \ --code 'async () => { const r = await cloudflare.request({ method: "GET", path: "/zones", query: { name: "usemarkd.app", "account.id": accountId } }); return { success: r.success, zones: r.result }; }'The same request executed sequentially completed normally in about one second. This points to a concurrent lifecycle/cleanup race rather than a permanently pending Cloudflare request.
All four spinning processes exited immediately after
SIGTERM;SIGKILLwas not required.Expected behavior
Every
mcpx ... executeinvocation must terminate after producing its result or error, including when multiple CLI processes run concurrently. No timer, transport, stdio handle, worker, or cleanup loop should keep the Bun process alive or busy-spin after completion.Suggested regression coverage
Add a process-level test that launches several
executecommands concurrently against a deterministic test MCP server, waits for all outputs, and asserts:A CPU-spin detector or bounded post-result process-exit assertion would expose this class of leak directly.