Skip to main content

MCP server: @oid4pay/oid4pay-mcp

The OID4Pay MCP server is a stdio-mode Model Context Protocol server that surfaces OID4AC as agent-callable tools. Drop it into Claude Desktop, Cline, Continue, or any MCP-aware toolchain; the agent can then initiate a payment against any OID4AC-enabled merchant without you implementing the protocol.

Twelve tools ship in oid4pay-mcp 0.1.0. Each tool wraps a deterministic slice of the protocol wire shapes: registration, payment, mandate verification, merchant discovery, wallet management, GDPR DSAR, and the audit chain. The MCP refuses to call any AS endpoint that violates the algorithm whitelist.

Install

npm install -g @oid4pay/oid4pay-mcp

Claude Desktop configuration

Add to ~/.config/Claude/claude_desktop_config.json (or the Windows / macOS equivalent):

{
  "mcpServers": {
    "oid4pay": {
      "command": "oid4pay-mcp",
      "args": ["--as-origin", "https://sandbox.oid4pay.com"],
      "env": {
        "OID4PAY_CLIENT_ID": "client_...",
        "OID4PAY_DPOP_PRIVATE_JWK": "{...}",
        "OID4PAY_PKJ_PRIVATE_JWK": "{...}",
        "OID4PAY_DISCOVERY_URL": "https://sandbox.discover.oid4pay.com"
      }
    }
  }
}

The agent's DPoP and private_key_jwt keys are written to ~/.local/share/oid4pay-mcp/keys.json on first registration. The persisted client_id lands at ~/.local/share/oid4pay-mcp/client.json; rotate by deleting the file and calling agent_register again with a fresh setup_token.

Token types

Each tool documents which token type the AS accepts on the wire:

Tools (table of contents)

ToolPurposeProtocol
agent_registerRFC 7591 Dynamic Client Registration (DCR).private_key_jwt assertion
agent_payment_initiateFull OID4AC chain (PAR : authorize : token : verify-mandate).PAR response through authorization code single-use
agent_verify_mandateSD-JWT VC + KB-JWT presentation at a merchant /verify-mandate.SD-JWT VC mandate
discovery_list_merchantsOID4Pay directory query (country, currency, rail filters).Directory query (see HTTP message signature for catalog signing)
agent_browse_merchantSigned catalog preview for a single merchant.HTTP message signature
agent_wallet_registerWallet-side DCR with display metadata (client_name, purpose).OIDC discovery metadata
agent_wallet_list_agentsRead the principal's registered agents (wallet:read).JWT-AT claim set + resource indicators
agent_wallet_revoke_agentCascade-revoke a registered agent (wallet:write).authorization code single-use + /oauth/revoke
agent_buy_cheapest_from_storeSugar: fetch catalog : pick cheapest : pay.PAR response through authorization code single-use
agent_dsar_initiateGDPR Article 15 (access) or Article 20 (portability) DSAR.GDPR privacy rights + audit chain entry envelope
agent_audit_chain_queryForensic query against the principal's audit chain entries.audit chain entry envelope
agent_payment_historyUI-friendly payment projection over the audit chain.audit chain entry envelope
Convention. Every tool's input schema and output payload is pinned to the shipped 0.1.0 server (oid4pay-mcp/src/server/index.ts). The MCP layer wraps the AS wire shapes verbatim. When a wire shape changes, the MCP layer follows it; when this page disagrees with the code, the code wins and we fix the page.

agent_register

Generates or loads the agent's DPoP and private_key_jwt keypairs, then calls POST /oauth/register on the AS with the supplied setup token. Persists the resulting client_id to ~/.local/share/oid4pay-mcp/client.json. RFC 7591 Dynamic Client Registration; client authentication follows the private_key_jwt assertion. Subsequent tool calls reuse the persisted registration.

Input schema

{
  "type": "object",
  "properties": {
    "setup_token":   { "type": "string", "minLength": 1 },
    "as_url":        { "type": "string", "format": "uri" },
    "redirect_uris": { "type": "array", "minItems": 1,
                       "items": { "type": "string", "format": "uri" } },
    "client_name":   { "type": "string" }
  },
  "required": ["setup_token", "as_url", "redirect_uris"]
}

Output

{
  "ok": true,
  "client_id": "agent_3ff8a1d2b9c4e7f0",
  "as_url": "https://sandbox.oid4pay.com"
}

Errors

Operator-facing messageCause
agent_register failed: invalid_client_metadataAS rejected the client_name or other RFC 7591 fields.
agent_register failed: invalid_redirect_uriOne of the redirect_uris uses a disallowed scheme.
agent_register failed: invalid_software_statementThe setup_token is expired, already used, or unbound to this AS.

Auth requirement

One-shot setup token issued by the Wallet Portal. The token binds the agent to a principal at issue time; the AS rejects reuse.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import OID4PayClient

client = OID4PayClient(as_url="https://sandbox.oid4pay.com")
reg = client.register(
    setup_token="setup_4f6a2c1d8b9e3f70",
    redirect_uris=["https://my-agent.example.com/callback"],
    client_name="acme-research-agent",
)
print(reg.client_id)

Node (@oid4pay/oid4ac-merchant)

import { OID4PayClient } from "@oid4pay/oid4ac-merchant";

const client = new OID4PayClient({ asUrl: "https://sandbox.oid4pay.com" });
const reg = await client.register({
  setupToken: "setup_4f6a2c1d8b9e3f70",
  redirectUris: ["https://my-agent.example.com/callback"],
  clientName: "acme-research-agent",
});
console.log(reg.client_id);

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "agent_register",
    "arguments": {
      "setup_token": "setup_4f6a2c1d8b9e3f70",
      "as_url": "https://sandbox.oid4pay.com",
      "redirect_uris": ["https://my-agent.example.com/callback"],
      "client_name": "acme-research-agent"
    }
  }
}

