Progressive disclosure for MCP - Minimal context bloat with on-demand tool discovery and dynamic server provisioning.
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.
PMCP acts as a single MCP server that Claude Code connects to. Instead of exposing all downstream tools, it provides:
- 11 stable meta-tools (not the 50+ underlying tools)
- Auto-starts essential servers (Playwright, Context7) with no configuration
- Dynamically provisions new servers on-demand from a manifest of 25+
- Progressive disclosure: Compact capability cards first, detailed schemas only on request
- Policy enforcement: Output size caps and optional secret redaction
# With uv (recommended)
uv pip install pmcp
# Or run directly without installing
uvx pmcp
# With pip
pip install pmcp
# With LLM-enhanced features (optional, see below)
uv pip install pmcp[llm]PMCP can use an LLM for smarter capability matching and summarization. Without an API key, it falls back to keyword matching and templates.
Features enabled with LLM:
| Feature | Without API Key | With API Key |
|---|---|---|
| Capability matching | Keyword-based | Semantic understanding |
| Tool summaries | Static templates | LLM-generated descriptions |
| Code snippets | Static examples | Dynamic, context-aware examples |
Setup:
- Get a free API key from Groq Console
- Add to your
.envfile:
# In your project's .env file (or ~/.env)
GROQ_API_KEY=gsk_your_groq_api_key_here- Install with LLM support:
uv pip install pmcp[llm]PMCP uses BAML with Groq's fast inference API for sub-second LLM responses. The LLM features are entirely optional - PMCP works fully without them.
Create/update ~/.claude/mcp.json:
{
"mcpServers": {
"gateway": {
"command": "pmcp",
"args": []
}
}
}That's it! PMCP auto-starts with Playwright and Context7 servers ready to use.
PMCP works with any MCP-compatible client. Below are configuration examples for popular clients.
Create ~/.codex/mcp.json (verify path in Codex documentation):
{
"mcpServers": {
"gateway": {
"command": "pmcp",
"args": []
}
}
}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.
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
┌─────────────────────────────────────────────────────────────┐
│ Claude Code │
│ Only connects to PMCP (single server in config) │
└────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ PMCP │
│ • 11 meta-tools (catalog, invoke, provision, etc.) │
│ • Progressive disclosure (compact cards → full schemas) │
│ • Policy enforcement (allow/deny lists) │
└────────────────────────────┬────────────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Auto-Start │ │ Manifest │ │ Custom Servers │
│ (Playwright, │ │ (25+ servers │ │ (your own MCP │
│ Context7) │ │ on-demand) │ │ servers) │
└───────────────┘ └─────────────────┘ └─────────────────┘
Key principle: Users configure ONLY pmcp in Claude Code.
The gateway discovers and manages all other servers.
- No context bloat - Claude sees 11 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
The gateway exposes 11 meta-tools organized into three categories:
| Tool | Purpose |
|---|---|
gateway.catalog_search |
Search available tools, returns compact capability cards |
gateway.describe |
Get detailed schema for a specific tool |
gateway.invoke |
Call a downstream tool with argument validation |
gateway.refresh |
Reload backend configs and reconnect |
gateway.health |
Get gateway and server health status |
| 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.provision_status |
Check installation progress |
| Tool | Purpose |
|---|---|
gateway.list_pending |
List pending tool invocations with health status |
gateway.cancel |
Cancel a pending tool invocation |
PMCP follows a progressive disclosure pattern - start with natural language, get recommendations, drill down as needed.
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"
}gateway.catalog_search({ query: "documentation" })
gateway.describe({ tool_id: "context7::get-library-docs" })
gateway.invoke({
tool_id: "context7::get-library-docs",
arguments: { libraryId: "/npm/react/19.0.0" }
})
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 refreshNote: Cached tools show metadata only. Full schemas are available after the server starts (use gateway.describe to trigger lazy start).
PMCP can install and start MCP servers on-demand from a curated manifest of 25+ servers.
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"
}]
}# 1. Set API key (if required)
export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_...
# 2. Provision via gateway
gateway.provision({ server_name: "github" })These servers start automatically (no configuration required):
| 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) |
The manifest includes 25+ servers that can be provisioned on-demand:
| 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 |
| 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 |
See .env.example for all supported environment variables.
PMCP includes built-in guidance to encourage models to use code execution patterns, reducing context bloat and improving workflow efficiency.
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-executionresource
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 guideLevels:
minimal(default): L0 + L1 (~200 tokens overhead)standard: L0 + L1 + L2 (~320 tokens overhead)off: No guidance
pmcp guidance # Show configuration
pmcp guidance --show-budget # Show token estimates- Minimal mode: ~200 tokens typical workflow (L0 + search)
- Standard mode: ~320 tokens (L0 + search + 1 describe)
- 80% reduction vs loading all tool schemas upfront!
PMCP discovers MCP servers from:
- Project config:
.mcp.jsonin project root (highest priority) - User config:
~/.mcp.jsonor~/.claude/.mcp.json - Custom config: Via
--configflag orPMCP_CONFIGenv var
For MCP servers not in the manifest, add them to ~/.mcp.json:
{
"mcpServers": {
"my-custom-server": {
"command": "node",
"args": ["./my-server.js"],
"env": {
"API_KEY": "..."
}
}
}
}Important: Don't add pmcp itself to this file. PMCP is configured
in Claude Code's config (~/.claude/mcp.json), not in the downstream server list.
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\"']+)"# Start the gateway server (default)
pmcp
# Check server status
pmcp status
pmcp status --json # JSON output
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 initBy 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
pmcpPer-project lock (not recommended):
pmcp --lock-dir ./.mcp-gateway# 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# 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# 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 -vpmcp/
├── 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 (25+ servers)
│ │ ├── loader.py # Manifest loading
│ │ ├── installer.py # Server provisioning
│ │ └── environment.py # Platform/CLI detection
│ └── baml_client/ # BAML-generated LLM client (optional)
├── tests/ # 310+ tests
├── Dockerfile
├── docker-compose.yml
├── .env.example
├── pyproject.toml
└── README.md
pmcp status
pmcp logs --level debug
pmcp refresh --force# Check which key is needed
pmcp status --server github
# Set the key
export GITHUB_PERSONAL_ACCESS_TOKEN=ghp_...gateway.catalog_search({ query: "tool-name" })
gateway.describe({ tool_id: "server::tool-name" })
gateway.list_pending()
MIT