Skip to Content
DocsMCP Egress Operations

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.

ModeBehavior
offDirect MCP configuration and reusable credentials reach executors. No gateway guarantee.
observeDirect behavior remains, with secret-free observation logs. No gateway guarantee.
compatibilityEligible servers use opaque capabilities. Ineligible servers are omitted rather than receiving raw secrets.
enforcedThe 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: POST and DELETE; protocol headers; JSON responses; and JSON-only SSE responses that terminate within 30 seconds and 16 MiB. Each SSE response is reconstructed from validated JSON data fields 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 ask tool rule: omitted with the actionable approval_not_mediated state in compatibility and enforced modes. Live one-shot approval is deferred; the existing interactive approval behavior is unchanged in off and observe modes. deny is independently checked before mediated egress.
  • Indeterminate templates: omitted with template_configuration. Gateway templates may use absolute user.env.KEY references and registered static helpers (including nested/default fallback expressions). @root, this, relative paths, blocks, partials, dynamic/unknown helpers, and lookup are 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_ms by 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.

  1. Deploy with every tenant at off.
  2. Use observe to inventory direct clients and transports.
  3. Enable compatibility; verify that required servers are eligible and that omitted ask, stdio, SSE-handoff, and WebSocket servers show actionable status.
  4. 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.

Last updated on