Skip to main content

PMCP - Progressive MCP: Minimal context bloat with on-demand tool discovery

Project description

PMCP - Progressive MCP

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 three-tier pure-Python router: explicit name match → category match → 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, require a bearer token:

# Start with auth token
pmcp --transport http --auth-token mysecrettoken

# Or via environment variable
PMCP_AUTH_TOKEN=mysecrettoken pmcp --transport http

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.

Assumptions and trust model:

  • PMCP binds to 127.0.0.1 by default — not safe to expose publicly without --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]
ExecStart=/usr/local/bin/pmcp --transport http --auth-token %i
Restart=on-failure

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

Or with nohup:

nohup pmcp --transport http --auth-token "$PMCP_AUTH_TOKEN" >> ~/.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?

  1. No context bloat - Claude sees 26 tools, not 50+
  2. No restarts - Provision new servers without restarting Claude Code
  3. Consistent interface - All tools accessed via gateway.invoke
  4. 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.

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
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 with CLI preference
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 public MCP Registry for external servers
gateway.register_discovered_server Register a registry result for provisioning

Monitoring Tools

Tool Purpose
gateway.list_pending List pending tool invocations with health status
gateway.cancel Cancel a pending tool invocation
gateway.tasks_list List brokered downstream MCP tasks by opaque task ID
gateway.tasks_get Get current status for one downstream MCP task
gateway.tasks_result Fetch and process a downstream MCP task result
gateway.tasks_cancel Cancel a downstream MCP task

gateway.refresh is intentionally conservative in shared-service mode. If a downstream request or active MCP task is in flight, refresh returns ok=false without disconnecting or reconnecting servers. Use gateway.list_pending to inspect active PMCP request IDs and gateway.tasks_list to inspect downstream MCP task IDs, then retry with force=true only when cancelling that work is acceptable.

gateway.disconnect_server and gateway.restart_server follow the same shared-service disruption policy for the target server: they refuse while that server has pending requests or active MCP tasks unless force=true. With force=true, only pending requests and active tasks for the named server are cancelled. These controls are runtime-only; they free local resources and update live gateway state, but they do not edit .mcp.json, remove server definitions, or change autoStart. In HTTP shared service mode, stopping or restarting a downstream server can affect other clients using the same PMCP gateway.

MCP task IDs are downstream server identifiers and remain distinct from PMCP pending request IDs such as server::local_id. Use gateway.cancel only for PMCP request IDs from gateway.list_pending; use gateway.tasks_cancel for MCP task IDs. Task records are transient in-memory gateway state. PMCP can bind visibility to the server and requestor context it observes, but unauthenticated local transports cannot provide cross-user authorization isolation.

Auth And Elicitation

PMCP reports downstream authorization as structured, non-secret state. Gateway outputs and health rows may include auth_state values of none, missing_auth, insufficient_scope, elicitation_required, policy_denied, or unknown, plus optional next_step, auth_methods, scope names, sanitized metadata URLs, and URL-mode elicitation summaries.

Supported flows:

  • Local API-key servers continue to use env-store credentials. When gateway.provision reports auth_state="missing_auth" and auth_mode="api_key", call gateway.auth_connect with a credential and PMCP stores it in the selected user or project env file. User scope writes ~/.config/pmcp/pmcp.env; project scope writes <project>/.env.pmcp. Project scope is useful for local development and CI workspaces, while user scope is better for credentials that should follow one operator across projects.
  • Remote bearer headers use env placeholders such as Authorization: Bearer ${REMOTE_API_TOKEN}. PMCP resolves placeholders from process env, project env-store, and user env-store values, but status, doctor, health, and feedback output only show required or missing env var names, not the resolved header value.
  • Remote authorization discovery is diagnostic-only. PMCP can preserve and report OAuth Protected Resource Metadata, Authorization Server Metadata, OpenID Connect discovery, Client ID Metadata Document URLs, and declared scopes when a server or WWW-Authenticate challenge provides them.
  • URL-mode elicitation is out of band. PMCP returns a sanitized URL and elicitation_id; complete that URL flow outside PMCP, then acknowledge it with gateway.auth_connect(auth_mode="url_elicitation", elicitation_id=..., consent_acknowledged=true).

