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.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.
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 |
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: theUser foreign key on the AccessToken row that the
OAuth server minted for the consent grant.
Concretely:
- The agent obtains a bearer token via OAuth 2.0
authorization_code(typical for Anthropic Connectors or any user-consent flow). The token row in Closient’s database has a non-nulluser_idpointing at the consenting user. - The agent calls a Closient MCP tool with
Authorization: Bearer <token>. - 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
Everytools/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. |
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_urlin the past 30 days, broken down byend_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_eventstable directly.
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 theget_current_end_user() accessor:
authorization_codetoken → returns theUserrow that consented.client_credentialstoken → returnsNone.- No token → returns
None.
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, etc.). When something lands, Closient will adopt it. The shape of the audit log line and theget_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.