mcp-gateway
Allows connecting a GitHub MCP backend via OAuth and exposing its tools under a github_ namespace through the gateway.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@mcp-gatewayUse the GitHub backend to list my open pull requests."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Gateway
A lightweight, self-hosted MCP aggregator gateway: one public MCP endpoint in front of any number of protected backend MCP servers, with a spec-compliant OAuth 2.1 authorization server facing the MCP client — the piece most existing gateways are missing.
flowchart LR
Client["Claude Code / Claude.ai"]
subgraph Gateway["MCP Gateway"]
MCP["/mcp\n(Streamable HTTP)"]
end
GitHub["GitHub MCP"]
Docs["Microsoft Learn MCP"]
More["…more backends"]
Client -- "OAuth 2.1\n(DCR/CIMD + PKCE)" --> MCP
MCP -- "own credentials" --> GitHub
MCP -- "own credentials" --> Docs
MCP -- "own credentials" --> MoreBuilt with FastAPI + FastMCP, configured by a single YAML file, stores its state
in a single encrypted SQLite database, and ships as one small standalone container —
no reverse proxy required, though you can put one in front of it for TLS. Prebuilt
images are published at
ghcr.io/r0wi/mcp-gateway;
building from source is only needed if you want to change the code.
Features
Client-facing (MCP authorization spec, 2025-11-25):
OAuth 2.1 authorization code flow with mandatory PKCE (S256)
Dynamic Client Registration (RFC 7591) at
/register—claude mcp addworks with no pre-shared credentialsClient ID Metadata Documents (CIMD) — HTTPS URLs as client IDs, including
private_key_jwtclient authentication, advertised viaclient_id_metadata_document_supported: trueAuthorization Server Metadata (RFC 8414) + OIDC discovery alias
Protected Resource Metadata (RFC 9728); 401 responses carry
WWW-Authenticate: Bearer resource_metadata="…"as Claude's connector requiresResource indicators (RFC 8707) accepted and bound to issued tokens
Short-lived opaque access tokens, rotating refresh tokens, single-use authorization codes — all stored hashed; client records encrypted at rest
Loopback redirect URIs match port-agnostically (Claude Code CLI registers one port and authorizes with another); non-loopback URIs require exact registration
Small Svelte 5 login + consent UI (single local identity from the config file)
Backend-facing:
none— public servers (e.g. Microsoft Learn MCP)bearer— static token injection (Authorization: Bearer …, e.g. PATs)headers— arbitrary static headers (API keys)oauth— full OAuth client per the MCP spec: metadata discovery, CIMD when the upstream AS supports it (the gateway hosts its own client metadata document), DCR fallback, PKCE, automatic token refresh. Connected once via the browser; tokens persisted encrypted (Fernet) in SQLite.The client's gateway token is never forwarded upstream (no token passthrough, as the spec demands); backends only ever see credentials the gateway holds.
Aggregation:
Tools/resources/prompts namespaced per backend:
github_create_issue,msdocs_microsoft_docs_search, …Live proxying over Streamable HTTP; a down or not-yet-connected backend only removes its own tools instead of breaking the gateway
Built-in
gateway_statustool
Related MCP server: MCP OAuth Test
Quick start
cp config.example.yaml config.yaml
$EDITOR config.yaml # set public_url, users, backends
cp .env.example .env
$EDITOR .env # set MCP_GATEWAY_ENCRYPTION_KEY (openssl rand -base64 32)
docker compose up -ddocker compose up -d pulls the prebuilt
ghcr.io/r0wi/mcp-gateway
image by default — no build step required. If you want to run from source instead
(e.g. to test local changes), uncomment the build: . line in docker-compose.yml and
comment out image:.
The gateway runs standalone and listens on :8000; docker compose picks up
MCP_GATEWAY_ENCRYPTION_KEY from .env automatically. Put it behind a reverse proxy of
your choice for TLS, or expose the port directly. For a file-based alternative to
.env (recommended for production), see Encryption key.
Generate a password hash for the config file:
docker compose run --rm mcp-gateway mcp-gateway hash-passwordConnect Claude Code (CLI)
claude mcp add --transport http gateway https://mcp.example.com/mcpClaude Code discovers the gateway's authorization server, registers itself via DCR (or
uses its CIMD client ID), and opens your browser: log in with a user from
config.yaml, approve, done. No tokens to paste.
Connect Claude.ai / Claude Code web (custom connector)
Add https://mcp.example.com/mcp as a custom connector. The browser redirect to
https://claude.ai/api/mcp/auth_callback goes through the same login/consent flow.
Connect OAuth backends
Open https://mcp.example.com/ui/backends, sign in, and press Connect next to each
OAuth backend (e.g. GitHub MCP). You'll be redirected to the backend's authorization
server once; afterwards the gateway refreshes tokens automatically.
Configuration
Everything lives in one YAML file (see config.example.yaml).
Values support ${ENV_VAR} / ${ENV_VAR:-default} expansion.
server:
public_url: https://mcp.example.com # behind your reverse proxy
auth:
encryption_key: ${MCP_GATEWAY_ENCRYPTION_KEY} # encrypts secrets at rest
users:
- username: admin
password_hash: "$2b$12$…" # mcp-gateway hash-password
access_token_expiry_seconds: 3600
refresh_token_expiry_seconds: 2592000
storage:
path: /data/gateway.db # SQLite; the only state
backends:
github: # → tools namespaced github_*
url: https://api.githubcopilot.com/mcp/
auth:
type: oauth
# GitHub's authorization server supports neither CIMD nor DCR, so
# register a GitHub OAuth App and provide its credentials directly:
client_id: ${GITHUB_OAUTH_CLIENT_ID}
client_secret: ${GITHUB_OAUTH_CLIENT_SECRET}
microsoft-docs: # → tools namespaced microsoft-docs_*
url: https://learn.microsoft.com/api/mcp
auth: { type: none }
something-with-a-pat:
url: https://example.com/mcp
auth: { type: bearer, token: "${SOME_PAT}" }Adding a backend is config-only — no code changes.
Backend auth reference
type | fields | behaviour |
| – | no credentials sent |
|
|
|
|
| static headers (API keys etc.) |
|
| full OAuth client: CIMD → DCR fallback, PKCE, refresh, encrypted store |
For oauth backends the gateway hosts its own Client ID Metadata Document at
<public_url>/oauth/client-metadata.json and uses it as its client ID whenever the
upstream AS advertises CIMD support (requires an HTTPS public_url); otherwise it
falls back to Dynamic Client Registration. If the upstream AS supports neither (e.g.
GitHub's), set client_id (and client_secret, if the app is confidential) to use a
pre-registered OAuth client instead — CIMD/DCR are skipped entirely.
Encryption key
auth.encryption_key protects everything the gateway stores at rest: registered
OAuth client records and upstream backends' access/refresh tokens (see
Security for how). Two ways to supply it:
MCP_GATEWAY_ENCRYPTION_KEY(default) — a plain environment variable, referenced fromconfig.yamlas${MCP_GATEWAY_ENCRYPTION_KEY}. Simple, and fine for a self-hosted single-admin deployment.MCP_GATEWAY_ENCRYPTION_KEY_FILE— path to a file containing the key, e.g./run/secrets/encryption_key. Takes precedence overMCP_GATEWAY_ENCRYPTION_KEYwhen set, andconfig.yamldoesn't need any change either way. This is the Docker Composesecrets:convention — it keeps the raw key out ofdocker inspectanddocker compose configoutput, and is what KubernetesSecretvolumes, Vault Agent, and most KMS sidecars all expect too.docker-compose.ymlhas a commented-out example; switching is a few uncommented lines, no rebuild required.
Internally, the key you supply is a Key Encryption Key (KEK): it never encrypts
data directly. On first run the gateway generates a random Data Encryption Key (DEK)
that does the actual encrypting, and stores the DEK wrapped by the KEK. This means
rotating encryption_key only has to re-wrap that one small DEK, not re-encrypt the
whole database:
openssl rand -base64 32 > new-key.txt
mcp-gateway rotate-key -c config.yaml --old-key-file old-key.txt --new-key-file new-key.txtThis updates the database in place (an O(1) operation, regardless of how much data
it holds); update MCP_GATEWAY_ENCRYPTION_KEY/MCP_GATEWAY_ENCRYPTION_KEY_FILE to
new-key.txt's contents and restart the gateway afterwards. If --old-key-file
doesn't match the key currently protecting the database, the command fails with an
error and changes nothing.
If you lose the key
There is no recovery path — that's what "encrypted" means. Every registered OAuth
client would need to re-register (most do this automatically via DCR/CIMD on next
use) and every OAuth backend would need reconnecting via /ui/backends. Back the key
up the way you'd back up any other irreplaceable credential — a password manager, a
sealed secret in your org's vault — not just as a file that only exists on the host
running the gateway.
Database migrations
Schema changes are managed with Alembic (migration
scripts live in src/mcp_gateway/migrations/). mcp-gateway run applies any pending
migrations automatically before the server starts -- this covers both a brand-new
database (created from scratch on first run) and upgrading an older one, so there's
nothing to do for a normal deployment.
If you'd rather apply migrations as an explicit step -- e.g. as part of a deploy pipeline, or before scaling up multiple replicas against the same database -- run:
mcp-gateway migrate -c config.yamlThis only touches the database (it doesn't start the server) and is safe to run
before and after mcp-gateway run: migrations are idempotent and tracked in the
database itself (an alembic_version table), so applying them twice, or having both
migrate and the next run see an already-up-to-date database, is a no-op.
Logging
The gateway logs to stdout/stderr (docker logs, docker compose logs -f), at INFO by
default: startup/shutdown, config summary, login attempts, OAuth authorize/consent/token
issuance, upstream backend connect/disconnect, and backend mount status. DEBUG adds
finer-grained detail (client construction, token rotation, CIMD refreshes, storage
housekeeping). No credentials or tokens are ever logged, at any level.
Set the level via the MCP_GATEWAY_LOG_LEVEL environment variable (debug, info,
warning, error, or critical):
# .env (picked up by docker compose)
MCP_GATEWAY_LOG_LEVEL=debug# or inline
docker compose run --rm -e MCP_GATEWAY_LOG_LEVEL=debug mcp-gatewaydocker-compose.yml already forwards this variable to the container, defaulting to
info when unset.
Outside Docker, --log-level on mcp-gateway run works the same way and takes
precedence over the env var:
mcp-gateway run -c config.yaml --log-level debugEndpoints
Path | Purpose |
| MCP endpoint (Streamable HTTP) |
| RFC 9728 protected resource metadata |
| RFC 8414 AS metadata (+ OIDC alias) |
| OAuth 2.1 endpoints (PKCE, DCR, revocation) |
| login + consent (Svelte 5) |
| backend connection status / connect / disconnect |
| the gateway's own CIMD document (upstream leg) |
| upstream OAuth connect flow |
| liveness |
Security
A quick tour of the primitives in use and what they're for, before the detailed list below:
Secrets at rest (registered OAuth client records, upstream backends' access/refresh tokens) are encrypted with Fernet — AES-128-CBC plus HMAC-SHA256, authenticated symmetric encryption from Python's well-audited
cryptographypackage, not hand-rolled crypto. See Encryption key for how the key itself is supplied, wrapped (envelope encryption), and rotated.Passwords: local users' passwords are hashed with bcrypt (
mcp-gateway hash-password); a passphrase-styleencryption_keyis stretched into a Fernet key with scrypt (n=2**17, current OWASP guidance) plus a random per-database salt, rather than used directly.Tokens: access/refresh tokens and authorization codes are never stored in recoverable form — only a SHA-256 hash of each, the same way a password would be.
Sessions: browser login sessions are signed cookies (itsdangerous), not server-side session IDs, but are still revocable — logout records the session in SQLite so it stops validating immediately, not just when the cookie expires.
None of this protects a fully compromised gateway process — a key that's decrypting and using secrets on every request necessarily has to be in that process's memory. What it protects against is the database file leaking without the process: a misdirected backup, a shared volume snapshot, a support bundle.
Hardening measures
PKCE (S256) is mandatory; authorization codes are single-use and expire in 5 min.
Refresh tokens rotate on every use (OAuth 2.1 public-client requirement).
The consent screen names the client and the exact redirect target, and warns on loopback redirects (CIMD localhost-impersonation guidance from the spec).
Tokens issued to MCP clients are never forwarded to backends, and backend credentials never reach MCP clients.
Sessions are
HttpOnly,SameSite=Lax,Secureon HTTPS.Login and Dynamic Client Registration (
/register) are rate-limited per source IP; password checks run off the event loop so a flood of attempts can't stall the server.Anonymous DCR/CIMD client registrations that never complete an authorization are reclaimed after 24h; storage doesn't grow unbounded from unauthenticated
/registertraffic.Security headers (CSP,
X-Frame-Options: DENY,Referrer-Policy,X-Content-Type-Options) are set on every response.X-Forwarded-*headers are trusted only fromserver.trusted_proxy_ips(default: loopback). Set this to your reverse proxy's address if you run one — see Configuration.No credentials are logged. A decrypt failure caused by a mismatched
encryption_keyis logged loudly at startup rather than silently treated as missing data — see Encryption key.
Development
uv venv && uv pip install -e ".[dev]" # or: pip install -e ".[dev]"
(cd ui && npm install && npm run build) # build the Svelte UI
pytest # 71 tests incl. full e2e OAuth flows
mcp-gateway run -c config.yamlThe test suite spins up real gateways (and a second instance acting as an OAuth-protected upstream) and drives complete DCR/CIMD + PKCE flows over HTTP.
Adding a migration
There's no SQLAlchemy ORM layer in this project (see src/mcp_gateway/storage.py), so
migrations are plain, hand-written DDL rather than autogenerated from models. Point the
Alembic CLI at a scratch database (alembic.ini at the repo root is set up for exactly
this):
alembic -x db-path=/tmp/dev.db revision -m "describe the change"then edit the generated file under src/mcp_gateway/migrations/versions/ using
op.execute(...) / op.add_column(...) etc., following the existing revisions there.
Since the schema was already out in the wild before this project adopted Alembic, a
migration that alters an existing table (unlike CREATE TABLE IF NOT EXISTS, which is
always safe to repeat) should guard itself with an existence check -- see
0002_session_revocation_and_client_ttl.py for the pattern. Run it against the scratch
database to sanity-check it (alembic -x db-path=/tmp/dev.db upgrade head), then add a
regression test in tests/test_db_migrations.py.
Architecture
flowchart TB
MCPClient["MCP client\n(Claude Code / Claude.ai)"]
Browser["Browser\n(login / consent / backends UI)"]
subgraph Frontend["Frontend — ui/ (Svelte 5 + Vite SPA)"]
UI["Login · Consent · Backend connections"]
end
subgraph Backend["Backend — FastAPI + FastMCP (app.py / web.py)"]
subgraph ClientFacing["Client-facing — oauth_server.py"]
AS["OAuth 2.1 authorization server\nDCR · CIMD · PKCE · metadata"]
Consent["Login / consent transaction flow"]
end
subgraph Aggregation["Aggregation — gateway.py"]
Proxy["FastMCP server\nnamespaced tool/resource proxying"]
end
subgraph UpstreamLayer["Upstream — upstream.py"]
BackendClients["Backend clients\nnone · bearer · headers · oauth"]
end
subgraph Security["Security"]
Storage["Encrypted SQLite\n(Fernet, hashed tokens, bcrypt)"]
end
end
Upstream1["GitHub MCP"]
Upstream2["Microsoft Learn MCP"]
Upstream3["…more backends"]
MCPClient -- "/mcp (Streamable HTTP)\nBearer token" --> AS
Browser -- "/ui/authorize, /ui/backends" --> UI
UI -- "JSON API" --> Consent
AS --> Consent
AS -- "issues/validates tokens" --> Proxy
Consent -- "clients, sessions" --> Storage
Proxy -- "routes per backend namespace" --> BackendClients
BackendClients -- "tokens, client records" --> Storage
BackendClients -- "own credentials" --> Upstream1
BackendClients -- "own credentials" --> Upstream2
BackendClients -- "own credentials" --> Upstream3src/mcp_gateway/oauth_server.py— the client-facing OAuth AS. Builds on the MCP SDK's authorization-server handlers and FastMCP's CIMD manager rather than hand-rolling protocol code; the gateway adds SQLite persistence, the login/consent transaction flow, and token issuance/rotation policy.src/mcp_gateway/upstream.py— backend clients. OAuth backends use the official SDKOAuthClientProvider(discovery, CIMD/DCR, refresh) with encrypted SQLite token storage and a browser-driven connect flow.src/mcp_gateway/gateway.py— FastMCP server; each backend is mounted as a live proxy under its namespace.src/mcp_gateway/app.py/web.py— FastAPI app: JSON API for the UI, upstream callback, CIMD document, static Svelte app; the FastMCP app (MCP endpoint + OAuth routes + well-known) is mounted at the root.ui/— Svelte 5 + Vite SPA (login, consent, backends).
Single-instance by design (SQLite + in-memory connect flows). Runs standalone; put it behind a reverse proxy of your own if you want TLS termination, and back up one file.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.
No tool schema history has been recorded yet.
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
MCP server for mandates, delegation, policy-gated execution, credential grants, and audit.
111The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
Related MCP Servers
- FlicenseNot gradedqualityNot gradedmaintenanceAggregates multiple MCP servers behind a single, secure endpoint with unified tool/resource discovery, OAuth authentication, and resilient request routing. Enables users to manage and interact with multiple MCP backends through one centralized interface with load balancing and circuit breakers.2-
- FlicenseNot gradedqualityBmaintenanceMulti-tenant MCP server with OAuth 2.1 authorization, enabling tenant-scoped tool access and audit logging.-
- AlicenseAqualityCmaintenanceA federated MCP gateway that consolidates multiple plain-HTTP backends into a single, OAuth-protected MCP server, enabling agents to access diverse tools through one endpoint with centralized authentication and audit.512MIT
- AlicenseNot gradedqualityCmaintenanceAuthenticating reverse proxy for MCP servers providing credential isolation, OAuth2 token management, and composite tool aggregation.BSD Zero Clause
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/R0Wi/mcp-gateway'
If you have feedback or need assistance with the MCP directory API, please join our Discord server