# 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

```mermaid
sequenceDiagram
  actor U as User
  participant P as Pomerium
  participant A as Your App Backend
  participant L as LLM API (OpenAI)
  participant S as MCP Server
  U ->> P: GET https://chat-app.your-domain.com
  P ->> A: Authorization: Bearer (TE)
  A ->> P: GET /.pomerium/mcp/routes (discover servers)
  P ->> A: Server list with connection status
  A ->> L: Chat request + MCP server URLs + Bearer (TE)
  L ->> P: tools/call Authorization: Bearer (TE)
  P ->> S: Proxied request (policy enforced)
  S ->> P: Tool result
  P ->> L: Response
  L ->> A: Chat completion with tool results
  A ->> U: Rendered response
```

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.

```yaml
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
```

## 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](https://www.pomerium.com/docs/capabilities/mcp/protect-mcp-server.md) or [MCP + Upstream OAuth](https://www.pomerium.com/docs/capabilities/mcp/mcp-upstream-oauth.md).

### 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:

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

**Response:**

```json
{
  "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](https://www.pomerium.com/docs/capabilities/mcp/limit-mcp-tools.md)), 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:

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

See [Observability](https://www.pomerium.com/docs/capabilities/mcp/reference.md#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](https://www.pomerium.com/docs/capabilities/service-accounts.md). 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

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](https://www.pomerium.com/docs/capabilities/mcp/protect-mcp-server.md)), 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](https://www.pomerium.com/docs/capabilities/service-accounts.md) 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:

```bash
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:

```json
{
  "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:

```bash
export POMERIUM_SERVICE_ACCOUNT_JWT="eyJhbGciOi..."
```

See the [Claude Code MCP documentation](https://code.claude.com/docs/en/mcp) 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:

```toml
[mcp_servers.my-mcp-server]
url = "https://mcp-server.your-domain.com"
bearer_token_env_var = "POMERIUM_MCP_TOKEN"
```

```bash
export POMERIUM_MCP_TOKEN="Pomerium-eyJhbGciOi..."
```

See the [Codex MCP documentation](https://developers.openai.com/codex/mcp/) for full configuration options.

**Other agents and CI**

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

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

## Sample repos and next steps

- **[pomerium/mcp-app-demo](https://github.com/pomerium/mcp-app-demo)** — Full working Pomerium Chat app with MCP server integration, LLM API calling, and audit logging
- [Service Accounts](https://www.pomerium.com/docs/capabilities/service-accounts.md) — Create and manage service accounts
- [MCP + Upstream OAuth](https://www.pomerium.com/docs/capabilities/mcp/mcp-upstream-oauth.md) — Configure upstream OAuth for services like GitHub
- [Limit MCP Tool Calling](https://www.pomerium.com/docs/capabilities/mcp/limit-mcp-tools.md) — Control which tools the LLM can call
- [MCP Full Reference](https://www.pomerium.com/docs/capabilities/mcp/reference.md) — Token types, session lifecycle, configuration details
