SentnelOpsSentnelOpsbeta
LAYER 3 · THE MCP GATEWAY

How the gateway works

What happens to one tool call between your agent and your MCP server — step by step, with the two caches, the off-path log writer, and the guarantees each one buys.

One request, start to finish

Every proxied call hits /proxy/<server>/<path> (any HTTP method) and walks the same seven steps:

#StepWhat happens
1AuthenticateThe Authorization: Bearer agent JWT is extracted and signature-verified. Missing or invalid → 401, before anything else runs.
2Revocation checkThe exact token is looked up by its SHA-256 hash against revoked-token records. Rotated or decommissioned tokens → 401. Cached ~5 s, so a just-revoked token survives at most that window.
3Resolve identity + upstreamOne query joins the agent row (name, org, status, environment, allowed/denied servers) with the org's registered upstream for <server>. Unknown agent → 401; unregistered server → 404. Cached ~5 s; only positive lookups are cached, so a just-registered server resolves immediately.
4Extract the toolMCP JSON-RPC tools/call bodies yield the tool name and arguments from params. Other JSON-RPC methods log the method name; non-JSON traffic logs the path — every call stays attributable.
5Policy verdictThe layer-2 engine evaluates server scope, agent status, and tool rules into permitted / denied, a reason, and a blast radius. An engine error in observe mode fails open: the call passes, a warning is logged.
6Observe: record and forwardThe verdict is recorded — a denied decision sets would_block=true — and the call is forwarded regardless. Observe mode never blocks.
7Stream the responseThe upstream response streams back byte-identical, chunk by chunk — SSE is never buffered. Only hop-by-hop headers (RFC 9110) are stripped in each direction.
The agent's Authorization header is not hop-by-hop, so it is forwarded to the upstream today. See honest limits.

The call-log write path

Logging is deliberately off the response path. When a call completes, the gateway builds the log row (id and timestamp included), publishes it to the live feed immediately, and enqueues it; a single writer task drains the queue into Postgres in batches. Your call never waits on a database round trip — per-call INSERTs interleaving into request timings is exactly what blows a latency tail.

Fail-open extends to observability itself: a full queue or a failed batch write is logged as an error and the rows are dropped — a logging failure never fails a customer call. The trade is explicit: under write failure you lose log rows, not traffic.

would_block semantics

In observe mode a denied verdict does not block — it stamps the log row with would_block=true, the violated rule's reason, and the blast radius, then forwards the call. Accumulated over real traffic, these rows answer the only question that matters before enforcement: "what would my policy have blocked?" When layer 4 flips a rule to enforce, the identical verdict starts returning 403 — nothing else in the pipeline changes.

curl -s "https://api.sentnelops.com/mcp-calls?would_block=true" \
  -H "Authorization: Bearer $SNOPS_API_KEY"

Ghost detection

The /ghosts page is a paste-driven diff, not an automatic scanner: you paste the caller identities observed at your MCP servers (one per line — agent id, 8-character id prefix, or name) and it diffs them against the registry. Three buckets come back: matched (caller maps to a registered agent), ghosts (calling your servers, registered nowhere — nobody owns them), and silent (registered but absent from traffic — decommission candidates). Traffic routed through the gateway can't be a ghost — it authenticates as a registered agent — so ghosts are, by construction, callers bypassing the proxy.

The latency budget and the two caches

The integration suite asserts p99 proxy overhead under a 15 ms budget; measured overhead in practice is about 2 ms. That budget cannot survive an awaited Postgres round trip per call, which is why the hot path carries two small in-memory caches, both governed by the same TTL (proxy_identity_ttl_seconds, default 5):

CacheKeyed byStaleness bound
Identity + upstream(agent_id, server)A suspension or scope change takes effect within the TTL. Positive lookups only — new registrations are never delayed.
Token revocationSHA-256 of the exact tokenA revoked token keeps working for at most the TTL — the staleness this path explicitly accepts.

CIMD: the resolvable identity document

Every agent gets a public identity document at GET /cimd/{agent_id}.json — name, owner, environment, risk level, status, and allowed servers; metadata only, never secrets. An MCP server receiving a call can resolve who is calling before it answers, which puts the gateway inside the MCP identity fabric rather than merely in front of it.