Secure Agent Access
The Secure Agent Access use case describes an agent authenticating against one endpoint instead of negotiating auth with every upstream individually. This tutorial runs a gateway fanning out to three toolkits, billing, support, and inventory, and shows a single initialize and tools/list surfacing all three.
Prerequisites
docker-compose.yaml
services:
gateway:
image: ghcr.io/aklivity/zilla:latest
pull_policy: always
depends_on:
- api
ports:
- 7114:7114
volumes:
- ./zilla.yaml:/etc/zilla/zilla.yaml
command: start -v -e
api:
image: kennethreitz/httpbin
ports:
- 8000:80zilla.yaml
name: secure-agent-access
stores:
cache:
type: memory
catalogs:
api_catalog:
type: inline
options:
subjects:
id_params:
schema: |
{
"type": "object",
"properties": { "id": { "type": "string" } },
"required": [ "id" ]
}
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
north_mcp_server:
type: mcp
kind: server
exit: north_mcp_proxy
#region mcp_proxy
north_mcp_proxy:
type: mcp
kind: proxy
options:
cache:
store: cache
ttl: PT5M
routes:
- exit: billing_http_proxy
when:
- toolkit: billing
- exit: support_http_proxy
when:
- toolkit: support
- exit: inventory_http_proxy
when:
- toolkit: inventory
#endregion mcp_proxy
billing_http_proxy:
type: mcp-http
kind: proxy
options:
tools:
get_payment_status:
description: Look up the status of a payment by identifier.
summary: "Routed to ${result.url}"
schemas:
input:
model: json
catalog:
api_catalog:
- subject: id_params
version: latest
routes:
- when:
- tool: get_payment_status
exit: sys:http_client
with:
headers:
":method": GET
":scheme": http
":authority": api:80
":path": /anything/billing/${args.id}
support_http_proxy:
type: mcp-http
kind: proxy
options:
tools:
get_ticket_status:
description: Look up the status of a support ticket by identifier.
summary: "Routed to ${result.url}"
schemas:
input:
model: json
catalog:
api_catalog:
- subject: id_params
version: latest
routes:
- when:
- tool: get_ticket_status
exit: sys:http_client
with:
headers:
":method": GET
":scheme": http
":authority": api:80
":path": /anything/support/${args.id}
inventory_http_proxy:
type: mcp-http
kind: proxy
options:
tools:
get_stock_level:
description: Look up the stock level of an item by identifier.
summary: "Routed to ${result.url}"
schemas:
input:
model: json
catalog:
api_catalog:
- subject: id_params
version: latest
routes:
- when:
- tool: get_stock_level
exit: sys:http_client
with:
headers:
":method": GET
":scheme": http
":authority": api:80
":path": /anything/inventory/${args.id}Three routes, three toolkits, one entrypoint:
north_mcp_proxy:
type: mcp
kind: proxy
options:
cache:
store: cache
ttl: PT5M
routes:
- exit: billing_http_proxy
when:
- toolkit: billing
- exit: support_http_proxy
when:
- toolkit: support
- exit: inventory_http_proxy
when:
- toolkit: inventoryStart the Stack
docker compose up -dConnect Once, See Every Upstream
Point an MCP client at http://localhost:7114/mcp. A single tools/list after initialize returns tools from all three toolkits:
{"tools":[
{"name":"billing__get_payment_status", "...":"..."},
{"name":"inventory__get_stock_level", "...":"..."},
{"name":"support__get_ticket_status", "...":"..."}
]}The agent authenticated once, against one endpoint, and never had to know it was talking to three independent upstreams behind it. Calling any of the three tools works the same way:
curl -X POST http://localhost:7114/mcp \
-H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' -H "Mcp-Session-Id: <session>" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"inventory__get_stock_level","arguments":{"id":"sku_42"}}}'Stop the Stack
docker compose downNext Steps
- See Centralized Auth for guarding this same entrypoint with a
jwtguard, one policy protecting all three toolkits. - See Token Exchange for swapping the agent's one inbound credential for a different downstream credential per upstream, instead of forwarding it unchanged.

