Secure MCP with OAuth
The MCP Gateway guide describes authenticating AI agents at the gateway before any tool call reaches an upstream. OAuth Guard covers three flows in total, JWT Bearer for the inbound agent connection, and Client Credentials and Token Exchange for Zilla's own outbound calls to an upstream. This tutorial adds a jwt guard, using JWT bearer, to the two-upstream gateway from Get Started, validated against a JWKS endpoint.
Editions
JWT bearer is fully open source: it reuses the OSS jwt guard and the mcp server binding's own options.authorization, neither of which is Zilla Plus gated. Client Credentials and Token Exchange, the other two OAuth Guard flows, use the oauth guard instead and require Zilla Plus.
See JWT Bearer for the guard reference, and OAuth Guard for how this flow fits alongside Client Credentials and Token Exchange.
Prerequisites
- Docker Compose
- The
mcp.proxyexample running from Get Started - An OIDC-compliant identity provider that issues JWTs and exposes a
.well-known/jwks.jsonendpoint (Auth0, Okta, Keycloak, or your own)
Add a JWT Guard
Define a jwt guard with your identity provider's issuer and audience. If the issuer exposes a .well-known/jwks.json file, Zilla fetches the signing keys remotely; nothing else is required:
guards:
agent_jwt:
type: jwt
options:
issuer: https://auth.example.com
audience: https://mcp.example.comSee the jwt guard reference for manually configured keys, the roles claim, and the challenge window.
Guard the Gateway Entrypoint
Auth runs directly on north_mcp_server, the binding that terminates the AI agent's connection, through its own options.authorization field. north_http_server stays a plain HTTP-to-MCP router, with no auth logic of its own: it just routes the /mcp path to north_mcp_server, which validates the session itself before forwarding to the MCP proxy.
north_mcp_server:
type: mcp
kind: server
options:
authorization:
agent_jwt:
credentials: "Bearer {credentials}"
exit: north_mcp_proxyoptions.authorization names the agent_jwt guard and a credentials template, Bearer {credentials}, matched against the inbound Authorization header. A request whose header doesn't match the template, or whose extracted token the guard rejects, is turned away at north_mcp_server with a 401 response and a WWW-Authenticate challenge, before it ever reaches the MCP proxy or either upstream MCP server.
Full zilla.yaml config
name: secure-mcp-oauth
#region guard
guards:
agent_jwt:
type: jwt
options:
issuer: https://auth.example.com
audience: https://mcp.example.com
#endregion guard
stores:
cache:
type: memory
bindings:
north_tcp_server:
type: tcp
kind: server
options:
host: 0.0.0.0
port: 7114
routes:
- when:
- port: 7114
exit: north_http_server
north_http_server:
type: http
kind: server
options:
access-control:
policy: cross-origin
routes:
- when:
- headers:
":path": /mcp
exit: north_mcp_server
#region authorization
north_mcp_server:
type: mcp
kind: server
options:
authorization:
agent_jwt:
credentials: "Bearer {credentials}"
exit: north_mcp_proxy
#endregion authorization
telemetry:
metrics:
- mcp.*
attributes:
method: ${mcp.method}
tool: ${mcp.tool}
outcome: ${mcp.outcome}
north_mcp_proxy:
type: mcp
kind: proxy
options:
cache:
store: cache
ttl: PT5M
routes:
- exit: bluesky_mcp_client
when:
- toolkit: bluesky
- exit: payments_mcp_client
when:
- toolkit: payments
bluesky_mcp_client:
type: mcp
kind: client
options:
server: http://bluesky-mcp:3001/mcp
exit: sys:http_client
payments_mcp_client:
type: mcp
kind: client
options:
server: http://payments-mcp:3002/mcp
exit: sys:http_client
telemetry:
metrics:
- mcp.initialize
- mcp.initialize.duration
- mcp.tools.list
- mcp.tools.list.duration
- mcp.tools.call
- mcp.tools.call.duration
exporters:
prometheus_exporter:
type: prometheus
options:
endpoints:
- scheme: http
port: 7190
path: /metricsApply and Verify
Update zilla.yaml in zilla/examples/mcp.proxy with the guard and north_mcp_server's options.authorization, then reload:
docker compose --project-directory mcp.proxy up -dA request to /mcp with an Authorization header that doesn't match the Bearer {credentials} template, or that carries a token the agent_jwt guard rejects, is turned away at north_mcp_server with a 401 response, before it reaches the MCP proxy or either upstream MCP server:
curl -i http://localhost:7114/mcp -H 'Authorization: Bearer not-a-valid-token'A request with no Authorization header at all is admitted as unauthorized rather than rejected outright, the same as an unauthenticated request reaching an unguarded route anywhere else in Zilla:
curl -i http://localhost:7114/mcpA request with a valid bearer token issued by your identity provider is authenticated and forwarded as before:
curl -i http://localhost:7114/mcp -H 'Authorization: Bearer <token>'When you're done, stop the stack:
docker compose --project-directory mcp.proxy downNext Steps
- Add Kafka as MCP Tools or MCP Gateway Setup upstreams behind the same guarded entrypoint.
- If an upstream itself needs Zilla to authenticate to it, see Client Credentials and Token Exchange for the outbound side of the connection.
- Running more than one gateway instance behind a load balancer? See Stores for sharing JWKS and auth state across replicas via Redis or Hazelcast instead of in-memory.

