Files
alknet/docs/architecture/decisions/037-mcp-stdio-transport-exclusion.md
glm-5.2 ab47dac4ad docs(http): draft alknet-http architecture specs and ADRs 036-039
First speccing pass for alknet-http (HTTP interface crate: h2/http1.1/h3
server + from_openapi/to_openapi/from_mcp/to_mcp adapters).

Specs (crates/http/):
- README.md, overview.md — crate index, two-roles-in-one-crate framing,
  adapter location map, feature gates (h3, mcp), no-env-vars invariant
- http-server.md — HttpAdapter for h2/http1.1, axum over QUIC stream,
  Bearer auth, SSE projection for subscriptions, /healthz, stealth decoy
- http-adapters.md — from_openapi (reqwest) and to_openapi (projection),
  error fidelity (HTTP_<status> per ADR-023), type definitions
- http-mcp.md — from_mcp/to_mcp (feature-gated), streamable-HTTP-only
- webtransport.md — h3/WebTransport handler, browser streaming path,
  HTTP/3 request vs WebTransport session distinguished at framing layer

ADRs:
- ADR-036 HTTP-to-Call Operation Mapping (Proposed) — direct path
  mapping; to_openapi is projection, not router (the load-bearing one-way
  door from Phase 0 DH-3)
- ADR-037 MCP Stdio Transport Exclusion (Proposed) — streamable HTTP
  only; stdio is not built (RCE-vector security position)
- ADR-038 HTTP/3 and WebTransport as First-Class HTTP Transports
  (Proposed) — corrects the Phase 0 DH-2 deferral framing; h3 is in
  scope, not deferred, per ADR-009 §'What this framework is NOT'
- ADR-039 HTTP Server and Client Host Colocated in alknet-http
  (Proposed) — one crate for server + client host (shared HTTP deps,
  shared operation-spec->HTTP mapping)
- ADR-003 Amendment 1 — clarifies alknet-call is a protocol-foundation
  crate (the alknet-http -> alknet-call dependency edge)

Open questions (OQ-38, OQ-39, OQ-40 added under 'Theme: alknet-http'):
- OQ-38 WebTransport relay-as-proxy scope (genuine scope question, not
  a deferral — the decision is made when the use case becomes concrete)
- OQ-39 to_openapi published-spec versioning (one-way after first
  publication)
- OQ-40 reqwest client config and connection pooling (two-way-door)

Architecture README and overview updated with doc table, ADR table
(036-039), current-state note, and crate graph (alknet-http ->
alknet-call edge).

Reviewed by architecture-reviewer subagent: 3 critical, 4 warning, 5
suggestion issues found and fixed (missing ADR-039, WebTransport stream
routing conflation, undefined types, stale OQ-37 deferral language,
README OQ table completeness, Bearer-only attribution, cross-references,
ADR-038 ALPN quote, feature-gate placeholder, MCP temporal language).
2026-06-29 05:53:38 +00:00

8.0 KiB

ADR-037: MCP Stdio Transport Exclusion

Status

Proposed

Context

The Model Context Protocol (MCP) defines multiple transports for communicating between an MCP client and an MCP server. The MCP Rust SDK (rmcp at /workspace/rust-sdk/) implements two:

  1. Streamable HTTP (transport-streamable-http-client-reqwest for clients, transport-streamable-http-server for servers). The client connects to an HTTP endpoint; the server serves an HTTP endpoint. Network-isolated, auth-gatable (Bearer token middleware, per the rmcp simple_auth_streamhttp.rs example), and runs under whatever auth/ identity/capabilities machinery the host applies to HTTP.

  2. stdio (transport-child-process). The client spawns the MCP server as a child process and pipes JSON-RPC over its stdin/stdout. This is the model the MCP spec promotes for "just download an MCP server and run it locally."

The alknet-http crate implements from_mcp (import remote MCP tools as call-protocol operations) and to_mcp (expose local operations as MCP tools). Both are feature-gated behind an mcp feature (the rmcp dependency is optional). The question this ADR resolves is which MCP transports alknet-http supports.

The stdio security problem

MCP stdio transport is transport-child-process — the rmcp client calls StdioClientTransport { command, args, env, cwd }, which spawns an arbitrary executable and pipes JSON-RPC over its stdin/stdout. An MCP server is an arbitrary program that the MCP client executes with whatever privileges the client process has.

The "download untrusted MCP servers and run them via stdio" model is indistinguishable from curl | sh with extra steps:

  • Arbitrary code execution. The MCP server is an executable. Running it is RCE. There is no sandbox — the child process has the full privileges of the client process (filesystem, network, environment variables, ability to spawn further processes).
  • No auth boundary. The MCP protocol messages flow over stdin/stdout; there is no TLS, no auth token, no identity resolution. The server is trusted by construction (you spawned it).
  • The "download untrusted MCP server" UX. The MCP ecosystem's promoted workflow is: find an MCP server on a registry, install it, point your client at it. This is the npm-without-the-checksums model, but the "package" is a process with full local privileges, not a library that runs in-process.

