Security
Securitybeta12 min

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:

json
"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 requirementConfiguration
Accept only Session Tokens issued for the server's urlaudiences: ["https://mcp.example.com/mcp"] on the provider
Enforce the tokens' scopesgovernance.policy.tool_access.rules[].required_scopes
Protected resource metadata whose authorization_servers includes auth.issuerresource_metadata.authorization_servers
Accept Bearer Session Tokens; new servers also accept DPoP onesgovernance.access.dpop
yaml
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_id attribute 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_in is true. Without that attribute, a caller is signed in when it has the account attribute.

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.

yaml
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

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

yaml
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
FieldWhat it does
endpointThe public URL of the operations endpoint. It must be https, on the origin of a resource_metadata resource, with the path /poppy/operations.
tools[].summaryThe terms in plain language, so that the User can decide from them alone. A template over the call's arguments.
tools[].action_keyThe 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[].urlA page of the Company's website that shows the operation.
tools[].expires_after_secsHow long a revision can be confirmed (60–86400, default 900).
tools[].user_approval_requiredAccept only the User's own approval, not a standing permission.
without_extensionperform (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_secsHow long a confirm waits for the action before it answers in_progress (1–30, default 5).
retain_final_secsHow 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.

RequestResult
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}/cancelA 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_required or insufficient_scope: the tool's scopes refuse the confirming Session, as a tools/call of the tool is refused. Any other refusal of the tool's access rules is 403 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 key pap-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_progress and MCPG logs an error. It becomes failed only 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 metric mcpg_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.