agent_payment_initiate

Drives the full OID4AC dance against the configured AS: PAR : authorize : token : verify-mandate. Returns the mandate id and (when issued) the signed receipt JWS. Every leg follows its wire shape: PAR response, JWT-AT claim set, SD-JWT VC mandate, KB-JWT, DPoP, resource indicators, algorithm whitelist, and authorization code single-use.

Input schema

{
  "type": "object",
  "properties": {
    "merchant_url":         { "type": "string", "format": "uri" },
    "merchant_verify_url":  { "type": "string", "format": "uri" },
    "amount": {
      "type": "object",
      "properties": {
        "currency":     { "type": "string", "length": 3 },
        "amount_minor": { "type": "integer", "minimum": 1 }
      },
      "required": ["currency", "amount_minor"]
    },
    "line_items": {
      "type": "array",
      "minItems": 1,
      "items": {
        "type": "object",
        "properties": {
          "sku":              { "type": "string", "minLength": 1 },
          "qty":              { "type": "integer", "minimum": 1 },
          "unit_price_minor": { "type": "integer", "minimum": 0 },
          "currency":         { "type": "string", "length": 3 }
        },
        "required": ["sku", "qty"]
      }
    },
    "offer_id":           { "type": "string" },
    "scope":              { "type": "string", "default": "openid payment:initiate" },
    "redirect_uri":       { "type": "string", "format": "uri" },
    "consent_mode_hint":  { "enum": ["auto", "dashboard", "scan"] }
  },
  "required": ["merchant_url", "merchant_verify_url", "amount", "line_items", "redirect_uri"]
}

Output

{
  "mandate_id": "mandate_7b2c4d6e8f0a1b3c",
  "mandate_jwt": "eyJhbGciOiJFZERTQSIsImtpZCI6...",
  "presentation": "eyJhbGciOiJFZERTQSIsInR5cCI6InNkLWp3dC12YyJ9...~WyJzYWx0...~eyJhbGciOiJFZERTQSIsInR5cCI6ImtiK2p3dCJ9...",
  "payment_intent_id": "pi_3OQ8Z2L8KZ4f5G2c1AbCdE",
  "payment_provider_ref": "ch_3OQ8Z2L8KZ4f5G2c0HiJkLmN",
  "settled_at": "2026-05-15T05:21:28Z",
  "receipt_url": "https://sandbox.oid4pay.com/receipts/r_4f6a2c1d8b9e.json"
}

Errors

Operator-facing messageCause
agent_payment_initiate failed: PAR failed: 400PAR body rejected: missing authorization_details, bad resource indicator, or stale DPoP nonce.
agent_payment_initiate failed: token failed: 400 invalid_grantAuthorization code expired or reused. Re-run PAR.
agent_payment_initiate failed: token failed: 400 invalid_targetThe resource indicator does not match the PAR-bound audience (see resource indicators).
agent_payment_initiate failed: verify-mandate failed: 422Merchant rejected the SD-JWT VC presentation (audience mismatch, expired, status-revoked, or KB-JWT nonce wrong).
agent_payment_initiate failed: verify-mandate failed: 502Merchant /verify-mandate upstream error; the mandate is still valid; retry with the same idempotency_key.
agent_payment_initiate failed: declined: no mandate issuedPrincipal declined consent at the wallet; no JWT-AT was issued.

Auth requirement

Agent-registered client_id with a DPoP keypair. The MCP reads the persisted client.json; agent_register must have run first.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import OID4PayClient, Money, LineItem

client = OID4PayClient.from_persisted()
result = client.pay(
    merchant_url="https://shop.alpacanica.com",
    merchant_verify_url="https://shop.alpacanica.com/verify-mandate",
    amount=Money(currency="EUR", amount_minor=1299),
    line_items=[LineItem(sku="alpaca-sock-blue-43", qty=1)],
    redirect_uri="https://my-agent.example.com/callback",
)
print(result.payment_intent_id, result.mandate_id)

Node (@oid4pay/oid4ac-merchant)

import { OID4PayClient } from "@oid4pay/oid4ac-merchant";

const client = await OID4PayClient.fromPersisted();
const result = await client.pay({
  merchantResource: "https://shop.alpacanica.com",
  merchantVerifyUrl: "https://shop.alpacanica.com/verify-mandate",
  authorizationDetails: [{
    type: "oid4ac_mandate",
    amount_minor: 1299,
    currency: "EUR",
    merchant: "https://shop.alpacanica.com",
    line_items: [{ sku: "alpaca-sock-blue-43", qty: 1 }],
  }],
  redirectUri: "https://my-agent.example.com/callback",
  scope: "openid payment:initiate",
  lineItems: [{ sku: "alpaca-sock-blue-43", qty: 1 }],
});
console.log(result.payment_intent_id, result.mandate_id);

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/call",
  "params": {
    "name": "agent_payment_initiate",
    "arguments": {
      "merchant_url": "https://shop.alpacanica.com",
      "merchant_verify_url": "https://shop.alpacanica.com/verify-mandate",
      "amount": { "currency": "EUR", "amount_minor": 1299 },
      "line_items": [{ "sku": "alpaca-sock-blue-43", "qty": 1 }],
      "redirect_uri": "https://my-agent.example.com/callback",
      "scope": "openid payment:initiate"
    }
  }
}

agent_verify_mandate

Posts a compact SD-JWT VC presentation (mandate JWT + selective disclosures + KB-JWT) to the merchant's /verify-mandate endpoint and returns the verifier's reply. Standalone helper for agents acting as their own merchant or for re-verification after a network failure. The presentation envelope follows the SD-JWT VC mandate shape; KB-JWT key binding enforces the cnf.jkt claim end-to-end.

Input schema

{
  "type": "object",
  "properties": {
    "merchant_verify_url": { "type": "string", "format": "uri" },
    "presentation":        { "type": "string", "minLength": 1 },
    "line_items":          { "type": "array",
                              "items": { "$ref": "#/definitions/LineItem" } },
    "offer_id":            { "type": "string" },
    "idempotency_key":     { "type": "string" }
  },
  "required": ["merchant_verify_url", "presentation", "line_items"]
}

Output

{
  "mandate_id": "mandate_7b2c4d6e8f0a1b3c",
  "verified_at": "2026-05-15T05:21:28Z",
  "verifier_principal_id": "principal_3ff8a1d2b9c4e7f0",
  "spend_cap_remaining_minor": 4701
}

Errors

Operator-facing messageCause
agent_verify_mandate failed: verify-mandate failed: 422 mandate_audience_mismatchMandate aud claim does not match merchant_verify_url origin.
agent_verify_mandate failed: verify-mandate failed: 422 mandate_kb_nonce_mismatchKB-JWT nonce does not match the merchant's challenge nonce.
agent_verify_mandate failed: verify-mandate failed: 422 mandate_status_revokedMandate revoked at the AS (cascade-revoke or principal-initiated).
agent_verify_mandate failed: verify-mandate failed: 422 mandate_expiredMandate exp claim in the past.

Auth requirement

None at the AS layer; the presentation carries its own cryptographic proof. The agent must hold the SD-JWT VC + KB-JWT (issued during agent_payment_initiate).

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import OID4PayClient, LineItem

client = OID4PayClient.from_persisted()
reply = client.verify_mandate(
    merchant_verify_url="https://shop.alpacanica.com/verify-mandate",
    presentation=presentation_compact,
    line_items=[LineItem(sku="alpaca-sock-blue-43", qty=1)],
    idempotency_key="ik_4f6a2c1d8b9e3f70",
)
print(reply.mandate_id, reply.spend_cap_remaining_minor)

Node (@oid4pay/oid4ac-merchant)

import { OID4PayClient } from "@oid4pay/oid4ac-merchant";

const client = await OID4PayClient.fromPersisted();
const reply = await client.verifyMandate({
  merchantVerifyUrl: "https://shop.alpacanica.com/verify-mandate",
  presentation: presentationCompact,
  lineItems: [{ sku: "alpaca-sock-blue-43", qty: 1 }],
  idempotencyKey: "ik_4f6a2c1d8b9e3f70",
});
console.log(reply.mandate_id, reply.spend_cap_remaining_minor);

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "agent_verify_mandate",
    "arguments": {
      "merchant_verify_url": "https://shop.alpacanica.com/verify-mandate",
      "presentation": "eyJhbGciOiJFZERTQSIs...~WyJzYWx0...~eyJhbGciOiJFZERTQSIs...",
      "line_items": [{ "sku": "alpaca-sock-blue-43", "qty": 1 }],
      "idempotency_key": "ik_4f6a2c1d8b9e3f70"
    }
  }
}

discovery_list_merchants

Calls GET /merchants on the OID4Pay discovery service and returns the paginated list of verified merchants. Filters: country (ISO 3166-1 alpha-2), currency (ISO 4217), rail (card, ideal, sepa_debit, etc.), free-text search against business_name. Pagination via page + page_size (server caps page_size at 100). Returns merchant identities only (id, audience, business name, country, accepted currencies and rails); no inventory.

Input schema

{
  "type": "object",
  "properties": {
    "country":   { "type": "string", "length": 2 },
    "currency":  { "type": "string", "length": 3 },
    "rail":      { "type": "string" },
    "q":         { "type": "string" },
    "page":      { "type": "integer", "minimum": 1 },
    "page_size": { "type": "integer", "minimum": 1, "maximum": 100 }
  }
}

Output

{
  "merchants": [
    {
      "id": "merch_alpacanica",
      "audience": "https://shop.alpacanica.com",
      "business_name": "Alpacanica Outfitters",
      "country": "NL",
      "accepted_currencies": ["EUR", "USD"],
      "accepted_rails": ["card", "ideal", "sepa_debit"],
      "verified_at": "2026-04-01T00:00:00Z"
    },
    {
      "id": "merch_bristleandslate",
      "audience": "https://shop.bristleandslate.com",
      "business_name": "Bristle and Slate",
      "country": "NL",
      "accepted_currencies": ["EUR"],
      "accepted_rails": ["card", "ideal"],
      "verified_at": "2026-04-12T00:00:00Z"
    }
  ],
  "page": 1,
  "page_size": 20,
  "total": 2
}

Errors

Operator-facing messageCause
discovery_list_merchants failed: 400 invalid_filterFilter value rejected (e.g. country not ISO 3166-1, currency not ISO 4217).
discovery_list_merchants failed: 500 server_errorDiscovery service upstream error.
discovery_list_merchants failed: fetch failedTransport-level failure (ECONNREFUSED, DNS, TLS).

Auth requirement

None. The discovery service is unauthenticated for read; results are filtered to verified, in-good-standing merchants.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import DiscoveryClient

disc = DiscoveryClient(base_url="https://sandbox.discover.oid4pay.com")
reply = disc.list_merchants(country="NL", currency="EUR", rail="ideal")
for m in reply.merchants:
    print(m.id, m.business_name, m.accepted_rails)

Node (@oid4pay/oid4ac-merchant)

import { listMerchants } from "@oid4pay/oid4ac-merchant/discovery";

const reply = await listMerchants(
  { country: "NL", currency: "EUR", rail: "ideal" },
  { baseUrl: "https://sandbox.discover.oid4pay.com" },
);
for (const m of reply.merchants) {
  console.log(m.id, m.business_name, m.accepted_rails);
}

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 4,
  "method": "tools/call",
  "params": {
    "name": "discovery_list_merchants",
    "arguments": {
      "country": "NL",
      "currency": "EUR",
      "rail": "ideal",
      "page": 1,
      "page_size": 20
    }
  }
}

agent_browse_merchant

Calls GET /merchants/{id}/catalog-preview on the OID4Pay discovery service. The discovery service proxies the merchant's RFC 9421 signed catalog (first page only); the response carries the merchant's signature, signature_input, and content_digest fields. The MCP rejects responses missing any signed field (UnverifiedCatalogError); refusing to act on an unsigned catalog is where the HTTP message signature is enforced.

Input schema

{
  "type": "object",
  "properties": {
    "merchant_id": {
      "type": "string",
      "minLength": 1,
      "pattern": "^[A-Za-z0-9_:-]+$"
    }
  },
  "required": ["merchant_id"]
}

Output

{
  "merchant_id": "merch_alpacanica",
  "audience": "https://shop.alpacanica.com",
  "signature": ":MEUCIQDt...:",
  "signature_input": "sig1=(\"@method\" \"@target-uri\" \"content-digest\");keyid=\"merch_alpacanica_2026Q2\";alg=\"ed25519\";created=1715750488;expires=1715750548",
  "content_digest": "sha-256=:bdaTFvfksp...:",
  "items": [
    {
      "sku": "alpaca-sock-blue-43",
      "title": "Alpaca wool sock, sky blue, size 43",
      "unit_price_minor": 1299,
      "currency": "EUR",
      "in_stock": true,
      "category": "apparel",
      "offer_url": "https://shop.alpacanica.com/oid4ac/offer/alpaca-sock-blue-43"
    }
  ]
}

Errors

Operator-facing messageCause
agent_browse_merchant rejected upstream catalog (missing signed field "signature")Upstream catalog response omitted a signed field; the MCP refuses to act on an unsigned catalog.
agent_browse_merchant failed: 404 not_foundNo merchant with that id in the directory.
agent_browse_merchant failed: 429 rate_limitedDiscovery service rate-limit. Backoff before retrying.
agent_browse_merchant failed: 502 bad_gatewayUpstream merchant catalog endpoint unreachable.
agent_browse_merchant failed: invalid_merchant_idPath-traversal defence: merchant_id must match ^[A-Za-z0-9_:-]+$.

Auth requirement

None. The MCP verifies the merchant's RFC 9421 signature before returning.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import DiscoveryClient

disc = DiscoveryClient(base_url="https://sandbox.discover.oid4pay.com")
catalog = disc.browse_merchant("merch_alpacanica")
for item in catalog.items:
    print(item.sku, item.unit_price_minor, item.currency)

Node (@oid4pay/oid4ac-merchant)

import { browseMerchant } from "@oid4pay/oid4ac-merchant/discovery";

const catalog = await browseMerchant(
  "merch_alpacanica",
  { baseUrl: "https://sandbox.discover.oid4pay.com" },
);
for (const item of catalog.items) {
  console.log(item.sku, item.unit_price_minor, item.currency);
}

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 5,
  "method": "tools/call",
  "params": {
    "name": "agent_browse_merchant",
    "arguments": {
      "merchant_id": "merch_alpacanica"
    }
  }
}

agent_wallet_register

RFC 7591 Dynamic Client Registration against the AS, persisting the new client_id and DPoP keypair locally. Differs from agent_register in that it ships a client_name and purpose that the Wallet Portal renders in its agents list (the user-facing labels). Returns the bound principal_id alongside the issued client_id. Follows the OIDC discovery metadata.

Input schema

{
  "type": "object",
  "properties": {
    "setup_token":   { "type": "string", "minLength": 1 },
    "as_url":        { "type": "string", "format": "uri" },
    "redirect_uris": { "type": "array", "minItems": 1,
                       "items": { "type": "string", "format": "uri" } },
    "client_name":   { "type": "string", "minLength": 1 },
    "purpose":       { "type": "string", "minLength": 1 },
    "scope":         { "type": "string", "default": "wallet:read wallet:write" }
  },
  "required": ["setup_token", "as_url", "redirect_uris", "client_name", "purpose"]
}

Output

{
  "ok": true,
  "client_id": "agent_3ff8a1d2b9c4e7f0",
  "principal_id": "principal_8a2c1d8b9e3f70a4",
  "client_name": "acme-research-agent",
  "purpose": "Find and book research papers under EUR 50/month.",
  "scope": "wallet:read wallet:write",
  "as_url": "https://sandbox.oid4pay.com"
}

Errors

Operator-facing messageCause
agent_wallet_register failed: invalid_client_metadataclient_name empty or violates the AS validators.
agent_wallet_register failed: invalid_software_statementSetup token expired, single-use, or unbound.
agent_wallet_register failed: invalid_redirect_uriOne of the redirect_uris rejected (disallowed scheme, mismatched host).

Auth requirement

One-shot setup token issued by the Wallet Portal QR scan.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import WalletClient

wallet = WalletClient(as_url="https://sandbox.oid4pay.com")
reg = wallet.register_agent(
    setup_token="setup_4f6a2c1d8b9e3f70",
    redirect_uris=["https://my-agent.example.com/callback"],
    client_name="acme-research-agent",
    purpose="Find and book research papers under EUR 50/month.",
    scope="wallet:read wallet:write",
)
print(reg.client_id, reg.principal_id)

Node (@oid4pay/oid4ac-merchant)

import { WalletClient } from "@oid4pay/oid4ac-merchant/wallet";

const wallet = new WalletClient({ asUrl: "https://sandbox.oid4pay.com" });
const reg = await wallet.registerAgent({
  setupToken: "setup_4f6a2c1d8b9e3f70",
  redirectUris: ["https://my-agent.example.com/callback"],
  clientName: "acme-research-agent",
  purpose: "Find and book research papers under EUR 50/month.",
  scope: "wallet:read wallet:write",
});
console.log(reg.client_id, reg.principal_id);

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 6,
  "method": "tools/call",
  "params": {
    "name": "agent_wallet_register",
    "arguments": {
      "setup_token": "setup_4f6a2c1d8b9e3f70",
      "as_url": "https://sandbox.oid4pay.com",
      "redirect_uris": ["https://my-agent.example.com/callback"],
      "client_name": "acme-research-agent",
      "purpose": "Find and book research papers under EUR 50/month.",
      "scope": "wallet:read wallet:write"
    }
  }
}

agent_wallet_list_agents

Runs a wallet:read OAuth dance against the AS to mint a JWT-AT scoped wallet:read, then GET /wallet/agents. Returns the principal's agents (IDOR-safe: the AS filters on the JWT-AT sub claim resolved by the wallet session). Errors with insufficient_scope if the AS refuses the requested scope.

Input schema

{
  "type": "object",
  "properties": {
    "as_url":       { "type": "string", "format": "uri" },
    "resource":     { "type": "string", "format": "uri" },
    "redirect_uri": { "type": "string", "format": "uri" }
  },
  "required": ["as_url", "resource", "redirect_uri"]
}

Output

{
  "agents": [
    {
      "client_id": "agent_3ff8a1d2b9c4e7f0",
      "client_name": "acme-research-agent",
      "purpose": "Find and book research papers under EUR 50/month.",
      "registered_at": "2026-05-01T09:14:22Z",
      "last_used_at": "2026-05-15T05:21:28Z",
      "scope": "wallet:read wallet:write",
      "status": "active"
    }
  ]
}

Errors

Operator-facing messageCause
agent_wallet_list_agents: insufficient_scope (need wallet:read); ...AS refused the wallet:read scope (the principal has not granted it).
agent_wallet_list_agents failed (401 invalid_token): ...JWT-AT expired or DPoP proof invalid.
agent_wallet_list_agents failed (403 forbidden): ...Token valid but principal mismatch; the AS-side IDOR filter rejected the request.

Auth requirement

Wallet-session JWT-AT, scope wallet:read.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import OID4PayClient, WalletClient

client = OID4PayClient.from_persisted()
token = client.get_access_token(
    redirect_uri="https://my-agent.example.com/callback",
    resource="https://sandbox.oid4pay.com",
    scope="wallet:read",
)
wallet = WalletClient(as_url="https://sandbox.oid4pay.com")
reply = wallet.list_agents(token.access_token)
for a in reply.agents:
    print(a.client_id, a.client_name, a.status)

Node (@oid4pay/oid4ac-merchant)

import { OID4PayClient } from "@oid4pay/oid4ac-merchant";
import { WalletClient } from "@oid4pay/oid4ac-merchant/wallet";

const client = await OID4PayClient.fromPersisted();
const token = await client.getAccessToken({
  redirectUri: "https://my-agent.example.com/callback",
  resource: "https://sandbox.oid4pay.com",
  scope: "wallet:read",
});
const wallet = new WalletClient({ asUrl: "https://sandbox.oid4pay.com" });
const reply = await wallet.listAgents(token.access_token);
for (const a of reply.agents) console.log(a.client_id, a.client_name);

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 7,
  "method": "tools/call",
  "params": {
    "name": "agent_wallet_list_agents",
    "arguments": {
      "as_url": "https://sandbox.oid4pay.com",
      "resource": "https://sandbox.oid4pay.com",
      "redirect_uri": "https://my-agent.example.com/callback"
    }
  }
}

agent_wallet_revoke_agent

Runs a wallet:write OAuth dance against the AS to mint a JWT-AT scoped wallet:write, then POST /wallet/agents/{agent_client_id}/revoke. The AS cascade-revokes every active token family for the agent (mandates, refresh tokens, access tokens), as set out in authorization code single-use and /oauth/revoke. Errors with insufficient_scope if the AS refuses the requested scope.

Input schema

{
  "type": "object",
  "properties": {
    "as_url":          { "type": "string", "format": "uri" },
    "agent_client_id": { "type": "string", "minLength": 1 },
    "resource":        { "type": "string", "format": "uri" },
    "redirect_uri":    { "type": "string", "format": "uri" }
  },
  "required": ["as_url", "agent_client_id", "resource", "redirect_uri"]
}

Output

{
  "ok": true,
  "agent_client_id": "agent_3ff8a1d2b9c4e7f0",
  "revoked_at": "2026-05-15T05:21:28Z",
  "cascade": {
    "mandates_revoked": 12,
    "refresh_tokens_revoked": 1,
    "access_tokens_revoked": 1
  }
}

Errors

Operator-facing messageCause
agent_wallet_revoke_agent: insufficient_scope (need wallet:write); ...AS refused the wallet:write scope.
agent_wallet_revoke_agent failed (404 not_found): ...No agent with that client_id bound to this principal.
agent_wallet_revoke_agent failed (409 conflict): ...Agent already revoked; the cascade is idempotent but the AS reports the prior state.
agent_wallet_revoke_agent failed (503 server_error): ...AS transient failure; retry safe (revoke is idempotent).

Auth requirement

Wallet-session JWT-AT, scope wallet:write.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import OID4PayClient, WalletClient

client = OID4PayClient.from_persisted()
token = client.get_access_token(
    redirect_uri="https://my-agent.example.com/callback",
    resource="https://sandbox.oid4pay.com",
    scope="wallet:write",
)
wallet = WalletClient(as_url="https://sandbox.oid4pay.com")
reply = wallet.revoke_agent(token.access_token, "agent_3ff8a1d2b9c4e7f0")
print(reply.cascade.mandates_revoked)

Node (@oid4pay/oid4ac-merchant)

import { OID4PayClient } from "@oid4pay/oid4ac-merchant";
import { WalletClient } from "@oid4pay/oid4ac-merchant/wallet";

const client = await OID4PayClient.fromPersisted();
const token = await client.getAccessToken({
  redirectUri: "https://my-agent.example.com/callback",
  resource: "https://sandbox.oid4pay.com",
  scope: "wallet:write",
});
const wallet = new WalletClient({ asUrl: "https://sandbox.oid4pay.com" });
const reply = await wallet.revokeAgent(
  token.access_token,
  "agent_3ff8a1d2b9c4e7f0",
);
console.log(reply.cascade.mandates_revoked);

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 8,
  "method": "tools/call",
  "params": {
    "name": "agent_wallet_revoke_agent",
    "arguments": {
      "as_url": "https://sandbox.oid4pay.com",
      "agent_client_id": "agent_3ff8a1d2b9c4e7f0",
      "resource": "https://sandbox.oid4pay.com",
      "redirect_uri": "https://my-agent.example.com/callback"
    }
  }
}

agent_buy_cheapest_from_store

Sugar tool. Fetches the merchant's /.well-known/oid4ac-catalog, picks the cheapest in-stock item matching the optional category and shipping filters under max_amount_minor, and pays for it through the full OID4AC flow. The MCP combines fetchCatalog, pickCheapest, and agent_payment_initiate in one call. Returns the chosen item + the payment result.

Input schema

{
  "type": "object",
  "properties": {
    "store_url":          { "type": "string", "format": "uri" },
    "merchant_verify_url":{ "type": "string", "format": "uri" },
    "max_amount_minor":   { "type": "integer", "minimum": 1 },
    "currency":           { "type": "string", "length": 3, "default": "EUR" },
    "category":           { "type": "string" },
    "ships_to_country":   { "type": "string" },
    "redirect_uri":       { "type": "string", "format": "uri" }
  },
  "required": ["store_url", "merchant_verify_url", "max_amount_minor", "redirect_uri"]
}

Output

{
  "chosen": {
    "sku": "alpaca-sock-blue-43",
    "title": "Alpaca wool sock, sky blue, size 43",
    "unit_price_minor": 1299,
    "currency": "EUR",
    "in_stock": true,
    "category": "apparel"
  },
  "result": {
    "mandate_id": "mandate_7b2c4d6e8f0a1b3c",
    "payment_intent_id": "pi_3OQ8Z2L8KZ4f5G2c1AbCdE",
    "payment_provider_ref": "ch_3OQ8Z2L8KZ4f5G2c0HiJkLmN",
    "settled_at": "2026-05-15T05:21:28Z"
  }
}

Errors

Operator-facing messageCause
agent_buy_cheapest_from_store: no in-stock matching items under the capNo item satisfies the category, currency, max_amount_minor, and ships_to_country filters.
agent_buy_cheapest_from_store failed: catalog 404The merchant does not serve a catalog at the expected well-known path.
agent_buy_cheapest_from_store failed: catalog 502Upstream merchant catalog endpoint unreachable.
agent_buy_cheapest_from_store failed: PAR failed: 400PAR rejected after the pick; same causes as agent_payment_initiate.
agent_buy_cheapest_from_store failed: verify-mandate failed: 502Mandate was issued but merchant verifier upstream errored; the mandate is still valid; retry verify with the same idempotency key.

Auth requirement

Agent-registered client_id with a DPoP keypair (same preconditions as agent_payment_initiate).

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import OID4PayClient

client = OID4PayClient.from_persisted()
out = client.buy_cheapest_from_store(
    store_url="https://shop.alpacanica.com",
    merchant_verify_url="https://shop.alpacanica.com/verify-mandate",
    max_amount_minor=2500,
    currency="EUR",
    category="apparel",
    redirect_uri="https://my-agent.example.com/callback",
)
print(out.chosen.sku, out.result.payment_intent_id)

Node (@oid4pay/oid4ac-merchant)

import { OID4PayClient } from "@oid4pay/oid4ac-merchant";

const client = await OID4PayClient.fromPersisted();
const out = await client.buyCheapestFromStore({
  storeUrl: "https://shop.alpacanica.com",
  merchantVerifyUrl: "https://shop.alpacanica.com/verify-mandate",
  maxAmountMinor: 2500,
  currency: "EUR",
  category: "apparel",
  redirectUri: "https://my-agent.example.com/callback",
});
console.log(out.chosen.sku, out.result.payment_intent_id);

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 9,
  "method": "tools/call",
  "params": {
    "name": "agent_buy_cheapest_from_store",
    "arguments": {
      "store_url": "https://shop.alpacanica.com",
      "merchant_verify_url": "https://shop.alpacanica.com/verify-mandate",
      "max_amount_minor": 2500,
      "currency": "EUR",
      "category": "apparel",
      "redirect_uri": "https://my-agent.example.com/callback"
    }
  }
}

agent_dsar_initiate

Initiates a GDPR Article 15 (access) or Article 20 (portability) Data Subject Access Request on behalf of the principal. The agent presents the principal's wallet-session JWT-AT as proof of consent. Article 15 dumps deliver asynchronously to the principal's verified email; Article 20 dumps stream synchronously as JSON in the response. Agent-only JWT-ATs are refused (DSAR requires a wallet-session principal). Rate-limit is 10 per day per principal per endpoint.

Input schema

{
  "type": "object",
  "properties": {
    "right":            { "enum": ["access", "portability"] },
    "principal_token":  { "type": "string", "minLength": 1 },
    "delivery_preference": {
      "enum": ["email", "synchronous_json"],
      "default": "email"
    }
  },
  "required": ["right", "principal_token"]
}

Output (Article 15 access, asynchronous email)

{
  "request_id": "dsar_4f6a2c1d8b9e3f70",
  "status": "queued",
  "right": "access",
  "estimated_delivery": "2026-05-15T05:51:28Z",
  "delivery_channel": "email"
}

Output (Article 20 portability, synchronous JSON)

{
  "schema_version": "oid4pay.dsar.portability.v1",
  "principal": {
    "principal_id": "principal_8a2c1d8b9e3f70a4",
    "wallet_operator": "https://wallet.oid4pay.com"
  },
  "agents": [...],
  "mandates": [...],
  "payments": [...],
  "audit_chain": [...]
}

Errors

Operator-facing messageCause
agent_dsar_initiate failed: DSAR requires a wallet-session JWT-AT; agent-only tokens are refusedAS returned 401 invalid_token; the principal_token was an agent-only AT.
agent_dsar_initiate failed: rate limited; DSAR caps at 10/day per principal per endpointAS returned 429 rate_limited.
agent_dsar_initiate failed: insufficient_scope (need privacy:dsar); ...Wallet-session JWT-AT does not carry the privacy:dsar scope.
agent_dsar_initiate failed: agent is not registered yet; call agent_register firstNo persisted client.json.

Auth requirement

Wallet-session JWT-AT (the principal's), passed in the principal_token input. Agent-only tokens are refused.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import WalletClient

wallet = WalletClient(as_url="https://sandbox.oid4pay.com")
reply = wallet.dsar_initiate(
    right="access",
    principal_token=principal_jwt_at,
    delivery_preference="email",
)
print(reply.request_id, reply.estimated_delivery)

Node (@oid4pay/oid4ac-merchant)

import { WalletClient } from "@oid4pay/oid4ac-merchant/wallet";

const wallet = new WalletClient({ asUrl: "https://sandbox.oid4pay.com" });
const reply = await wallet.dsarInitiate(
  "access",
  principalJwtAt,
  "email",
);
console.log(reply.request_id, reply.estimated_delivery);

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 10,
  "method": "tools/call",
  "params": {
    "name": "agent_dsar_initiate",
    "arguments": {
      "right": "access",
      "principal_token": "eyJhbGciOiJFZERTQSIs...",
      "delivery_preference": "email"
    }
  }
}

agent_audit_chain_query

Forensic query against the audit chain for events involving the authenticated principal. The agent presents the principal's wallet-session JWT-AT as proof of consent. Returns the audit entries within the requested time window, scoped to the principal (the AS-side IDOR filter prevents an agent from reading another principal's chain). Rate-limit is 30/min per principal. Hard cap of 200 results per call.

Input schema

{
  "type": "object",
  "properties": {
    "principal_token": { "type": "string", "minLength": 1 },
    "from_iso":        { "type": "string", "format": "date-time" },
    "to_iso":          { "type": "string", "format": "date-time" },
    "event_types": {
      "type": "array",
      "items": {
        "enum": [
          "oid4ac.mandate.issued",
          "oid4ac.payment.succeeded",
          "oid4ac.payment.failed",
          "oid4ac.payment.disputed",
          "oid4ac.token.revoked",
          "oid4ac.privacy.access",
          "oid4ac.privacy.erasure"
        ]
      }
    },
    "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 200,
      "default": 50
    }
  },
  "required": ["principal_token"]
}

Output

{
  "entries": [
    {
      "seq": 4231,
      "ts": "2026-05-15T05:21:28Z",
      "event": "oid4ac.payment.succeeded",
      "tenant_id": "tenant_oid4pay_prod",
      "actor": {
        "principal_id": "principal_8a2c1d8b9e3f70a4",
        "agent_client_id": "agent_3ff8a1d2b9c4e7f0"
      },
      "payload": {
        "mandate_id": "mandate_7b2c4d6e8f0a1b3c",
        "payment_intent_id": "pi_3OQ8Z2L8KZ4f5G2c1AbCdE",
        "amount_minor": 1299,
        "currency": "EUR",
        "merchant_audience": "https://shop.alpacanica.com"
      }
    }
  ],
  "has_more": false
}

Errors

Operator-facing messageCause
agent_audit_chain_query failed: audit chain query requires a wallet-session JWT-ATAS returned 401 invalid_token (agent-only token).
agent_audit_chain_query failed: principal_id mismatchAS returned 403 principal_mismatch (IDOR defence).
agent_audit_chain_query failed: rate limited; audit chain caps at 30/min per principalAS returned 429 rate_limited.
agent_audit_chain_query failed: insufficient_scope (need audit:read); ...Wallet-session JWT-AT does not carry the audit:read scope.
agent_audit_chain_query failed: agent is not registered yet; call agent_register firstNo persisted client.json.

Auth requirement

Wallet-session JWT-AT, scope audit:read. Agent-only tokens are refused.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import WalletClient

wallet = WalletClient(as_url="https://sandbox.oid4pay.com")
reply = wallet.audit_chain_query(
    bearer=principal_jwt_at,
    from_iso="2026-05-01T00:00:00Z",
    to_iso="2026-05-15T23:59:59Z",
    event_types=["oid4ac.payment.succeeded", "oid4ac.payment.disputed"],
    limit=100,
)
for e in reply.entries:
    print(e.seq, e.event, e.payload)

Node (@oid4pay/oid4ac-merchant)

import { WalletClient } from "@oid4pay/oid4ac-merchant/wallet";

const wallet = new WalletClient({ asUrl: "https://sandbox.oid4pay.com" });
const reply = await wallet.auditChainQuery(
  principalJwtAt,
  { fromIso: "2026-05-01T00:00:00Z", toIso: "2026-05-15T23:59:59Z" },
  ["oid4ac.payment.succeeded", "oid4ac.payment.disputed"],
  100,
);
for (const e of reply.entries) console.log(e.seq, e.event, e.payload);

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 11,
  "method": "tools/call",
  "params": {
    "name": "agent_audit_chain_query",
    "arguments": {
      "principal_token": "eyJhbGciOiJFZERTQSIs...",
      "from_iso": "2026-05-01T00:00:00Z",
      "to_iso": "2026-05-15T23:59:59Z",
      "event_types": ["oid4ac.payment.succeeded", "oid4ac.payment.disputed"],
      "limit": 100
    }
  }
}

