Auth Modes
MCP Auth has three enforcement levels that control how the server handles tool calls with and without proofs.
The three modes
Required
Reject any tool call that does not include a valid _proof. If the proof is missing, malformed, or fails verification, the call is rejected with an error.
Use when: Production deployments where every agent must be identified and authorized. This is the mode you want for multi-tenant systems, financial operations, or any environment where anonymous tool calls are not acceptable.
Optional
Verify the proof if present. If no proof is attached, allow the call through with verified: null. If a proof is present but invalid, the call is rejected - a present-but-broken proof is never silently ignored.
Use when: Gradual rollout. Start with optional to see which agents are already attaching proofs, then switch to required once all agents are updated. Also useful for servers that serve both authenticated agents and legacy clients.
Disabled
Skip all verification. The _proof field is still stripped from arguments (it is a reserved protocol field), but no cryptographic checks are performed. The verified field is always null.
Use when: Development, testing, or servers where auth is handled at a different layer (e.g., the MCP transport itself handles authentication).
Behavior summary
| Scenario | Required | Optional | Disabled |
|---|---|---|---|
| No proof attached | Error | Pass (verified: null) | Pass (verified: null) |
| Valid proof attached | Pass (verified: result) | Pass (verified: result) | Pass (verified: null) |
| Invalid proof attached | Error | Error | Pass (verified: null) |
_proof field in clean args | Stripped | Stripped | Stripped |
The key distinction between Optional and Disabled: in Optional mode, a present but invalid proof causes a rejection. In Disabled mode, the proof is stripped but never checked.
Configuration
Both environment variables are optional. Defaults: auth mode = optional, root key = none (required for required and optional modes).
Environment variables
| Variable | Values | Default |
|---|---|---|
KANONIV_MCP_AUTH | required, optional, disabled | optional |
KANONIV_ROOT_PUBLIC_KEY | Hex-encoded Ed25519 public key (64 chars) | (none) |
Aliases for required: enforce, strict. Aliases for disabled: off, none. Any unrecognized value defaults to optional.
Generating a root key
The root key is the server operator's Ed25519 public key. Generate a keypair and configure the server with the public half:
import { generateKeyPair, bytesToHex } from "@kanoniv/agent-auth";
const root = generateKeyPair();
// Public key for server config (KANONIV_ROOT_PUBLIC_KEY)
console.log(bytesToHex(root.identity.publicKeyBytes));
// e.g. "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
// Secret key - store securely, use to create delegations
console.log(bytesToHex(root.secretKey));use kanoniv_agent_auth::AgentKeyPair;
let root = AgentKeyPair::generate();
// Public key for server config (KANONIV_ROOT_PUBLIC_KEY)
println!("{}", hex::encode(&root.identity().public_key_bytes));
// Secret key - store securely, use to create delegations
println!("{}", hex::encode(root.secret_bytes()));from kanoniv_agent_auth import AgentKeyPair
root = AgentKeyPair.generate()
# Public key for server config (KANONIV_ROOT_PUBLIC_KEY)
print(root.identity().public_key_bytes.hex())
# Secret key - store securely, use to create delegations
print(bytes(root.secret_bytes()).hex())Mode examples in code
Required mode
import {
verifyMcpToolCall,
identityFromBytes,
hexToBytes,
CryptoError,
} from "@kanoniv/agent-auth";
const rootIdentity = identityFromBytes(
hexToBytes(process.env.KANONIV_ROOT_PUBLIC_KEY!)
);
function handleToolCall(name: string, args: Record<string, unknown>) {
try {
const outcome = verifyMcpToolCall(name, args, rootIdentity, "required");
// outcome.verified is always non-null in required mode (or it throws)
console.log(`Agent: ${outcome.verified!.invoker_did}`);
return executeToolLogic(name, outcome.args);
} catch (err) {
if (err instanceof CryptoError) {
return { error: `Unauthorized: ${err.message}` };
}
throw err;
}
}use kanoniv_agent_auth::mcp::{verify_mcp_tool_call, McpAuthMode};
use kanoniv_agent_auth::AgentIdentity;
fn handle_tool_call(
name: &str,
args: &serde_json::Value,
root: &AgentIdentity,
) -> Result<serde_json::Value, String> {
let outcome = verify_mcp_tool_call(name, args, root, McpAuthMode::Required)
.map_err(|e| format!("Unauthorized: {}", e))?;
// outcome.verified is always Some in required mode (or it returns Err)
let verified = outcome.verified.unwrap();
println!("Agent: {} (depth: {})", verified.invoker_did, verified.depth);
execute_tool_logic(name, &outcome.args)
}Optional mode
import { verifyMcpToolCall, identityFromBytes, hexToBytes } from "@kanoniv/agent-auth";
const rootIdentity = identityFromBytes(
hexToBytes(process.env.KANONIV_ROOT_PUBLIC_KEY!)
);
function handleToolCall(name: string, args: Record<string, unknown>) {
try {
const outcome = verifyMcpToolCall(name, args, rootIdentity, "optional");
if (outcome.verified) {
console.log(`Authenticated agent: ${outcome.verified.invoker_did}`);
} else {
console.log("Anonymous call - no proof provided");
}
return executeToolLogic(name, outcome.args);
} catch (err) {
// A present-but-invalid proof still causes an error in optional mode
return { error: `Bad proof: ${err}` };
}
}use kanoniv_agent_auth::mcp::{verify_mcp_tool_call, McpAuthMode};
use kanoniv_agent_auth::AgentIdentity;
fn handle_tool_call(
name: &str,
args: &serde_json::Value,
root: &AgentIdentity,
) -> Result<serde_json::Value, String> {
let outcome = verify_mcp_tool_call(name, args, root, McpAuthMode::Optional)
.map_err(|e| format!("Bad proof: {}", e))?;
match &outcome.verified {
Some(v) => println!("Authenticated agent: {}", v.invoker_did),
None => println!("Anonymous call - no proof provided"),
}
execute_tool_logic(name, &outcome.args)
}Disabled mode
import { verifyMcpToolCall, identityFromBytes, hexToBytes } from "@kanoniv/agent-auth";
// Root identity is still needed (the function signature requires it),
// but it is never used for verification in disabled mode.
const rootIdentity = identityFromBytes(hexToBytes("00".repeat(32)));
function handleToolCall(name: string, args: Record<string, unknown>) {
const outcome = verifyMcpToolCall(name, args, rootIdentity, "disabled");
// outcome.verified is always null in disabled mode
// outcome.args still has _proof stripped
return executeToolLogic(name, outcome.args);
}use kanoniv_agent_auth::mcp::{verify_mcp_tool_call, McpAuthMode};
use kanoniv_agent_auth::AgentIdentity;
fn handle_tool_call(
name: &str,
args: &serde_json::Value,
root: &AgentIdentity,
) -> Result<serde_json::Value, String> {
let outcome = verify_mcp_tool_call(name, args, root, McpAuthMode::Disabled)
.expect("disabled mode never fails");
// outcome.verified is always None
// outcome.args still has _proof stripped
execute_tool_logic(name, &outcome.args)
}Migration path
The recommended rollout sequence:
- Start with Disabled. Deploy the library, confirm
_proofstripping does not break existing tool handlers. - Switch to Optional. Monitor logs to see which agents are attaching proofs and which are not. Fix any agents sending malformed proofs.
- Switch to Required. All agents must now authenticate. Anonymous calls are rejected.
Each step is a single environment variable change - no code changes needed.
# Step 1: Deploy with auth disabled
KANONIV_MCP_AUTH=disabled
# Step 2: Enable optional verification
KANONIV_MCP_AUTH=optional
KANONIV_ROOT_PUBLIC_KEY=a1b2c3d4...
# Step 3: Require authentication
KANONIV_MCP_AUTH=required
KANONIV_ROOT_PUBLIC_KEY=a1b2c3d4...