Skip to content

Proof Format

An McpProof is a self-contained cryptographic proof that an agent has authority to call an MCP tool. It contains everything needed for verification - no external lookups required.

Structure

An McpProof has two top-level fields:

FieldTypeDescription
invocationInvocationThe signed invocation - action, args, delegation chain, and signature
invoker_public_keystringThe invoker's Ed25519 public key as hex (64 characters)

The invoker_public_key is embedded so the server can reconstruct the agent's identity (did:agent:...) and verify the signature without any key resolver or directory lookup.

Invocation

FieldTypeDescription
invoker_didstringThe invoker's DID (did:agent:...)
actionstringThe tool name being called
argsobjectThe tool arguments (at time of signing)
delegationDelegationThe delegation chain granting authority
proofSignedMessageThe invocation signature

Delegation

FieldTypeDescription
issuer_didstringDID of the agent granting authority
delegate_didstringDID of the agent receiving authority
issuer_public_keystringIssuer's Ed25519 public key (hex in JS/Python, bytes in Rust)
caveatsCaveat[]Constraints on the delegated authority
parent_proofDelegation | nullParent delegation (null for root)
proofSignedMessageThe delegation signature

Delegations form a linked list. The innermost delegation has parent_proof: null and was issued by the root authority. Each outer delegation was issued by the delegate of its parent, adding additional constraints.

SignedMessage

FieldTypeDescription
payloadobjectThe signed content
signer_didstringDID of the signer
noncestringUnique nonce (prevents replay)
timestampstringRFC 3339 timestamp
signaturestringEd25519 signature (hex, 128 characters)

Caveat types

Caveats are constraints embedded in delegations. Every caveat in the chain is checked during verification.

TypeValueDescription
action_scopestring[]Restrict to specific tool names
expires_atstringRFC 3339 expiration timestamp
max_costnumberMaximum cost for the operation (checked against args.cost)
resourcestringGlob pattern for allowed resources (checked against args.resource)
context{key, value}Required key-value match in args
custom{key, value}Arbitrary user-defined constraint

JSON serialization

A complete McpProof serialized as JSON:

json
{
  "invocation": {
    "invoker_did": "did:agent:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
    "action": "resolve",
    "args": {
      "source": "crm",
      "external_id": "123"
    },
    "delegation": {
      "issuer_did": "did:agent:f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3",
      "delegate_did": "did:agent:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
      "issuer_public_key": "f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3b2a1f6e5",
      "caveats": [
        { "type": "action_scope", "value": ["resolve", "search"] },
        { "type": "expires_at", "value": "2026-12-31T23:59:59.999Z" }
      ],
      "parent_proof": null,
      "proof": {
        "payload": {
          "issuer_did": "did:agent:f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3",
          "delegate_did": "did:agent:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
          "caveats": [
            { "type": "action_scope", "value": ["resolve", "search"] },
            { "type": "expires_at", "value": "2026-12-31T23:59:59.999Z" }
          ],
          "parent_hash": null
        },
        "signer_did": "did:agent:f6e5d4c3b2a1f6e5d4c3b2a1f6e5d4c3",
        "nonce": "a8f3e2d1c0b9a8f3e2d1c0b9",
        "timestamp": "2026-03-15T12:00:00.000Z",
        "signature": "e3d2c1b0a9f8e7d6c5b4a3928170e3d2c1b0a9f8e7d6c5b4a3928170e3d2c1b0a9f8e7d6c5b4a3928170e3d2c1b0a9f8e7d6c5b4a3928170e3d2c1b0a9f8e7d6"
      }
    },
    "proof": {
      "payload": {
        "invoker_did": "did:agent:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
        "action": "resolve",
        "args": { "source": "crm", "external_id": "123" },
        "delegation_hash": "b3a2f1e0d9c8b7a6958473b3a2f1e0d9c8b7a6958473b3a2f1e0d9c8b7a69584"
      },
      "signer_did": "did:agent:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4",
      "nonce": "d1c0b9a8f3e2d1c0b9a8f3e2",
      "timestamp": "2026-03-15T12:00:01.000Z",
      "signature": "a1b2c3d4e5f6a7b8c9d0e1f2a1b2c3d4e5f6a7b8c9d0e1f2a1b2c3d4e5f6a7b8c9d0e1f2a1b2c3d4e5f6a7b8c9d0e1f2a1b2c3d4e5f6a7b8c9d0e1f2a1b2c3d4"
    }
  },
  "invoker_public_key": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"
}

The _proof field (current polyfill)

Today, proofs are embedded in tool arguments as a _proof field:

