Phase 1 (SDD) — architecture documentation: Ported specs (adapted for alkcall, producer/consumer terms, 6-endpoint gateway, channels-over-WS, Sub/Pub operation types): - overview.md, http-server.md, http-adapters.md, http-mcp.md - README.md index (rewritten for alkhttp) New ADRs: - 067: WebSocket carries the channels protocol (8-byte chunk demux, channel 0 = alk/call, upgrade path /alk/channels) - 068: gateway /publish endpoint for Pub operations (NDJSON body) - 069: WebTransport out of scope in alkhttp (alknet concern) - 070: from_wss consumer adapter (wss feature, tokio-tungstenite) Ported ADRs (25, same numbers, port notes + amendments where the extraction changed facts): 001-004, 010, 014, 015, 017, 022, 023, 027, 034, 036, 037, 039, 041, 042, 044, 045, 046, 047, 048, 049, 051, 066. websocket.md rewritten for the channels session; open-questions.md seeded (OQ-01 WS byte-stream adapter, OQ-02 /publish framing, OQ-03 from_wss reconnect, OQ-04 browser client ownership). Verified: cargo test, clippy -D warnings, fmt, doc --no-deps.
9.7 KiB
ADR-037: MCP Stdio Transport Exclusion
Ported from alknet ADR-037 (MCP Stdio Transport Exclusion); re-targeted to alkhttp.
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:
-
Streamable HTTP (
transport-streamable-http-client-reqwestfor clients,transport-streamable-http-serverfor servers). The client connects to an HTTP endpoint; the server serves an HTTP endpoint. Network-isolated, auth-gatable (Bearer token middleware, per the rmcpsimple_auth_streamhttp.rsexample), and runs under whatever auth/ identity/capabilities machinery the host applies to HTTP. -
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 alkhttp 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 alkhttp 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.
alkhttp's security posture is the opposite of this. alknet-vault is
local-only by construction (alknet ADR-025 — the vault crate and its
decision record live in the alknet mono-repo); the no-env-vars invariant
(ADR-014; spec in the alkcall crate's 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 alkhttp would import the exact RCE vector the rest of the
architecture is designed to avoid.
Decision
alkhttp 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." alkhttp'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 alkhttp. 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 — alkhttp
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
alkhttp feature flag.
This is the same posture as alknet ADR-025 (vault local-only dispatch: 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:
- alkhttp does not import the MCP stdio RCE vector. There is no code path in alkhttp that spawns an arbitrary executable.
- The streamable HTTP path is network-isolated, auth-gatable (Bearer middleware), and runs under alkhttp's auth/identity/capabilities machinery — the same machinery that gates every other HTTP request.
from_mcpoperations (imported MCP tools) areInternalby default (ADR-015, ADR-022) — composition material, not directly callable from the wire. The MCP server is reached over HTTP with a Bearer token fromCapabilities(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 rmcpsimple_auth_streamhttp.rspattern. An external MCP client (an editor, an AI tool) discovers and calls alkhttp operations through streamable HTTP, with alkhttp'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_mcpdirectly. 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 alkhttp user who wants that UX has to build the bridge and own the RCE risk. This is the correct tradeoff for alkhttp's threat model, but it means alkhttp is not a drop-in client for the stdio MCP ecosystem.
Assumptions
-
Streamable HTTP is the supported MCP transport in alkhttp. 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.
-
The MCP ecosystem's stdio UX is not a target. alkhttp 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.
-
rmcp's streamable HTTP features are the right subset. The
mcpfeature gate pulls intransport-streamable-http-client-reqwest(forfrom_mcp) andtransport-streamable-http-server(forto_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) areInternalby default - ADR-022
—
from_mcpprovenance is a leaf - alknet ADR-025 (vault local-only dispatch) — the analogous "dangerous
thing is not built by default; a separate crate with its own ADR"
pattern (textual reference; the vault decision record lives in the
alknet mono-repo's
docs/architecture/decisions/) docs/research/alknet-http/phase-0-findings.md§4 (MCP stdio exclusion) — alknet mono-repo research doc/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 (theto_mcppattern)/workspace/rust-sdk/examples/clients/src/streamable_http.rs— streamable HTTP MCP client (thefrom_mcppattern)docs/architecture/http-mcp.md— this crate's spec that implementsfrom_mcp/to_mcp
Port notes
- Renames: "alknet-http" → alkhttp throughout (crate name, security posture, feature-flag and threat-model references).
- alknet ADR-025 (vault local-only dispatch) is cited textually: it is not ported to alkhttp or alkcall; the vault decision record remains in the alknet mono-repo. ADR-014/015/022 are ported to this crate under the same numbers and linked.
- The
client-and-adapters.mdcitation became a textual "the alkcall crate'sclient-and-adapters.md" reference (no relative link; the document lives in alkcall's docs/architecture/). crates/http/http-mcp.md→docs/architecture/http-mcp.md(this crate's docs/architecture/). The alknet mono-repo research-doc path (docs/research/alknet-http/phase-0-findings.md) is kept and labeled as a mono-repo doc.- MCP server/client terminology untouched (inherent MCP directionality).
- No frontmatter in the source; status kept as Proposed.
- No decision content changed — the streamable-HTTP-only decision, the stdio exclusion rationale, the bridge posture, the feature-gate shape, and the consequences/assumptions are verbatim from the alknet ADR modulo the renames logged above.