alknet's security posture is the opposite of this. alknet-vault is local-only by construction (ADR-025); the no-env-vars invariant (ADR-014, client-and-adapters.md) exists specifically to avoid the "download untrusted code that reads your secrets" pattern; capabilities are injected by the assembly layer, not read from the environment a spawned process can inspect. Building stdio support into alknet-http would import the exact RCE vector the rest of the architecture is designed to avoid.

Decision

alknet-http supports only streamable HTTP for MCP. Stdio is not built.

The mcp feature gate pulls in rmcp with the streamable HTTP transport features only:

[features]
mcp = [
    "dep:rmcp",
    # rmcp client transport (for from_mcp) — streamable HTTP only
    # rmcp server transport (for to_mcp) — streamable HTTP only
]

The stdio transport (transport-child-process) is explicitly not a dependency and not feature-gated. It is not built, not optional, not "behind a separate feature." alknet-http's from_mcp uses rmcp's StreamableHttpClientTransport (reqwest-based); to_mcp uses rmcp's StreamableHttpService (axum-based, a tower service that nests into an axum Router — see the rmcp simple_auth_streamhttp.rs:134-159 example). No stdio code path exists in the crate.

If someone wants stdio MCP

They run it themselves, outside alknet. An operator who wants to use a stdio-only MCP server can spawn it as a subprocess, run a small streamable-HTTP-to-stdio bridge, and point from_mcp at the bridge's HTTP endpoint. That bridge is the operator's responsibility — alknet does not ship it, does not endorse it, and the bridge is where the RCE risk lives, explicitly in the operator's hands, not hidden behind an alknet feature flag.

This is the same posture as ADR-025 (remote vault access requires a separate crate with its own ADR and threat model): the dangerous thing is not built by default; if someone wants it, they build it themselves and own the security model.

Consequences

Positive:

  • alknet-http does not import the MCP stdio RCE vector. There is no code path in alknet-http that spawns an arbitrary executable.
  • The streamable HTTP path is network-isolated, auth-gatable (Bearer middleware), and runs under alknet's auth/identity/capabilities machinery — the same machinery that gates every other HTTP request.
  • from_mcp operations (imported MCP tools) are Internal by default (ADR-015, ADR-022) — composition material, not directly callable from the wire. The MCP server is reached over HTTP with a Bearer token from Capabilities (the no-env-vars invariant), not by spawning a process that could read the environment.
  • to_mcp (expose local ops as MCP tools) serves an axum route with Bearer auth middleware, matching the rmcp simple_auth_streamhttp.rs pattern. An external MCP client (an editor, an AI tool) discovers and calls alknet operations through streamable HTTP, with alknet's auth/identity model applied at the HTTP boundary.

Negative:

  • MCP servers that only support stdio (a significant fraction of the current MCP ecosystem) cannot be consumed by from_mcp directly. The operator runs a bridge (above). This is a deliberate exclusion, not a feature gap.
  • The "just download an MCP server and run it" UX that the MCP ecosystem promotes is not supported. An alknet user who wants that UX has to build the bridge and own the RCE risk. This is the correct tradeoff for alknet's threat model, but it means alknet-http is not a drop-in client for the stdio MCP ecosystem.

Assumptions

  1. Streamable HTTP is the supported MCP transport in alknet. This is a one-way door: removing stdio support later (if it were ever added) would break deployments that depend on it; not adding it is the stable position. The streamable HTTP transport is the MCP spec's network-isolated path and is what the rmcp examples use for auth-gated servers.

  2. The MCP ecosystem's stdio UX is not a target. alknet is not trying to be a drop-in client for "download untrusted MCP servers." If a user wants that, the bridge approach puts the RCE risk explicitly in their hands.

  3. rmcp's streamable HTTP features are the right subset. The mcp feature gate pulls in transport-streamable-http-client-reqwest (for from_mcp) and transport-streamable-http-server (for to_mcp). The exact rmcp feature names are a two-way-door implementation detail (rmcp may rename features across versions); the one-way constraint is "streamable HTTP only, no stdio."

References

  • ADR-014 — the no-env-vars invariant; spawned processes reading env vars is the pattern this ADR's exclusion prevents
  • ADR-015 — adapter-registered ops (from_mcp) are Internal by default
  • ADR-022from_mcp provenance is a leaf
  • ADR-025 — the analogous "dangerous thing is not built by default; a separate crate with its own ADR" pattern
  • docs/research/alknet-http/phase-0-findings.md §4 (MCP stdio exclusion)
  • /workspace/rust-sdk/ — MCP Rust SDK (rmcp v1.8.0); streamable HTTP transport
  • /workspace/rust-sdk/examples/servers/src/simple_auth_streamhttp.rs — streamable HTTP MCP server with Bearer auth (the to_mcp pattern)
  • /workspace/rust-sdk/examples/clients/src/streamable_http.rs — streamable HTTP MCP client (the from_mcp pattern)
  • crates/http/http-mcp.md — the spec that implements from_mcp/to_mcp