MCP Auth Proxy: Drop-in Authentication for Any MCP Server
Protect any MCP server with AIP token verification. Zero code changes to your server. One command to start.
The problem
MCP servers accept anonymous requests. Any client can call any tool. There is no way to know who is calling or whether they are authorized.
OAuth 2.1 was added to the MCP spec, but it only covers single-hop. When agents delegate to other agents, the auth chain disappears. A subagent three hops down the delegation tree can call the same tools as the orchestrator, with no record of how it got there.
The MCP auth proxy solves this without touching your server. It sits in front of any MCP server and verifies AIP tokens on every request. Your server sees only pre-verified calls with a full delegation audit trail attached.
Install
Install the AIP SDK, which includes the proxy binary:
pip install agent-identity-protocolVerify the proxy is available:
aip-proxy --helpUsage: aip-proxy [OPTIONS]
AIP authentication proxy for MCP servers.
Options:
--upstream TEXT Upstream MCP server URL [required]
--port INTEGER Port to listen on (default: 8080)
--trust-key TEXT Trusted public key in multibase format [required]
--config TEXT Path to TOML config file
--help Show this message and exit.
Start the proxy
Point the proxy at your upstream MCP server and give it a trusted public key:
aip-proxy --upstream http://localhost:3000 --port 8080 --trust-key z6Mkf...The proxy sits between your MCP client and server, verifying every request. Your MCP server at localhost:3000 requires no changes. Clients now connect to localhost:8080 instead.
AIP proxy listening on :8080
Upstream: http://localhost:3000
Trust key: z6Mkf...
Audit: enabled
Make an authenticated request
Create an AIP token and send it via the X-AIP-Token header to the proxy port:
from aip_core.crypto import KeyPair
from aip_token.claims import AipClaims
from aip_token.compact import CompactToken
import httpx
import time
kp = KeyPair.generate()
claims = AipClaims(
iss="aip:key:ed25519:" + kp.public_key_multibase(),
sub="aip:web:example.com/tools/search",
scope=["tool:search"],
budget_usd=1.0,
max_depth=0,
iat=int(time.time()),
exp=int(time.time()) + 3600,
)
token = CompactToken.create(claims, kp)
# Send to the proxy, not the upstream server directly
response = httpx.post(
"http://localhost:8080/mcp/tools/search",
headers={"X-AIP-Token": token},
json={"query": "latest research on agent identity"}
)
print(response.status_code)
200
On a valid token, the proxy verifies the signature, checks scope and expiry, then forwards the request to the upstream server at localhost:3000. The upstream sees a normal HTTP request with an added X-AIP-Verified: true header.
What gets rejected
Understanding what the proxy rejects is as important as what it accepts.
Missing token (401):
# No X-AIP-Token header at all
response = httpx.post(
"http://localhost:8080/mcp/tools/search",
json={"query": "test"}
)
# 401 Unauthorized: missing X-AIP-Token header
Wrong key (401):
# Token signed by a different keypair
other_kp = KeyPair.generate()
claims = AipClaims(
iss="aip:key:ed25519:" + other_kp.public_key_multibase(),
sub="aip:web:example.com/tools/search",
scope=["tool:search"],
budget_usd=1.0,
max_depth=0,
iat=int(time.time()),
exp=int(time.time()) + 3600,
)
token = CompactToken.create(claims, other_kp)
response = httpx.post(
"http://localhost:8080/mcp/tools/search",
headers={"X-AIP-Token": token},
json={"query": "test"}
)
# 401 Unauthorized: issuer not in trust set
Expired token (401):
# Token that expired 10 seconds ago
claims = AipClaims(
iss="aip:key:ed25519:" + kp.public_key_multibase(),
sub="aip:web:example.com/tools/search",
scope=["tool:search"],
budget_usd=1.0,
max_depth=0,
iat=int(time.time()) - 3610,
exp=int(time.time()) - 10,
)
expired_token = CompactToken.create(claims, kp)
response = httpx.post(
"http://localhost:8080/mcp/tools/search",
headers={"X-AIP-Token": expired_token},
json={"query": "test"}
)
# 401 Unauthorized: token expired
Security self-audit
The proxy does not just verify tokens. It audits them for hygiene problems and attaches a report to every response as an X-AIP-Audit header.
X-AIP-Audit: {"passed": true, "warnings": ["TTL is 7200s (2h 0m), exceeds recommended maximum of 3600s"], "errors": []}
A request passes audit even with warnings. Warnings are informational: the token is valid but has characteristics that reduce security posture. Errors block the request entirely.
What the audit checks:
- TTL hygiene -- tokens with TTLs above the configured maximum generate a warning. Overly long-lived tokens increase the blast radius of a compromise.
- Empty or wildcard scope -- a token with no scope or
*scope is flagged. Principle of least privilege applies to agents too. - High budget -- tokens with
budget_usdabove the configured threshold generate a warning. Runaway spend from a compromised token is a real risk. - Delegation chain depth -- deep delegation chains are harder to audit. The proxy tracks
max_depthand warns when it approaches the configured ceiling.
Configuration
For production deployments, use a TOML config file instead of command-line flags:
[proxy]
upstream = "http://localhost:3000"
port = 8080
trust_keys = ["z6Mkf..."]
[audit]
max_ttl_seconds = 1800
max_budget_usd = 5.0
[logging]
log_file = "/var/log/aip-proxy.log"
Start with the config file:
aip-proxy --config proxy.tomlThe [audit] section controls what thresholds trigger warnings. Set max_ttl_seconds = 1800 to flag any token with a TTL above 30 minutes. Set max_budget_usd = 5.0 to flag tokens with budgets above $5. Both defaults are conservative; adjust for your threat model.
The trust_keys array accepts multiple keys. Add all agent public keys you want to authorize. Any token signed by a key not in this list is rejected with 401.
Next steps
- CrewAI integration guide -- add AIP to an existing CrewAI application
- Multi-agent delegation example -- 3-hop delegation chain with scope attenuation
- Full specification -- protocol details for implementers
- Read the paper -- design rationale, experiments, adversarial evaluation