PMCP is not an authorization server and does not implement enterprise SSO, Cross-App Access, DPoP, workload identity federation, or third-party refresh token storage. Do not paste OAuth codes or third-party credentials into URL-mode gateway calls.

Subordinate MCP Updates

  • gateway.update_server is the phase-1 update path for subordinate MCPs.
  • pmcp update <server> and pmcp update --all call the same gateway update workflow.
  • gateway.describe, gateway.invoke, and gateway.provision may return update_warning when a newer package version is detected.
  • Background stale-version indexing is active — warnings are zero-latency via hourly pre-population.

Feedback Telemetry

  • PMCP can emit failure feedback hints and generate GitHub issue payload previews for agents.
  • Telemetry is technical-only and warns before submission; payloads include PMCP/tool context.
  • Disable permanently with pmcp guidance --telemetry off.

Progressive Disclosure Workflow

PMCP follows a progressive disclosure pattern - start with natural language, get recommendations, drill down as needed.

Step 1: Request a Capability

You: "I need to look up library documentation"

gateway.request_capability({ query: "library documentation" })

Returns:

{
  "status": "candidates",
  "candidates": [{
    "name": "context7",
    "candidate_type": "server",
    "relevance_score": 0.95,
    "is_running": true,
    "reasoning": "Context7 provides up-to-date documentation for any package"
  }],
  "recommendation": "Use context7 - already running"
}

Step 2: Search Available Tools

gateway.catalog_search({ query: "documentation" })

Step 3: Get Tool Details

gateway.describe({ tool_id: "context7::get-library-docs" })

Step 4: Invoke the Tool

gateway.invoke({
  tool_id: "context7::get-library-docs",
  arguments: { libraryId: "/npm/react/19.0.0" }
})

Offline Tool Discovery

When using gateway.catalog_search, you can discover tools from servers that haven't started yet:

// Search all tools including offline/lazy servers
gateway.catalog_search({
  "query": "browser",
  "include_offline": true
})

This uses pre-cached tool descriptions from .mcp-gateway/descriptions.yaml. To refresh the cache:

pmcp refresh

Note: Cached tools show metadata only. Full schemas are available after the server starts (use gateway.describe to trigger lazy start).

Dynamic Server Provisioning

PMCP can install and start MCP servers on-demand from a curated manifest of 90+ servers.

Example: Adding GitHub Support

You: "I need to manage GitHub issues"

gateway.request_capability({ query: "github issues" })

Returns (if not already configured):

{
  "status": "candidates",
  "candidates": [{
    "name": "github",
    "candidate_type": "server",
    "is_running": false,
    "requires_api_key": true,
    "env_var": "GITHUB_PERSONAL_ACCESS_TOKEN",
    "env_instructions": "Create at https://github.com/settings/tokens with repo scope"
  }]
}

Provisioning

# 1. Set API key (if required)
export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_...

# 2. Provision via gateway
gateway.provision({ server_name: "github" })

Optional Eager Startup

Packaged manifest servers do not start automatically. They are lazy by default: PMCP can discover or provision them from the manifest, then connect on first use.

To eagerly start a server every time PMCP starts, list it in top-level autoStart:

{
  "autoStart": ["playwright", "context7"],
  "mcpServers": {}
}

Common opt-in choices:

Server Description API Key
playwright Browser automation - navigation, screenshots, DOM inspection Not required
context7 Library documentation lookup - up-to-date docs for any package Optional (for higher rate limits)

Startup policy decisions are visible through gateway.health and live pmcp status --verbose. Health rows keep the existing name, status, tool_count, and error fields, and may also include:

Field Meaning
startup_policy eager, lazy, skipped, or unknown
startup_source Resolver source such as project, user, manifest, configured, or auto_start
startup_skip_reason Machine-readable skip reason such as policy_denied, missing_auth, or unknown_auto_start
startup_env_var Required environment variable name for missing-auth skips
auth_state Machine-readable downstream auth state such as missing_auth, insufficient_scope, elicitation_required, or policy_denied
next_step Non-secret suggested next action when an auth state needs operator action

For persistent administration, use the config tools:

