Per-request cryptographic user attestation for MCP servers.
MCP already has OAuth 2.1. It tells you who the user is at session level. That's not the problem this solves.
The problem: OAuth proves a service is connected and a user authenticated. It does not prove that a specific user authorized this exact request, over this exact payload, at this exact moment. For high-stakes tool calls — deleting data, sending messages, executing transactions — "the session was valid" is not the same as "the user signed off on this." There is no non-repudiation. There is no per-request audit trail. If something goes wrong, you cannot prove who authorized what.
The fix: One HTTP header. Every request signed by the user's key over the exact payload. Your server verifies it in milliseconds. Works alongside OAuth — additive, not a replacement.
Use mcp-identity when your MCP server does anything a user might later dispute: financial operations, data deletion, external communications, or any autonomous agent action taken on a user's behalf.
Install
pip install mcp-identity
Quickstart
Server side (add to any MCP server)
from mcp_identity.middleware import MCPIdentityMiddleware, InMemoryNonceStore # Wrap your ASGI app app = MCPIdentityMiddleware( app=your_mcp_app, nonce_store=InMemoryNonceStore(), # swap for Redis in production mode="permissive", # start here; switch to "strict" later timestamp_window_seconds=30, ) # In your handler, inspect the result: async def handle(scope, receive, send): identity = scope["mcp_identity"] if identity.status == "verified": print(f"Request from {identity.did}")
Client side (sign outgoing requests)
from mcp_identity import generate_identity, sign_request from mcp_identity.identity import save_identity, load_identity from pathlib import Path # Generate once, save to disk identity = generate_identity() save_identity(identity, Path("~/.mcp-identity/key.json").expanduser()) # Load on subsequent runs identity = load_identity(Path("~/.mcp-identity/key.json").expanduser()) # Sign each request body = b'{"tool": "list_files", "args": {}}' header_value = sign_request(identity, body) # Add to your HTTP request headers = {"X-MCP-Identity-Attestation": header_value}
Modes
| Mode | No header | Invalid signature |
|---|---|---|
strict |
HTTP 401 | HTTP 401 |
permissive |
Pass through (logged as UNVERIFIED) | HTTP 401 |
Start with permissive during rollout. Switch to strict once all clients sign.
Distributed deployments
InMemoryNonceStore is for single-instance use only. For load-balanced deployments, implement the NonceStore protocol:
from mcp_identity.middleware import NonceStore import redis class RedisNonceStore: def __init__(self, client: redis.Redis): self._r = client def is_seen(self, nonce: str) -> bool: return self._r.exists(f"nonce:{nonce}") == 1 def mark_seen(self, nonce: str, ttl_seconds: int) -> None: self._r.setex(f"nonce:{nonce}", ttl_seconds, "1")
⚠ Without a shared nonce store, replay protection is silently inactive across instances.
How it works
Each request carries X-MCP-Identity-Attestation — a base64url-encoded JSON object:
{
"did": "did:key:z...",
"timestamp": "2026-05-05T12:00:00Z",
"nonce": "a3f2c1d4e5b607182930a4b5",
"body_hash": "e3b0c4...",
"signature": "3a9f2c..."
}The signature covers did|timestamp|nonce|body_hash with ed25519. The server verifies timestamp window (default 30s), body integrity, nonce uniqueness, and signature — in that order.
Full protocol details: SPEC.md
Relationship to OAuth 2.1
mcp-identity is not an alternative to OAuth. Use both:
| Concern | Solution |
|---|---|
| Who is this user? | OAuth 2.1 (MCP spec) |
| Did this user authorize this exact request? | mcp-identity |
| Can I prove it later? | mcp-identity audit trail |
OAuth handles session identity and service authorization. mcp-identity handles per-request non-repudiation. They solve different problems at different layers.
Known constraints (v0)
- Key management: v0 assumes technically capable users who can manage a keypair file. UX for non-technical users (browser extension, wallet integration) is v0.5.
- Single conformance implementation: Python only. Other language implementations welcome — use the SPEC.md test vectors to verify compatibility.
- No key rotation: Keypair is fixed at generation time. Rotation and revocation are v0.5.
License
MIT
























