Status: RESOLVED in golang/v0.9.3 (2026-07-14) — stdio Start now uses
context.Background() as proposed below; consumer rag_go removed its skip gate and
the stdio integration test runs unconditionally on v0.9.3.
Severity: High for stdio (local) MCP servers; remote http/sse unaffected
Introduced by: ADR-0001 Gap 3 (ctx-aware LoadMcp) — the fix that threaded a
deadline through the connect path.
rag_go (the ADR-0001 consumer) adopted v0.9.0 and its two stdio MCP
integration tests + the stdio ListMcpTools path began failing with
transport error: transport closed at listTools, immediately after a
successful connect. v0.6.0 works; v0.8.0/v0.9.0 fail; same mcp-go v0.48.0.
Reproduced minimally (spawn a local stdio MCP echo server → CreateToolkit →
list tools). The remote http/sse path is unaffected — verified live against
the production remote HRMS MCP server (glm-5.2 tool call → result → grounded
answer), so all production MCP (Cobra/HRMS, all remote) is safe.
connectServer (golang/mcp.go:419-424) for the stdio branch:
stdioTransport := transport.NewStdioWithOptions(cfg.Command[0], env, cfg.Command[1:], opts...)
startCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel() // <-- fires when connectServer returns
if err := stdioTransport.Start(startCtx); err != nil { ... }
client := mcpclient.NewClient(stdioTransport, ...)
if err := initClient(ctx, client, timeout); err != nil { ... }
return client, nil // <-- defer cancel() runs here
For stdio, transport.Start(startCtx) spawns the child process and ties its
lifetime to startCtx. When connectServer returns, defer cancel()
cancels startCtx, which terminates the subprocess. Every subsequent call on
the returned client (listTools, callTool) then hits a dead transport →
transport closed.
The http/sse branches (mcp.go:453, :476) use the same
WithTimeout(ctx, timeout) + cancel, but for http/sse Start() only performs
the connection handshake — the persistent connection is not bound to the start
context — so they survive the cancel. Only stdio couples the process to the
start ctx.
Note the irony: the ADR-0001 Gap 3 change (make the load path ctx-aware, whose
real motivation was the unbounded SSE Start) is correct for http/sse but
over-applied to stdio, where binding the process to a short-lived timeout ctx
is a lifetime bug.
Decouple the stdio subprocess lifetime from the connect-time timeout. Match the
v0.6.0 behavior (process lives until client.Close()), while keeping the Gap 3
SSE-hang fix intact:
// stdio: the process must outlive connectServer — do NOT bind it to a ctx
// cancelled on return. Start on a background context; the client owns the
// process and closes it via client.Close().
if err := stdioTransport.Start(context.Background()); err != nil { ... }
Keep a startup deadline, if desired, without owning the process lifetime — e.g.
enforce the timeout around initClient/listTools (which already take their
own WithTimeout), not around the process-spawning Start. The http/sse
branches stay as-is (their timeout is correct and is the actual Gap 3 fix).
npx …/binary tools) on the Go port.skipIfStdioMCPRegressed
(RAG_GO_TOOLNEXUS_STDIO_FIXED=1 re-enables them); those flip back to
always-on once a fixed tag ships.A regression test in golang/: spawn the repo's own stdio echo MCP server via
CreateToolkit, then Execute a tool — must succeed (today: transport closed). Assert the child process is still alive after CreateToolkit
returns.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |