Personal Agent Protocol — serve as a Company's MCP server (experimental)
How MCPG serves as the MCP server a Personal Agent Protocol Company lists in its poppy.json — the Company's Session Tokens with Bearer and DPoP, sign_in_required, agent extensions, the discovery document, and operations that the User confirms.
Draft protocol, experimental support. The Personal Agent Protocol (PAP) is a draft. MCPG follows draft 0.2. The draft can change in incompatible ways before 1.0, and MCPG follows it.
With PAP, a User's Personal Agent acts at a Company for the User. The agent finds the
Company in its poppy.json, gets short-lived Session Tokens from the Company's
authorization server, and calls the Company's APIs and MCP servers with them. A Company
that runs MCPG in front of its systems lists MCPG as an MCP server:
"apis": [
{ "type": "mcp", "url": "https://mcp.example.com/mcp", "description": "Orders and returns" }
]
MCPG does not issue Session Tokens. The Company's authorization server issues them, and
MCPG verifies them with oidc_oauth or jwks.
The token checks
PAP §6 sets what a listed MCP server must do. Each item maps to MCPG configuration:
| PAP requirement | Configuration |
|---|---|
Accept only Session Tokens issued for the server's url | audiences: ["https://mcp.example.com/mcp"] on the provider |
| Enforce the tokens' scopes | governance.policy.tool_access.rules[].required_scopes |
Protected resource metadata whose authorization_servers includes auth.issuer | resource_metadata.authorization_servers |
| Accept Bearer Session Tokens; new servers also accept DPoP ones | governance.access.dpop |
governance:
access:
resource_metadata:
resource: "https://mcp.example.com/mcp"
authorization_servers: ["https://auth.example.com"]
oidc_oauth:
providers:
- issuer: "https://auth.example.com"
discovery_uri: "https://auth.example.com/.well-known/oauth-authorization-server"
audiences: ["https://mcp.example.com/mcp"]
verification:
kind: oidc_jwks
allowed_algs: ["ES256"]
claim_mappings:
attribute_claim_mappings:
client_id: client_id
sid: session_id
uid: user_id
acct: account
signed_in: signed_in
dpop:
enabled: true
allowed_algs: ["ES256"]
personal_agent: {}
discovery_uri can point at the issuer's RFC 8414 metadata: it needs jwks_uri. For
opaque Session Tokens, use verification with introspection. The dev.mcpg.identity.oidc
plugin entry lists the same providers. Use oidc_oauth, not jwks: only oidc_oauth maps
claims to identity attributes, so with jwks every caller is a plain MCP client. The
personal-agent-company.yaml deployment example under examples/deployments/ in the
distribution has the whole configuration.
The PAP profile
governance.access.personal_agent reads the PAP values of a Session Token from identity
attributes. The verifier's claim mappings fill them. The names in attributes default to
client_id, session_id, user_id, account and signed_in. A claim that is not a
string maps too: a boolean as true or false, and an object as compact JSON.
- A caller with a
client_idattribute is a Personal Agent. A caller without one is a plain MCP client that signed in with MCP's own flow. - A caller is signed in when
signed_inistrue. Without that attribute, a caller is signed in when it has theaccountattribute.
sign_in_required
When a tool's required_scopes refuse a Personal Agent that is not signed in, MCPG
answers 403 with WWW-Authenticate: Bearer error="sign_in_required". The Personal Agent
then uses its Account Token, or signs the User in. A signed-in Personal Agent that lacks a
scope still gets error="insufficient_scope" with the missing scopes. So does a plain MCP
client: it signed in with MCP's own flow, and PAP treats its Session as signed in. A
caller that presented a DPoP token gets both challenges in the DPoP scheme.
Agent extensions and extension_required
A Personal Agent lists the extensions it supports in its client metadata document, at its
client_id URL. With the default agent_extensions.source: client_metadata, MCPG fetches
that document over https: without redirects, with private addresses refused, at most
16 KiB, and only when the document names the same client_id. It caches the document per
Cache-Control, for at most cache_secs. With source: attribute, the Company's token
carries the list instead, in the attribute identity attribute.
governance:
access:
personal_agent:
require_extensions: ["operations"]
A Personal Agent that does not list every name in require_extensions is refused on every
request: 403 with WWW-Authenticate: Bearer error="extension_required", extension="operations". PAP lets an MCP server refuse an agent this way only as a whole,
never for one tool. Plain MCP clients are not refused.
Discovery
governance:
access:
personal_agent:
discovery:
document:
protocol_version: "0.2"
organization: { name: "Example", domain: "example.com" }
auth:
issuer: "https://auth.example.com"
direct: { scopes: ["poppy:read", "poppy:write"] }
apis:
- type: mcp
url: "https://mcp.example.com/mcp"
description: "Orders and returns"
MCPG serves the document at GET /.well-known/poppy.json with Cache-Control: max-age=300. At load, it checks protocol_version, the organization, and that every URL
is https. PAP binds the document to the host that serves it, so publish it on the
Company's own domain only.
Operations
The PAP operations extension (version 1) puts the User between a tool call and its
action. A tool that you list in operations.tools does not act when a Personal Agent that
lists operations calls it. It returns a proposed operation: the terms in plain
language, and an operation_id. The Personal Agent shows the terms to the User. When the
User approves, the agent confirms that revision at the operations endpoint, and MCPG
performs the action one time.
governance:
access:
resource_metadata:
resource: "https://mcp.example.com/mcp"
dpop:
enabled: true
personal_agent:
operations:
endpoint: "https://mcp.example.com/poppy/operations"
tools:
- tool: refund_order
summary: "Refund ${arguments.amount} EUR for order ${arguments.order_id}."
action_key: "${arguments.order_id}"
url: "https://example.com/orders/${arguments.order_id}"
expires_after_secs: 900
user_approval_required: true
| Field | What it does |
|---|---|
endpoint | The public URL of the operations endpoint. It must be https, on the origin of a resource_metadata resource, with the path /poppy/operations. |
tools[].summary | The terms in plain language, so that the User can decide from them alone. A template over the call's arguments. |
tools[].action_key | The same action again. While its operation is proposed, a call with the same key and equal terms returns that operation. A call with other terms makes the next revision. Without a key, each call proposes a new operation. |
tools[].url | A page of the Company's website that shows the operation. |
tools[].expires_after_secs | How long a revision can be confirmed (60–86400, default 900). |
tools[].user_approval_required | Accept only the User's own approval, not a standing permission. |
without_extension | perform (default): a Personal Agent that does not list operations gets the action at once, as without the extension. refuse_agent: such an agent is refused on every request with extension_required. Plain MCP clients always get the action. |
confirm_wait_secs | How long a confirm waits for the action before it answers in_progress (1–30, default 5). |
retain_final_secs | How long a final operation stays readable (3600–2592000, default 7 days). |
The templates cannot name a ${cred://…} reference, because the User sees what they
render. The operation's terms are the call's arguments, after MCPG validates them
against the tool's input schema.
The operations endpoint
The endpoint takes DPoP Session Tokens only, so governance.access.dpop (or the embedded
authorization server's dpop) must be on. DPoP needs a licence with the oauth.dpop
feature (Enterprise), so operations need it too. A Bearer token gets 401 with a DPoP
challenge.
| Request | Result |
|---|---|
GET /poppy/operations/{operation_id} | The operation. |
POST /poppy/operations/{operation_id}/confirm with { "revision": 2, "approved_by": "user" } | Confirms that revision and performs the action. approved_by is user or standing_permission. |
POST /poppy/operations/{operation_id}/cancel | A proposed operation becomes cancelled. |
An operation is proposed, in_progress, succeeded, failed, cancelled or
expired. The errors are:
404 operation_not_found: the operation is unknown, or this Session cannot use it. Only the Session that proposed it can use it. When that Session was signed in, every Session of the same Personal Agent and User signed in to the same account can use it too.409 terms_changed: the revision is not the current one. The body has the current operation, so that the agent can show the new terms to the User.403 user_approval_required: the tool needs the User's own approval.403 sign_in_requiredorinsufficient_scope: the tool's scopes refuse the confirming Session, as atools/callof the tool is refused. Any other refusal of the tool's access rules is403 access_denied.
What MCPG guarantees
- One action per operation. A confirm takes a claim on the operation. Only the confirm that takes the claim performs the action. Every other confirm, on any replica, returns the operation as it is. In a cluster, MCPG keeps operations in the coordinator's key-value store. Without a shared store, a confirm finds an operation only on the replica that proposed it, and MCPG logs a warning at boot.
- The whole tool-call path. The confirmed call goes through the same policy, gates,
transforms and backend as a
tools/call. It carries the idempotency keypap-op:{operation_id}, so a backend that supports idempotency can deduplicate it too. A proposal meets the policy and the input schema only: tool gates, such as a rate limit or a human-approval gate, run when the operation is confirmed. - An unknown outcome is not a failure. When the backend does not answer, MCPG cannot
know if the action took effect. The operation stays
in_progressand MCPG logs an error. It becomesfailedonly when the tool's result is an error, or when the call was refused before it reached the backend. - The audit trail. Each step is an audit event
pap.operation.{proposed, revised, confirmed, succeeded, failed, unknown, cancelled}with the operation, its revision, the Personal Agent, the account and the approval. The terms are not in the event. The metricmcpg_pap_operations_total{tool, event}counts the steps.
When discovery is set and its document does not list extensions.operations, MCPG adds
{ "version": "1", "endpoint": "<endpoint>" } to the poppy.json that it serves. A
document that lists it must give that same value.
Check a deployment
The PAP repository has a validator kit for a whole Company (kit/company/validate). Its
mcp.* checks test the MCP server: a 401 without a token that names the protected
resource metadata, the metadata itself, a Bearer Session Token for the server's url, and
the refusal of a token issued for any other resource.