Agent identity

TL;DR — AuthPlane extends the standard OAuth token with two claims: agent_id — the outermost agent’s client_id when it’s registered with is_agent: true, and agent_chain — an ordered list of agent client_ids extracted from the act chain (capped at 8). Your MCP server can gate per-agent quotas, filter by agent in audit queries, and enforce per-agent policy without walking the nested RFC 8693 act tree. Not standardized — AuthPlane extension. Set an OAuth client’s is_agent: true and (optional) agent_description at registration to have it flagged as an agent.

The problem

RFC 8693 gives you an act chain for delegation (see Concepts: Delegation & act-chain). It’s rich — the full delegation tree is reconstructable — but walking it in every MCP server tool handler is annoying:

if token.act && token.act.sub == "agent-A" { ... }
else if token.act.act && token.act.act.sub == "agent-A" { ... }

AuthPlane emits flat claims that pre-compute the useful bits.

agent_id claim

Set to the outermost agent’s client_id when it’s registered with is_agent: true. In the vast majority of flows, that’s the client who initiated the current OAuth call.

Example — direct agent call (user auth-code flow with agent-A as the client):

{
  "sub": "user-42",
  "aud": "https://mcp.example.com/mcp",
  "client_id": "agent-A",
  "agent_id": "agent-A"
}

Example — delegated call (agent-A exchanged the user token, sub-agent B is now holding):

{
  "sub": "user-42",
  "aud": "https://downstream.example.com",
  "client_id": "agent-B",
  "act": { "sub": "agent-B", "actor_type": "agent", "act": { "sub": "agent-A" } },
  "agent_id": "agent-B"
}

agent_id names the current actor — same as act.sub when there’s an act chain, same as client_id otherwise. Precomputed for convenience.

agent_chain claim

Ordered list of every client_id in the act chain, capped at 8 entries. First entry = originator (root of the chain), last entry = current actor. AuthPlane walks the nested act structure once and then reverses so the list reads in causal order.

Example — chained delegation A delegates to B, B delegates to C (C is now calling your MCP server):

{
  "sub": "user-42",
  "act": {
    "sub": "agent-C",
    "act": {
      "sub": "agent-B",
      "act": { "sub": "agent-A" }
    }
  },
  "agent_id": "agent-C",
  "agent_chain": ["agent-A", "agent-B", "agent-C"]
}

Your MCP server can filter/log/gate on the chain without recursion:

if token.agent_chain[0] == "agent-A":
    # This request originated from agent-A (the root of the delegation chain)

Registering a client as an agent

At registration time, set is_agent: true:

authserver admin client create \
    --name my-research-agent \
    --agent \
    --agent-description "Research assistant that reads GitHub + Notion" \
    --grant-types authorization_code,refresh_token,urn:ietf:params:oauth:grant-type:token-exchange \
    --scopes 'tools/read||Read tools'

REST:

POST /admin/clients
{
  "client_name": "my-research-agent",
  "is_agent": true,
  "agent_description": "Research assistant that reads GitHub + Notion",
  "grant_types": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:token-exchange"],
  "scope": "tools/read"
}

agent_description — max 255 chars — shown on consent screens (“Do you want to grant research assistant that reads GitHub + Notion access to your data?”). Optional but recommended for user-facing agents.

DCR clients can also self-register as agents by including "agent": true in the registration payload.

When agent_id and agent_chain are present

  • agent_id present when the outermost actor’s client has is_agent: true.
  • agent_chain present when the issuing (outermost) client is registered as an agent; in that case it contains every hop in the delegation chain, including any non-agent service clients. There is no filtering by is_agent on intermediate hops.

Absent when the flow is entirely non-agent — machine-to-machine service calls, admin operations, plain user auth-code with a non-agent client. Your MCP server should treat missing claims as “not an agent-mediated call”.

Optional: agents.enable_jwks_listing

When agents.enable_jwks_listing: true (config), AuthPlane publishes a JWKS document per agent at /.well-known/agents/{agent_id}.json — useful for third-party verification of agent-issued signatures if agents sign anything themselves. Off by default; enable only if agents actually publish their own JWKs.

AS metadata advertisement

/.well-known/oauth-authorization-server includes:

{
  "authplane_agent_identity_supported": true,
  ...
}

Clients can key off this to know AuthPlane emits agent_id / agent_chain claims.

What downstream services should do with agent identity

  • Per-agent rate limits — throttle at the MCP layer by agent_id.
  • Per-agent quotas — count tool invocations per (user, agent_id) pair for billing / usage-based limits.
  • Enhanced audit — log agent_id + agent_chain on every request; enables “show me every action this agent has ever taken across our fleet” queries.
  • Agent-scoped policy — deny certain tools based on agent identity (e.g., “financial writeback tools not allowed for agents in agent_chain”).

What NOT to do: don’t authorize solely on agent_chain inner entries. Same rule as RFC 8693 §4.1 ¶6 — inner-hop identities (originator + intermediaries) are informational, not authoritative. Only agent_id (the current actor, the last entry in the chain) is trustworthy for access-control decisions.

Multi-agent example

User asks orchestrator agent → orchestrator delegates to research agent → research agent hits summarizer:

Token at summarizer:
{
  "sub": "user-42",
  "client_id": "agent-summarizer",
  "act": {
    "sub": "agent-summarizer",
    "act": {
      "sub": "agent-research",
      "act": { "sub": "agent-orchestrator" }
    }
  },
  "agent_id": "agent-summarizer",
  "agent_chain": ["agent-orchestrator", "agent-research", "agent-summarizer"],
  "scope": "tools/summarize"
}

Summarizer knows:

  • Currently holding: agent-summarizer (authoritative for authorization).
  • Delegation chain: orchestrator started, delegated to research, who delegated to summarizer.
  • Ultimate principal: user-42.

Everything reconstructable from the token alone. No external correlation needed.