MCP egress gateway operations
Agor’s MCP egress gateway keeps reusable bearer, JWT-client, OAuth, custom-header,
and referenced environment secrets in the daemon. Eligible executors receive an
authenticated-encrypted opaque capability scoped to one tenant, live Task, Session, principal,
credential owner, server, saved config_version, credential-material binding,
and OAuth grant identity. The capability is routing authority, not a provider
credential.
The daemon resolves the final credential and owns every provider dispatch. The capability has no fixed one-hour expiry: it remains usable for a long-running Task, while every use reloads the Task and rejects it as soon as that Task is no longer live. It cannot be reused for another task, user, tenant, session, or server.
Exact guarantee
For each outbound HTTP hop, Agor validates the durable Task, users, attachment,
branch access, rollout mode, server config_version, referenced environment
material, and OAuth grant identity immediately before opening that hop’s socket.
Therefore:
No request hop is admitted after a relevant mutation has committed. A request admitted before that commit may complete and may already have been observed by the provider.
All constituent reads use one SQLite immediate transaction or PostgreSQL repeatable-read snapshot. Runtime OAuth access/refresh values are excluded from the canonical durable material identity; grant generation and binding remain included.
This is an admission guarantee, not a claim that request.end() proves physical
provider receipt or that Agor can retract bytes already accepted by a provider.
A local AbortController cancels matching work as a best-effort accelerator.
Its reasons are a closed set of safe gateway codes; durable revalidation wins
when available, and an unknown shutdown is reported only as generic egress
unavailability. It is not a cross-daemon correctness primitive. Active-active
daemons obtain the same decision from PostgreSQL; Redis is not required for correctness.
Credential rotation that preserves an OAuth grant’s generation and binding does
not invalidate that grant. Disconnect, invalid_grant deletion, grant
replacement, hard deletion of the shared grant’s consenting account, binding
change, configuration change, detach, task completion, or principal/role loss
prevents the next admission. Consenter deletion retires the local grant; Agor
makes no provider revocation request, synchronously or asynchronously.
OAuth refresh is part of this boundary: after token-endpoint DNS resolution and immediately before a refresh token, client secret, or Basic header is sent, the OAuth service rechecks the same task/session/tenant/server/rollout assertion. An authority rejection at that point is a typed, known no-send cancellation: the exact refresh claim returns to idle and the prior grant remains usable. Failures after dispatch remain ambiguous and keep their stricter recovery state.
Rollout modes
Configure the per-tenant mode through the authenticated
PATCH /mcp-egress/status operator API. The Settings UI does not expose gateway
status or rollout controls.
| Mode | Behavior |
|---|---|
off | Direct MCP configuration and reusable credentials reach executors. No gateway guarantee. |
observe | Direct behavior remains, with secret-free observation logs. No gateway guarantee. |
compatibility | Eligible servers use opaque capabilities. Ineligible servers are omitted rather than receiving raw secrets. |
enforced | The same supported set is mediated and every raw-secret projection path fails closed. Enabling it requires an operator attestation that pre-gateway executors were terminated. |
An administrator can always make an emergency downgrade to observe or off,
even while local requests exist. The API requires an explicit raw-secret-egress
acknowledgement in the request, and the daemon writes a security audit log with
tenant, actor, and old/new modes. A downgrade restores direct secrets; it is an
escape hatch, not a revocation operation.
Supported and refused transports
This phase deliberately supports a small, reviewable set:
- Bounded Streamable HTTP:
POSTandDELETE; protocol headers; JSON responses; and JSON-only SSE responses that terminate within 30 seconds and 16 MiB. Each SSE response is reconstructed from validated JSONdatafields only. GET, unsupported methods, and every redirect are refused before a second send. - All stdio: refused in both compatibility and enforced modes, before any process spawn or pipe write. Agor does not claim stdio revocation in this phase.
- Legacy SSE endpoint handoff and WebSocket: refused. Current SDK seams do not let the daemon own every subsequent send.
- Unbounded or non-JSON streaming: refused. The gateway buffers a bounded response before releasing it so it never retains an indefinite unscanned tail.
- Servers with an
asktool rule: omitted with the actionableapproval_not_mediatedstate in compatibility and enforced modes. Live one-shot approval is deferred; the existing interactive approval behavior is unchanged in off and observe modes.denyis independently checked before mediated egress. - Indeterminate templates: omitted with
template_configuration. Gateway templates may use absoluteuser.env.KEYreferences and registered static helpers (including nested/default fallback expressions).@root,this, relative paths, blocks, partials, dynamic/unknown helpers, andlookupare not mediated. If such a template might name user environment data, Agor scrubs every user-defined environment key from the executor as defense in depth while leaving unrelated servers available.
Remote and delegated executors do not receive a silent raw-secret fallback in a mediated mode. If their SDK cannot consume the HTTP capability projection, the MCP server is unavailable for that task.
Connection pooling is disabled: a pooled socket could bypass destination validation and obscure the per-hop admission point.
Admission also reserves bounded process/tenant/task/server capacity before any
credential work (32/16/4/8 concurrent calls respectively). Exhaustion returns
429 egress_capacity_exceeded; clients should retry with bounded backoff. This
bounds the 16 MiB response buffers and open sockets.
Reflection boundary
The MCP provider is an intentional credential recipient and therefore a trust boundary. Agor does not claim to stop a malicious provider from encoding or exfiltrating a credential it received.
To close accidental reflection, the daemon builds candidate values from final
authorization/custom headers, resolved auth and environment fields, every
referenced user environment value, and URL path/query material. Candidates
shorter than eight characters or with fewer than four distinct characters are
ignored; untemplated literal URL parts use a stricter 16-character/eight-distinct
floor, so common paths and values such as DEBUG=1 do not create false positives. The gateway
parses JSON and each JSON SSE data frame, inspects decoded string values, and
releases only a bounded, validated body. Public MCP response headers are
allowlisted and scanned. Provider bodies, URLs, and errors are never logged.
Active conversation recovery
When an attachment, configuration, credential binding, OAuth grant, or tool permission changes, the gateway’s next admission uses the new durable authority. An affected active Task receives a secret-free recovery notice instead of a generic MCP failure.
Claude can rebuild only its MCP transports in place while retaining the provider
conversation handle. Reconnect MCP requests a fresh task-scoped projection;
it does not clear sdk_session_id, restart the conversation, or replay a tool
call. Codex, Gemini, Copilot, OpenCode, Cursor, and legacy stored
claude-code-cli sessions currently apply the new MCP projection on the next
turn because their shipped Agor adapters do not expose a safe task-scoped
current-turn replacement boundary. The UI says this explicitly rather than
claiming hot reload.
Claude transport replacement has a bounded timeout. An uncertain timeout
blocks later live replacement in that executor for the turn, so a late SDK
completion cannot overwrite a newer transport. Claude does not expose live
replacement of the query’s disallowedTools option: gateway permission
admission changes immediately, while permission-driven tool visibility is
truthfully marked for the next turn.
No provider adapter currently exposes an exact retry operation for the failed model MCP invocation. Agor therefore never automatically retries it. Recovery state distinguishes a request proven not to have started from an ambiguous provider dispatch, where a side effect may already have happened.
Routine OAuth access-token refresh is invisible because it retains the same
grant authority. Grant replacement, disconnect, or changed binding requires new
authority and may require sign-in. Mediated live reprojection does not turn an
ask decision into capability authority; those servers remain excluded.
When ready and excluded servers are attached together, ready mediated servers
are still refreshed. The session owner/admin notice retains each excluded
server’s name and action; broader Task viewers receive a redacted count and the
same required action.
A permanent stdio, template, OAuth, or ask exclusion does not dead-end the
ready subset.
Automatic recovery signals and reprojection run only in compatibility and
enforced. The pre-existing direct MCP behavior in off and observe is not
turned into a reconnect path: configuration changes apply on the next turn.
Downgrading to either direct mode converts pending mediated reconnect state into a
new-generation, persistent next-turn notice, so an old reconnect warning does
not loop or hide a later sign-in/configuration action.
Operations and health
GET /mcp-egress/status is tenant-scoped and available to members;
PATCH /mcp-egress/status is restricted to admins. The status response reports
the mode, exact guarantee, supported/refused
transports, and process-local active capacity count, split into provider
in-flight requests and credential/admission reservations. Oldest age covers
both phases. It reports admission
availability as unknown unless an independent probe exists rather than
manufacturing a healthy value from the status request itself. It also lists
each visible server excluded for transport, ask, or missing OAuth authority
with a recovery action; non-admins can read these diagnostics but cannot change
rollout mode. Detail is bounded to 100 visible servers per response and reports
when additional diagnostics were truncated. These status and rollout endpoints
remain available for operator automation but are not mounted by Settings.
Reprojection rate limits are bounded, process-local, per-tenant availability controls; HA
nodes do not share one aggregate rate-limit budget. The exact request ID,
recovery generation, and reprojection claim are persisted and compared under the
Task row lock. The local key budget leaves headroom above one bounded 500-Task
hint fanout, and an exact durable claim retry bypasses that availability budget.
Successful refresh retains a monotonic generation tombstone.
It also retains the settlement time and bounded keyed hashes of the installed
per-server authority. A late rejection is suppressed only when its refresh
identity, captured authority, or observation time proves it predates that
settlement; a newly stale capability can still recover after a missed hint.
Claude serializes transport application and calls a durable, capability-free
validation operation immediately before setMcpServers, so a delayed older
response cannot replace a newer successful transport. A duplicate request after
restart or on another HA daemon can continue only when current authority matches
the immutable projection digest first bound to its durable claim; authority drift
fails closed rather than changing what that request would attest. Executors
derive a missed hint from durable Task recovery state when their refresh handler
registers or reconnects. Gateway admission still rechecks current authority, and
neither recovery handling nor idempotency replays a provider call.
GET /health reports the rollout mode, database probe, local in-flight count,
and oldest local request. Useful metrics are:
mcp_egress.proxy_msby outcome and transport;- rejected gateway requests by stable, secret-free reason in logs;
- database health/latency from the normal daemon health path.
The target budgets remain <=5 ms p95 and <=25 ms p99 admission and <=20 ms p95
forwarding overhead. This phase has no measured HA/99.99% availability evidence. It performs one
initial authority/material load and one final durable authority check per
physical hop; it does not maintain lease, mutation, quarantine, or outbox rows.
Run the checked-in SQLite benchmark harness after storage or authorization-path
changes. PostgreSQL/HA numbers require AGOR_TEST_POSTGRES_URL and the managed
HA environment.
On a quiet, otherwise idle host, the implementation review run on 2026-08-25 measured 200 warmed SQLite
admissions at p50 3.450 ms, p95 4.236 ms, and p99 5.088 ms. Reproduce it with
AGOR_RUN_MCP_EGRESS_BENCHMARK=1 pnpm --filter @agor/daemon exec vitest run src/mcp-egress/gateway.test.ts. These local figures are not a PostgreSQL/HA
availability measurement. The opt-in harness reports host-sensitive figures and
does not enforce them as a CI release threshold; run it only on a quiet idle host
and compare with a recorded baseline.
A crash loses only best-effort local cancellation state. The next daemon does not mass-quarantine servers: enforced admission still fails closed when its durable checks are unavailable, and already-observed provider work remains outside the stronger claim.
Migration and rollback
There is no schema migration in this phase. The mode uses existing tenant app
variables, authority uses existing MCP config_version, and OAuth uses the
existing grant generation/binding.
- Deploy with every tenant at
off. - Use
observeto inventory direct clients and transports. - Enable
compatibility; verify that required servers are eligible and that omittedask, stdio, SSE-handoff, and WebSocket servers show actionable status. - Terminate every executor created before mediation, independently verify the
fence, then attest and enable
enforced.
To recover availability, an admin may explicitly downgrade. Rolling back to a
binary without gateway support also restores raw-secret projection even if the
tenant row says enforced; treat that software rollback as the same audited
security downgrade. Conversation handles and unrelated tasks are not invalidated
by an MCP server mutation.