Skip to main content

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

PrincipalIdentifierSource
AgentOAuth client_idThe OAuth Application that issued the bearer token
End-user actorClosient User.idThe 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 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:
FieldTypeMeaning
eventstringAlways mcp_tool_call — pin LogQL queries to this.
toolstringJSON-RPC params.name (e.g. generate_qr_url).
client_idstring | nullAgent OAuth client_id, or null for unauthenticated public-tool calls.
end_user_idstring | nullEnd-user actor’s User ID, or null for client_credentials / anonymous.
statusstringallowed, denied_missing_token, denied_insufficient_scope, denied_oversize, or error.
required_scopesarray<string>Scopes the tool requires. [] for public tools.
input_hashstringFirst 16 hex chars of SHA-256 over canonical JSON of params.arguments.
request_idanyJSON-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:
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, 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.