mcp-gway
Click on "Deploy 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-gwaylist all connected MCP servers"
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 standalone CLI gateway that aggregates multiple MCP (Model Context Protocol) servers behind a single headless HTTP/SSE endpoint with Code Mode — reducing LLM input token usage by up to 92% when using multiple MCP servers. Manage it from the CLI or from the built-in admin dashboard (v3.1.0): mcp-gway serve --transport http, then open http://127.0.0.1:8080/.
Features
Multi-Server Aggregation — Connect to multiple MCP servers (
remote/local, OpenCode format) and expose them through a single endpointCode Mode — 4 meta-tools that let LLMs discover and use tools dynamically without loading all schemas upfront
OAuth 2.0 Support — Built-in OAuth flow with dynamic client registration (RFC 7591) and token storage
Hermetic Sandbox — Starlark-based sandbox for safe code execution
MCP Protocol Compliant — Works with Claude Desktop, Cursor, and any MCP-compatible client
Related MCP server: ToolMux
Installation
pip install mcp-gwayBoth mcp-gway (canonical) and mgw (shortcut, 1:1) are exposed.
Or with mise:
mise install
uv sync --all-groups # installs dev group with pre-commitQuick Start
OpenCode Format (Primary)
OpenCode schema — remote / local with transport auto-detection. This is the recommended path.
# Remote — auto-detects transport (streamable-http → sse → http)
mcp-gway add youtube --type remote --url https://api.example.com/mcp
# SSRF-guard: private/loopback/link-local hosts rejected (src/mcp_gway/models.py:115-163); localhost solo en tests.
# Remote with headers
mcp-gway add supabase --type remote --url https://mcp.supabase.com/mcp --header "Authorization=Bearer TOKEN"
# Remote with pre-registered OAuth
mcp-gway add supabase --type remote --url https://mcp.supabase.com/mcp --oauth-client-id ID --oauth-client-secret SECRET --oauth-scope "openid profile"
> **Shell-history warning:** no pases secretos reales en `--header` / `--oauth-client-secret` (quedan en `~/.bash_history` / `ps`). Prefiere `mcp-gway refresh <name> --auth` o variables de entorno efímeras.
# Remote with timeout and enable toggle
mcp-gway add api --type remote --url https://api.example.com/mcp --timeout 10000 --enabled
mcp-gway add api --type remote --url https://api.example.com/mcp --timeout 10000 --no-enabled
# Local
mcp-gway add filesystem --type local --command "npx -y @anthropic/mcp-filesystem"
mcp-gway add tools --type local --command "python -m my_mcp_server" --env MY_VAR=value --cwd /path/to/workdir
mcp-gway add tools --type local --command "npx -y my-mcp" --env KEY=VALUE --env OTHER=123 --cwd /srv/mcp/workdir
# List and serve (local-first)
mcp-gway list
mcp-gway serve --transport http --port 8080 # bindea 127.0.0.1 por defecto
mcp-gway serve --transport http --host 127.0.0.1 --port 8080
curl -s http://127.0.0.1:8080/health | jqServer Types (only remote / local)
--type only accepts local|remote (cli.py:50). Legacy values http|stdio|sse|streamable-http are rejected by click, and --args / --docs-url do not exist. For local, --command is a single string (split via shlex).
Management — CLI + Admin Dashboard
Server management (add/remove/list/inspect/refresh/local-unrestricted) lives in the CLI with full parity in the admin dashboard: mcp-gway serve --transport http, then open http://127.0.0.1:8080/ (pages /admin/*, htmx partials /admin/partials/*).
One Gateway(registry, host) process serves /mcp, /health, /ready, /live, /metrics and the admin routes on the same Starlette app. Registry (servers/*.json + servers/*.pyi) is the single source of truth. The dashboard is loopback-only and CSRF-protected; the legacy /dashboard + catalog surfaces stay retired.
Local-First Security
# Default seguro — solo loopback
mcp-gway serve --transport http --port 8080 # bindea 127.0.0.1
# Exponer en 0.0.0.0 requiere opt-in explícito
MCP_GWAY_ALLOW_REMOTE=1 mcp-gway serve --transport http --host 0.0.0.0 --port 8080
# └─ log warning "server exposed on non-loopback host"
# Protege con firewall + auth reversa: nunca expongas 0.0.0.0 sin firewall/auth delante.
# Sin opt-in → error controlado
mcp-gway serve --transport http --host 0.0.0.0
# Error: binding to non-loopback host '0.0.0.0' requires MCP_GWAY_ALLOW_REMOTE=1
# exit 2Observability — Logs + Metrics + Health (Approach C, v2.4.0)
Zero vendor lock-in: stdlib
jsonlogs (nostructlog), vendoredMetricsRegistry(noprometheus_client), correlation viaX-Request-ID+contextvars, health probes/health|/ready|/live+ Prometheus text/metrics. Local-first + masking***preserved;X-Warning: exposedsolo enGET /metrics→403.
Health & Metrics:
curl -s http://127.0.0.1:8080/health | jq
# {"status":"ok","version":"3.1.0","checks":{"registry":"ok","routes":"ok"},"uptime_seconds":42}
curl -s http://127.0.0.1:8080/ready | jq # 200 ready / 503 not_ready (registry/routes/event_loop checks)
curl -s http://127.0.0.1:8080/live | jq # 200 alive — no FS I/O, <5ms
curl -s http://127.0.0.1:8080/metrics | head -n 20
# # HELP mcp_gway_http_requests_total Total HTTP requests
# # TYPE mcp_gway_http_requests_total counter
# mcp_gway_http_requests_total{method="GET",path="/health",status="200"} 7Correlation & JSON logs:
curl -s -H "X-Request-ID: demo123" http://127.0.0.1:8080/health -D - | grep -i X-Request-ID
# X-Request-ID: demo123 ← echo on every response; json log line also has "request_id":"demo123"
uv run mcp-gway serve --transport http --port 8080 2>&1 | head # each line valid JSON: timestamp, level, logger, message, request_id, method, path, status, duration_msX-Request-IDorX-Correlation-IDaccepted, sanitized to^[A-Za-z0-9_-]{1,64}$, truncated; autouuid4if absent.Labels bounded:
pathcollapsed to/mcpor/mcp/messages(alias SSE al mismo handler_mcp_post, no endpoint independiente; all other routes recorded as-is), server sanitized[^A-Za-z0-9_]→_32 chars.Metrics:
http_requests_total,http_request_duration_seconds(buckets 0.005..5),mcp_tool_calls_total{server,tool,status},discovery_duration_seconds,sandbox_execute_total{status},registry_operations_total{op},gateway_sessions_active.
FEAT-007 hardening (v2.2.1): process/build lifecycle, stdio coverage, upstream telemetry, cardinality cap.
Lifecycle:
build_info{version},process_start_time_seconds,uptime_seconds(heartbeat, 30s tick),lifetime_seconds(set at shutdown) + a JSONgateway shutdown summary(uptime + totals) on exit.stdio (
serve --transport stdio, default): per-requeststdio_requests_total{method,status}+stdio_request_duration_seconds{method}and a JSON access log (transport:"stdio", same shape as HTTP).Upstream CodeMode calls (tool execution):
upstream_tool_calls_total{server,tool,status}(ok/timeout/error),upstream_tool_duration_seconds{server,tool}; opt-in retries counted inupstream_retries_total{server}.Corruption visibility:
code_mode_servers_skipped_total{reason}+ structured WARN + degraded banner hint (mcp-gway refresh <name>) when a server fails injection at startup.Label cardinality hard-capped per metric (
_MAX_LABEL_COMBOS=200); overflow coalesces into a reserved_otherseries so a label storm cannot grow memory without bound.Slow-request WARN: requests over
_SLOW_REQUEST_THRESHOLD_MS(1000) additionally log aslow requestJSON line with duration.CLI structured outcomes:
cli <action> <status>JSON events — WARNING always emitted, INFO only whenMCP_GWAY_LOG_LEVELis set (zero operator noise by default).New opt-in flag:
mcp-gway add --retry-on-transport-error— retries exactly once ONLY when the transport/connect phase fails (never after the tool call starts; non-idempotency-safe, ADR-012 decision 9). Default off → zero behavior change.
Local-first gating: /metrics never leaks secrets; serve on non-loopback without MCP_GWAY_ALLOW_REMOTE=1 exits 2; X-Warning: exposed only on GET /metrics → 403 (src/mcp_gway/observability/health.py:127-139).
Connect from Claude Desktop
Add to your Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json):
{
"mcpServers": {
"gateway": {
"url": "http://localhost:8080/mcp"
}
}
}Transport note: gateway pairing is transport-agnostic — the routes are per-transport (
serve --transport http→POST /mcponly;serve --transport sse→GET /mcp+POST /mcp/messagesonly). A mispaired client gets405+Allowheader telling it which transport serves that method. If you upgrade from v2.x, see MIGRATION.md.
Commands
Command | Description |
| Add an MCP server and generate |
| Remove an MCP server |
| Update tools for a server |
| List all connected servers |
| Show tool signatures for a server |
| Refresh connection and re-discover tools |
| Start gateway (default |
| DEPRECATED hidden alias: |
| Break-glass marker 72h (explicit only) — enable/remove, or status without side effects |
Types: only
--type local|remote(cli.py:50). Legacyhttp|stdio|sse|streamable-httpare rejected by click. There is no--args/--docs-url.
Options for add (OpenCode) — 13 flags (cli.py:45-95):
Option | Description |
| Server type (primary) |
| URL for |
| HTTP header for |
| Command for |
| Environment variable for |
| Working directory for |
| Pre-registered OAuth client ID |
| Pre-registered OAuth client secret |
| OAuth scope |
| Local port for OAuth callback (default 8989) |
| Connection timeout in ms (default 5000) |
| Enable/disable without removal (default enabled) |
| Comma-separated tool filter (default |
Local Commands — Dynamic Allow-List (feat-006)
Dynamic-no-static: no hardcoded binaries. Operators allow-list once via env; see ADR-009.
Default-deny: empty MCP_GWAY_ALLOW_LOCAL_COMMANDS denies every local command (vacío = deny, se mantiene).
Recomendado: MCP_GWAY_ALLOW_LOCAL_COMMANDS="npx,uvx,python3,bunx".
# Allow-list (CSV basenames, `*` = invalid → deny + warn)
export MCP_GWAY_ALLOW_LOCAL_COMMANDS="npx,uvx,python3,bunx"
mcp-gway add fs --type local --command "npx -y @anthropic/mcp-filesystem" --cwd /srv/mcp/workdirBreak-glass 72h (bootstrap only, time-boxed) — marker required, env alone never activates
$env:MCP_GWAY_ALLOW_UNRESTRICTED_LOCAL="1"
mcp-gway local-unrestricted enable
mcp-gway local-unrestricted status
mcp-gway refreshDisable — both steps required (explicit only, never auto-created on add/refresh/spawn)
mcp-gway local-unrestricted disable # 1. removes marker file
Remove-Item Env:\MCP_GWAY_ALLOW_UNRESTRICTED_LOCAL # 2. unsets env, returns to allow-list modeNota CISO opt-in:
bunxsolo como recomendado en documentación (no default en código, default-deny vacío se mantiene); solo opt-in con pin + owner + regate 90d;bunruntime sigue fuera; denylist EXACT PATH,PATHEXT,SYSTEMROOT,COMSPEC,LD_PRELOAD,LD_LIBRARY_PATH,PYTHONPATH,PYTHONHOME,NODE_OPTIONS,NODE_PATH,NODE_EXTRA_CA_CERTS,NODE_TLS_REJECT_UNAUTHORIZED + PREFIXES DYLD_,NPM_CONFIG_,BUN_,UV_ + PATH controlado (NODE_ENVpermitido, no denylisted); prohibido*, paths o shell.Tag
v2.0.0interno no publicado — no anuncio externo. Tras actualizar, borra la caché vieja manualmente:rm ~/.config/mcp-gway/catalog.json.Marker
~/.config/mcp-gway/.local_unrestricted(epoch,0o600, 72h TTL) — fail-closed: missing, expired, future, insecure, unreadable, or invalid → deny. States viamcp-gway local-unrestricted status:disabled(env unset),marker-missing,expired,future(timestamp in the future),marker-insecure(permissions !=0o600on posix),marker-unreadable,marker-invalid,active.Allow-list still applies when break-glass inactive: env alone never activates; allow-listed binaries remain allowed when marker missing/expired/etc. Final deny carries an actionable hint (
local-unrestricted enable/status).Disable requires both steps:
local-unrestricted disableremoves the marker file, plusRemove-Item Env:\MCP_GWAY_ALLOW_UNRESTRICTED_LOCALunsets the env and returns to allow-list mode.Any syntactically valid basename allowed while marker fresh; otherwise deny.
unsetreturns to allow-list mode.CLI
add/refreshenforces allow-list/unrestricted plus re-validation before persist.cwdmust be absolute + real +is_dir, elsereason_code=invalid_cwd. Env denylist (PATH,LD_PRELOAD,PYTHONPATH, …) →reason_code=denied_env.Spawn only resolved via PATH lookup (
shutil.which(basename)); nevershell=True/cmd /c/sh -c. Errors carryreason_code(not_allowlisted,binary_not_found, …).
Code Mode
When connected, the gateway exposes 4 meta-tools:
Tool | Description |
| List all available |
| Read function signatures from a stub |
| Get detailed documentation for a tool |
| Execute code in a sandboxed Starlark interpreter |
OAuth Authentication
For servers requiring OAuth (e.g., Supabase):
# Trigger OAuth flow (preferido — no deja secretos en shell-history)
mcp-gway refresh supabase --auth
# Or store token manually (solo fallback; chmod 600 obligatorio)
mkdir -p ~/.config/mcp-gway/tokens
echo '{"access_token": "YOUR_TOKEN"}' > ~/.config/mcp-gway/tokens/supabase.json
chmod 600 ~/.config/mcp-gway/tokens/supabase.jsonDevelopment
# Install dependencies
uv sync --all-groups # installs dev group with pre-commit
uv run pre-commit install # once per clone — hooks already configured in .pre-commit-config.yaml
# Run checks
uv run pre-commit run --all-files # ruff + ruff-format + hygiene (trailing-whitespace, end-of-file-fixer, check-yaml, check-added-large-files)
uv run pytest -v # 570 tests — CLI, MCP, Code Mode, stdio, OAuth, observability
uv run ruff check src/ tests/
uv run ruff format --check src/ tests/
# Verification probes (sin Node, sin build)
curl -s http://127.0.0.1:8080/health | jq .status # "ok"
curl -s http://127.0.0.1:8080/ready | jq .status # "ready"
curl -s http://127.0.0.1:8080/metrics | head -n 5 # # HELP mcp_gway_...
# Local-first check
mcp-gway serve --transport http --host 0.0.0.0 2>&1 | grep -q "requires MCP_GWAY_ALLOW_REMOTE" && echo "gate ok"Pre-commit is already in place (.pre-commit-config.yaml — ruff v0.16.4, ruff-format, trailing-whitespace, end-of-file-fixer, check-yaml, check-added-large-files).
Architecture
┌──────────────────────────────────────────────────────────────────────┐
│ MCP Gateway v3.1.0 │
├──────────────────────────────────────────────────────────────────────┤
│ CLI (click) │ Gateway (Starlette + uvicorn, CSP) │
│ - add remote/local │ - POST /mcp (JSON-RPC) [http] │
│ - remove/inspect/list │ - GET /mcp (SSE endpoint event) [sse] │
│ - refresh --auth │ - POST /mcp/messages (alias) [sse] │
│ - serve --host 127.0.0.1 │ - GET /health │
│ (local-first default) │ - GET /ready, /live, /metrics │
├──────────────────────────────────────────────────────────────────────┤
│ Code Mode (4 meta-tools) │ Starlark Sandbox │
│ - listToolFiles │ - Hermetic execution │
│ - readToolFile │ - Server injection │
│ - getToolDocs │ │
│ - executeToolCode │ │
├──────────────────────────────────────────────────────────────────────┤
│ Registry (única fuente) │ OAuth2 (RFC 7591, reutilizado) │
│ - servers/*.pyi = signatures │ - Dynamic registration │
│ - servers/*.json = config │ - PKCE + FileTokenStorage │
│ - last-write-wins, atómico │ - tokens/ no expuesto vía API │
└──────────────────────────────────────────────────────────────────────┘
│ │ │
┌────┴────┐ ┌────┴────┐ ┌────┴────┐
│ Server1 │ │ Server2 │ │ Server3 │
│ (remote)│ │ (local) │ │ (remote)│
└─────────┘ └─────────┘ └─────────┘Sin Node en runtime ni CI: sin UI ni assets vendoreados,
ruffúnico linter,uv_buildbackend.Release híbrido (ADR-007):
push tags v*→uv build+pypi-publish(GAv2.0.0tag manual, release interno no publicado) +workflow_run Tests completed→python-semantic-release@v10 (>=10.0.0, uv.lock 10.6.1)parafix/perfpatches auto (línea v2.0.1..v2.4.0 ya liberada así).concurrency: release,fetch-depth:0,[tool.semantic_release]syncpyproject.toml+__init__.py+uv.lock(3.1.0exacta).
License
MIT
This server cannot be deployed
Maintenance
Related MCP Connectors
MCP server for secureFlows: token-free URL builders and integration-linting tools for AI agents.
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
Governed MCP gateway: one endpoint for your tools, with credential custody and audit log.
Hosted MCP server with managed OAuth for 15+ toolkits: Google Workspace, Fitbit, Oura, Kalshi, etc.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceAggregates multiple MCP servers into a single endpoint, enabling LLM clients to access tools, resources, and prompts from various backends through one connection.4 npmMIT
- AlicenseBqualityCmaintenanceAggregates multiple MCP servers via a single interface with token optimization and multiple operating modes (Gateway, Meta, Proxy, Search, Code).78MIT
- AlicenseNot gradedqualityAmaintenanceCentralizes management and routing of multiple MCP servers, exposing them to AI clients via HTTP/SSE endpoints with configurable visibility, sharing, and smart routing.977 npmApache 2.0
- AlicenseNot gradedqualityBmaintenanceCentralized management and routing of multiple MCP servers via unified Streamable HTTP/SSE endpoints, with granular group visibility, smart routing, and authentication.977 npmApache 2.0