---
title: "The MCP proxy"
description: "Why hosted and brokered MCP calls pass through one gateway — identity checks, per-app grants, secret injection, and the OAuth credential broker."
---

When your agent calls a hosted MCP server, or a third-party server whose users sign in with OAuth, the call does not go straight to the server. It goes through Guuey's MCP gateway — one process, one door, where every check happens and every secret lives. This page explains what that door does and what it guarantees; for declaring servers in the first place, see [MCP servers: bring your own](/hosting/#mcp-servers-bring-your-own).

The design premise is simple: **for anything that crosses the gateway, the agent pod never holds a long-lived credential.** All it carries for those calls is a short-lived, KMS-signed federation token naming one user and one resource ([Identity & verification](/security/#identity--verification)). Everything sensitive — a hosted server's API secrets, a user's OAuth tokens — is sealed at rest and resolved by the gateway at call time. For those services a compromised pod has nothing durable to steal, and revoking access is one central act, not a hunt through deployed images. (The exception is by your choice: an external server declared with static `headers` carries your app's own `${env.NAME}` values, which do live in the pod and never touch the gateway — see [MCP servers: bring your own](/hosting/#mcp-servers-bring-your-own).)

## The gateway path: hosted servers

A call to a hosted MCP server clears three checks before it is forwarded:

1. **Identity.** The gateway verifies the federation token's signature, expiry, and audience — the token must be addressed to this route, for this user. A token minted for anything else is refused before any lookup happens.
2. **Grant.** The gateway checks that _this app_ has been granted access to _this server_. No grant, no forward — a valid token from the wrong app gets nothing.
3. **Secrets.** If the hosted server needs credentials for its own upstream calls (set with `guuey mcp secrets set`), the gateway decrypts them from KMS ciphertext and injects them per invocation. Secret values are never baked into a server image, never placed in its environment, and never returned by any API ([Credential custody](/security/#credential-custody)).

Then the request is forwarded in-cluster and the response — including SSE streams — is relayed back byte-for-byte. Gateway logging is redacted by design: method names and outcomes, never payloads, tokens, or upstream bodies.

## The credential broker: third-party OAuth servers

Declaring an external server with `"credential": "oauth"` (Linear, Notion, GitHub — any server fronted by its own OAuth authorization server) engages the gateway's second half: a credential broker in the style of claude.ai's connectors.

The model has two layers, and the distinction is the point:

- **A connection is authorize-once, per user and provider.** Your user signs in to Linear once. That connection belongs to the _user_, not to any app.
- **A grant is explicit, per app.** Each agent that wants to use the connection must be granted it individually — the user answers a consent card in chat ("_app_ wants to use your _server_ account" — **Always allow**, **Allow this chat**, or **Not now**). Nothing is ever auto-granted; **Not now** records nothing and the agent simply asks again.

### How a call works

Before each turn, the platform checks whether this user has connected this server to this agent. If not, the turn runs without that server's tools and the consent card appears — the agent degrades, it doesn't break. (What that flow looks like on your surface, including the return redirect, is covered in [Connecting third-party accounts](/sdk/#connecting-third-party-accounts).)

Once connected and granted, a tool call flows like this:

1. The pod sends the call to the gateway's brokered route for this app and server, carrying only its federation token.
2. The gateway verifies the token is addressed to _exactly_ that app-and-server route, and that the app named in the token matches the route. One app cannot present another app's route, even with an otherwise valid token.
3. The gateway checks the grant: allowed always, allowed for this chat only, or blocked. A "this chat" grant is bound to the thread it was given in.
4. The gateway decrypts the user's token, injects it as the upstream `Authorization` header, **strips the federation bearer**, and forwards only a fixed allowlist of headers. The third-party token exists in plaintext only inside the gateway process, for the duration of the forwarded call.
5. The response streams back through the same relay the hosted path uses.

The authorization dance itself is standards-grade OAuth, with the strict options non-negotiable: PKCE with S256 is required (providers that don't support it are refused), the anti-forgery state is single-use with a ten-minute lifetime, and every URL the broker fetches during provider discovery — including every redirect hop — passes an SSRF guard that refuses internal and non-HTTPS targets. Where the user lands after authorizing is validated against an allowlist: your app's allowed domains plus Guuey's own surfaces, nothing else.

The client identity presented to the provider is **Guuey's, never the builder's** — the broker registers itself via client-ID metadata or dynamic client registration, and a builder cannot inject their own `client_id` into the flow. One consequence today: a provider whose authorization server supports neither of those and requires manually registered clients is not yet supported by the broker.

### Token lifecycle

- **Refresh** happens at call time, inside the gateway, and is single-flight per connection — concurrent tool calls share one refresh instead of racing the provider.
- **Expiry** is handled honestly: an upstream 401 triggers one refresh and one retry; if the token still doesn't work, the connection is marked expired and the next turn's check re-asks the user. No silent retry loops.
- **Disconnect** (from the Guuey app's _Settings → Connected services_, or `guuey mcp connections revoke`) deletes the stored credential — the KMS ciphertext is gone — drains every app grant on the connection, and makes a best-effort revocation call to the provider so the token is dead on their side too.
- **Erasure** rides the platform's deletion cascades: deleting a user deletes their connections and sealed credentials; deleting an app deletes its grants.

## What the boundaries guarantee

- **The pod never holds a third-party token.** Not in a file, not in an environment variable, not in transit to the upstream. It holds a minutes-lived Guuey federation token addressed to the gateway, nothing more.
- **Hosted-server secrets never leave the gateway's custody.** Write-only from your side; injected per invocation; absent from images, pod environments, and every API response.
- **Guuey receives tokens, never passwords.** The sign-in happens at the provider; the broker sees the OAuth grant, not the credentials behind it.
- **Users hold the grant, builders see aggregates.** As a builder you see per-server health on your app's Tools tab — how many users connected, how many granted your agent, the last refresh error — never a user list and never a token.
- **Anonymous guests never store or use third-party credentials.** A guest turn gets no consent card and no brokered tools; the check fails closed.
- **Blocking is deliberate.** The only way a server is recorded as blocked for an app is the user saying so in their connected-services settings — a dismissed card is not a "no", it's a "not now".

Tokens are KMS-sealed at rest, decrypted only inside the gateway process, and every mechanism above holds on the production gateway today. If you're evaluating the wider isolation story — pods, networks, data at rest — start at [Security & data protection](/security/).