A production-grade reverse proxy for MCP (Model Context Protocol) servers. mcpgate sits between MCP clients (Claude Desktop, IDE extensions, custom agents) and upstream MCP servers, adding authentication (SAML 2.0), authorization (RBAC), structured audit logging, Prometheus metrics, and a React admin console — without modifying MCP message semantics.
MCP Clients → mcpgate (auth · RBAC · logging · metrics) → Upstream MCP Servers
mcpgate is a front door between users (or AI agents) and all upstream MCP servers. Users speak only to mcpgate; mcpgate checks their identity and permissions, then forwards the request to the right server. Upstream servers are never exposed directly.
An admin registers an upstream MCP server via the admin console or REST API, supplying its name, transport type, and connection details. The server name becomes the routing key in every URL:
POST /mcp/github-tools/message → forwards to the "github-tools" upstream
POST /mcp/jira-tools/message → forwards to the "jira-tools" upstream
Registration is configuration only — mcpgate does not connect to the server, send initialize, or fetch its tool list at registration time. The first real request from a client triggers the connection.
Every request through the proxy passes through a fixed middleware chain before reaching the upstream:
Client request
│
├─ 1. Auth — validate JWT (SAML session or M2M JWKS); extract identity + roles
├─ 2. Logging — pre-parse JSON-RPC body; start audit log entry
├─ 3. Metrics — increment Redis request counters (global + per-server)
├─ 4. Rate limit — check RPM / RPD / concurrent connection limits per user
├─ 5. RBAC — map MCP method → action category; check Casbin policy
└─ 6. Proxy — forward raw bytes to upstream; return raw response verbatim
mcpgate maps MCP JSON-RPC methods to action categories for access-control decisions:
| Category | MCP methods |
|---|---|
list |
tools/list, resources/list, prompts/list |
call |
tools/call |
read |
resources/read, resources/subscribe, prompts/get |
write |
resources/write |
connect |
initialize, ping, SSE stream open |
Casbin policies are stored as (role, server, action) tuples. Wildcards are supported:
(developer, *, call) → developer can call tools on any server
(developer, github-tools, list) → developer can list tools on github-tools only
(admin, *, *) → admin can do anything on any server
Registering a new server does not create any Casbin policy. Whether a role can reach a new server depends entirely on whether an existing wildcard rule covers it.
mcpgate never modifies MCP message payloads. The raw JSON-RPC bytes from the client are forwarded unchanged to the upstream; the raw bytes from the upstream are returned unchanged to the client. The specific tool name inside a tools/call body is not parsed for routing — it is passed through verbatim and captured in the audit log.
For a detailed walkthrough of each step, the Casbin policy model, and the action category mapping, see docs/understanding.md.
| Tool | Version | Purpose |
|---|---|---|
| gvm | any | Go version manager |
| Go | 1.26.0 (via gvm) | Backend |
| Docker | 24+ | Postgres, Redis, dev services |
| Node.js | 22 LTS | Admin console frontend |
| nvm | any | Node version manager (optional but recommended) |
| openssl | any | Dev certificate generation |
| psql | any | make seed-idp — upsert IdP config |
| golang-migrate CLI | v4 | Database migrations |
| golangci-lint | v2.12+ | Linting |
gvm + Go 1.26.0
bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
source ~/.gvm/scripts/gvm
gvm install go1.26.4 -B
gvm use go1.26.4 --defaultDocker Follow the official guide for your OS: https://docs.docker.com/get-docker/
Node.js 22 via nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash
source ~/.nvm/nvm.sh
nvm install 22
nvm use 22golang-migrate CLI
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latestgolangci-lint
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latestpsql (macOS)
brew install libpq
brew link --force libpqgit clone https://github.com/K3y5tr0ke/mcpgate
cd mcpgate
# First-time tool installation (gvm, migrate, golangci-lint, certs, Docker services, migrations)
bash scripts/setup.sh
# Seed the DB and start everything (backend :8080 + frontend :5173)
make setup-devCheck it's running:
curl http://localhost:8080/health # {"status":"ok"}
curl http://localhost:8080/ready # {"status":"ok"} (requires Postgres + Redis)Open the admin console: http://localhost:8080/console
Step-by-step sequences for the most common tasks. Run these from the repo root.
# 1. Install the pre-push git hook (runs make ci-fast before every push)
make install-hooks
# 2. Start Postgres + Redis, apply migrations, seed DB + Auth0 IdP,
# build web assets (required — go:embed needs web/dist/ to compile), then start everything
make setup-dev
# → backend on :8080, frontend dev server on :5173# Start Docker services (Postgres + Redis) if not already running
make docker-up
# Start backend + frontend together
make dev-all
# Or start them separately:
make dev # backend on :8080
make dev-frontend # frontend on :5173 (separate terminal)make test-unit
# Runs in ~seconds. Safe to run any time, no Docker needed.# 1. Start services and wait until healthy
make ci-services
# 2. Apply migrations (idempotent — safe to re-run)
make migrate-up
# 3. Run integration tests
make test-integrationmake ci
# Runs: lint → unit tests → start services → migrate → integration tests → build → security scan
# Use this before opening a PR.make ci-fast
# Runs: lint → unit tests → build (~30 s)
# The pre-push hook runs this automatically on every git push (after make install-hooks).The repo includes three example MCP servers (one per transport) for integration testing and local development. Run these after make setup-dev has completed.
# 1. Build all three example binaries (required — stdio runs as a host process)
make build-examples
# → bin/mcp-streamable-example, bin/mcp-sse-example, bin/mcp-stdio-example
# 2. Start the two containerised example servers (Streamable HTTP + SSE)
make docker-up-examples
# Adds mcp-streamable (:8081) and mcp-sse (:8082) to the running dev stack.
# 3. Get an admin JWT (password is printed by make seed)
TOKEN=$(curl -s -X POST http://localhost:8080/auth/local/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@localhost","password":"<password from make seed output>"}' \
| jq -r .token)
# 4. Register all three servers in mcpgate via the admin API
MCPGATE_TOKEN=$TOKEN make seed-examples
# 5. Smoke test — probes all three transports end-to-end through the proxy
# Exits non-zero if any probe fails.
MCPGATE_TOKEN=$TOKEN make smoke-examplesFor an exploratory walkthrough that calls every tool, resource, and prompt on each registered server:
MCPGATE_TOKEN=$TOKEN make call-examplesSee examples/seed-example-servers.sh for --upstream-host options when mcpgate itself runs in a container.
- Go module (
github.com/K3y5tr0ke/mcpgate), pinned to Go 1.26.0 via gvm koanf-based config loader: YAML +MCPGATE_env var overrides- Store interfaces (
UserStore,SessionStore,ServerStore,RoleStore, etc.) - Postgres (
pgx/v5) and Redis (go-redis/v9) clients with connection pooling chiHTTP server with/healthand/readyendpoints, graceful shutdown- SQL migrations (
golang-migrate) underinternal/store/postgres/migrations/ - Docker Compose dev stack (
deployments/docker/docker-compose.dev.yml) - CI pipeline (lint → unit → integration → build)
scripts/setup.sh— one-shot dev environment bootstrap
- MCP proxy handler supporting all three transports:
- Streamable HTTP (
POST /mcp/{serverName}/message) - SSE (
GET /mcp/{serverName}/sse,POST /mcp/{serverName}/sse) - stdio — subprocess-based upstream with JSON-RPC framing
- Streamable HTTP (
- JSON-RPC 2.0 parser and message router
- Semaphore-bounded upstream HTTP connection pool
- 30-second in-memory config cache for server lookups
ServerStore(pgximplementation) — full CRUD- Admin REST API skeleton:
GET/POST /api/v1/admin/servers,GET/PUT/DELETE /api/v1/admin/servers/{id} - Prometheus metrics registration (
/metricson:9090) testcontainers-gointegration test helper +MockMCPServer
- SAML 2.0 SP-initiated login via
crewjam/saml:GET /auth/saml/login— initiates HTTP-Redirect binding to IdPPOST /auth/saml/acs— assertion consumer: validates signature, replay-detects, JIT-provisions user, issues JWTGET /auth/saml/metadata— serves SP metadata XMLGET /auth/saml/logout— invalidates session + clears cookie
- RS256 JWT session tokens (
golang-jwt/jwt/v5); ephemeral key in dev, file-backed in prod - Session management: durable sessions in Postgres + sliding-window cache in Redis
- Auth middleware: validates JWT → looks up live session → injects
UserIdentityinto context - Brute-force guard: Redis-backed rate limiting on login and ACS endpoints
- RBAC middleware: role-check (
adminrequired for all/api/v1/admin/*routes) - JIT user provisioning: auto-creates or links users from SAML assertions; attribute mapping from IdP claim URIs
- Replay detection: Redis
SET NXon assertion ID prevents replayed assertions - IDP config CRUD:
GET/POST /api/v1/admin/idps,GET/PUT/DELETE /api/v1/admin/idps/{id} - Auth0 integration: pre-configured SAML IdP connection to Auth0 tenant via
make seed-idp - React admin console (Vite + TanStack Router + Tailwind CSS):
- Dashboard with metrics summary + timeseries charts
- MCP Servers list
- Audit log viewer
- Served at
/consolefrom embeddedweb/dist/
Copy configs/mcpgate.example.yaml, or override with MCPGATE_ env vars:
MCPGATE_DATABASE.PASSWORD=secret \
MCPGATE_SERVER.PORT=9000 \
./bin/mcpgate serve --config configs/mcpgate.yamlFull annotated reference: configs/mcpgate.example.yaml
Sensitive values (JWT signing key path, DB password, Redis password) should be
supplied via env vars or a secrets manager — never committed to the repo.
The .env file is gitignored and holds local dev credentials only.
| Target | Description |
|---|---|
make setup-dev |
Full first-time setup: starts Docker services, applies migrations, seeds DB + Auth0 IdP config, then starts backend and frontend |
make dev |
Run the backend server with dev config (:8080) |
make dev-frontend |
Run the Vite dev server (:5173) with proxy to backend |
make dev-all |
Start Docker services + backend + frontend (skips migrate/seed) |
| Target | Description |
|---|---|
make build |
Build web assets then compile binary to bin/mcpgate |
make build-web |
Build React admin console to web/dist/ |
make build-docker |
Build production Docker image |
make build-examples |
Build example MCP server binaries to bin/ (required for stdio) |
make clean |
Remove bin/, web/dist/, coverage artifacts |
| Target | Description |
|---|---|
make migrate-up |
Apply all pending migrations |
make migrate-down |
Roll back the last migration |
make migrate-create name=<suffix> |
Create a new migration file pair |
make seed |
Seed default roles and admin user |
make seed-idp |
Upsert Auth0 dev SAML IdP config into Postgres (idempotent) |
| Target | Description |
|---|---|
make test-unit |
Unit tests — no external services required (-short) |
make test-integration |
Integration tests — requires Postgres + Redis (-tags=integration) |
make test-coverage |
Generate HTML coverage report at coverage.html |
| Target | Description |
|---|---|
make lint |
Run golangci-lint |
make lint-fix |
Run golangci-lint with auto-fix |
make security-scan |
Run govulncheck |
| Target | Description |
|---|---|
make ci-fast |
Lint + unit tests + build — no Docker (~30 s); run by the pre-push git hook |
make ci-services |
Start Postgres + Redis and wait until healthy |
make ci |
Full local CI pipeline: lint → unit → services → migrate → integration → build → security scan |
| Target | Description |
|---|---|
make install-hooks |
Install the pre-push git hook (scripts/pre-push.hook → .git/hooks/pre-push) — run once after cloning |
make docker-up |
Start all Docker services (Postgres, Redis, Prometheus, Grafana) |
make docker-down |
Stop and remove all Docker services including example containers if running (destroys volumes) |
make docker-up-examples |
Start dev stack + containerised example MCP servers (Streamable HTTP + SSE) |
make seed-examples |
Register example MCP servers via admin API (requires MCPGATE_TOKEN) |
make smoke-examples |
Probe all three example transports end-to-end through the proxy (requires MCPGATE_TOKEN) |
make call-examples |
Walk every registered server — invoke all tools, resources, and prompts (requires MCPGATE_TOKEN) |
make stop |
Kill the running mcpgate server (ports 8080 + 9090) |
make help |
Print all targets with descriptions |
cmd/mcpgate/ Entry point: serve / migrate / seed / version
internal/
config/ koanf config loader (YAML + MCPGATE_ env vars)
server/ chi HTTP server, SPA handler, graceful shutdown
proxy/ MCP proxy engine, transport adapters
auth/
jwt/ RS256 JWT manager (Issue + Validate)
saml/ SAML 2.0 SP: handlers, SP init, attribute mapping
middleware.go TokenValidator interface + Authenticate() chi middleware
context.go UserIdentity type + context helpers
rbac/ RBAC middleware (role enforcement)
admin/api/ Admin REST API handlers (servers, users, idps, logs, metrics)
store/
interfaces.go Store interface definitions + shared types
postgres/ pgx repositories (users, sessions, servers, roles, saml)
redis/ go-redis session cache + rate limiter
logging/ Async audit log pipeline, hash-chain tamper evidence
metrics/ Prometheus metrics registration
web/ React admin console (Vite + TanStack Router + Tailwind)
deployments/docker/ docker-compose.dev.yml, Dockerfile
scripts/ setup.sh, generate-certs.sh, seed.sh, seed_auth0_idp.sql
configs/ mcpgate.example.yaml, mcpgate.dev.yaml
certs/ Dev TLS + SAML certificates (gitignored)
docs/ Architecture deep-dives, implementation notes, PRD
Pinned to go1.26.4 via gvm. The Makefile
invokes $HOME/.gvm/gos/go1.26.4/bin/go directly — no PATH manipulation required.
- Branching: short-lived feature branches off
main(< 3 days) - Every PR requires passing
make ci(lint → unit → integration → build → security scan) - Security-sensitive PRs (auth, RBAC, crypto) require a second review
- Definition of done: feature works end-to-end, unit test covers the happy path and at least one error path
Copyright 2026 Sai Prathik S.B. and Namrata A Shettar
Licensed under the Apache License, Version 2.0. See NOTICE for attribution.