gateway.config_status({})
gateway.get_startup_policy({})
gateway.set_startup_policy({
  "operation": "add",
  "names": ["playwright"],
  "source": "project"
})

gateway.set_startup_policy is preview-only by default. To write, select exactly one source or path and pass both "apply": true and "dry_run": false. The writer updates only top-level autoStart, preserves unrelated .mcp.json keys and server definitions, writes atomically, and returns a refresh next step instead of silently reconnecting servers. Diagnostics report stale autoStart, legacy disableAutoStart conflicts, policy-denied rows, and missing-auth rows without printing secret values.

PMCP negotiates the current MCP protocol version with downstream servers and continues to connect to older supported servers. The local conformance matrix covers negotiated status handling for 2024-11-05, 2025-03-26, 2025-06-18, and 2025-11-25, with 2025-11-25 preferred for new initialization attempts. gateway.health and pmcp status --json can include the negotiated protocol_version and declared server capabilities when a connected server reports them.

Modern MCP task support is conservative. PMCP forwards task-augmented tool calls only when a tool advertises execution.taskSupport and the downstream server advertises task capability. Required-task tools fail before dispatch if the server does not advertise task support. Task records are transient gateway state, not durable PMCP storage.

Gateway observability is local and structured. gateway.invoke accepts trace context through _meta.traceparent, _meta.tracestate, and _meta.baggage and preserves those string values on PMCP-owned downstream request metadata. The same keys are tolerated on HTTP requests. PMCP does not require or configure an OpenTelemetry exporter.

gateway.health may include gateway_diagnostics and recent audit_events. Diagnostics report transport/header compatibility, trace support, audit buffer readiness, auth metadata presence, and rate-limit configuration without secret values. Audit events are bounded in memory and include method/action, server or tool identity, protocol version when known, task ID when present, outcome, latency, auth state, and redacted error text.

PMCP's Streamable HTTP endpoint remains compatible with existing clients that send no draft headers. It also tolerates MCP-Protocol-Version, Mcp-Method, and Mcp-Name request headers for clients experimenting with draft MCP transport conventions. These headers are compatibility inputs, not a promise that PMCP implements every draft MCP extension.

Servers stopped with gateway.disconnect_server remain visible in health as offline or lazy when PMCP still knows their configuration, and startup policy observation fields are preserved.

Example missing-auth health row:

{
  "name": "github",
  "status": "offline",
  "tool_count": 0,
  "startup_policy": "skipped",
  "startup_source": "manifest",
  "startup_skip_reason": "missing_auth",
  "startup_env_var": "GITHUB_PERSONAL_ACCESS_TOKEN"
}

Available Servers

The manifest includes 90+ servers that can be provisioned on-demand:

No API Key Required

Server Description
filesystem File operations - read, write, search
memory Persistent knowledge graph
fetch HTTP requests with robots.txt compliance
sequential-thinking Problem solving through thought sequences
git Git operations via MCP
sqlite SQLite database operations
time Timezone operations
puppeteer Headless Chrome automation

Requires API Key

Server Description Environment Variable
github GitHub API - issues, PRs, repos GITHUB_PERSONAL_ACCESS_TOKEN
gitlab GitLab API - projects, MRs GITLAB_PERSONAL_ACCESS_TOKEN
slack Slack messaging SLACK_BOT_TOKEN
notion Notion workspace NOTION_TOKEN
linear Linear issue tracking LINEAR_API_KEY
postgres PostgreSQL database POSTGRES_URL
brave-search Web search BRAVE_API_KEY
google-drive Google Drive files GDRIVE_CREDENTIALS
sentry Error tracking SENTRY_AUTH_TOKEN
stripe Payments and billing STRIPE_SECRET_KEY
github-actions CI/CD workflows GITHUB_PERSONAL_ACCESS_TOKEN
datadog Monitoring and observability DATADOG_API_KEY
cloudflare Edge network and Workers CLOUDFLARE_API_TOKEN
figma Design files and components FIGMA_ACCESS_TOKEN
jira Issue tracking JIRA_API_TOKEN
airtable Spreadsheet database AIRTABLE_TOKEN
hubspot CRM and marketing HUBSPOT_ACCESS_TOKEN
twilio SMS and voice TWILIO_ACCOUNT_SID
...and 80+ more Use gateway.catalog_search to explore

