The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the MCP Gateway listing page.
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.
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.
Developers and teams running AI coding assistants with many connected tools — especially anyone hitting "too many tools" or context-full limits.
autoStart.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.
pmcp setupPMCP 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:
Named profiles cover the common modes:
Write directly into your client config with --write:
Without --write, pmcp setup prints the config so you can paste it into:
~/.mcp.json~/.config/opencode/opencode.jsonUse shared-service HTTP mode when running one PMCP service for multiple sessions or clients. Use single-process stdio mode for local testing.
If you prefer manual config, point each client to the shared HTTP endpoint:
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:
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:
/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.
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:
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:
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 and is rejected when its host is a non-public IP literal. 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.
In public auth metadata URLs it rejects hosts written as non-public IP
literals — private, CGNAT, link-local, loopback, multicast, site-local, and
unspecified — including IPv4 addresses embedded in IPv6 literals and legacy
numeric forms such as 2852039166. This is a filter on literals only: a DNS
name is accepted without being resolved, so a name that points at an
internal address still passes. PMCP therefore no longer presents such a URL as
one it checked: a server-supplied URL is relayed unverified and presented as
such — UrlElicitationInfo.url_verified, AuthMetadataInfo.verified_urls,
and AuthChallengeInfo.resource_metadata_url_verified all default to
unverified, and the caveat is carried in the next_step an agent follows and in
CLI output. Where PMCP fetches a URL itself it fails closed instead, requiring
a verified public literal
(#211). 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:
127.0.0.1 by default — not safe to expose publicly without
PMCP_AUTH_TOKEN..mcp.json) are trusted inputs — treat them like code; do not load untrusted configs..env files are passed to child MCP server processes; protect the .env file with filesystem permissions.Production background service (Linux systemd):
Or with nohup:
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):
Caddy (Caddyfile):
Caddy handles TLS automatically via Let's Encrypt.
PMCP works with any MCP-compatible client. Below are configuration examples for popular clients.
Create ~/.codex/mcp.json (verify path in Codex documentation):
Create the appropriate config file (verify path in Gemini CLI documentation):
Note: Configuration paths and formats vary by client. Verify the exact location and format in each client's official documentation.
Key principle: Users configure ONLY pmcp in Claude Code.
The gateway discovers and manages all other servers.
gateway.invokeThe 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 SPEC_COMPLIANCE.md for the current MCP specification
compliance matrix and next-revision tracking checklist.
| 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 |
| 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 |
| 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 |
| 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.
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:
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.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.WWW-Authenticate challenge provides them.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.
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.update_server; the gateway does not volunteer unprompted "update available" notices. It cannot observe which package version a running server is actually executing, so a volunteered notice could be wrong in either direction (Consiliency/pmcp#150).npx/npm server the gateway asks the host npm's own nopt, config definitions and npm-package-arg which package that command line would run, and refuses rather than guess when anything could redirect resolution — a cwd inside a node project, an npm_config_* variable in the server's env or the gateway's own, or any flag beyond --yes/--package. A refused server keeps working; it just loses auto-update and version reporting, and refreshes its cached descriptions every cycle. Where node is not installed the gateway falls back to its own flag tables, which is the pre-2.5.2 behaviour (Consiliency/pmcp#195).The environment across the update's probe window. gateway.update_server
probes for a new package version and then re-resolves the server config before
restarting anything. Two different kinds of environment change behave
differently across that window, and they are not one rule:
env — which includes every manifest credential, since a
manifest credential is resolved into the server config at load time. If it
changes while the probe is running, update_server returns ok=False and
does not restart: the package is fetched but not activated, and no version
is recorded. The guarantee is that the config restarted onto is the config
that was probed (Consiliency/pmcp#151). Your change is not lost — rotate a
credential mid-update and it applies on the next update, rather than to a
process that was probed with the old value.gateway.connect_server,
gateway.refresh and auto-reconnect: every spawn path reads the ambient
environment at spawn time.Freezing the ambient environment across an update is deliberately not done (Consiliency/pmcp#162).
pmcp guidance --telemetry off.PMCP follows a progressive disclosure pattern - start with natural language, get recommendations, drill down as needed.
For local work where an installed native CLI is the right surface, PMCP returns compact CLI guidance and does not execute the command:
Returns:
After status: "use_cli", use Bash/direct CLI. PMCP stops at guidance here:
it does not execute the command and does not fetch full native help output for
the normal compact path. If PMCP returns server candidates instead, continue
with MCP provisioning, gateway.describe, and gateway.invoke.
Returns:
CLI recommendations are returned separately from MCP tool cards:
Returns:
Use cli_hints as recommendations for Bash/direct CLI commands. They are not
MCP tools, do not appear in results, and cannot be passed to
gateway.describe or gateway.invoke. Start with either
gateway.request_capability or gateway.catalog_search; when PMCP returns
use_cli or matching cli_hints, that is enough context to switch to
Bash/direct CLI. Otherwise stay on the MCP path.
Registry-backed matches can appear as registry_candidates in
gateway.catalog_search or as status="candidates" from
gateway.request_capability. They are read-only discovery metadata from the
MCP Registry cache and may include package identifiers, transport, remote
(streamable-http/sse) endpoints for hosted servers, server-card URLs,
protected-resource metadata URLs, authorization-server metadata URLs,
declared scopes, and placeholder header names. Candidates are deduplicated to
the latest published version. PMCP does not install, connect, or pass
credentials for a registry result until you explicitly register and provision
the selected server.
When using gateway.catalog_search, you can discover tools from servers that haven't started yet:
This uses pre-cached tool descriptions from .mcp-gateway/descriptions.yaml. To refresh the cache:
Note: Cached tools show metadata only. Full schemas are available after the server starts (use gateway.describe to trigger lazy start).
The MCP Registry cache is stored separately under .mcp-gateway; PMCP uses the
cache when the public registry is unavailable. Registry candidates can coexist
with cached offline tool cards without changing total_available.
By default PMCP discovers only from the public MCP Registry and surfaces GA-shaped, latest-version entries. Developers debugging their own private MCP servers can opt in with an environment flag (default off):
When enabled, PMCP fetches from the configured private endpoint and tolerates
draft/non-GA server.json schema fields, surfacing all versions (including
non-latest entries) for inspection. This is a debugging aid, not for
production discovery; with the flag off, behavior is unchanged.
PMCP can install and start MCP servers on-demand from a curated manifest of 90+ servers.
Returns (if not already configured):
requires_api_key here reflects the effective requirement, not merely
whether the entry declares one — a server whose manifest entry carries
api_key_optional_when and whose named variable is set reports
requires_api_key: false and no auth_connect recommendation, even though
the underlying entry still has requires_api_key: true. See
Private manifest overlay below.
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:
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.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.
The tenant code-mode host contract in
specs/tenant-code-mode-host-contract.md freezes the PMCP/companion-server
boundary for future hosted sandbox execution. PMCP remains the broker; the
companion tenant server remains the execution authority.
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 serves two protocol eras on the same /mcp
route, upstream of clients. A client that negotiates through initialize is
served the handshake era — 2024-11-05 through 2025-11-25. A client that
instead sends an MCP-Protocol-Version: 2026-07-28 header together with a
params._meta envelope carrying io.modelcontextprotocol/protocolVersion
and io.modelcontextprotocol/clientCapabilities is served the modern era —
2026-07-28 — for tools/list, tools/call, resources/list,
resources/read, prompts/list, prompts/get, and server/discover. The
modern era is not reachable through initialize; it is selected per request
by those headers. The modern era has no server-initiated request
back-channel, so sampling/createMessage and elicitation/create do not
exist at 2026-07-28 — PMCP does not proxy either today, so this is a
protocol-era limitation, not a PMCP gap. (Server-initiated notifications
are a separate mechanism — subscriptions/listen, below.) Downstream, PMCP's
connections to the servers it proxies are negotiated only at the handshake
era described above (2025-11-25 preferred) — no downstream server is ever
reached at 2026-07-28. A modern-era client's tools/call is still proxied
live over that handshake-era downstream connection; only the upstream
envelope differs.
GET /mcp is retired. It now answers 405 Method Not Allowed with
Allow: POST, DELETE instead of accepting a standing connection; there is no
persistent GET/SSE channel of any kind, pre-session or otherwise. GET /health and GET /metrics are unaffected and remain separate, unauthenticated
routes. The replacement for server-initiated notifications is
subscriptions/listen — a long-lived POST stream reachable only at
protocol version 2026-07-28 — over which a client that opens a subscription
receives notifications/tools/list_changed, notifications/resources/list_changed,
and notifications/prompts/list_changed as PMCP's own catalog changes — a
gateway.connect_server, gateway.disconnect_server, or gateway.refresh
call that adds, removes, or updates downstream tools, resources, or prompts,
or a downstream server's own notifications/*/list_changed. A downstream
notification is not relayed as-is: PMCP re-indexes that server's catalog
first and publishes only once reconciliation confirms something actually
moved, so a client that refetches on receipt of the notification sees the
new catalog rather than the stale one. Progress and logging notifications
from a downstream server are not proxied — catalog-change notifications
only. No existing client loses delivered data from the GET
retirement — PMCP never published anything on the old GET stream, so this
removes a channel PMCP never wrote to, not one clients were receiving events
over. The concurrency cap the old pre-session keep-alive shim enforced
returns one-for-one as PMCP_MAX_LISTEN_STREAMS (default 64, same as
before), now bounding subscriptions/listen instead. The old shim's
absolute-lifetime cap (PMCP_KEEPALIVE_MAX_SECONDS) is deliberately
not replaced — a subscription is long-lived by design, and severing it
every N seconds was the defect this release fixes, not a property worth
preserving. PMCP_MAX_KEEPALIVE_STREAMS and PMCP_KEEPALIVE_MAX_SECONDS are
both gone; if you relied on either, there is no drop-in replacement for the
lifetime cap — what bounds exposure instead is the concurrency cap, the SDK's
own per-stream event-backlog cap, and /mcp auth whenever auth_mode is
configured.
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:
The manifest includes 90+ 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 |
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.
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)
L1 (Code Hints): Ultra-terse hints in search results (~8-12 tokens/card)
L2 (Code Snippets): Minimal examples in describe output (~40-80 tokens, opt-in)
L3 (Methodology Resource): Full guide (lazy-loaded, 0 tokens)
pmcp://guidance/code-execution resourceCreate ~/.claude/gateway-guidance.yaml:
Levels:
minimal (default): L0 + L1 (~200 tokens overhead)standard: L0 + L1 + L2 (~320 tokens overhead)off: No guidancePMCP discovers MCP servers from:
.mcp.json in project root (highest priority)~/.mcp.json or ~/.claude/.mcp.json--config flag or PMCP_CONFIG env varIf you built your own MCP servers (or want private provisionable definitions),
you can add manifest entries without editing the shipped manifest. PMCP
merges these overlay files over the built-in manifest, so your servers get
first-class treatment in gateway.request_capability keyword matching,
gateway.catalog_search offline discovery, gateway.provision, and startup
resolution — answering "can I add my own private manifest items?" with yes.
Overlay locations, lowest → highest precedence (later overrides earlier, by server name; a same-named entry is replaced whole, not deep-merged):
~/.pmcp/manifest.yaml<project>/.pmcp/manifest.yaml (nearest ancestor of the cwd)PMCP_MANIFEST_PATH env var (wins over all)Overlays use the same entry schema as the shipped manifest. Example
~/.pmcp/manifest.yaml:
A server that supports a self-hosted, keyless deployment can declare which
extra_env variable makes its credential optional via
api_key_optional_when. Declaring the field alone changes nothing — an
operator must separately supply that variable. The shipped firecrawl entry
already declares api_key_optional_when: ["FIRECRAWL_API_URL"], so supplying
the URL is all an overlay needs to do — via a server_env patch, not a
servers: block (servers: is whole-entry replace: a partial firecrawl:
entry here would erase its command/install metadata and reset
requires_api_key to its unset default, turning the credential gate off):
Both parties must act — the manifest entry names the variable, and the
operator supplies it — so no overlay can unilaterally relax a credential the
entry never declared relaxable. A server naming its own credential as its own
relaxer is ignored, and an unset, empty, or unexpanded ${VAR} value fails
closed: the credential stays required.
Overlay loading is fail-soft: a missing file is skipped silently, and a malformed file or a single bad entry logs a warning and is skipped without crashing the gateway — the shipped manifest always still loads.
Security: a manifest entry can specify an arbitrary
command/argsto run when provisioned — treat an overlay file with the same trust as your own.mcp.json. Policy still applies (denied servers stay denied).
For MCP servers not in the manifest, add them to ~/.mcp.json:
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.
index-it-mcp code-index pilotFor a fleet pilot of the local-first code indexer, add index-it-mcp to your
.mcp.json with a pinned version and its operational env. PMCP spawns it
over stdio and passes the env block verbatim into the child process
(_connect_stdio does env = os.environ.copy(); env.update(config.env)), so
this is the supported channel for the server's configuration:
Notes:
index-it-mcp==<approved-version>) so a PyPI release-line
change can't silently swap the indexer under a running fleet. Replace
<approved-version> with the operator-approved pin.OPENAI_API_KEY is only the token the server presents to a local
OpenAI-compatible endpoint (e.g. a vLLM embedding server at
SEMANTIC_EMBEDDING_BASE_URL); it is not an api.openai.com secret..mcp.json. The shipped-manifest and private-overlay
entry schema has no env: block — putting env: in a manifest overlay
will be ignored. Per-entry environment is only honored from .mcp.json
mcpServers entries."args": ["--python", "3.12", "--from", …].
index-it-mcp==1.2.0 depends on tree-sitter-languages, which has no wheel for
CPython 3.13; without the pin, uvx may pick 3.13 and fail to launch.Repository registration must use the same storage env as the config. Before
agents get indexed results, each repo must be registered and the gateway-spawned
server must read the registry the registration wrote. The registry location is
resolved from MCP_INDEX_STORAGE_PATH / MCP_REPO_REGISTRY. If you set those in
the .mcp.json env above but run index-it-mcp repository register without
them, the CLI writes to the default ~/.mcp/repository_registry.json while the
PMCP-spawned server reads $MCP_INDEX_STORAGE_PATH/repository_registry.json — so
the server reports repositories: [] and every query falls back to native search
(unregistered_repository). Register with the same env the config uses:
Check readiness before trusting indexed answers. The server's status/query
tools report a readiness state (ready, unregistered_repository,
missing_index, stale_commit, …) and, when not ready, safe_fallback: "native_search". Agents should treat any non-ready state as non-authoritative
and fall back to native search rather than reporting stale/empty index results.
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:
CLI mutation previews by default. --apply is required before writing.
Lazy Excalidraw example:
Eager Excalidraw example:
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):
url should be the full remote endpoint for that server.headers values support ${ENV_VAR} interpolation (Issue #40).~/.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.
PMCP can broker a separate tenant code-mode MCP server as a normal downstream
server. The contract in specs/tenant-code-mode-host-contract.md defines the
boundary: PMCP discovers, invokes, monitors, truncates, and redacts through
gateway surfaces; the companion tenant server owns sandbox execution, tenant
authorization, logs, and artifacts. PMCP does not run scripts itself.
Register the hosted server in .mcp.json with the configured name
tenant-code-mode:
For local companion-server development, use a replaceable stdio command from that server's checkout:
The registration is lazy by default. Add tenant-code-mode to top-level
autoStart only when the operator wants PMCP to connect during startup.
Discovery and startup use the existing gateway.request_capability,
gateway.catalog_search with include_offline: true, gateway.provision, and
gateway.invoke flow.
Tenant runs use the existing task broker. Submit long-running work with
gateway.invoke and non-secret task.metadata, task.ttl,
task.poll_interval, task.requestor_context, and trace keys such as
_meta.traceparent; PMCP forwards those fields to the downstream server only
when the server and tool advertise task support. The returned downstream MCP
task ID is then used with gateway.tasks_list, gateway.tasks_get,
gateway.tasks_result, and gateway.tasks_cancel. Do not use PMCP request IDs
from gateway.list_pending or gateway.cancel for tenant task operations.
gateway.tasks_result continues to apply host-side truncation and optional
secret redaction to sandbox-shaped logs and diagnostics.
pmcp secrets)PMCP stores secrets in environment files by scope:
user scope: ~/.config/pmcp/pmcp.envproject scope: <project_root>/.env.pmcpYou can manage both scopes with pmcp secrets:
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.
Create a policy file to control access and limits:
~/.claude/gateway-policy.yaml:
An explicitly requested policy (--policy or PMCP_POLICY) is a fail-closed
boundary: a missing, unreadable, malformed, or schema-invalid file terminates
startup.
An automatically discovered policy at a default location is fail-closed too, with one deliberate exception. A discovered file that parses but is not a valid policy terminates startup exactly like an explicit one, because falling back would replace it with the default allow-all policy and silently unrestrict the gateway. Best-effort fallback now covers only a file that cannot be read, or that the parser rejects outright — which could be anything rather than a policy; that case warns, says that no policy is in effect, and continues.
The line between the two is drawn by the parser, not by the file's shape, so it
falls in different places for the two formats. In a .yaml file a list root, a
scalar root and an empty file all load cleanly — yaml.safe_load returns a
list, a str and None — and so all three are fatal. In a .json file, a
document whose root is valid JSON but not an object ([], 42, null) is
likewise fatal, but an empty .json file is not valid JSON at all, so it
takes the warn-and-continue path. If you are testing this behaviour, use an empty
.yaml file to see the refusal.
PMCP v1.20.0 adds the scoped_advisor_audit.v1 profile for isolated advisor
research. Start each seat with the shipped policy, a unique lock directory, and
an explicit audit sink:
The profile exposes only gateway.health, gateway.catalog_search,
gateway.describe, and gateway.invoke; downstream invocation is limited to
the policy's Firecrawl and Bright Data research patterns. MCP resource and
prompt surfaces are denied, and scoped catalog results omit native-CLI,
registry, and provision candidates. Every invoke must
supply run_correlation_id, seat_correlation_id, and a SHA-256
evidence_label_digest together. The append-only audit stores correlations,
tool/status/policy/result digests, and a hashed public-source reference—not raw
URLs, queries, arguments, credentials, or result bodies—and ends with one
fsynced completeness marker.
Consumers can fail closed on older installations with:
The capability is active in gateway.health only when the exact explicit
advisor policy and audit sink are both present. Concurrent seats must use unique
--lock-dir and --audit-jsonl paths.
Tenant code-mode hosting uses the same policy fields. This example allows only the tenant server, blocks a high-risk submission tool, bounds output, and adds a tenant artifact redaction pattern without granting access to unrelated MCP servers:
For hosted tenant auth, keep credentials in PMCP env storage or tenant-scoped
project storage and reference only placeholders from config:
${TENANT_CODE_MODE_MCP_TOKEN} and ${TENANT_CODE_MODE_TENANT_ID}. Use
pmcp secrets set ... --scope project or gateway.auth_connect to populate
env-store values for non-tenant mode; tenant mode uses isolated per-tenant env
files derived from the resolved project root. PMCP diagnostics report missing
field or env-var names such as
TENANT_CODE_MODE_MCP_TOKEN; they must not print token values.
Hosted operators should require Bearer auth on /mcp, tune --rate-limit or
PMCP_RATE_LIMIT for the deployment, and keep /health and /metrics behind
network controls. gateway.refresh, gateway.disconnect_server, and
gateway.restart_server can disrupt in-flight downstream work unless forced by
policy; use downstream task IDs with gateway.tasks_cancel for tenant run
cancellation. PMCP task records are transient. Durable sandbox logs, artifacts,
tenant authorization, and artifact retention remain responsibilities of the
companion tenant server and its deployment controls.
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.lockmode: detects local command-mode MCP config conflicts when a shared PMCP system service is runninghttp: probes the unauthenticated /health endpoint derived from PMCP_GATEWAY_URL or http://127.0.0.1:3344/mcpremote: detects unresolved remote downstream header environment referencesinstall: detects conflicting uv tool and pip --user installsExample:
If any checks fail, follow the command in the output and rerun pmcp doctor.
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:
Per-project lock (not recommended):
mcp-gateway command naming is deprecated in documentation and examples.pmcp for all CLI commands going forward.mcp-gateway refresh --force -> pmcp refresh --forcemcp-gateway status --json -> pmcp status --jsonIf 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.
MIT