Skip to main content
Version: vNext (upcoming release)

Delegate MCP Access to an LLM

Let AI agents call MCP tools through Pomerium on behalf of a user. There are two patterns:

  • Client application — build a web app that captures the current user's session token and passes it to an LLM API
  • Service account — mint a long-lived credential for headless agents (CI, Claude Code, Codex) that reuses the user's upstream connections

Both patterns enforce the same authorization policies and log every tool call.

Pattern 1: Client application with token delegation

Build a web application where the authenticated user's Pomerium token is captured and passed to an LLM API (such as OpenAI). The LLM then uses that token to call MCP servers behind Pomerium on behalf of the user — with full authentication, authorization, and audit logging.

What you get:

  • Your app receives an External Token (TE) for the authenticated user via the mcp: client route
  • Your app passes that token to an LLM API, which uses it to call MCP servers on the user's behalf
  • Pomerium enforces policies on every MCP tool call — the LLM has exactly the same access as the user
  • Server discovery API to list available MCP servers and their connection status

Architecture

MCP ServerLLM API (OpenAI)Your App BackendPomeriumMCP ServerLLM API (OpenAI)Your App BackendPomeriumUserGET https://chat-app.your-domain.comAuthorization: Bearer (TE)GET /.pomerium/mcp/routes (discover servers)Server list with connection statusChat request + MCP server URLs + Bearer (TE)tools/call Authorization: Bearer (TE)Proxied request (policy enforced)Tool resultResponseChat completion with tool resultsRendered responseUser

The key insight: your app never calls MCP servers directly. It hands the user's token to the LLM, and the LLM makes the MCP calls through Pomerium. Pomerium enforces the same policies it would for any other client.

Configuration

Your deployment needs two types of routes: a client route for your chat app and one or more server routes for MCP servers.

runtime_flags:
mcp: true

routes:
# Your chat app (MCP client)
- from: https://chat-app.your-domain.com
to: http://chat-app:3000
mcp:
client: {}
policy: {}

# MCP servers the LLM can call
- from: https://db-mcp.your-domain.com
to: http://db-mcp-server:8080/mcp
name: Database Server
mcp:
server: {}
policy:
allow:
and:
- domain:
is: company.com

# MCP server with upstream OAuth (e.g., GitHub)
- from: https://github-mcp.your-domain.com
to: http://github-mcp-server:8080/mcp
name: GitHub
mcp:
server:
upstream_oauth2:
client_id: xxxxxxxxxxxx
client_secret: yyyyyyyyy
scopes: ["read:user", "repo"]
endpoint:
auth_url: "https://github.com/login/oauth/authorize"
token_url: "https://github.com/login/oauth/access_token"
policy:
allow:
and:
- domain:
is: company.com
Experimental Feature

MCP support is currently an experimental feature only available in the main branch or Docker images built from main. To enable MCP functionality, you must set runtime_flags.mcp: true in your Pomerium configuration.

Step-by-step

1. Set up MCP server routes

Configure one or more MCP server routes for the tools you want to expose. See Protect an MCP Server or MCP + Upstream OAuth.

2. Configure the client route

Add a route with mcp.client: {}. Pomerium passes an External Token (TE) to your app backend in the Authorization: Bearer header on every request.

3. Discover available MCP servers

Your app calls the /.pomerium/mcp/routes endpoint to list servers and their connection status:

GET https://chat-app.your-domain.com/.pomerium/mcp/routes
Authorization: Bearer <external-token>
Accept: application/json

Response:

{
"servers": [
{
"name": "Database Server",
"url": "https://db-mcp.your-domain.com",
"connected": true
},
{
"name": "GitHub",
"url": "https://github-mcp.your-domain.com",
"connected": false
}
]
}

4. Handle upstream OAuth (if needed)

If a server shows connected: false, redirect the user to complete upstream authentication:

https://github-mcp.your-domain.com/.pomerium/mcp/connect?redirect_url=https://chat-app.your-domain.com/callback

The redirect_url must match a configured MCP client route host. After authentication, the user is redirected back to your app.

5. Pass the token to the LLM

Your app backend extracts the External Token (TE) from the request and provides it to the LLM API along with the MCP server URLs. The LLM uses the token to call MCP tools on behalf of the user:

