Install
$ agentstack add mcp-consiliency-pmcp ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
PMCP - Progressive MCP
[](https://pypi.org/project/pmcp/) [](LICENSE)
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), 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/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):
| 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 without
PMCP_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.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 cards → full schemas) │
│ • Policy enforcement (allow/deny lists) │
└────────────────────────────┬────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Explicit │ │ Manifest │ │ Custom Servers │
│ autoStart │ │ (90+ servers │ │ (your own MCP │
│ servers │ │ on-demand) │ │ servers) │
└───────────────┘ └─────────────────┘ └─────────────────┘
Key principle: Users configure ONLY pmcp in Claude Code. The gateway discovers and manages all other servers.
Why Single-Gateway?
- No context bloat - Claude sees 26 tools, not 50+
- No restarts - Provision new servers without restarting Claude Code
- Consistent interface - All tools accessed via
gateway.invoke - Policy control - Centralized allow/deny rules
Gateway Tools
The gateway exposes 26 meta-tools organized into four categories:
Tool annotations are preserved as untrusted hints only; policy and safety notes continue to use PMCP's own risk model. When a tool schema omits $schema, PMCP reports the JSON Schema dialect as https://json-schema.org/draft/2020-12/schema. See [SPECCOMPLIANCE.md](SPECCOMPLIANCE.md) for the current MCP specification compliance matrix and next-revision tracking checklist.
Core Tools
| Tool | Purpose | |------|---------| | gateway.catalog_search | Search available tools, returns compact capability cards with small metadata such as title, icons, execution hints, and schema dialect, plus additive compact CLI hints, registry candidates, and (with include_offline=true) manifest provision candidates (manifest_candidates) carrying provisionable/provision_tool/requires_api_key/api_key_available/env_var so an agent can provision the exact server | | gateway.describe | Get detailed schema and richer metadata for a specific tool, including output schema, annotations, execution/task support, icons, and schema dialect | | gateway.invoke | Call a downstream tool with argument validation, including task-augmented execution for task-capable tools | | gateway.refresh | Reload backend configs and reconnect; refuses while requests or active MCP tasks are pending unless force=true | | gateway.health | Get gateway and server health status | | gateway.config_status | Read effective config and startup/auth status with source attribution | | gateway.get_startup_policy | Read persisted autoStart and legacy disableAutoStart entries by source | | gateway.set_startup_policy | Preview or explicitly apply autoStart add/remove/set operations against one selected source |
Lifecycle Tools
| Tool | Purpose | |------|---------| | gateway.connect_server | Connect or start a known configured, manifest/provisioned, or registered discovered server | | gateway.disconnect_server | Runtime-stop a server without editing .mcp.json or changing autoStart | | gateway.restart_server | Runtime-stop then reconnect a server without changing persistent config |
Capability Discovery Tools
| Tool | Purpose | |------|---------| | gateway.request_capability | Natural language capability matching that can return direct CLI guidance or MCP server candidates | | gateway.sync_environment | Detect platform and available CLIs | | gateway.provision | Install and start MCP servers on-demand | | gateway.update_server | Update an MCP server package and reconnect it | | gateway.auth_connect | Store API-key credentials or acknowledge URL-mode elicitation and retry provisioning | | gateway.submit_feedback | Preview/submit technical PMCP feedback issues to GitHub | | gateway.provision_status | Check installation progress | | gateway.search_registry | Search the cached public MCP Registry metadata for external servers | | gateway.register_discovered_server | Register a registry result for provisioning |
Monitoring Tools
| Tool | Purpose | |------|---------| | gateway.list_pending | List pending tool invoca
…
Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Consiliency
- Source: Consiliency/pmcp
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.