PMCP - Progressive MCP
<!-- mcp-name: io.github.ViperJuice/pmcp -->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_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/mcpSecurity
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 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.comArchitecture
┌─────────────────────────────────────────────────────────────┐
│ Claude Code │
│ Only connects to PMCP (single server in config) │
└────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PMCP │
│ • 26 meta-tools (catalog, invoke, tasks, config, etc.) │
│ • Progressive disclosure (compact c
…