See .env.example for all supported environment variables.

Code Execution Guidance

PMCP includes built-in guidance to encourage models to use code execution patterns, reducing context bloat and improving workflow efficiency.

Guidance Layers

L0 (MCP Instructions): Brief philosophy in server instructions (~30 tokens)

  • "Write code to orchestrate tools - use loops, filters, conditionals"

L1 (Code Hints): Ultra-terse hints in search results (~8-12 tokens/card)

  • Single-word hints: "loop", "filter", "try/catch", "poll"

L2 (Code Snippets): Minimal examples in describe output (~40-80 tokens, opt-in)

  • 3-4 line code examples showing practical usage

L3 (Methodology Resource): Full guide (lazy-loaded, 0 tokens)

  • Accessible via pmcp://guidance/code-execution resource

Guidance Configuration

Create ~/.claude/gateway-guidance.yaml:

guidance:
  level: "minimal"  # Options: "off", "minimal", "standard"

  layers:
    mcp_instructions: true   # L0 philosophy
    code_hints: true         # L1 hints
    code_snippets: false     # L2 examples (default: off)
    methodology_resource: true  # L3 guide

Levels:

  • minimal (default): L0 + L1 (~200 tokens overhead)
  • standard: L0 + L1 + L2 (~320 tokens overhead)
  • off: No guidance

View Guidance Status

pmcp guidance                 # Show configuration
pmcp guidance --show-budget  # Show token estimates

Token Budget

  • Minimal mode: ~200 tokens typical workflow (L0 + search)
  • Standard mode: ~320 tokens (L0 + search + 1 describe)
  • 80% reduction vs loading all tool schemas upfront!

Configuration

Config Discovery

PMCP discovers MCP servers from:

  1. Project config: .mcp.json in project root (highest priority)
  2. User config: ~/.mcp.json or ~/.claude/.mcp.json
  3. Custom config: Via --config flag or PMCP_CONFIG env var

Adding Custom Servers

For MCP servers not in the manifest, add them to ~/.mcp.json:

{
  "autoStart": ["my-custom-server"],
  "mcpServers": {
    "my-custom-server": {
      "command": "node",
      "args": ["./my-server.js"],
      "env": {
        "API_KEY": "..."
      }
    }
  }
}

PMCP supports both local command-based and remote URL-based downstream entries from discovered config files. Entries in mcpServers make downstream servers available lazily/on demand; they do not by themselves mean the server should be eagerly started.

The top-level autoStart list controls explicit eager startup. Names can refer to servers defined in mcpServers or packaged manifest entries such as playwright and context7. Omit a server from autoStart to keep it lazy.

The legacy top-level disableAutoStart list remains supported for deployments that temporarily enable PMCP_LEGACY_MANIFEST_AUTOSTART=1, but packaged PMCP defaults no longer require it.

The same policy is available locally from the CLI:

pmcp config status --json
pmcp config startup-policy
pmcp config set-startup-policy add playwright --source project
pmcp config set-startup-policy add playwright --source project --apply

CLI mutation previews by default. --apply is required before writing.

Lazy Excalidraw example:

{
  "mcpServers": {
    "excalidraw": {
      "type": "http",
      "url": "https://mcp.excalidraw.com/mcp"
    }
  }
}

Eager Excalidraw example:

{
  "autoStart": ["excalidraw"],
  "mcpServers": {
    "excalidraw": {
      "type": "http",
      "url": "https://mcp.excalidraw.com/mcp"
    }
  }
}

Remote Downstream Servers

You can also configure downstream MCP servers over HTTP/SSE directly in .mcp.json using type: "sse" or type: "http" (or type: "remote" for generic remote transport):

