Back to MCP Servers

Gateway

A meta-server for minimal Claude Code tool bloat with progressive disclosure and dynamic server provisioning. Exposes 9 stable meta-tools, auto-starts Playwright and Context7, and can dynamically provision 25+ MCP servers on-demand from a curated manifest.

aggregators
By ViperJuice
144Updated 3 days agoPythonMIT

Installation

npx -y mcp-gateway

Configuration

{
  "mcpServers": {
    "mcp-gateway": {
      "command": "npx",
      "args": ["-y", "mcp-gateway"]
    }
  }
}

How to use

  1. Run the installation command above (if needed)
  2. Open your Claude Code settings file (~/.claude/settings.json)
  3. Add the configuration to the mcpServers section
  4. Restart Claude Code to apply changes

PMCP - Progressive MCP

<!-- mcp-name: io.github.ViperJuice/pmcp -->

PyPI version License: MIT

Progressive disclosure for MCP - Minimal context bloat with on-demand tool discovery and dynamic server provisioning.

The Problem

When Claude Code connects directly to multiple MCP servers (GitHub, Jira, DB, etc.), it loads all tool schemas into context. This causes:

  • Context bloat: Dozens of tool definitions consume tokens before you even ask a question
  • Static configuration: Requires Claude Code restart to see new servers
  • No progressive disclosure: Full schemas shown even when not needed

Anthropic has highlighted context bloat as a key challenge with MCP tooling.

The Solution

PMCP acts as a single MCP server that Claude Code connects to. Instead of exposing all downstream tools, it provides:

  • 26 stable meta-tools (not the 50+ underlying tools)
  • Lazy by default: downstream servers are available on demand and only eager-start when listed in autoStart
  • Dynamically provisions new servers on-demand from a manifest of 90+
  • Progressive disclosure: Compact capability cards first, detailed schemas only on request
  • Policy enforcement: Output size caps and optional secret redaction

Quick Start

Installation

# With uv (recommended)
uv pip install pmcp

# Or run directly without installing
uvx pmcp

# With pip
pip install pmcp

Capability matching is built-in — no API key needed. gateway.request_capability uses a pure-Python matcher that can return direct CLI guidance for installed native tools, MCP server candidates, or registry search guidance.

Configure with pmcp setup

PMCP includes a wizard-style helper that can render ready-to-use MCP client config for Claude and OpenCode. The generated config only connects your client to the PMCP gateway. Downstream MCP servers stay lazy until first use unless you add them to autoStart in your .mcp.json.

Use pmcp setup to print the generated config:

pmcp setup --client claude --mode stdio    # Claude local stdio
pmcp setup --client claude --mode http     # Claude shared-service HTTP
pmcp setup --client opencode --mode stdio  # OpenCode local stdio
pmcp setup --client opencode --mode http   # OpenCode shared-service HTTP

Named profiles cover the common modes:

pmcp setup --profile local-stdio
pmcp setup --profile shared-local-http
pmcp setup --profile authenticated-shared-http
pmcp setup --profile ci

Write directly into your client config with --write:

pmcp setup --client claude --mode http --write

Without --write, pmcp setup prints the config so you can paste it into:

  • Claude: ~/.mcp.json
  • OpenCode: ~/.config/opencode/opencode.json

Use shared-service HTTP mode when running one PMCP service for multiple sessions or clients. Use single-process stdio mode for local testing.

Shared Service Mode (Manual)

If you prefer manual config, point each client to the shared HTTP endpoint:

{
  "mcpServers": {
    "pmcp": {
      "type": "http",
      "url": "http://127.0.0.1:3344/mcp"
    }
  }
}

Why this mode: PMCP uses a singleton lock (~/.pmcp/gateway.lock), so multiple local launches can conflict. One shared service avoids lock collisions and keeps tool state consistent.

Shared gateway state:

  • All clients connected to one PMCP HTTP gateway share downstream server connections, pending requests, provisioned tools, and live lifecycle state.
  • gateway.refresh(force=true), gateway.disconnect_server(force=true), and gateway.restart_server(force=true) can cancel or interrupt downstream work started by another client using the same gateway.
  • gateway.health and live pmcp status --verbose show startup policy observations for downstream servers without exposing secret values.
  • --rate-limit / PMCP_RATE_LIMIT applies per observed source IP on /mcp; localhost clients and reverse-proxied clients can share one bucket unless the proxy preserves distinct client IPs.

Quick verification:

systemctl --user is-active pmcp
curl -sS http://127.0.0.1:3344/mcp

Security

HTTP transport is unauthenticated by default. For any non-localhost exposure, choose an HTTP auth mode and terminate TLS in front of PMCP.

shared-secret mode is the backward-compatible single-tenant guard. It accepts one static bearer value on /mcp:

# Start with bearer auth from the environment
PMCP_AUTH_TOKEN=mysecrettoken pmcp --transport http

Avoid passing production tokens with --auth-token; command-line arguments can be visible in process listings on shared hosts.

Clients must then include Authorization: Bearer mysecrettoken on /mcp requests. /health and /metrics remain unauthenticated by design; protect them with firewall rules, IP allowlists, or reverse-proxy policy before any non-localhost exposure.

resource-server mode makes PMCP validate Authorization Server issued access tokens as an OAuth 2.1 Resource Server. Configure the HTTP app with a public issuer, JWKS URL, resource audience, required scopes, and exact allowed origins:

create_http_app(
    mcp_server,
    auth_mode="resource-server",
    resource_server_issuer="https://issuer.example",
    resource_server_jwks_url="https://issuer.example/.well-known/jwks.json",
    resource_server_audience="https://pmcp.example/mcp",
    resource_server_allowed_algorithms=("RS256", "ES256"),
    required_scopes=["pmcp.invoke"],
    allowed_origins=["https://app.example"],
)

