MCP Hero: an MCP gateway aggregating upstream MCP servers with OAuth, access policies, and audit logging
███╗ ███╗ ██████╗██████╗ ██╗ ██╗███████╗██████╗ ██████╗
████╗ ████║██╔════╝██╔══██╗ ██║ ██║██╔════╝██╔══██╗██╔═══██╗
██╔████╔██║██║ ██████╔╝ ███████║█████╗ ██████╔╝██║ ██║
██║╚██╔╝██║██║ ██╔═══╝ ██╔══██║██╔══╝ ██╔══██╗██║ ██║
██║ ╚═╝ ██║╚██████╗██║ ██║ ██║███████╗██║ ██║╚██████╔╝
╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝
MCP Hero
One endpoint for all your MCP servers, with the access control, auth, and auditing a team needs.
Hosted version: mcphero.io · or self-host with this repo.
What is MCP Hero?
MCP Hero is a self-hostable gateway for Model Context Protocol servers. It mounts many upstream MCP servers behind a single URL and adds the things you need to run them for real:
- One endpoint — point your MCP client (Claude and friends) at one gateway URL instead of wiring up each server separately.
- Role-based access control — decide which roles can reach which servers, down to individual tools.
- Per-user OAuth to upstreams — each user authenticates to upstream servers as themselves; tokens are stored encrypted.
- Hosted stdio MCPs — run
command:-style stdio MCP servers in isolated sandboxes, not just remote HTTP ones. - Audit logging — every tool call is recorded.
- A web dashboard — manage servers, roles, and users without touching config files.
You can run it two ways, controlled by a couple of environment variables (see below): a zero-config standalone mode for a single person or team, or a multi-tenant cloud mode (what powers mcphero.io).
Quick start (standalone)
The fastest way to try it. No Python or Node toolchain required — just Docker.
git clone https://github.com/nseniak/mcphero.git
cd mcphero
docker compose --profile standalone up --build
Then open http://localhost:8080. On first launch you'll see an email picker (standalone uses a no-login dev auth by default); pick any email and you become the admin of the default org. Config and data persist in Docker volumes (mcpolis-config, mcpolis-data), so your setup survives restarts.
That's the whole install. To add your first MCP server, use Upstreams → Add in the dashboard.
Standalone defaults to a no-real-auth email picker, which is perfect on your own machine. If you expose the dashboard to a network, turn on real Google sign-in — see below.
Turn on Google sign-in
The default email picker (dev_stub) has no real login. To require Google sign-in instead:
-
Create a Google OAuth client. In the Google Cloud Console → Credentials, create an OAuth client ID (Web application). Add your dashboard URL as an Authorized JavaScript origin, and set the Authorized redirect URI to
<your-url>/mcp/oauth/google/callback(for local use that'shttp://localhost:8080/mcp/oauth/google/callback). -
Set these env vars (and point
MCPOLIS_SERVER_URLat the same URL whose callback you registered):MCPOLIS_OAUTH_PROVIDER=google MCPOLIS_GOOGLE_CLIENT_ID=<client id> MCPOLIS_GOOGLE_CLIENT_SECRET=<client secret> MCPOLIS_SERVER_URL=http://localhost:8080 # the URL whose redirect URI you registeredWhere they go depends on how you run:
- From source:
backend/.env. - Cloud:
.env.cloud.docker.prod(cloud already requiresgoogle). - Docker standalone: add them under an
environment:block on thestandaloneservice indocker-compose.yml(that image takes no runtime env file).
- From source:
-
Restart. Logins now go through Google, and the returned email is matched to roles.
The detailed walkthrough (and per-upstream OAuth) is in the backend README.
The three switches that decide how it runs
Almost all behavior comes down to three environment variables:
| Variable | Values | What it controls |
|---|---|---|
| MCPOLIS_MODE | standalone (default) · cloud | Storage + tenancy. standalone = a single org, file-backed storage, no external services. cloud = multi-org SaaS, MongoDB + Redis, encrypted tokens, horizontally scalable. |
| MCPOLIS_OAUTH_PROVIDER | dev_stub (default) · google | Dashboard auth. dev_stub = pick-an-email with no real login (ideal for local/standalone). google = real Google OAuth. Cloud mode forces google and rejects dev_stub at startup. |
| MCPOLIS_SANDBOX_PROVIDER | empty = auto (default) · e2b · local-subprocess | How stdio MCP servers execute. Auto-selects e2b (isolated remote sandbox) when MCPOLIS_E2B_API_KEY is set, otherwise local-subprocess (spawned on the host, no isolation — dev only). Cloud mode requires e2b. |
The first two combine into the setups you'd actually want:
standalone+dev_stub— zero-config local run. This is the Quick start above.standalone+google— a hardened single-tenant deployment you can expose safely.cloud+google— the multi-tenant SaaS (what mcphero.io runs).
MCPOLIS_SANDBOX_PROVIDER is mostly automatic: drop in an MCPOLIS_E2B_API_KEY and stdio servers run isolated in a remote sandbox; leave it unset and they run as local subprocesses (fine on your own machine, flagged as unsafe at startup). Whether stdio MCPs are allowed at all is a separate gate — MCPOLIS_ALLOW_STDIO_MCP (on by default in standalone, off in cloud).
Set these in backend/.env (from-source) or your Docker env file. The defaults already give you the standalone experience, so you only touch them when you want real auth, cloud mode, or remote sandboxing.
Develop from source
For working on MCP Hero itself (hot-reloading dashboard, tests, etc.).
Prerequisites
- Python 3.12 in an environment of your choice (venv, conda, uv, ...). The dependency manager is Poetry.
- Node.js 20+
- Docker (only for cloud mode, or to run tests against Mongo)
Setup
# Activate your Python 3.12 environment first, then:
cd backend && poetry install && cd ..
cd frontend && npm install && cd ..
The shell scripts assume your project Python environment is already active. (They source an optional, gitignored run-in-env.local.sh if you want them to activate it for you — handy if you use conda or pyenv. See run-in-env.sh.)
Run
bash start.sh standalone # standalone, file-backed, no containers
bash start.sh cloud # cloud: starts Docker (Mongo + Redis), multi-org
bash start.sh # defaults to cloud
From source, the dashboard runs on the Vite dev server:
- Dashboard: http://localhost:5173
- Gateway/API backend: http://localhost:8080 (logs at
/tmp/mcpolis-backend.log)
bash stop.sh tears it down (--all also stops the Mongo + Redis containers).
Tests and linting
bash backend/run-unit-tests.sh # backend unit tests (pytest)
bash frontend/run-unit-tests.sh # frontend unit tests (vitest)
bash tests/run-e2e-tests.sh # full-stack E2E (Playwright)
bash backend/run-pyright.sh src/ tests/ # type check
cd backend && poetry run ruff check . # lint
Integration tests that hit the live E2B API are gated by an API key — see backend/tests/integration/.env.test.example.
Running in cloud mode (multi-org, scalable)
Cloud mode is the multi-tenant SaaS shape: MongoDB-backed, Redis-coordinated, horizontally scalable behind nginx, with encrypted OAuth tokens and real Google auth.
-
Create the secrets file from the template and fill it in:
cp .env.cloud.docker.example .env.cloud.docker.prodMCPOLIS_SESSION_SECRET=<random-string> MCPOLIS_ENCRYPTION_KEY=<random-string> MCPOLIS_SERVER_URL=https://your-domain.com MCPOLIS_OAUTH_PROVIDER=google # cloud requires real OAuth MCPOLIS_GOOGLE_CLIENT_ID=<your-client-id> MCPOLIS_GOOGLE_CLIENT_SECRET=<your-client-secret> -
Build and start:
docker compose --profile cloud up --build # single backend docker compose --profile cloud up --build --scale backend=2 # horizontal scalingThis starts nginx (port 80) + backend(s) + MongoDB + Redis.
Behind an external reverse proxy
When co-tenanting MCP Hero behind another proxy (Caddy, Traefik, an ALB, K8s ingress), use the docker-compose.proxied.yml overlay, which drops nginx's host port binding and joins a shared external Docker network:
docker network create web
docker compose -f docker-compose.yml -f docker-compose.proxied.yml --profile cloud up -d --build
Cloud architecture
┌─────────┐
:80 ──────────>│ nginx │
└────┬────┘
│ sticky sessions (mcp-session-id header)
┌────────┼────────┐
v v v
┌────────┐┌────────┐┌────────┐
│backend1││backend2││backend3│
└───┬────┘└───┬────┘└───┬────┘
│ │ │
┌────┴─────────┴─────────┴────┐
│ MongoDB Redis │
└─────────────────────────────┘
- nginx serves the dashboard SPA and reverse-proxies API/MCP traffic with sticky routing on the
mcp-session-idheader. - Backends are stateless except for in-memory upstream connections; a Mongo-backed distributed lock coordinates token refresh, and SIGTERM drains gracefully.
- MongoDB stores org config, upstream definitions, OAuth tokens (AES-256-GCM encrypted), and audit logs.
- Redis provides cross-backend pub/sub and rate limiting.
Health checks
/health— always{"status": "ok"}./healthz—{"status": "ok"}normally,{"status": "draining"}(503) during graceful shutdown. Use this for load-balancer checks.
Standalone vs cloud: what actually differs
Most domain logic is shared; divergence is concentrated in the storage factory, the startup-secret validator, and a handful of org-context branches, all selected at boot via MCPOLIS_MODE.
| Concern | Standalone | Cloud |
|---|---|---|
| Persistence | JSON files in backend/config/ + backend/data/ | MongoDB |
| Org model | Single default org auto-created at boot | Multi-org; users create orgs at signup |
| First-user bootstrap | First login becomes admin of default | Orgs created via signup flow |
| Dashboard auth default | dev_stub (in-app email picker) | google (required) |
| Field encryption | None (plaintext on disk) | AES-256-GCM on OAuth tokens via MCPOLIS_ENCRYPTION_KEY |
| Rate limiter | In-process counter | Redis sliding window |
| Event stream / dashboard SSE | In-process pub/sub | Redis channels, per-org isolation |
| Distributed lock | No-op | Mongo TTL collection |
| Sandbox refs (E2B reattach) | In-memory (lost on restart) | Mongo-backed (survives restart) |
| stdio MCP default | Allowed | Disabled (override via MCPOLIS_ALLOW_STDIO_MCP) |
| Sandbox provider | e2b or local-subprocess | e2b only |
| Startup secret validation | Skipped | Enforces session/encryption/Mongo/Redis (+ E2B when used) |
| Multi-org UI (OrgSwitcher, Team page) | Hidden | Shown |
Repo layout
backend/— Python (Poetry) MCP gateway servicefrontend/— React + Vite + Tailwind admin dashboarddocker/— Dockerfiles, nginx config, entrypoint scriptdocs/— user-facing documentation
Securing secrets
In standalone mode, backend/config/ and backend/data/ hold sensitive material (API keys, OAuth tokens, client secrets). For production, consider encrypting at rest:
- macOS: encrypted disk image (
hdiutil create -encryption AES-256) - Linux: gocryptfs or LUKS
- Cross-platform: VeraCrypt
In cloud mode, secrets live in .env.cloud.docker.prod (gitignored) and tokens are encrypted in MongoDB via MCPOLIS_ENCRYPTION_KEY.
Warning: rotating
MCPOLIS_ENCRYPTION_KEYinvalidates all stored OAuth tokens — users will need to re-authenticate their upstream connections.
Security
Found a vulnerability? Please report it privately — see SECURITY.md. Don't open a public issue for security problems.
License
MCP Hero is licensed under the GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later). See LICENSE for the full text.
The AGPL's network-use clause (section 13) means that if you run a modified version of MCP Hero as a network service, you must make the corresponding source available to its users. If the AGPL doesn't fit your use case, a commercial license is available — reach out via the contact page at mcphero.io.
Trademarks
"MCP Hero", the MCP Hero name, and the MCP Hero logos are trademarks of Nitsan Seniak. The AGPL covers the source code only; it does not grant any right to use the MCP Hero name or logos.
You may run, modify, and redistribute the code under the AGPL. The brand assets (hero-* logos, favicon, OG images) are bundled so the official build renders correctly — but if you operate your own or a modified instance, please replace them with your own branding and don't present it as the official MCP Hero service.