Add mcp.proxy example with JWT auth and multi-toolkit aggregation#1793
Draft
jfallows wants to merge 7 commits into
Draft
Add mcp.proxy example with JWT auth and multi-toolkit aggregation#1793jfallows wants to merge 7 commits into
jfallows wants to merge 7 commits into
Conversation
9e7199a to
b2ddbc9
Compare
This was referenced May 23, 2026
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.
c42e97e to
1efdc93
Compare
…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.
1efdc93 to
45cbeb6
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds a new
examples/mcp.proxyexample demonstrating a production-ready MCP (Model Context Protocol) gateway that aggregates multiple upstream MCP servers behind a single Streamable HTTP endpoint.Key Features
everything,time,github) to different upstream MCP serversauthn_jwtguard with RS256 signature verificationtools,prompts, andresourceslistingsx-tenantheader for upstream attributionContents
etc/zilla.yaml— Complete gateway configuration with TCP/HTTP/MCP bindings, JWT authentication, caching, and routing rulescompose.yaml— Docker Compose setup with Zilla, Nodeeverythingreference server, and Pythontimeserver (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 handshakeTesting
The included test script (
test.sh) validates:Run via:
./.github/test.shordocker compose up && ./.github/test.shManual verification with MCP Inspector:
npx @modelcontextprotocol/inspector http://localhost:7114/mcphttps://claude.ai/code/session_0184bhWpuAK8AdyQrSFgGQtc