Skip to content

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

ScenarioRequiredOptionalDisabled
No proof attachedErrorPass (verified: null)Pass (verified: null)
Valid proof attachedPass (verified: result)Pass (verified: result)Pass (verified: null)
Invalid proof attachedErrorErrorPass (verified: null)
_proof field in clean argsStrippedStrippedStripped

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

VariableValuesDefault
KANONIV_MCP_AUTHrequired, optional, disabledoptional
KANONIV_ROOT_PUBLIC_KEYHex-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:

typescript
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));
rust
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()));
python
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

typescript
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;
  }
}
rust
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

typescript
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}` };
  }
}
rust
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

typescript
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);
}
rust
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:

  1. Start with Disabled. Deploy the library, confirm _proof stripping does not break existing tool handlers.
  2. Switch to Optional. Monitor logs to see which agents are attaching proofs and which are not. Fix any agents sending malformed proofs.
  3. Switch to Required. All agents must now authenticate. Anonymous calls are rejected.

Each step is a single environment variable change - no code changes needed.

bash
# 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...

The identity and delegation layer for AI agents.