agent_payment_history

UI-friendly projection over the audit chain. Filters the chain to oid4ac.payment.succeeded (and optionally oid4ac.payment.disputed), then projects each entry into a payment-centric row shape suitable for direct rendering in a chat-style agent UI. Returns at most 100 payments per call; paginate older payments via from_iso. Inherits the same wallet-session JWT-AT requirement and IDOR discipline as agent_audit_chain_query.

Input schema

{
  "type": "object",
  "properties": {
    "principal_token":  { "type": "string", "minLength": 1 },
    "from_iso":         { "type": "string", "format": "date-time" },
    "to_iso":           { "type": "string", "format": "date-time" },
    "include_disputed": { "type": "boolean", "default": true },
    "limit": {
      "type": "integer",
      "minimum": 1,
      "maximum": 100,
      "default": 50
    }
  },
  "required": ["principal_token"]
}

Output

{
  "payments": [
    {
      "payment_intent_id": "pi_3OQ8Z2L8KZ4f5G2c1AbCdE",
      "merchant_audience": "https://shop.alpacanica.com",
      "amount_minor": 1299,
      "currency": "EUR",
      "processed_at_iso": "2026-05-15T05:21:28Z",
      "status": "succeeded",
      "mandate_id": "mandate_7b2c4d6e8f0a1b3c",
      "agent_client_id": "agent_3ff8a1d2b9c4e7f0",
      "receipt_url": "https://sandbox.oid4pay.com/receipts/r_4f6a2c1d8b9e.json"
    },
    {
      "payment_intent_id": "pi_3OQ8Z2L8KZ4f5G2c2XyZ12",
      "merchant_audience": "https://shop.bristleandslate.com",
      "amount_minor": 4500,
      "currency": "EUR",
      "processed_at_iso": "2026-05-12T11:08:01Z",
      "status": "disputed_pending",
      "mandate_id": "mandate_9c3d5e7f1a2b4c6d",
      "agent_client_id": "agent_3ff8a1d2b9c4e7f0"
    }
  ],
  "total_count": 2,
  "has_more": false
}

Status values

StatusSource event
succeededoid4ac.payment.succeeded
disputed_pendingoid4ac.payment.disputed with dispute_status=pending
disputed_wonoid4ac.payment.disputed with dispute_status=won
disputed_lostoid4ac.payment.disputed with dispute_status=lost

Errors

Operator-facing messageCause
agent_payment_history failed: payment history query requires a wallet-session JWT-ATAS returned 401 invalid_token (agent-only token).
agent_payment_history failed: principal_id mismatchAS returned 403 principal_mismatch (IDOR defence).
agent_payment_history failed: rate limited; payment history caps at 30/min per principalAS returned 429 rate_limited.
agent_payment_history failed: insufficient_scope (need audit:read); ...Wallet-session JWT-AT does not carry the audit:read scope.
agent_payment_history failed: agent is not registered yet; call agent_register firstNo persisted client.json.

Auth requirement

Wallet-session JWT-AT, scope audit:read. Agent-only tokens are refused.

Cross-references

Code samples

Python (oid4pay-oid4ac)

from oid4pay_oid4ac import WalletClient

wallet = WalletClient(as_url="https://sandbox.oid4pay.com")
reply = wallet.payment_history(
    bearer=principal_jwt_at,
    from_iso="2026-05-01T00:00:00Z",
    to_iso="2026-05-15T23:59:59Z",
    include_disputed=True,
    limit=50,
)
for p in reply.payments:
    print(p.processed_at_iso, p.amount_minor, p.currency, p.status)

Node (@oid4pay/oid4ac-merchant)

import { WalletClient } from "@oid4pay/oid4ac-merchant/wallet";

const wallet = new WalletClient({ asUrl: "https://sandbox.oid4pay.com" });
const reply = await wallet.paymentHistory(
  principalJwtAt,
  { fromIso: "2026-05-01T00:00:00Z", toIso: "2026-05-15T23:59:59Z" },
  true,
  50,
);
for (const p of reply.payments) {
  console.log(p.processed_at_iso, p.amount_minor, p.currency, p.status);
}

Raw JSON-RPC (MCP transport)

{
  "jsonrpc": "2.0",
  "id": 12,
  "method": "tools/call",
  "params": {
    "name": "agent_payment_history",
    "arguments": {
      "principal_token": "eyJhbGciOiJFZERTQSIs...",
      "from_iso": "2026-05-01T00:00:00Z",
      "to_iso": "2026-05-15T23:59:59Z",
      "include_disputed": true,
      "limit": 50
    }
  }
}

Permissions and consent

The MCP server prompts for principal consent through the wallet on every new merchant audience. Mandates are scoped per merchant; the agent cannot silently extend a mandate from one merchant to another. The wallet:read + wallet:write scopes gate the wallet-management tools; audit:read gates the audit-chain and payment-history tools; privacy:dsar gates the DSAR tool. Scopes that the principal has not granted come back as insufficient_scope at the AS.

Algorithm whitelist

The MCP server enforces the algorithm whitelist internally and refuses to call any endpoint that returns a JWT or RFC 9421 signature using a rejected algorithm. EdDSA only for JWT-AT / SD-JWT VC / KB-JWT signatures; ed25519 or ecdsa-p256-sha256 for RFC 9421 HTTP message signatures. alg=none, HMAC for DPoP, and RS256 outside the legacy window are refused.

Source

The server lives at oid4pay-mcp/ in the OID4Pay repo. Run oid4pay-mcp --help for the full CLI reference, or npm test in the package directory to run the conformance harness against the sandbox AS. The tool-by-tool test inventory lives at oid4pay-mcp/docs/test-inventory.md.