Your App → LLM API: "Here are the MCP servers and the user's token"
LLM API → Pomerium: tools/call with Authorization: Bearer (TE)
Pomerium → MCP Server: Authenticated, authorized request

Pomerium validates the token, applies policies (including tool-level restrictions), and proxies the request.

6. Audit tool calls

Every MCP tool call is logged by Pomerium with full details. Enable MCP-specific logging to monitor what the LLM does on behalf of your users:

authorize_log_fields:
- request-id
- email
- mcp-method
- mcp-tool
- mcp-tool-parameters

See Observability for the full logging reference.

Pattern 2: Service accounts for headless agents

For CI pipelines, coding agents, and background jobs where no human is present to complete an interactive login, use a Pomerium service account. A service account provides a long-lived JWT that authenticates MCP requests directly — no browser required.

How it works

A service account is tied to a specific user in your identity provider. When the service account makes MCP requests, Pomerium evaluates the same identity-based policies (email, groups) as it would for that user's interactive session.

For MCP servers that require upstream OAuth (e.g., GitHub, Notion), the service account reuses upstream tokens that the user has already provisioned through an interactive session. This means:

  1. The user logs in and connects to upstream MCP servers via the routes portal
  2. An administrator creates a service account for that user
  3. The service account can now access those same MCP servers using the cached upstream tokens
note

Service accounts cannot independently initiate upstream OAuth flows. If an upstream connection hasn't been established by the user's interactive session, the service account's request will receive a 401 from the upstream server. The user must hydrate their upstream connections first.

Step-by-step

1. Hydrate upstream connections

Log in to Pomerium and visit the routes portal. For each MCP server you want the agent to access, click Connect to complete the upstream OAuth consent flow. This caches upstream tokens under your user identity.

For MCP servers that don't require upstream OAuth (i.e., internal servers behind a basic MCP route), this step is not needed.

2. Create a service account

Create a service account for your user in the Enterprise Console or Pomerium Zero. The service account must be associated with the same identity (email) that hydrated the upstream connections.

See Service Accounts for detailed setup instructions.

3. Configure your coding agent

Pass the service account JWT to your agent as a bearer token. The Pomerium- prefix tells Pomerium to authenticate the request as a service account:

Authorization: Bearer Pomerium-<SERVICE_ACCOUNT_JWT>

Below are examples for popular coding agents. In all examples, POMERIUM_SERVICE_ACCOUNT_JWT is set to the raw JWT (without any prefix).

Claude Code

Add a Pomerium-fronted MCP server to Claude Code using the --header flag:

claude mcp add --transport http my-mcp-server \
https://mcp-server.your-domain.com \
--header "Authorization: Bearer Pomerium-${POMERIUM_SERVICE_ACCOUNT_JWT}"

Or configure it in .mcp.json for a shared project setup:

{
"mcpServers": {
"my-mcp-server": {
"type": "http",
"url": "https://mcp-server.your-domain.com",
"headers": {
"Authorization": "Bearer Pomerium-${POMERIUM_SERVICE_ACCOUNT_JWT}"
}
}
}
}

Claude Code expands ${POMERIUM_SERVICE_ACCOUNT_JWT} from your shell environment. Set it before starting Claude Code:

export POMERIUM_SERVICE_ACCOUNT_JWT="eyJhbGciOi..."

See the Claude Code MCP documentation for full configuration options.

OpenAI Codex

Codex uses bearer_token_env_var to read a token from an environment variable and sends it as Authorization: Bearer <value>. Because Codex adds the Bearer prefix automatically, the environment variable must include the Pomerium- prefix:

[mcp_servers.my-mcp-server]
url = "https://mcp-server.your-domain.com"
bearer_token_env_var = "POMERIUM_MCP_TOKEN"
export POMERIUM_MCP_TOKEN="Pomerium-eyJhbGciOi..."

See the Codex MCP documentation for full configuration options.

Other agents and CI

For any agent or CI system that supports MCP over HTTP, set the full header directly:

curl -H "Authorization: Bearer Pomerium-${POMERIUM_SERVICE_ACCOUNT_JWT}" \
https://mcp-server.your-domain.com/mcp

Sample repos and next steps

Feedback