PMCP validates token signature, issuer, expiry, not-before, and audience. The audience is bound to the configured resource_server_audience (the server's canonical resource URI, per RFC 8707); it is never derived from the request Host header. resource-server mode fails closed at startup if the issuer, JWKS URL, or audience is missing, and resource_server_jwks_url must be an https URL on a public host. Token signatures are only accepted for the operator-configured resource_server_allowed_algorithms allowlist (default RS256/ES256); the token's own alg header is never trusted. JWKS is fetched asynchronously and cached, so validation never blocks the event loop; an unreachable JWKS endpoint returns 503 while an invalid token returns 401. It rejects private, link-local, loopback, multicast, and unspecified hosts in public auth metadata URLs. PMCP is still not an Authorization Server and does not provide dynamic client registration, SSO, RBAC, billing, or a complete multi-tenant identity service.

Auth mode and OAuth resource-server parameters are configurable from the CLI or environment (CLI flags take precedence; env values are read only when the flag is unset):

FlagEnv varPurpose
--auth-mode {none,shared-secret,resource-server}PMCP_AUTH_MODESelect the HTTP auth mode. When unset, PMCP infers shared-secret if a token is present, otherwise none.
--oauth-issuerPMCP_OAUTH_ISSUERAuthorization Server issuer (resource-server mode).
--oauth-jwks-urlPMCP_OAUTH_JWKS_URLPublic https JWKS URL (resource-server mode).
--oauth-audiencePMCP_OAUTH_AUDIENCECanonical resource audience, RFC 8707 (resource-server mode).
--required-scope (repeatable)PMCP_REQUIRED_SCOPES (comma-separated)Scopes every token must present.
--allowed-origin (repeatable)PMCP_ALLOWED_ORIGINS (comma-separated)Browser Origins permitted on /mcp; also enables Host-header validation.

Origin and Host posture (DNS-rebinding defense). The Origin check runs by default in every auth mode, even when no --allowed-origin is configured: a request carrying a browser Origin header is rejected with 403 unless the origin is loopback, same-origin with the request Host, or explicitly allow-listed. Requests with no Origin header — the normal case for non-browser MCP clients — always pass. Configuring --allowed-origin (or PMCP_ALLOWED_ORIGINS) additionally turns on Host-header validation: the request Host must be loopback or one of the hosts derived from the configured origins and the gateway's own canonical resource host (--oauth-audience / protected-resource metadata URL); other Hosts get 403. Host validation stays off by default so that reverse-proxy deployments that forward an arbitrary public Host keep working; if you enable it behind a proxy, make sure your gateway's public hostname is reachable through the configured origins or audience so the proxied Host is accepted.

Assumptions and trust model:

  • PMCP binds to 127.0.0.1 by default — not safe to expose publicly without PMCP_AUTH_TOKEN.
  • Config files (.mcp.json) are trusted inputs — treat them like code; do not load untrusted configs.
  • Secrets in .env files are passed to child MCP server processes; protect the .env file with filesystem permissions.

Production background service (Linux systemd):

# ~/.config/systemd/user/pmcp.service
[Unit]
Description=PMCP MCP Gateway

[Service]
Environment=PMCP_AUTH_TOKEN=replace-with-secret-token
ExecStart=/usr/local/bin/pmcp --transport http
Restart=on-failure

[Install]
WantedBy=default.target
systemctl --user enable --now pmcp

Or with nohup:

PMCP_AUTH_TOKEN=replace-with-secret-token nohup pmcp --transport http >> ~/.pmcp/logs/gateway.log 2>&1 &

TLS / Reverse Proxy

PMCP's HTTP transport is plaintext. For any exposure beyond localhost, terminate TLS at a reverse proxy and forward to 127.0.0.1:3344. Keep --host 127.0.0.1 (the default) so PMCP only listens on the loopback interface.

Nginx (/etc/nginx/sites-available/pmcp):

server {
    listen 443 ssl;
    server_name pmcp.example.com;

    ssl_certificate     /etc/letsencrypt/live/pmcp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/pmcp.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3344;
        proxy_set_header Authorization $http_authorization;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
    }
}

Caddy (Caddyfile):

pmcp.example.com {
    reverse_proxy 127.0.0.1:3344
}

Caddy handles TLS automatically via Let's Encrypt.

Other MCP Clients

PMCP works with any MCP-compatible client. Below are configuration examples for popular clients.

Codex CLI

Create ~/.codex/mcp.json (verify path in Codex documentation):

{
  "mcpServers": {
    "gateway": {
      "command": "pmcp",
      "args": []
    }
  }
}

Gemini CLI

Create the appropriate config file (verify path in Gemini CLI documentation):

{
  "mcpServers": {
    "gateway": {
      "command": "pmcp",
      "args": []
    }
  }
}

Note: Configuration paths and formats vary by client. Verify the exact location and format in each client's official documentation.

Your First Interaction

You: "Take a screenshot of google.com"

Claude uses: gateway.invoke {
  tool_id: "playwright::browser_navigate",
  arguments: { url: "https://google.com" }
}
// Then: gateway.invoke { tool_id: "playwright::browser_screenshot" }

Returns: Screenshot of google.com

Architecture

┌─────────────────────────────────────────────────────────────┐
│                        Claude Code                          │
│  Only connects to PMCP (single server in config)            │
└────────────────────────────┬────────────────────────────────┘
                             │
                             ▼
┌─────────────────────────────────────────────────────────────┐
│                          PMCP                               │
│  • 26 meta-tools (catalog, invoke, tasks, config, etc.)     │
│  • Progressive disclosure (compact c

…
View source on GitHub