{
  "mcpServers": {
    "acme-sse": {
      "type": "sse",
      "url": "https://mcp.acme.dev/sse",
      "headers": {
        "Authorization": "Bearer ${ACME_MCP_TOKEN}",
        "X-Tenant": "${ACME_TENANT_ID}"
      }
    },
    "acme-http": {
      "type": "http",
      "url": "https://mcp.acme.dev/mcp",
      "headers": {
        "Authorization": "Bearer ${ACME_MCP_TOKEN}"
      }
    }
  }
}
  • url should be the full remote endpoint for that server.
  • headers values support ${ENV_VAR} interpolation (Issue #40).
  • Resolve those environment variables from your shell environment or ~/.config/pmcp/pmcp.env.

Important: Don't add pmcp itself to this file. PMCP is configured in your MCP client config, not in the downstream server list.

Credential Scope Management (pmcp secrets)

PMCP stores secrets in environment files by scope:

  • user scope: ~/.config/pmcp/pmcp.env
  • project scope: <project_root>/.env.pmcp

You can manage both scopes with pmcp secrets:

# Store a secret in user scope (shared by all projects)
pmcp secrets set API_TOKEN your-token --scope user

# Store a secret in project scope
pmcp secrets set API_TOKEN your-token --scope project --project /path/to/project

# Copy all user-scoped secrets into project scope
pmcp secrets sync --from-scope user --to-scope project --overwrite

# Copy project-scoped secrets into user scope
pmcp secrets sync --from-scope project --to-scope user --overwrite

Use scope-appropriate values such as API_TOKEN and keep the values in the generated .env files; PMCP and downstream MCP servers read from these files according to your active mode.

For service users, ~/.config/pmcp/pmcp.env is ideal for shared tokens used by all sessions.

Policy File

Create a policy file to control access and limits:

~/.claude/gateway-policy.yaml:

servers:
  allowlist: []  # Empty = allow all
  denylist:
    - dangerous-server

tools:
  denylist:
    - "*::delete_*"
    - "*::drop_*"

limits:
  max_tools_per_server: 100
  max_output_bytes: 50000
  max_output_tokens: 4000

redaction:
  patterns:
    - "(api[_-]?key)[\\s]*[:=][\\s]*[\"']?([^\\s\"']+)"
    - "(password|secret)[\\s]*[:=][\\s]*[\"']?([^\\s\"']+)"

CLI Commands

# Start the gateway server (default)
pmcp

# Check server status
pmcp status
pmcp status --json              # JSON output
pmcp status --verbose           # Include startup policy details when available
pmcp status --server playwright # Filter by server

# View logs
pmcp logs
pmcp logs --follow              # Live tail
pmcp logs --tail 100            # Last 100 lines

# Refresh server connections
pmcp refresh
pmcp refresh --server github    # Refresh specific server
pmcp refresh --force            # Force reconnect all

# Initialize config (interactive)
pmcp init

# Render client setup snippets
pmcp setup
pmcp setup --client claude --mode stdio
pmcp setup --client opencode --mode http --write

# Run diagnostics for lock/mode/http checks
pmcp doctor
pmcp doctor --project /path/to/project

# Manage project/user secrets
pmcp secrets set API_TOKEN my-token --scope user
pmcp secrets sync --from-scope user --to-scope project --overwrite

pmcp doctor (Recommended before/after upgrades)

Use pmcp doctor to diagnose common PMCP startup and connectivity issues. It checks:

  • lock: detects singleton lock state and stale lock collisions at ~/.pmcp/gateway.lock
  • mode: detects local command-mode MCP config conflicts when a shared PMCP system service is running
  • http: probes the unauthenticated /health endpoint derived from PMCP_GATEWAY_URL or http://127.0.0.1:3344/mcp
  • remote: detects unresolved remote downstream header environment references
  • install: detects conflicting uv tool and pip --user installs

Example:

pmcp doctor

If any checks fail, follow the command in the output and rerun pmcp doctor.

Singleton Lock

By default, PMCP uses a global lock at ~/.pmcp/gateway.lock to ensure only one gateway runs per user. This prevents multiple gateway instances from spawning duplicate downstream servers.

Override the lock directory:

# CLI flag
pmcp --lock-dir /custom/path

# Environment variable
export PMCP_LOCK_DIR=/custom/path
pmcp

Per-project lock (not recommended):

pmcp --lock-dir ./.mcp-gateway

Deprecations

  • mcp-gateway command naming is deprecated in documentation and examples.
  • Use pmcp for all CLI commands going forward.
  • Migration examples:
    • mcp-gateway refresh --force -> pmcp refresh --force
    • mcp-gateway status --json -> pmcp status --json

Docker

# Using Docker
docker run -it --rm \
  -v ~/.mcp.json:/home/appuser/.mcp.json:ro \
  -v ~/.env:/app/.env:ro \
  ghcr.io/viperjuice/pmcp:latest

# Using Docker Compose
docker-compose up -d

Development

# Clone the repo
git clone https://github.com/ViperJuice/pmcp
cd pmcp

# Install with uv (recommended)
uv sync --all-extras

# Run tests
uv run pytest

# Run with debug logging
uv run pmcp --debug

Running Tests

# Run all tests
uv run pytest

# Run with coverage
uv run pytest --cov=pmcp

# Run specific test file
uv run pytest tests/test_policy.py -v

Project Structure

pmcp/
├── src/pmcp/
│   ├── __init__.py
│   ├── __main__.py           # python -m pmcp entry
│   ├── cli.py                # CLI commands (status, logs, init, refresh)
│   ├── server.py             # MCP server implementation
│   ├── config/
│   │   └── loader.py         # Config discovery (.mcp.json)
│   ├── client/
│   │   └── manager.py        # Downstream server connections
│   ├── policy/
│   │   └── policy.py         # Allow/deny lists
│   ├── tools/
│   │   └── handlers.py       # Gateway tool implementations
│   ├── manifest/
│   │   ├── manifest.yaml     # Server manifest (90+ servers)
│   │   ├── loader.py         # Manifest loading
│   │   ├── installer.py      # Server provisioning
│   │   └── environment.py    # Platform/CLI detection
│   └── baml_client/          # BAML-generated client (used for structured parsing; no outbound LLM calls since v1.8.0)
├── tests/                    # 310+ tests
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── pyproject.toml
└── README.md

Troubleshooting

Server Won't Connect

pmcp status
pmcp logs --level debug
pmcp refresh --force

Missing API Key

# Check which key is needed
pmcp status --server github

# Set the key
export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_...

Tool Invocation Fails

gateway.catalog_search({ query: "tool-name" })
gateway.describe({ tool_id: "server::tool-name" })
gateway.list_pending()

If gateway.refresh reports pending requests or active MCP tasks, inspect them with gateway.list_pending() and gateway.tasks_list(), or retry refresh with force=true to cancel them before reloading server configuration.

If gateway.disconnect_server or gateway.restart_server reports pending requests or active MCP tasks, inspect gateway.list_pending(server="<name>") and gateway.tasks_list(server_name="<name>"), or retry with force=true to cancel only that server's pending work.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pmcp-1.12.0.tar.gz (571.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pmcp-1.12.0-py3-none-any.whl (202.1 kB view details)

Uploaded Python 3

File details

Details for the file pmcp-1.12.0.tar.gz.

File metadata

  • Download URL: pmcp-1.12.0.tar.gz
  • Upload date:
  • Size: 571.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pmcp-1.12.0.tar.gz
Algorithm Hash digest
SHA256 338324f59189d828dd9516e3847a8bee831ec14aebc913ece344a1f633ab943f
MD5 6a7d0cc78f006dc5e80b0860cbe1c744
BLAKE2b-256 b793672337cfac8d649f782b9c7a0aab430dd5dd4f8653525a0192d210657fda

See more details on using hashes here.

Provenance

The following attestation bundles were made for pmcp-1.12.0.tar.gz:

Publisher: release.yml on ViperJuice/pmcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pmcp-1.12.0-py3-none-any.whl.

File metadata

  • Download URL: pmcp-1.12.0-py3-none-any.whl
  • Upload date:
  • Size: 202.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pmcp-1.12.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d65afdcfba0a91900556fb4914b6d61892238eb056e25b3b9573411909bb65e5
MD5 23e835b508952de1e28086a72af45cdb
BLAKE2b-256 0042b6306a75b1d3c16df9fdf1d7eb32900ee744d171f473c14cdebfc77187ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for pmcp-1.12.0-py3-none-any.whl:

Publisher: release.yml on ViperJuice/pmcp

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page