json
{
  "method": "tools/call",
  "params": {
    "name": "resolve",
    "arguments": {
      "source": "crm",
      "external_id": "123",
      "_proof": { "invocation": { "..." : "..." }, "invoker_public_key": "..." }
    }
  }
}

When the server calls McpProof.extract(args), it returns:

  • The parsed proof (or null if absent/malformed)
  • A clean copy of arguments with _proof removed

The _proof field is always stripped from arguments, even when:

  • The proof is malformed (cannot be deserialized)
  • The proof is null
  • Auth mode is disabled
  • Verification fails

This prevents the _proof field from leaking into tool handler logic or being forwarded to upstream APIs.

Proposed: _meta.auth

The proposed MCP spec extension moves proofs to the request envelope:

json
{
  "method": "tools/call",
  "params": {
    "name": "resolve",
    "arguments": {
      "source": "crm",
      "external_id": "123"
    },
    "_meta": {
      "auth": { "invocation": { "..." : "..." }, "invoker_public_key": "..." }
    }
  }
}

This keeps tool arguments clean and makes the proof a first-class protocol concept. The library will check _meta.auth first, then fall back to arguments._proof for backward compatibility.

Creating and serializing a proof

typescript
import {
  generateKeyPair,
  createRootDelegation,
  McpProof,
} from "@kanoniv/agent-auth";

const root = generateKeyPair();
const agent = generateKeyPair();

// Create delegation: root grants agent "resolve" scope
const delegation = createRootDelegation(root, agent.identity.did, [
  { type: "action_scope", value: ["resolve"] },
]);

// Create proof for a specific tool call
const proof = McpProof.create(
  agent,
  "resolve",
  { source: "crm", external_id: "123" },
  delegation,
);

// Serialize to JSON (for inspection or storage)
const json = JSON.stringify(proof, null, 2);
console.log(json);

// Inject into tool arguments
const args = McpProof.inject(proof, { source: "crm", external_id: "123" });
// args = { source: "crm", external_id: "123", _proof: { ... } }
rust
use kanoniv_agent_auth::{AgentKeyPair, Delegation, Caveat};
use kanoniv_agent_auth::mcp::McpProof;

let root = AgentKeyPair::generate();
let agent = AgentKeyPair::generate();

// Create delegation: root grants agent "resolve" scope
let delegation = Delegation::create_root(
    &root,
    &agent.identity().did,
    vec![Caveat::ActionScope(vec!["resolve".into()])],
).unwrap();

// Create proof for a specific tool call
let proof = McpProof::create(
    &agent,
    "resolve",
    serde_json::json!({"source": "crm", "external_id": "123"}),
    delegation,
).unwrap();

// Serialize to JSON
let json = serde_json::to_string_pretty(&proof).unwrap();
println!("{}", json);

// Inject into tool arguments
let mut args = serde_json::json!({"source": "crm", "external_id": "123"});
proof.inject(&mut args);
// args now contains _proof field
python
import json
from kanoniv_agent_auth import AgentKeyPair, Delegation, McpProof, inject_mcp_proof

root = AgentKeyPair.generate()
agent = AgentKeyPair.generate()

# Create delegation: root grants agent "resolve" scope
delegation = Delegation.create_root(
    root,
    agent.identity().did,
    json.dumps([{"type": "action_scope", "value": ["resolve"]}]),
)

# Create proof for a specific tool call
proof = McpProof.create(
    agent,
    "resolve",
    json.dumps({"source": "crm", "external_id": "123"}),
    delegation,
)

# Serialize to JSON (for inspection or storage)
print(proof.to_json())

# Inject into tool arguments
args_with_proof = inject_mcp_proof(
    proof,
    json.dumps({"source": "crm", "external_id": "123"}),
)
# args_with_proof is a JSON string with _proof field

Self-contained verification

The key design property of McpProof is that verification requires only two inputs:

  1. The proof itself - contains the invocation signature, the full delegation chain, and every public key needed to verify every signature
  2. The root public key - the single trust anchor the server is configured with

The verification algorithm:

  1. Decode invoker_public_key from hex and reconstruct the invoker's DID
  2. Confirm the reconstructed DID matches invocation.invoker_did
  3. Verify the invocation signature against the invoker's public key
  4. Walk the delegation chain from innermost to outermost:
    • Reconstruct each issuer's identity from their embedded public key
    • Verify each delegation's signature
    • Confirm each delegation's delegate_did matches the next issuer's DID
  5. Confirm the chain terminates at the configured root identity
  6. Check every caveat in the chain against the invocation's action and args

If any step fails, verification returns an error. There is no partial success.

The identity and delegation layer for AI agents.