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:
| Field | Type | Description |
|---|---|---|
invocation | Invocation | The signed invocation - action, args, delegation chain, and signature |
invoker_public_key | string | The 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
| Field | Type | Description |
|---|---|---|
invoker_did | string | The invoker's DID (did:agent:...) |
action | string | The tool name being called |
args | object | The tool arguments (at time of signing) |
delegation | Delegation | The delegation chain granting authority |
proof | SignedMessage | The invocation signature |
Delegation
| Field | Type | Description |
|---|---|---|
issuer_did | string | DID of the agent granting authority |
delegate_did | string | DID of the agent receiving authority |
issuer_public_key | string | Issuer's Ed25519 public key (hex in JS/Python, bytes in Rust) |
caveats | Caveat[] | Constraints on the delegated authority |
parent_proof | Delegation | null | Parent delegation (null for root) |
proof | SignedMessage | The 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
| Field | Type | Description |
|---|---|---|
payload | object | The signed content |
signer_did | string | DID of the signer |
nonce | string | Unique nonce (prevents replay) |
timestamp | string | RFC 3339 timestamp |
signature | string | Ed25519 signature (hex, 128 characters) |
Caveat types
Caveats are constraints embedded in delegations. Every caveat in the chain is checked during verification.
| Type | Value | Description |
|---|---|---|
action_scope | string[] | Restrict to specific tool names |
expires_at | string | RFC 3339 expiration timestamp |
max_cost | number | Maximum cost for the operation (checked against args.cost) |
resource | string | Glob 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:
{
"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:
{
"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
_proofremoved
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:
{
"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
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: { ... } }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 fieldimport 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 fieldSelf-contained verification
The key design property of McpProof is that verification requires only two inputs:
- The proof itself - contains the invocation signature, the full delegation chain, and every public key needed to verify every signature
- The root public key - the single trust anchor the server is configured with
The verification algorithm:
- Decode
invoker_public_keyfrom hex and reconstruct the invoker's DID - Confirm the reconstructed DID matches
invocation.invoker_did - Verify the invocation signature against the invoker's public key
- 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_didmatches the next issuer's DID
- Confirm the chain terminates at the configured root identity
- 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.
