Skip to content

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>.

Terminal window
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-server

Open a session and exercise it:

Terminal window
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 seal

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.