Gangway¶
Gangway is a small set of Go building blocks for running a Model Context Protocol server on the open internet. It handles the parts that have nothing to do with what your tools actually do:
- Authentication — verifies bearer tokens against any OpenID Connect issuer (Entra ID, Authentik, Keycloak, Auth0, Google — anything that speaks standard OIDC discovery).
- Origin filtering — refuses connections from addresses outside an allowlist before the request body is even read. The allowlist can be a fixed set of CIDR prefixes, a periodically refreshed remote list (for example an AI provider's published outbound ranges), or both combined.
- Per-tool authorization — separates tools into reading and writing, and lets you require a specific claim (a role, a group) before a writing tool runs. Reading tools are open to any authenticated caller.
- Access logging — an NGINX-format access log that also records the outcome of every tool call, including refusals that the MCP transport itself reports as a normal HTTP 200.
Gangway does not implement MCP itself. It wraps the official
modelcontextprotocol/go-sdk
— you register your tools with the SDK as usual, and Gangway supplies the
http.Handler chain and the SDK middleware that sit in front of them.
Why this exists¶
Exposing an MCP server to AI providers such as Claude or ChatGPT means
exposing it to the internet: an OAuth-capable connector has to reach it from
outside your network. That single requirement drags in a full stack of
concerns — token verification, network-level filtering, and authorization
that a nil-checked bearer token alone does not give you. Gangway packages
that stack once so it does not have to be rebuilt, and rebuilt correctly,
for every server.
Where to go next¶
- Getting started walks through the smallest server
that runs: five environment variables, one tool,
go run. - Configuration lists every environment variable Gangway reads.
- Backend credentials covers the other credential: the one your tools use to call the service behind your MCP server.
- Providers covers the two identity providers this project has been run against: Microsoft Entra ID and Authentik.
- Behind a proxy explains the one setting that, if chosen wrong, silently breaks the origin allowlist — and covers running behind Traefik, Caddy, NGINX, Pangolin, and Cloudflare.
Known limitations¶
- No MCP session outlives a single call — by design, and easy to build
around without noticing. Both
AttachMCPandAttachMCPSelectorforce stateless HTTP sessions (see Getting started for why); the SDK's own consequence of that mode is that every request gets a fresh, temporary session, torn down at the end of that same request, and a session ID a client sends is never read. Anything a tool handler keys to the session — a per-caller list built up across calls, a multi-step workflow's progress — is empty again on the very next call, and nothing about that fails loudly: the check just always sees nothing there. Persist across calls by keying to the caller's verified identity (serve.IdentityFrom(ctx)) in your own durable store instead — see Getting started for the full explanation and what that store needs on top. - Authorization and access logging cover tool calls only. The
authorization middleware that
AttachMCPinstalls — and, identically, the middlewareAttachMCPSelectorinstalls on every instance its selector returns — inspects exactly one MCP method,tools/call— it checksmethod != "tools/call"and, for everything else, passes the request straight through unchanged. A server that also registers MCP resources or resource templates (Server.AddResource,Server.AddResourceTemplate) or prompts (Server.AddPrompt) gets no authorization check and no access-log outcome for calls to them — the same authenticated caller a writing tool call correctly refuses can still read a resource's content outright. This matches the design brief's own framing — authorization per tool — but it is easy to miss if you assume every MCP capability is covered the same way tools are. If your server exposes resources or prompts with content that needs the same protection, that protection has to be added separately; Gangway does not provide it today. - Token exchange authenticates by form field only.
backend.Exchangeimplements RFC 8693 token exchange by sendingclient_idandclient_secretin the POST body, the form-based client authentication method most token-exchange endpoints accept. A provider that requires HTTP Basic client authentication instead —Authorization: Basic base64(id:secret)— is not currently supported by this source. - Only OIDC discovery, not opaque tokens or static keys.
identity.NewOIDCis the only verifier this project ships: it requires an issuer that serves a standard/.well-known/openid-configurationdocument and a JWKS endpoint. Opaque tokens verified through provider introspection, or statically configured signing keys for an issuer without discovery, are not implemented. - Provider IP allowlists are IPv4-only, by design.
origin.ParseOpenAIPrefixesreads only theipv4Prefixfield from a provider's published range and skips entries without one. This is not a gap in the parser: AI providers' outbound connector traffic is currently IPv4-only, so an entry never arrives on an IPv6 address regardless of whether Gangway itself also listens on IPv6.
Status¶
In development. Interfaces may still change. See the repository for the current state of the code.
This site is built from the docs/ directory of that repository on every
push to main, so it always reflects the current state of the code.