# On behalf of

On-behalf-of (OBO) carries the user identity from the caller all the way to the downstream MCP tools your agent invokes. Catalyst mints the delegated token for you; your agent's only job is to send the caller's token on the outbound call, which the identity-aware HTTP client does for you. For where this fits, see the [Enterprise identity overview](https://docs.diagrid.io/develop/agents/enterprise-identity) and the trust model in [Security](https://docs.diagrid.io/concepts/security).

## How it works

When your agent calls an MCP tool, the request passes through the Catalyst [MCP proxy](https://docs.diagrid.io/develop/mcp). For an agent running under enterprise identity, the proxy mints a **delegated token** that asserts the original user's identity, using Catalyst's delegation grant (the [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token-exchange standard). It attaches that token to the forwarded call.

```mermaid
flowchart LR
  AGENT(Your agent)
  subgraph DataPlane[Catalyst data plane]
    PROXY(MCP proxy)
    MINT(Mint delegated user token)
  end
  MCP(Downstream MCP server)

  AGENT-- MCP call + user token --> PROXY
  PROXY --> MINT
  MINT-- delegated user JWT --> MCP
```

## Send the caller's identity

An outbound call is a separate request from the one that invoked your agent, so the caller's token has to ride on it. Build your MCP client on the identity-aware HTTP client from `diagrid.identity.http` and that happens for you:

```python
from mcp.client.streamable_http import streamable_http_client
from diagrid.identity.http import AsyncClient

# A drop-in httpx2.AsyncClient that attaches the calling user's token.
client = AsyncClient()

async with streamable_http_client(mcp_url, http_client=client) as (read, write, _):
    ...
```

`AsyncClient` takes every `httpx2.AsyncClient` keyword argument and returns a plain client, so it drops into code that already builds one. Use `Client` for synchronous calls. If you own a client you cannot replace, attach the event hook instead:

```python
import httpx2
from diagrid.identity.http import attach_identity_headers_async

client = httpx2.AsyncClient(event_hooks={"request": [attach_identity_headers_async]})
```

The token is read when each request is sent, not when the client is built, so a single long-lived client is safe under concurrency — every request carries its own caller's identity. When there is no inbound user — a scheduled, pub/sub, or cron trigger — the header is omitted and the call goes out unauthenticated.

Sending the token is necessary but not sufficient — the receiving server has to ask for the identity before Catalyst will pass it on. That is the next section.

:::warning Send it only where it belongs

The client attaches the user's token to every request it makes, and drops it if a redirect leaves the origin you addressed. Use it for the Catalyst MCP proxy and sub-agents. Call third-party APIs with a plain `httpx2` client, so the user's token is never sent to them.

:::

## What the downstream server sees

One setting decides this: whether the MCP server [requires a user identity](https://docs.diagrid.io/develop/agents/enterprise-identity/require-user-identity).

| The server | What its tools receive |
|---|---|
| Requires a user identity | A delegated token naming the end user, in the `X-Diagrid-User-Token` header |
| Does not (the default) | No user identity — Catalyst removes the user's token before forwarding |

There is no third state. A server that does not require a user identity never receives one, even when the call that triggered it was made by a person.

This is separate from how Catalyst authenticates *itself* to your MCP server — the OAuth 2.0 client credentials, stored secret, or SPIFFE JWT you configure on the MCP server. That credential travels in its own header and is unchanged by any of this. See [Authentication for MCP servers](https://docs.diagrid.io/develop/mcp/mcp-authentication) for how it is configured.

## Read the delegated token

Your MCP server receives the delegated token in `X-Diagrid-User-Token`. It names two parties:

| Claim | Who it identifies |
|---|---|
| `sub` | The end user the call acts for |
| `act.sub` | The agent acting on their behalf |

It is a standard OIDC-discoverable RS256 JWT, so any JWT library validates it — the mechanics are the same as for the SPIFFE JWT, and [Verify the SPIFFE JWT on your server](https://docs.diagrid.io/develop/mcp/mcp-authentication#verify-the-spiffe-jwt-on-your-server) walks through discovery, the JWKS endpoint, and key rotation.

Validate it as a second, separate token rather than folding it into that check. They arrive in different headers and answer different questions:

| | Header | `sub` identifies |
|---|---|---|
| SPIFFE JWT | The header you configured, usually `Authorization` | The calling workload — proves the request came from Catalyst |
| Delegated user token | `X-Diagrid-User-Token` | The end user — tells you whose permissions to apply |

Authenticate the caller with the first, then authorize the work with the second.

## What's next

- [Require a user identity](https://docs.diagrid.io/develop/agents/enterprise-identity/require-user-identity) — reject tool calls that carry no verified user.
- [Troubleshooting](https://docs.diagrid.io/develop/agents/enterprise-identity/troubleshooting) — rejection codes, and why a tool might see no user.
- [Control tool access](https://docs.diagrid.io/develop/mcp/mcp-access-policies) — set the auth mode that shapes what the downstream server sees.
