> ## Documentation Index
> Fetch the complete documentation index at: https://docs.closient.com/llms.txt
> Use this file to discover all available pages before exploring further.

# MCP Identity Propagation

> How Closient's MCP server distinguishes the agent making a call from the end user on whose behalf it acts.

The Model Context Protocol authenticates the **agent** (the client app
that holds an OAuth token), but most real workflows have a second
identity in play: the **end user** the agent is acting on behalf of.
Closient's MCP server tracks both — the dual-principal model — so
audit logs, rate-limit decisions, and end-user revocation can answer
"which user asked the agent to do this," not just "which agent did it."

This page explains the two identities, how each is conveyed to
Closient, and what guarantees you can rely on as an integrator.

## The two principals

| Principal      | Identifier         | Source                                                               |
| -------------- | ------------------ | -------------------------------------------------------------------- |
| Agent          | OAuth `client_id`  | The OAuth Application that issued the bearer token                   |
| End-user actor | Closient `User.id` | The user who completed the `authorization_code` grant for this token |

The agent principal is always present on an authenticated MCP request
— if there's a valid bearer token, there's an OAuth Application
behind it. The end-user actor is **only** present when the token was
issued via the `authorization_code` flow, where the user is part of
the consent contract. Tokens issued via `client_credentials` (server-
to-server agents acting on their own behalf) carry no end-user actor;
the audit log records `end_user_id = null` for those, which is the
correct truth.

## How it gets to Closient

The MCP spec does not yet standardize a wire format for the end-user
actor — there's an active conversation in the MCP community about
whether and how to formalize it as a protocol primitive. Until that
lands, Closient derives the actor from a primitive that's already
unambiguous: the `User` foreign key on the `AccessToken` row that the
OAuth server minted for the consent grant.

Concretely:

1. The agent obtains a bearer token via OAuth 2.0
   `authorization_code` (typical for [Anthropic Connectors](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp)
   or any user-consent flow). The token row in Closient's database
   has a non-null `user_id` pointing at the consenting user.
2. The agent calls a Closient MCP tool with `Authorization: Bearer <token>`.
3. The MCP dispatch middleware validates the token, resolves the
   linked `User`, and exposes that user as the end-user actor for
   the duration of the tool call.

`client_credentials` tokens (no user consent step) have a null
`user_id`. The middleware sees that and sets `end_user_id = null` in
the audit log — the agent is the only principal on those calls.

## The audit log line

Every `tools/call` dispatch emits one structured log line on the
`closient.mcp.audit` logger. The line is JSON-shaped and ends up in
the same Grafana LGTM Loki index as the rest of the application logs.

Field-by-field:

| Field             | Type           | Meaning                                                                                        |
| ----------------- | -------------- | ---------------------------------------------------------------------------------------------- |
| `event`           | string         | Always `mcp_tool_call` — pin LogQL queries to this.                                            |
| `tool`            | string         | JSON-RPC `params.name` (e.g. `generate_qr_url`).                                               |
| `client_id`       | string \| null | Agent OAuth `client_id`, or null for unauthenticated public-tool calls.                        |
| `end_user_id`     | string \| null | End-user actor's User ID, or null for `client_credentials` / anonymous.                        |
| `status`          | string         | `allowed`, `denied_missing_token`, `denied_insufficient_scope`, `denied_oversize`, or `error`. |
| `required_scopes` | array\<string> | Scopes the tool requires. `[]` for public tools.                                               |
| `input_hash`      | string         | First 16 hex chars of SHA-256 over canonical JSON of `params.arguments`.                       |
| `request_id`      | any            | JSON-RPC envelope `id`, echoed for trace correlation.                                          |

The `input_hash` is deliberately a hash and not the raw arguments. It
is stable — same arguments produce the same hash — so operators can
correlate a log line with later state changes without storing GTINs,
batch / lot codes, or any other identifier in the audit log. If a
deeper investigation needs the raw input, it's recoverable from the
request-side trace (which is on a shorter retention window).

## What the audit log is *not*

The audit log line is best-effort observability. It's not the durable
audit trail for stateful operations — those are recorded in the
database via `@user_action` and `@org_action` decorators on the
service-layer functions the tool delegates to. Both records exist;
they serve different purposes:

* **Audit log line** (this page) — every tool call, success or failure.
  Fast to query, retains for the Loki retention window. Used for
  SOC2 evidence: "show me every MCP call to `generate_qr_url` in the
  past 30 days, broken down by `end_user_id`."
* **Database audit row** — only for service-layer mutations. Slower
  to query but durable forever. Used for "who created this resolver
  rule?" — answerable from the `audit_events` table directly.

If you need both — "who called the tool, what did it change, and
when" — join the audit-log `end_user_id` with the database audit row
on `user_id` and the timestamps will line up.

## Reading the actor from your tool body

If you're writing a Closient MCP tool that needs to act on behalf of
the end user (record an audit row, scope a query by user, etc.), use
the `get_current_end_user()` accessor:

```python theme={null}
from closient.mcp_auth import get_current_end_user

@mcp.tool()
@require_mcp_scope("qr:generate")
def my_tool(...) -> dict:
    end_user = get_current_end_user()  # User instance or None
    return service_layer_call(end_user, ...)
```

Behavior:

* `authorization_code` token → returns the `User` row that consented.
* `client_credentials` token → returns `None`.
* No token → returns `None`.

The same actor is also available on the ASGI request scope as
`scope["end_user"]`, which is what middleware-level consumers should
read.

## Forward compatibility

The MCP working group is discussing standardized identity propagation
(actor claims in the access token, structured headers, OAuth
[Token Exchange](https://www.rfc-editor.org/rfc/rfc8693), etc.). When
something lands, Closient will adopt it. The shape of the audit log
line and the `get_current_end_user()` accessor will stay stable —
those are the public contracts. The internal plumbing (DB FK lookup
today, parsed token claim later) is implementation detail.

If you depend on the audit-log shape, the safe fields are `event`,
`tool`, `client_id`, `end_user_id`, `status`, and `required_scopes`.
We may add fields without notice; we will not remove or rename those
six without a deprecation cycle.
