The HTTP gateway
The same gateway serves MCP over Streamable HTTP: one shared network
service instead of one process per agent. Each initialize opens a session
with its own instance of the wrapped server, its own audit chain and its own
identity. The token arrives per request in the Authorization header, where
enterprise SSO puts it. Deleting the session closes its chain; the ledger then
seals it like any other WAL, under <chain-id>-<session>.
Run it
Section titled “Run it”cargo run -p obsign-proxy -- \ --http 127.0.0.1:8080 \ --policy /tmp/demo/policy-bundle.json \ --trusted-keys /tmp/demo/trusted-keys.json \ --identity-bundle /tmp/demo/identity-bundle.json \ --wal /tmp/demo/wal --chain-id demo --env prod \ -- ./target/debug/mock-mcp-serverOpen a session and exercise it:
TOKEN=$(cat /tmp/demo/token.jwt)SID=$(curl -si http://127.0.0.1:8080/mcp \ -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ | tr -d '\r' | awk 'tolower($1)=="mcp-session-id:" {print $2}')
curl -s http://127.0.0.1:8080/mcp \ -H "Authorization: Bearer $TOKEN" -H "Mcp-Session-Id: $SID" \ -H 'Content-Type: application/json' \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"delete_production_db","arguments":{}}}'# → isError: refused by policy, recorded, never reached the server
curl -s -X DELETE http://127.0.0.1:8080/mcp -H "Mcp-Session-Id: $SID"# → the WAL now holds the complete session (chain demo-$SID),# ready for an obsign-ledger pass to sealWhy plain HTTP, and what that requires
Section titled “Why plain HTTP, and what that requires”The HTTP layer is written by hand on std::net, with no async runtime and no
web framework. The dependency list is part of the product, and the subset of
HTTP/1.1 this transport needs is smaller than any framework’s tree. Inbound
HTTP does not touch the “no network calls” invariant, which bans outbound
dependencies (JWKS fetches, ledger round trips): identity and policy still
arrive as signed files.
The transport is plain HTTP by the same argument, which keeps a TLS stack out of the auditable tree. The bearer token must therefore never cross a network in clear: TLS terminates in a reverse proxy in front, and the gateway listens only where TLS ends.
The tested nginx and Caddy configurations, the proxy contract (SSE buffering,
timeouts, the Origin allowlist), and the probes to check a deployment:
TLS in front of the gateway.