PMCP - Progressive MCP
<!-- mcp-name: io.github.ViperJuice/pmcp -->A gateway that lets your AI coding assistant use dozens of external tools without loading them all up front.
PMCP sits between your assistant (Claude Code, Codex, and others) and the services it can plug into — GitHub, Jira, databases, 90+ more. Instead of loading every tool's full schema before you've asked a question, it offers a short menu first and fetches the detailed schema only when a tool is actually used.
Why it exists
Assistants reach external services through MCP (Model Context Protocol) — a common interface for an AI to talk to other software. When an assistant connects to a dozen MCP servers directly, it loads all of their tool definitions into context at once.
Context is limited and metered: every tool definition costs tokens and crowds out room for the actual work. Loading 50+ tools you may never call is slow and expensive, and adding a new service usually means restarting the assistant. Anthropic has highlighted context bloat as a key challenge with MCP tooling.
PMCP is the single connection point that keeps this compact and on-demand.
Who it's for
Developers and teams running AI coding assistants with many connected tools — especially anyone hitting "too many tools" or context-full limits.
What you get
- Lower token cost — the assistant sees a compact capability card, not 50+ full schemas, before doing real work.
- Load tools only when needed — downstream servers stay dormant until first use ("lazy by default"), and eager-start only when listed in
autoStart. - Add tools without restarting — provision a new server on demand from a manifest of 90+.
- One stable connection — your assistant sees ~26 steady meta-tools instead of a shifting set of many.
- Guardrails built in — output size caps and optional secret redaction.
- Works out of the box — core capability matching needs no API key.
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_capabilityuses 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 HTTPNamed 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 ciWrite directly into your client config with --write:
pmcp setup --client claude --mode http --writeWithout --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), andgateway.restart_server(force=true)can cancel or interrupt downstream work started by another client using the same gateway.gateway.healthand livepmcp status --verboseshow startup policy observations for downstream servers without exposing secret values.--rate-limit/PMCP_RATE_LIMITapplies 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/health/mcp is POST-only as of 2.0.0 — a bare curl against it returns
405 Method Not Allowed with Allow: POST, DELETE, so use /health for a
liveness check.
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 httpAvoid 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):
| Flag | Env var | Purpose |
|---|---|---|
--auth-mode {none,shared-secret,resource-server} | PMCP_AUTH_MODE | Select the HTTP auth mode. When unset, PMCP infers shared-secret if a token is present, otherwise none. |
--oauth-issuer | PMCP_OAUTH_ISSUER | Authorization Server issuer (resource-server mode). |
--oauth-jwks-url | PMCP_OAUTH_JWKS_URL | Public https JWKS URL (resource-server mode). |
--oauth-audience | PMCP_OAUTH_AUDIENCE | Canonical 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.1by default — not safe to expose publicly withoutPMCP_AUTH_TOKEN. - Config files (
.mcp.json) are trusted inputs — treat them like code; do not load untrusted configs. - Secrets in
.envfiles are passed to child MCP server processes; protect the.envfile 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.targetsystemctl --user enable --now pmcpOr 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 o
…