36. MCP stdio Subprocess Transport¶
Status: Accepted
Context¶
milk's MCP client (internal/mcp/) previously supported only Streamable HTTP transport (MCP spec 2025-03-26). Many useful MCP servers — local tools, language-specific helpers, project-specific scripts — are designed as stdio servers: a subprocess that reads JSON-RPC requests from stdin and writes responses to stdout.
The config already had a transport field with "stdio" documented as "reserved for future use" and Command/Args fields were absent.
Decision¶
Add a stdio subprocess transport path to the existing Client. Key choices:
Process lifetime via context.Background()
The subprocess is started with exec.CommandContext(context.Background(), ...), not the connect-timeout context passed to Connect(). This is deliberate: the connect context may be as short as 5 seconds (the default connect_timeout). Tying the subprocess to it would kill the process as soon as the handshake context expires, causing every subsequent tool call to fail with a broken pipe. The subprocess must live for the lifetime of the milk session; Close() kills it explicitly.
stdioMu serializes the channel
Unlike HTTP (where each request is an independent connection), a stdio process has a single stdin/stdout channel. Concurrent roundtripStdio calls would interleave writes and reads unpredictably. stdioMu is a separate mutex from mu (which guards ready, tools, deadUntil) so that normal status checks do not block in-flight tool calls.
Response matching by ID
The subprocess may emit server-initiated notifications before the response to a given request. roundtripStdio reads lines in a loop, decoding each, and skips any message whose id doesn't match the outstanding request. This is consistent with how readSSEResult handles HTTP+SSE.
No session ID
The MCP session ID concept exists only in the HTTP transport (carried in the Mcp-Session-Id header). stdio servers maintain implicit session state through the persistent process; no session ID exchange is needed or expected.
writeMCPConfigFile for claude-cli
For claude-cli agents, MCP servers are passed via --mcp-config. Stdio servers are now serialized as {"type":"stdio","command":"...","args":[...]} entries, which Claude Code CLI accepts natively.
Wizard and inline /mcp add
The /mcp add wizard was extended with a transport step (defaults to http) and a command step (shown only when transport is stdio, replacing the url step). Inline key=val syntax supports transport=stdio command=<path> args=<arg1,arg2>.
Consequences¶
- Stdio MCP servers (local binaries, scripts) can be registered and assigned to agents exactly like HTTP servers.
- The subprocess is a long-lived child process of the milk TUI; it is killed when
Close()is called at session end. - If the subprocess exits unexpectedly mid-session, the next tool call will get a broken pipe error. The existing lazy-reconnect + backoff mechanism (
deadUntil) will suppress repeated reconnect attempts. MCPServerConfig.URLis nowomitemptyin JSON so stdio entries don't emit a spurious"url":""field.