MCP Server Auth Quickstart
MCP servers currently accept tool calls from any agent with no identity verification. This guide adds cryptographic auth to your MCP server so that every tool call carries a verifiable proof of who the agent is and what it is allowed to do.
The proof is self-contained. The server does not call any external service to verify it - everything it needs (public keys, delegation chain, signatures) is in the _proof field of the tool arguments.
This guide focuses on TypeScript since most MCP servers are written in TypeScript. Rust and Python equivalents are at the end.
1. Install
npm install @kanoniv/agent-auth2. Set up your server's root identity
Your MCP server needs a root identity - the keypair that anchors all delegation chains. In production, you would load this from a secure store. For this guide, we generate one and print it so you can save it.
import { generateKeyPair, bytesToHex, hexToBytes, keyPairFromBytes } from "@kanoniv/agent-auth";
// Generate once, save the secret key securely
const root = generateKeyPair();
console.log("Root DID:", root.identity.did);
console.log("Root secret (save this):", bytesToHex(root.secretKey));
// Later, reconstruct from saved secret
// const root = keyPairFromBytes(hexToBytes("your_saved_hex_secret"));3. Add verification to your tool handler
This is the core integration. Five lines inside your existing tool handler:
import { McpProof, verifyMcpCall } from "@kanoniv/agent-auth";
import type { AgentIdentity } from "@kanoniv/agent-auth";
// Your root identity (loaded at startup)
const rootIdentity: AgentIdentity = root.identity;
function handleToolCall(toolName: string, args: Record<string, unknown>) {
// --- Auth: 5 lines ---
const { proof, cleanArgs } = McpProof.extract(args);
if (proof) {
const result = verifyMcpCall(proof, rootIdentity);
console.log(`Verified: ${result.invoker_did} (chain depth: ${result.depth})`);
}
// --- End auth ---
// Use cleanArgs (never raw args) for your tool logic.
// The _proof field has been stripped out.
switch (toolName) {
case "resolve":
return handleResolve(cleanArgs);
case "search":
return handleSearch(cleanArgs);
default:
throw new Error(`Unknown tool: ${toolName}`);
}
}That is it for optional verification. If a proof is present, it is verified. If not, the call proceeds without auth. For stricter enforcement, see the auth modes section below.
4. Create a delegation for an agent
On the agent side (or in an admin setup script), create a delegation that grants an agent specific permissions:
import {
generateKeyPair,
createRootDelegation,
keyPairFromBytes,
hexToBytes,
} from "@kanoniv/agent-auth";
// Load the root keypair (same one the server uses)
// const root = keyPairFromBytes(hexToBytes("your_saved_hex_secret"));
const root = generateKeyPair(); // for this demo
// Generate the agent's keypair
const agent = generateKeyPair();
// Grant the agent authority to resolve and search, with a cost limit
const delegation = createRootDelegation(root, agent.identity.did, [
{ type: "action_scope", value: ["resolve", "search"] },
{ type: "max_cost", value: 5.0 },
]);
console.log("Agent DID:", agent.identity.did);
console.log("Delegation issuer:", delegation.issuer_did);
console.log("Delegation delegate:", delegation.delegate_did);
// Save agent.secretKey and delegation - the agent needs both to create proofs5. Agent attaches proof to tool calls
When the agent makes a tool call, it creates a proof and injects it into the arguments:
import { McpProof } from "@kanoniv/agent-auth";
// The agent has: its keypair (agent) and its delegation
const toolArgs = { source: "crm", external_id: "contact-123" };
// Create a proof for the "resolve" action
const proof = McpProof.create(agent, "resolve", toolArgs, delegation);
// Inject the proof into the tool arguments
const argsWithProof = McpProof.inject(proof, toolArgs);
// argsWithProof now looks like:
// {
// source: "crm",
// external_id: "contact-123",
// _proof: { invocation: {...}, invoker_public_key: "a1b2..." }
// }
// Send argsWithProof to the MCP server as the tool call arguments.
// The server extracts and verifies the _proof automatically.6. Complete working example
Here is a complete script that ties it all together - root setup, delegation, proof creation, and server-side verification:
import {
generateKeyPair,
createRootDelegation,
McpProof,
verifyMcpCall,
} from "@kanoniv/agent-auth";
// === Setup (done once) ===
// Server creates its root identity
const root = generateKeyPair();
console.log("Server root DID:", root.identity.did);
// Admin creates an agent and delegates authority
const agent = generateKeyPair();
const delegation = createRootDelegation(root, agent.identity.did, [
{ type: "action_scope", value: ["resolve", "search"] },
{ type: "max_cost", value: 5.0 },
]);
console.log("Agent DID:", agent.identity.did);
// === Runtime (every tool call) ===
// Agent side: create proof and attach to args
const toolArgs = { source: "crm", external_id: "contact-123", cost: 2.0 };
const proof = McpProof.create(agent, "resolve", toolArgs, delegation);
const argsWithProof = McpProof.inject(proof, toolArgs);
// Server side: extract and verify
const { proof: extractedProof, cleanArgs } = McpProof.extract(argsWithProof);
if (extractedProof) {
const result = verifyMcpCall(extractedProof, root.identity);
console.log("Verified agent:", result.invoker_did);
console.log("Root authority:", result.root_did);
console.log("Chain depth:", result.depth);
} else {
console.log("No proof - unauthenticated call");
}
console.log("Clean args:", cleanArgs);
// Output: Clean args: { source: "crm", external_id: "contact-123", cost: 2 }Auth modes
The verifyMcpToolCall function supports three enforcement levels:
import { verifyMcpToolCall, generateKeyPair } from "@kanoniv/agent-auth";
const root = generateKeyPair();
// "required" - reject calls without valid proof
// Throws CryptoError if no proof or invalid proof
const outcome1 = verifyMcpToolCall("resolve", argsWithProof, root.identity, "required");
// "optional" - verify if present, allow unauthenticated
// outcome.verified is null when no proof is present
const outcome2 = verifyMcpToolCall("resolve", argsWithProof, root.identity, "optional");
// "disabled" - skip verification, strip proof from args
const outcome3 = verifyMcpToolCall("resolve", argsWithProof, root.identity, "disabled");
// All modes return { verified: VerificationResult | null, args: cleanArgs }
console.log(outcome1.verified?.invoker_did);
console.log(outcome1.args); // _proof stripped outUse "optional" during development and migration. Switch to "required" when all your agents have been set up with delegations.
Rust and Python server equivalents
use kanoniv_agent_auth::mcp::{McpProof, verify_mcp_call};
use kanoniv_agent_auth::AgentIdentity;
fn handle_tool_call(
args: &serde_json::Value,
root_identity: &AgentIdentity,
) -> serde_json::Value {
let (proof, clean_args) = McpProof::extract(args);
if let Some(proof) = proof {
let result = verify_mcp_call(&proof, root_identity).unwrap();
println!("Verified: {} (depth: {})", result.invoker_did, result.depth);
}
clean_args
}from kanoniv_agent_auth import extract_mcp_proof, verify_mcp_call
def handle_tool_call(args_json: str, root_identity) -> str:
proof, clean_args = extract_mcp_proof(args_json)
if proof:
invoker_did, root_did, chain, depth = verify_mcp_call(proof, root_identity)
print(f"Verified: {invoker_did} (depth: {depth})")
return clean_argsWhat happens under the hood
When verifyMcpCall runs, it performs these checks in order:
- Reconstruct identity - The invoker's public key is in the proof. Derive the DID from it and confirm it matches the claimed invoker DID.
- Verify invocation signature - The invoker signed
{invoker_did, action, args, delegation_hash}. Verify that Ed25519 signature. - Walk the delegation chain - For each delegation in the chain:
- Reconstruct the issuer's identity from the embedded public key
- Verify the embedded key produces the claimed issuer DID
- Verify the issuer's Ed25519 signature on the delegation
- Check that the issuer is the delegate of the parent delegation
- Verify root - The chain must terminate at the root identity you provided.
- Check caveats - Every caveat from every delegation in the chain is checked against the invocation action and arguments. Caveats are read from the signed payload (not the outer fields) to prevent tampering.
If any check fails, verification throws an error with a descriptive message.
Next steps
- Delegation Quickstart - Learn all six caveat types and how chains work.
- Agent Identity Quickstart - Understand keypairs, DIDs, and signed messages.
- API Quickstart - Combine agent auth with entity resolution.
