Skip to content

Add mcp.proxy example with JWT auth and multi-toolkit aggregation#1793

Draft
jfallows wants to merge 7 commits into
developfrom
claude/gallant-cray-TVVuP
Draft

Add mcp.proxy example with JWT auth and multi-toolkit aggregation#1793
jfallows wants to merge 7 commits into
developfrom
claude/gallant-cray-TVVuP

Conversation

@jfallows
Copy link
Copy Markdown
Contributor

Description

Adds a new examples/mcp.proxy example demonstrating a production-ready MCP (Model Context Protocol) gateway that aggregates multiple upstream MCP servers behind a single Streamable HTTP endpoint.

Key Features

  • Multi-toolkit aggregation — Routes requests by toolkit (everything, time, github) to different upstream MCP servers
  • JWT-guarded access — Enforces authentication via the authn_jwt guard with RS256 signature verification
  • Shared caching — In-memory cache with 5-minute TTL for tools, prompts, and resources listings
  • Authorization elicitation — Missing or invalid tokens trigger MCP elicitation callbacks pointing clients to an auth endpoint
  • Per-route header injection — Each route stamps an x-tenant header for upstream attribution

Contents

  • etc/zilla.yaml — Complete gateway configuration with TCP/HTTP/MCP bindings, JWT authentication, caching, and routing rules
  • compose.yaml — Docker Compose setup with Zilla, Node everything reference server, and Python time server (via mcp-proxy)
  • private.pem — Demo RSA private key for JWT signing (test/development only)
  • README.md — Comprehensive documentation including setup, verification steps, manual JWT minting, and instructions for adding a GitHub upstream
  • .github/test.sh — Automated smoke test verifying authentication enforcement and MCP initialization handshake

Testing

The included test script (test.sh) validates:

  • Unauthenticated requests are rejected with 401/403/404
  • Valid JWT tokens allow successful MCP initialization
  • Response includes expected MCP protocol fields

Run via: ./.github/test.sh or docker compose up && ./.github/test.sh

Manual verification with MCP Inspector: npx @modelcontextprotocol/inspector http://localhost:7114/mcp

https://claude.ai/code/session_0184bhWpuAK8AdyQrSFgGQtc

claude added 5 commits May 28, 2026 04:58
Adds a runnable docker-compose example that aggregates multiple upstream
MCP servers behind a single Streamable HTTP endpoint on port 7114, with
JWT-guarded access via authn_jwt, a five-minute in-memory cache for
tools/prompts/resources listings, and an elicitation callback to drive
auth flows. Mirrors the wiring conventions used by examples/http.proxy
and examples/http.proxy.jwt. CI smoke test in .github/test.sh exercises
the Zilla layer (auth enforcement + initialize handshake) so it remains
independent of upstream availability.
Removes the with.headers stamping x-tenant on each mcp(proxy) route since
no upstream consumes it, and collapses the three south chains into one
shared south_http_client and option-less south_tcp_client. The tcp(client)
picks up its destination from the :authority pseudo-header set by each
mcp(client), matching the pattern used by the kafka examples.
- Adds the official ghcr.io/github/github-mcp-server in HTTP mode as the
  github upstream (profile-gated; no startup PAT needed because the
  server reads tokens per-request).
- Starts it with --scope-challenge so OAuth scope challenges are
  surfaced as MCP elicitation events.
- Removes the JWT guard, the gateway-level authorization on
  north_mcp_proxy, the jwt-cli compose service, the demo private key,
  and the JWT-minting step in test.sh. Authentication is now driven by
  the upstream: clients pass Authorization: Bearer <PAT> through Zilla
  to the upstream, and the upstream's challenge/elicitation responses
  flow back unchanged through the proxy.
Calls out the everything reference server's trigger-elicitation-request-async
and simulate-research-query tools as the headline way to see MCP
elicitation/create flow through the gateway, with github-mcp-server's
--scope-challenge framed as the same pass-through driven by auth instead
of a tool call. The gateway-side elicitation option in binding-mcp
requires an OAuth-style guard implementing GuardHandler.preauthorize(),
which the only shipped guard (guard-jwt) does not, so upstream-driven is
the practical demo today.
…ithub

Adds a passthrough identity guard at north_http_server that captures the
inbound Authorization bearer via {credentials}, and references it from
south_mcp_client_github's options.authorization. mcp(client) re-stamps
Authorization: Bearer <token> on the outbound request to github:3003 so
github-mcp-server receives the client's PAT for its own validation.

The everything and time mcp(client) bindings remain unguarded — no
Authorization is added on outbound to those upstreams. Without a client
token, github-mcp-server's HTTP 403 + WWW-Authenticate (from --scope-challenge)
still flows back through the proxy unchanged.

README walks through the full client-driven auth flow end to end, and the
top-level examples index description is refreshed to match the current
demonstrated capabilities.
@jfallows jfallows force-pushed the claude/gallant-cray-TVVuP branch 3 times, most recently from c42e97e to 1efdc93 Compare May 28, 2026 05:13
…er TLS

Drops the local github-mcp-server docker service in favor of GitHub's
public Streamable HTTP endpoint at https://api.githubcopilot.com/mcp/.
Adds a dedicated http→tls→tcp chain for the github route (TLS sni
api.githubcopilot.com, trustcacerts for public CA verification, ALPN
http/1.1, TCP 443). The everything and time routes keep their existing
plain http→tcp path unchanged.

Auth-challenge pass-through still works: an unauthenticated call returns
GitHub's HTTP 401 / WWW-Authenticate: Bearer through Zilla's mcp(server)
which renders the same 4xx + WWW-Authenticate back to the inbound client,
courtesy of the McpResetEx bearer propagation that landed in #1796.
@jfallows jfallows force-pushed the claude/gallant-cray-TVVuP branch from 1efdc93 to 45cbeb6 Compare May 28, 2026 05:22
Without an explicit alpn list on the TLS client, no ALPN extension is
sent in the ClientHello, so api.githubcopilot.com is free to respond
with h2 unsolicited while mcp(client)'s http(client) stays in HTTP/1.1
mode — h2 frames then mis-parse as HTTP/1.1 headers.

Explicit alpn lets TLS negotiate one of [h2, http/1.1], reports the
choice back to http(client) via the reply ProxyBeginEx, and http(client)
switches to the matching decoder.

This is a short-term workaround. The proper fix is for http(client) to
propagate pool.versions as ALPN proxy info to downstream TLS without
example-side config.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants