Skill Tracer
Skills › Scientific Agent Skills › pi-agent › pi-mcp-adapter.md

pi-mcp-adapter.md

204 lines · skills/pi-agent/references/pi-mcp-adapter.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# pi-mcp-adapter Package
2
3Source: https://pi.dev/packages/pi-mcp-adapter
4
5Reviewed against Pi 0.99.2 and the package versions listed in `../SKILL.md` on 2026-09-30.
6
7MCP adapter extension for Pi. Instead of loading hundreds of tool definitions upfront, it exposes one `mcp` proxy tool (~200 tokens) that discovers and calls tools on demand. Servers connect lazily and disconnect when idle; tool metadata is cached to disk so search/describe work offline.
8
9```bash
10pi install npm:pi-mcp-adapter
11```
13Version 4.0.0 was reviewed with Pi 0.99.2. Restart Pi after installation. Native Pi owns `/mcp` and `mcp.json`; the adapter uses `/mcp-adapter` and `mcp-adapter.json`. `/mcp` is an adapter alias only without native MCP. On first run the adapter reads standard MCP files automatically. If you only have host-specific configs (Cursor, Claude Code, Codex, …), run `/mcp-adapter setup` to adopt them, or `pi-mcp-adapter init` to scan and add compatibility imports to the Pi agent dir config.

pi-mcp-adapter Package

Source: https://pi.dev/packages/pi-mcp-adapter

Reviewed against Pi 0.99.2 and the package versions listed in ../SKILL.md on 2026-09-30.

MCP adapter extension for Pi. Instead of loading hundreds of tool definitions upfront, it exposes one mcp proxy tool (~200 tokens) that discovers and calls tools on demand. Servers connect lazily and disconnect when idle; tool metadata is cached to disk so search/describe work offline.

pi install npm:pi-mcp-adapter

Version 4.0.0 was reviewed with Pi 0.99.2. Restart Pi after installation. Native Pi owns /mcp and mcp.json; the adapter uses /mcp-adapter and mcp-adapter.json. /mcp is an adapter alias only without native MCP. On first run the adapter reads standard MCP files automatically. If you only have host-specific configs (Cursor, Claude Code, Codex, …), run /mcp-adapter setup to adopt them, or pi-mcp-adapter init to scan and add compatibility imports to the Pi agent dir config.

Raw lines 1 to 14
Uses an outside tool MCP :7
15## Configuration Files
17Precedence, lowest to highest:
191. `~/.config/mcp/mcp.json` — user-global shared
202. `~/.agents/mcp.json` — user-global tool-agnostic
213. `~/.agents/mcp/mcp.json` — user-global tool-agnostic
224. `<Pi agent dir>/mcp-adapter.json` — Adapter global override (`~/.pi/agent/mcp-adapter.json`, or `$PI_CODING_AGENT_DIR/mcp-adapter.json`)
235. `.mcp.json` — project-local shared (preferred for projects)
246. `.pi/mcp-adapter.json` — Adapter project override
26```json
28 "mcpServers": {
29 "chrome-devtools": { "command": "npx", "args": ["-y", "chrome-devtools-mcp@1.6.0"] }
30 }
32```
34Host-specific configs are detected but **not** loaded automatically, and the normal `/mcp-adapter` panel does not scan them while `settings.hostConfigDiscovery` is `"off"` (the default). Opt in with `"on"` (or `pi-mcp-adapter init --discover-host-configs`); `"prompt"` detects without activating. Host configs sit below every shared and Pi-owned source.
36Import specific host formats explicitly with `"imports": ["cursor", "claude-code", "claude-desktop", "opencode", "vscode", "windsurf", "codex"]`.

Configuration Files

Precedence, lowest to highest:

  1. ~/.config/mcp/mcp.json — user-global shared
  2. ~/.agents/mcp.json — user-global tool-agnostic
  3. ~/.agents/mcp/mcp.json — user-global tool-agnostic
  4. <Pi agent dir>/mcp-adapter.json — Adapter global override (~/.pi/agent/mcp-adapter.json, or $PI_CODING_AGENT_DIR/mcp-adapter.json)
  5. .mcp.json — project-local shared (preferred for projects)
  6. .pi/mcp-adapter.json — Adapter project override
{
  "mcpServers": {
    "chrome-devtools": { "command": "npx", "args": ["-y", "chrome-devtools-mcp@1.6.0"] }
  }
}

Host-specific configs are detected but not loaded automatically, and the normal /mcp-adapter panel does not scan them while settings.hostConfigDiscovery is "off" (the default). Opt in with "on" (or pi-mcp-adapter init --discover-host-configs); "prompt" detects without activating. Host configs sit below every shared and Pi-owned source.

Import specific host formats explicitly with "imports": ["cursor", "claude-code", "claude-desktop", "opencode", "vscode", "windsurf", "codex"].

Raw lines 15 to 37
38### Agent Plugins
40List [Agent Plugins](https://agent-plugins.org/) package directories in `settings.agentPluginPaths` to load their MCP servers:
42```json
43{ "settings": { "agentPluginPaths": ["./plugins/acme-tools"] }, "mcpServers": {} }
44```
46Each directory needs a valid Agent Plugins 1.0 `plugin.json`; a root `mcp.json` there contributes `mcpServers` entries prefixed `<plugin>__<server>`. The loader uses the transport declared by each server `type` and skips invalid entries without blocking others. For stdio plugin servers, `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` expand only in `args`, `env`, and `cwd`; both are set for the child process and plugin data is stored under the Pi agent directory. The adapter reads shared `.mcp.json`/`~/.config/mcp/mcp.json` plus adapter-owned overrides. It never reads native Pi global/project `mcp.json`.
48`/mcp-adapter disable <server>` / `/mcp-adapter enable <server>` persist only the `disabled` field into `.pi/mcp-adapter.json` (never rewriting the source file or copying credentials); run `/reload` to apply. The manual equivalent is `{ "disabled": true }` in any MCP config.

Agent Plugins

List Agent Plugins package directories in settings.agentPluginPaths to load their MCP servers:

{ "settings": { "agentPluginPaths": ["./plugins/acme-tools"] }, "mcpServers": {} }

Each directory needs a valid Agent Plugins 1.0 plugin.json; a root mcp.json there contributes mcpServers entries prefixed <plugin>__<server>. The loader uses the transport declared by each server type and skips invalid entries without blocking others. For stdio plugin servers, ${PLUGIN_ROOT} and ${PLUGIN_DATA} expand only in args, env, and cwd; both are set for the child process and plugin data is stored under the Pi agent directory. The adapter reads shared .mcp.json/~/.config/mcp/mcp.json plus adapter-owned overrides. It never reads native Pi global/project mcp.json.

/mcp-adapter disable <server> / /mcp-adapter enable <server> persist only the disabled field into .pi/mcp-adapter.json (never rewriting the source file or copying credentials); run /reload to apply. The manual equivalent is { "disabled": true } in any MCP config.

Raw lines 38 to 49
Uses an outside tool MCP :40
50### SDK Configuration
52```ts
53import { createMcpAdapter } from "pi-mcp-adapter";
54const extension = createMcpAdapter({ config: { mcpServers: { docs: { url: "https://mcp.example.com/mcp", lifecycle: "eager" } } } });
55```
57A supplied `config` is a complete isolated snapshot — never merged with files, imports, or `--mcp-config`, and cloned per adapter/session. Status, reconnect, explicit `/mcp-auth <server>`, proxy calls, and direct tools still work; setup and no-argument auth/status panels report the limitation. With `configPath` and no `config`, normal file merging applies and that path outranks argv and `--mcp-config`. The package ships TypeScript source, so standalone Node processes need a TS-capable loader (e.g. `node --import tsx`).
59OAuth credentials are stored in the OS credential store keyed by configured server name, with URL binding so credentials cannot be reused for a different server URL. `settings.oauthDir` / `MCP_OAUTH_DIR` are legacy plaintext `tokens.json` import locations only.
61Cooperating Pi extensions can reuse URL-bound tokens through the public `pi-mcp-adapter/oauth` subpath (`getMcpOAuthTokensForUrl(server, url)`, `updateMcpOAuthTokensForUrl(server, url, tokens)`, plus a status helper). The async read path applies the adapter's refresh logic first. It exposes token read/update only — never client registration secrets, PKCE verifiers, or OAuth state — and keeps secure-store storage, URL binding, refresh persistence, chunk handling, legacy import, and fail-closed credential-store errors.

SDK Configuration

import { createMcpAdapter } from "pi-mcp-adapter";
const extension = createMcpAdapter({ config: { mcpServers: { docs: { url: "https://mcp.example.com/mcp", lifecycle: "eager" } } } });

A supplied config is a complete isolated snapshot — never merged with files, imports, or --mcp-config, and cloned per adapter/session. Status, reconnect, explicit /mcp-auth <server>, proxy calls, and direct tools still work; setup and no-argument auth/status panels report the limitation. With configPath and no config, normal file merging applies and that path outranks argv and --mcp-config. The package ships TypeScript source, so standalone Node processes need a TS-capable loader (e.g. node --import tsx).

OAuth credentials are stored in the OS credential store keyed by configured server name, with URL binding so credentials cannot be reused for a different server URL. settings.oauthDir / MCP_OAUTH_DIR are legacy plaintext tokens.json import locations only.

Cooperating Pi extensions can reuse URL-bound tokens through the public pi-mcp-adapter/oauth subpath (getMcpOAuthTokensForUrl(server, url), updateMcpOAuthTokensForUrl(server, url, tokens), plus a status helper). The async read path applies the adapter's refresh logic first. It exposes token read/update only — never client registration secrets, PKCE verifiers, or OAuth state — and keeps secure-store storage, URL binding, refresh persistence, chunk handling, legacy import, and fail-closed credential-store errors.

Raw lines 50 to 62
63## Server Options
65| Field | Description |
66|---|---|
67| `command`, `args` | stdio transport; mutually exclusive with `url` and `socket` |
68| `socket` | `rmcp-mux` Unix socket path; supports `${VAR}`, `$env:VAR`, `~` |
69| `env`, `cwd` | Interpolation supported; an `env` value starting with `!` runs a command at connect (`!!` escapes) |
70| `url`, `headers` | StreamableHTTP with SSE fallback; interpolation supported, missing URL vars fail before any request |
71| `auth` | `"bearer"` or `"oauth"` |
72| `oauth.*` | `grantType` (`authorization_code` default, or `client_credentials`), `clientId` (MCP 2026 prefers pre-registered clients or Client ID Metadata Documents; Dynamic Client Registration is the fallback when omitted), `clientSecret`, `scope`, `redirectUri` (exact localhost callback for pre-registered clients), `clientName`, `clientUri` (defaults to the host manifest's `piConfig.clientUri`, omitted under a rebranded host), `logoUri` (absolute `http(s)` URL, RFC 7591 `logo_uri`), `authorizationParams` (extra authorization-URL parameters; flow-owned ones such as `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge`, `response_type`, `resource` cannot be overridden), `skipIssuerMetadataValidation` |
73| `bearerToken` / `bearerTokenEnv` | Token or env var name; interpolation and `!command` supported |
74| `lifecycle` | `"lazy"` (default), `"eager"`, `"keep-alive"`, `"lazy-keep-alive"` |
75| `idleTimeout` | Minutes before idle disconnect (overrides global) |
76| `requestTimeoutMs` | Per-server request timeout; omitted or `<= 0` uses the MCP SDK default |
77| `protocolVersion` | `"legacy"` (default), `"auto"`, or `"2026-07-28"` — modern negotiation is opt-in |
78| `exposeResources` | Expose MCP resources as tools (default `true`) |
79| `directTools` | `true`, `string[]`, `"search"`, or `false` |
80| `toolPrefix` | Per-server override of the global `toolPrefix` |
81| `includeTools` / `excludeTools` | Names or glob patterns; `excludeTools` applies after `includeTools` |
82| `searchKeywords` | `{ "tool-or-glob": ["keyword", …] }` extra keywords boosting `mcp({ search })` ranking; never shown to the model |
83| `approveTools` | `true`, `"destructive"`, or glob array requiring approval before calls (overrides the global setting) |
84| `debug` | Show server stderr (default `false`) |
85| `trace` | Metadata-only JSONL protocol tracing for this server |
86| `disabled` | Keep visible in config/status but block connections, auth, tools, resources (only literal `true`) |
88Secret values in `headers`, `bearerToken`, `oauth.clientSecret`, and stdio `env` may use a leading `!command` resolved at connect/auth time: stdin and stderr suppressed, stdout capped at 1 MiB and trimmed, 10-second limit, non-empty output required. Commands never run during discovery, merging, previewing, hashing, or rendering config.
90`oauth.skipIssuerMetadataValidation: true` disables the RFC 8414 issuer echo check for one server. It weakens OAuth mix-up protection — use it only for a known-misconfigured internal server while its metadata is being fixed, never for public or untrusted servers.
92**Protocol version negotiation** — the default `"legacy"` uses the classic MCP initialize sequence with no `server/discover` or 2026 headers, preserving compatibility with deployed 2025-era servers. `"auto"` probes for MCP 2026-07-28 and conservatively falls back to the classic handshake on legacy evidence; set it for Cloudflare Workers `createMcpHandler` and other MCP SDK v2 stateless servers. For stdio servers, `"auto"` probes with a short-lived sibling process before starting the session process, so each fresh connection adds one spawn and may wait out the request timeout; explicit Unix sockets probe in place. HTTP auto negotiation uses the real Streamable HTTP connection and falls back to legacy SSE only on definitive rejection (404/405/406/415) — never on auth failures, cancellation, timeouts, or server errors. `"2026-07-28"` pins that revision with no legacy or SSE fallback. Strict OAuth issuer validation applies in every mode. Adapter-level roots support, standard MCP logging presentation, and protocol cache-hint config are not yet implemented.
94**Lifecycle modes** — `lazy`: connect on first tool call, disconnect after idle, cached metadata keeps search working. `eager`: connect at startup, no auto-reconnect, no idle timeout unless set. `keep-alive`: connect at startup with health checks and auto-reconnect. `lazy-keep-alive`: connect on first use, then stay resident with auto-reconnect. Any enabled `eager`/`keep-alive` server also triggers initialization at extension load, supporting hosts that never emit `session_start`.
96**rmcp-mux** — point `socket` at an [`rmcp-mux`](https://github.com/VetCoders/rmcp-mux) service socket to share one stdio server across Pi sessions. The adapter owns only its client socket; the mux owns the upstream process, routing, restart policy, and socket permissions. A socket is an explicit trusted local endpoint.

Server Options

FieldDescription
command, argsstdio transport; mutually exclusive with url and socket
socketrmcp-mux Unix socket path; supports ${VAR}, $env:VAR, ~
env, cwdInterpolation supported; an env value starting with ! runs a command at connect (!! escapes)
url, headersStreamableHTTP with SSE fallback; interpolation supported, missing URL vars fail before any request
auth"bearer" or "oauth"
oauth.*grantType (authorization_code default, or client_credentials), clientId (MCP 2026 prefers pre-registered clients or Client ID Metadata Documents; Dynamic Client Registration is the fallback when omitted), clientSecret, scope, redirectUri (exact localhost callback for pre-registered clients), clientName, clientUri (defaults to the host manifest's piConfig.clientUri, omitted under a rebranded host), logoUri (absolute http(s) URL, RFC 7591 logo_uri), authorizationParams (extra authorization-URL parameters; flow-owned ones such as client_id, redirect_uri, scope, state, code_challenge, response_type, resource cannot be overridden), skipIssuerMetadataValidation
bearerToken / bearerTokenEnvToken or env var name; interpolation and !command supported
lifecycle"lazy" (default), "eager", "keep-alive", "lazy-keep-alive"
idleTimeoutMinutes before idle disconnect (overrides global)
requestTimeoutMsPer-server request timeout; omitted or <= 0 uses the MCP SDK default
protocolVersion"legacy" (default), "auto", or "2026-07-28" — modern negotiation is opt-in
exposeResourcesExpose MCP resources as tools (default true)
directToolstrue, string[], "search", or false
toolPrefixPer-server override of the global toolPrefix
includeTools / excludeToolsNames or glob patterns; excludeTools applies after includeTools
searchKeywords{ "tool-or-glob": ["keyword", …] } extra keywords boosting mcp({ search }) ranking; never shown to the model
approveToolstrue, "destructive", or glob array requiring approval before calls (overrides the global setting)
debugShow server stderr (default false)
traceMetadata-only JSONL protocol tracing for this server
disabledKeep visible in config/status but block connections, auth, tools, resources (only literal true)

Secret values in headers, bearerToken, oauth.clientSecret, and stdio env may use a leading !command resolved at connect/auth time: stdin and stderr suppressed, stdout capped at 1 MiB and trimmed, 10-second limit, non-empty output required. Commands never run during discovery, merging, previewing, hashing, or rendering config.

oauth.skipIssuerMetadataValidation: true disables the RFC 8414 issuer echo check for one server. It weakens OAuth mix-up protection — use it only for a known-misconfigured internal server while its metadata is being fixed, never for public or untrusted servers.

Protocol version negotiation — the default "legacy" uses the classic MCP initialize sequence with no server/discover or 2026 headers, preserving compatibility with deployed 2025-era servers. "auto" probes for MCP 2026-07-28 and conservatively falls back to the classic handshake on legacy evidence; set it for Cloudflare Workers createMcpHandler and other MCP SDK v2 stateless servers. For stdio servers, "auto" probes with a short-lived sibling process before starting the session process, so each fresh connection adds one spawn and may wait out the request timeout; explicit Unix sockets probe in place. HTTP auto negotiation uses the real Streamable HTTP connection and falls back to legacy SSE only on definitive rejection (404/405/406/415) — never on auth failures, cancellation, timeouts, or server errors. "2026-07-28" pins that revision with no legacy or SSE fallback. Strict OAuth issuer validation applies in every mode. Adapter-level roots support, standard MCP logging presentation, and protocol cache-hint config are not yet implemented.

Lifecycle modes — lazy: connect on first tool call, disconnect after idle, cached metadata keeps search working. eager: connect at startup, no auto-reconnect, no idle timeout unless set. keep-alive: connect at startup with health checks and auto-reconnect. lazy-keep-alive: connect on first use, then stay resident with auto-reconnect. Any enabled eager/keep-alive server also triggers initialization at extension load, supporting hosts that never emit session_start.

rmcp-mux — point socket at an [rmcp-mux](https://github.com/VetCoders/rmcp-mux) service socket to share one stdio server across Pi sessions. The adapter owns only its client socket; the mux owns the upstream process, routing, restart policy, and socket permissions. A socket is an explicit trusted local endpoint.

Raw lines 63 to 97
Uses an outside tool MCP :72
Starts an agent spawn :92
98## Global Settings
100```json
101{ "settings": { "toolPrefix": "server", "idleTimeout": 10, "requestTimeoutMs": 30000, "trace": { "enabled": true } } }
102```
104`toolPrefix` (`"server"` default, `"short"` strips a `-mcp` suffix, `"none"`, `"mcp"` prefixes `mcp__`; per-server `toolPrefix` overrides it), `idleTimeout` (minutes, default 10, `0` disables), `requestTimeoutMs`, `showStatusIcon` (default `true`), `mcpFooterStatus` (`"full"` default, `"compact"`, `"off"`), `toolResultRendering` (`"compact"` default self-rendered rows, or `"boxed"` for the legacy Pi tool row), `collapsedResultLines` (`1`–`3`; defaults `1` compact / `3` boxed), `notifyOnStartupConnect` (default `true`; `false` suppresses routine connect notices but keeps errors and auth warnings), `hostConfigDiscovery`, `agentPluginPaths`, `approveTools` (`"destructive"` asks for unknown/destructive tools), `oauthDir`, `directTools` (global default, default `false`), `freezeDirectTools` (default `false`), `scriptMode` (default `false`; registers the MCP-only `mcpScript` plain-JavaScript tool), `disableProxyTool`, `autoAuth` (default `false`), `sampling` (default `true` when UI approval is available; honors `modelPreferences.hints`), `samplingAutoApprove` (required for sampling in non-UI sessions), `elicitation` (default `true` with UI), `outputGuard`, `trace` (`{ enabled, file, maxBytes: 262144, maxEvents: 10000 }`; the per-session JSONL defaults to `.pi/mcp-traces/` and never records payloads, prompts, arguments/results, auth data, or URLs).
106Per-server `idleTimeout`, `requestTimeoutMs`, and `approveTools` override the global values.

Global Settings

{ "settings": { "toolPrefix": "server", "idleTimeout": 10, "requestTimeoutMs": 30000, "trace": { "enabled": true } } }

toolPrefix ("server" default, "short" strips a -mcp suffix, "none", "mcp" prefixes mcp__; per-server toolPrefix overrides it), idleTimeout (minutes, default 10, 0 disables), requestTimeoutMs, showStatusIcon (default true), mcpFooterStatus ("full" default, "compact", "off"), toolResultRendering ("compact" default self-rendered rows, or "boxed" for the legacy Pi tool row), collapsedResultLines (1–3; defaults 1 compact / 3 boxed), notifyOnStartupConnect (default true; false suppresses routine connect notices but keeps errors and auth warnings), hostConfigDiscovery, agentPluginPaths, approveTools ("destructive" asks for unknown/destructive tools), oauthDir, directTools (global default, default false), freezeDirectTools (default false), scriptMode (default false; registers the MCP-only mcpScript plain-JavaScript tool), disableProxyTool, autoAuth (default false), sampling (default true when UI approval is available; honors modelPreferences.hints), samplingAutoApprove (required for sampling in non-UI sessions), elicitation (default true with UI), outputGuard, trace ({ enabled, file, maxBytes: 262144, maxEvents: 10000 }; the per-session JSONL defaults to .pi/mcp-traces/ and never records payloads, prompts, arguments/results, auth data, or URLs).

Per-server idleTimeout, requestTimeoutMs, and approveTools override the global values.

Raw lines 98 to 107
Uses an outside tool MCP :104
108### Tool Approval
110`approveTools` keeps a tool visible but gates the call — useful for destructive or high-cost actions where hiding the tool would hurt planning:
112```json
113{ "settings": { "approveTools": ["github_delete_*", "notion_update_*"] },
114 "mcpServers": { "github": { "approveTools": ["delete_*", "merge_pull_request"] }, "docs": { "approveTools": false } } }
115```
117A matching call from the proxy tool, a direct MCP tool, `mcpScript`, a resource call, or an MCP UI iframe prompts **Allow once** / **Allow for session** / **Deny**; session approvals live in memory only, and headless sessions fail closed with an `approval_required` result. `excludeTools` still removes tools entirely — `approveTools` only gates visible ones.
119Permission extensions can broker decisions by listening on `MCP_TOOL_APPROVAL_REQUEST_EVENT` (`pi-mcp-adapter:tool-approval-request`) and claiming the request synchronously with `request.claim(async () => "allow_once" | "allow_for_session" | "deny" | "abstain")`. The request carries `serverName`, `originalToolName`, `prefixedToolName`, `args`, `origin`, and an optional `signal`; the first synchronous claim wins, `allow_for_session` updates the same in-memory cache as the dialog, and `abstain`/no claim keeps the fallback behavior. Brokered approval runs for every uncached MCP call regardless of `approveTools` config.

Tool Approval

approveTools keeps a tool visible but gates the call — useful for destructive or high-cost actions where hiding the tool would hurt planning:

{ "settings": { "approveTools": ["github_delete_*", "notion_update_*"] },
  "mcpServers": { "github": { "approveTools": ["delete_*", "merge_pull_request"] }, "docs": { "approveTools": false } } }

A matching call from the proxy tool, a direct MCP tool, mcpScript, a resource call, or an MCP UI iframe prompts Allow once / Allow for session / Deny; session approvals live in memory only, and headless sessions fail closed with an approval_required result. excludeTools still removes tools entirely — approveTools only gates visible ones.

Permission extensions can broker decisions by listening on MCP_TOOL_APPROVAL_REQUEST_EVENT (pi-mcp-adapter:tool-approval-request) and claiming the request synchronously with request.claim(async () => "allow_once" | "allow_for_session" | "deny" | "abstain"). The request carries serverName, originalToolName, prefixedToolName, args, origin, and an optional signal; the first synchronous claim wins, allow_for_session updates the same in-memory cache as the dialog, and abstain/no claim keeps the fallback behavior. Brokered approval runs for every uncached MCP call regardless of approveTools config.

Raw lines 108 to 120
Uses an outside tool MCP :117
121### Search Keywords
123Search matches literally, so per-server `searchKeywords` adds vocabulary for tools whose names and descriptions use different words:
125```json
126{ "mcpServers": { "github": { "searchKeywords": { "search_code": ["grep"], "*": ["gh"] } } } }
127```
129Keys match a tool's original name, prefixed name, or a glob (`*` covers every tool on the server), and all matching entries combine. Keywords are weighted like description text with an extra boost on exact phrase matches. They affect ranked and regex search only (including `tools.search` in `mcpScript`) and never appear in tool schemas, `describe` output, direct-tool registration, or the metadata cache — so keyword search works offline from cached metadata.

Search Keywords

Search matches literally, so per-server searchKeywords adds vocabulary for tools whose names and descriptions use different words:

{ "mcpServers": { "github": { "searchKeywords": { "search_code": ["grep"], "*": ["gh"] } } } }

Keys match a tool's original name, prefixed name, or a glob (* covers every tool on the server), and all matching entries combine. Keywords are weighted like description text with an extra boost on exact phrase matches. They affect ranked and regex search only (including tools.search in mcpScript) and never appear in tool schemas, describe output, direct-tool registration, or the metadata cache — so keyword search works offline from cached metadata.

Raw lines 121 to 130
131## Output Guard
133On by default: inline text is capped at **50 KiB / 2000 lines** (matching Pi's `bash` guard), with the full text spilled to a temp file whose path is included so the agent can `read`/`grep` it. Image blocks pass through unchanged. Binary resource blobs up to **10 MiB** are decoded to private temp files and replaced with file references, bounded to 100 MiB and 10,000 files per session and removed at session teardown. In proxy mode `details.mcpResult` stays raw when its JSON is ≤ 16 KiB; larger results become a compact summary with the raw JSON spilled to a temp file (direct tools never carry `mcpResult`). Tune with `{ maxBytes, maxLines, detailsMaxBytes }`; disable with `"outputGuard": false` or `MCP_OUTPUT_GUARD=0`. Text/raw-JSON spill files are mode `0600` under the system temp dir and are not cleaned up automatically; binary resource-file cleanup follows the separate session-bound lifecycle above.

Output Guard

On by default: inline text is capped at 50 KiB / 2000 lines (matching Pi's bash guard), with the full text spilled to a temp file whose path is included so the agent can read/grep it. Image blocks pass through unchanged. Binary resource blobs up to 10 MiB are decoded to private temp files and replaced with file references, bounded to 100 MiB and 10,000 files per session and removed at session teardown. In proxy mode details.mcpResult stays raw when its JSON is ≤ 16 KiB; larger results become a compact summary with the raw JSON spilled to a temp file (direct tools never carry mcpResult). Tune with { maxBytes, maxLines, detailsMaxBytes }; disable with "outputGuard": false or MCP_OUTPUT_GUARD=0. Text/raw-JSON spill files are mode 0600 under the system temp dir and are not cleaned up automatically; binary resource-file cleanup follows the separate session-bound lifecycle above.

Raw lines 131 to 134
135## Direct Tools
137```json
138{ "mcpServers": { "github": { "directTools": ["search_repositories", "get_file_contents"] } } }
139```
141`true` registers all of a server's tools individually, an array registers only those (original MCP names), omitted/`false` is proxy-only. Per-server overrides the global default. `includeTools`/`excludeTools` filter direct tools, proxy search/list/describe, and the `/mcp-adapter` panel. Each direct declaration increases prompt size; expose a targeted set or use search/proxy discovery for large servers.
143Direct tools can register from the metadata cache (`~/.pi/agent/mcp-cache.json`, or `$PI_CODING_AGENT_DIR/mcp-cache.json`). If required metadata is missing, startup may connect to populate it and hot-load declarations; `deferWithMissingMetadata: true` opts into deferral. Servers advertising list-change notifications refresh the current session. Force a refresh with `/mcp-adapter reconnect <server>`.
145Set `settings.freezeDirectTools: true` when prompt-cache stability matters more than hot-loading: the initial sync still runs, but later automatic reconnects, lazy-connects, and list-change notifications leave the registered tool surface unchanged. Explicit reconnects also preserve a frozen direct-tool surface; proxy/search/cache metadata still refreshes.

Direct Tools

{ "mcpServers": { "github": { "directTools": ["search_repositories", "get_file_contents"] } } }

true registers all of a server's tools individually, an array registers only those (original MCP names), omitted/false is proxy-only. Per-server overrides the global default. includeTools/excludeTools filter direct tools, proxy search/list/describe, and the /mcp-adapter panel. Each direct declaration increases prompt size; expose a targeted set or use search/proxy discovery for large servers.

Direct tools can register from the metadata cache (~/.pi/agent/mcp-cache.json, or $PI_CODING_AGENT_DIR/mcp-cache.json). If required metadata is missing, startup may connect to populate it and hot-load declarations; deferWithMissingMetadata: true opts into deferral. Servers advertising list-change notifications refresh the current session. Force a refresh with /mcp-adapter reconnect <server>.

Set settings.freezeDirectTools: true when prompt-cache stability matters more than hot-loading: the initial sync still runs, but later automatic reconnects, lazy-connects, and list-change notifications leave the registered tool surface unchanged. Explicit reconnects also preserve a frozen direct-tool surface; proxy/search/cache metadata still refreshes.

Raw lines 135 to 146
Uses an outside tool MCP :141
147## Proxy Tool API
149```javascript
150mcp({ }) // status / list servers
151mcp({ server: "name" }) // server details (+ instructions preview)
152mcp({ search: "screenshot navigate", limit: 12, offset: 0 }) // ranked tool search
153mcp({ describe: "tool_name" })
154mcp({ instructions: "name" }) // full server instructions
155mcp({ tool: "chrome_devtools_take_screenshot", args: { format: "png" } })
156mcp({ connect: "server-name" }) // connect or refresh
157mcp({ action: "ui-messages" })
158mcp({ action: "auth-start", server: "name" })
159mcp({ action: "auth-complete", server: "name", args: { redirectUrl: "http://localhost:19876/callback?code=...&state=..." } })
160```
162`args` accepts a JSON object or a JSON string. Search covers **MCP tools only** in the current adapter. Space-separated words are ranked by weighted matches across name, server, description, and any configured `searchKeywords`, then paginated (`limit` defaults to 12; follow `details.nextOffset`). `regex: true` still works but paginates without ranking. Names fuzzy-match on hyphens and underscores, and an unresolvable `describe`/`tool` name returns top suggestions so the agent can fix a typo in the same turn. With `includeSchemas`, search and describe render common JSON Schema parameters as compact TypeScript shapes like `{ query: string; limit?: number; }`. For HTTP servers, a failed connect runs a one-request shape probe that turns opaque transport errors into hints such as `endpoint returned HTML (200) — this URL does not appear to speak MCP`. Server `instructions` surface at three levels: a truncated head in the proxy tool description, a longer preview in `mcp({ server })`, and the full text via `mcp({ instructions })` — captured at connect time and cached.
164Remote/headless OAuth: `/mcp-auth <server>` first shows a clickable authorization URL. Open it in your local browser, approve, then select **Yes** in Pi to open the callback input — the browser's localhost callback page will usually fail to load (localhost is your workstation), so copy the full URL from its address bar and paste it into Pi. When the browser can reach Pi's callback directly, the authorization screen closes on its own instead. The same flow is available through the proxy tool (`auth-start` then `auth-complete` with `redirectUrl` or `args: { code }`) for non-interactive clients. By default persistent OAuth requires the OS credential store, such as an unlocked Secret Service on headless Linux. An explicit `oauthCredentialStore: "encrypted-file"` alternative needs `PI_MCP_ADAPTER_OAUTH_FILE_KEY`; neither mode silently falls back to plaintext. On Linux, when credential access fails because Pi inherited a revoked session keyring, the adapter attempts recovery through `keyctl session - node <packaged helper>` (requires `keyctl` and `node` on `PATH`) so re-authentication can write fresh credentials without killing a long-lived tmux server.

Proxy Tool API

mcp({ })                                        // status / list servers
mcp({ server: "name" })                         // server details (+ instructions preview)
mcp({ search: "screenshot navigate", limit: 12, offset: 0 })  // ranked tool search
mcp({ describe: "tool_name" })
mcp({ instructions: "name" })                   // full server instructions
mcp({ tool: "chrome_devtools_take_screenshot", args: { format: "png" } })
mcp({ connect: "server-name" })                 // connect or refresh
mcp({ action: "ui-messages" })
mcp({ action: "auth-start", server: "name" })
mcp({ action: "auth-complete", server: "name", args: { redirectUrl: "http://localhost:19876/callback?code=...&state=..." } })

args accepts a JSON object or a JSON string. Search covers MCP tools only in the current adapter. Space-separated words are ranked by weighted matches across name, server, description, and any configured searchKeywords, then paginated (limit defaults to 12; follow details.nextOffset). regex: true still works but paginates without ranking. Names fuzzy-match on hyphens and underscores, and an unresolvable describe/tool name returns top suggestions so the agent can fix a typo in the same turn. With includeSchemas, search and describe render common JSON Schema parameters as compact TypeScript shapes like { query: string; limit?: number; }. For HTTP servers, a failed connect runs a one-request shape probe that turns opaque transport errors into hints such as endpoint returned HTML (200) — this URL does not appear to speak MCP. Server instructions surface at three levels: a truncated head in the proxy tool description, a longer preview in mcp({ server }), and the full text via mcp({ instructions }) — captured at connect time and cached.

Remote/headless OAuth: /mcp-auth <server> first shows a clickable authorization URL. Open it in your local browser, approve, then select Yes in Pi to open the callback input — the browser's localhost callback page will usually fail to load (localhost is your workstation), so copy the full URL from its address bar and paste it into Pi. When the browser can reach Pi's callback directly, the authorization screen closes on its own instead. The same flow is available through the proxy tool (auth-start then auth-complete with redirectUrl or args: { code }) for non-interactive clients. By default persistent OAuth requires the OS credential store, such as an unlocked Secret Service on headless Linux. An explicit oauthCredentialStore: "encrypted-file" alternative needs PI_MCP_ADAPTER_OAUTH_FILE_KEY; neither mode silently falls back to plaintext. On Linux, when credential access fails because Pi inherited a revoked session keyring, the adapter attempts recovery through keyctl session - node <packaged helper> (requires keyctl and node on PATH) so re-authentication can write fresh credentials without killing a long-lived tmux server.

Raw lines 147 to 165
Uses an outside tool MCP :162
166## Commands
168`/mcp-adapter` (interactive panel: status, tools, direct/proxy toggles, reconnect, `ctrl+a` or Enter for OAuth, Save on `ctrl+s` — remappable via the `mcp.panel.save` keybinding), `/mcp-adapter setup` (imports, a minimal `.mcp.json`, curated known servers — DeepWiki, Context7, Notion, GitHub, Chrome DevTools — RepoPrompt quick-add, config-path inspection), `/mcp-adapter tools`, `/mcp-adapter prompts`, `/mcp-adapter reconnect [server]`, `/mcp-adapter disable <server>`, `/mcp-adapter enable <server>`, `/mcp-adapter logout <server>`, `/mcp-auth [server]`.

Commands

/mcp-adapter (interactive panel: status, tools, direct/proxy toggles, reconnect, ctrl+a or Enter for OAuth, Save on ctrl+s — remappable via the mcp.panel.save keybinding), /mcp-adapter setup (imports, a minimal .mcp.json, curated known servers — DeepWiki, Context7, Notion, GitHub, Chrome DevTools — RepoPrompt quick-add, config-path inspection), /mcp-adapter tools, /mcp-adapter prompts, /mcp-adapter reconnect [server], /mcp-adapter disable <server>, /mcp-adapter enable <server>, /mcp-adapter logout <server>, /mcp-auth [server].

Raw lines 166 to 169
170## Prompts, Elicitation, UI
172MCP prompt templates register as slash commands `/mcp__<server>__<prompt>`, refreshed on connect. Arguments support positional and `key=value` forms with quoting; required arguments are validated before `prompts/get`. Results flatten into one user message preserving `[user]`/`[assistant]` markers.
174Elicitation forms use Pi's `select()`/`input()` dialogs with validation and a review step; explicit refusal maps to MCP `decline`, dismissal to `cancel`. URL mode is TUI-only, always shows requesting server/host/URL, and requires consent; `-32042` URL-required tool errors are handled — retry the original call after completing the browser step.
176MCP UI resources open in a native macOS window via Glimpse (`pi install npm:glimpseui`) or fall back to the browser. `MCP_UI_VIEWER=browser|glimpse|none` forces or suppresses the viewer (`none` still runs the tool and returns inline results). UIs talk back — message types `prompt`, `intent`, `notify`, `message`, plus custom types forwarded as intents — retrieved with `mcp({ action: "ui-messages" })` (each with `type`, `sessionId`, `serverName`, `toolName`, `timestamp`). Calling the same tool again pushes a new result into the open window instead of replacing it. Tool consent gates whether UIs may call MCP tools (never / once-per-server / always), and `_meta.ui.visibility` controls audience — app-only tools stay out of the model tool list, model-only tools cannot be called from the UI iframe. Browser controls: Cmd/Ctrl+Enter completes, Escape cancels.

Prompts, Elicitation, UI

MCP prompt templates register as slash commands /mcp__<server>__<prompt>, refreshed on connect. Arguments support positional and key=value forms with quoting; required arguments are validated before prompts/get. Results flatten into one user message preserving [user]/[assistant] markers.

Elicitation forms use Pi's select()/input() dialogs with validation and a review step; explicit refusal maps to MCP decline, dismissal to cancel. URL mode is TUI-only, always shows requesting server/host/URL, and requires consent; -32042 URL-required tool errors are handled — retry the original call after completing the browser step.

MCP UI resources open in a native macOS window via Glimpse (pi install npm:glimpseui) or fall back to the browser. MCP_UI_VIEWER=browser|glimpse|none forces or suppresses the viewer (none still runs the tool and returns inline results). UIs talk back — message types prompt, intent, notify, message, plus custom types forwarded as intents — retrieved with mcp({ action: "ui-messages" }) (each with type, sessionId, serverName, toolName, timestamp). Calling the same tool again pushes a new result into the open window instead of replacing it. Tool consent gates whether UIs may call MCP tools (never / once-per-server / always), and _meta.ui.visibility controls audience — app-only tools stay out of the model tool list, model-only tools cannot be called from the UI iframe. Browser controls: Cmd/Ctrl+Enter completes, Escape cancels.

Raw lines 170 to 177
Uses an outside tool MCP :172
178## Status Snapshots
180```ts
181import { MCP_STATUS_EVENT, type McpStatusSnapshot } from "pi-mcp-adapter";
182pi.events.on(MCP_STATUS_EVENT, (snapshot) => { /* read-only */ });
183```
185Includes `totalTools`, `totalResources`, `connectedCount`, `disabledCount`, and per-server `name`, `status` (connected, cached, failed, needs-auth, not-connected, disabled), `toolCount`, `disabled`, plus `resourceCount` when known and `failedAgoSeconds` on active failure. Reading status never connects a lazy server, starts auth, or exposes clients, transports, credentials, or server definitions. An initial snapshot follows initialization; an empty snapshot is emitted on shutdown.

Status Snapshots

import { MCP_STATUS_EVENT, type McpStatusSnapshot } from "pi-mcp-adapter";
pi.events.on(MCP_STATUS_EVENT, (snapshot) => { /* read-only */ });

Includes totalTools, totalResources, connectedCount, disabledCount, and per-server name, status (connected, cached, failed, needs-auth, not-connected, disabled), toolCount, disabled, plus resourceCount when known and failedAgoSeconds on active failure. Reading status never connects a lazy server, starts auth, or exposes clients, transports, credentials, or server definitions. An initial snapshot follows initialization; an empty snapshot is emitted on shutdown.

Raw lines 178 to 186
187## Behavior Notes and Limitations
189npx-based servers resolve to direct binaries, avoiding an extra npm parent process. Advertised `outputSchema` supports JSON Schema draft-07 and 2020-12 (unstamped schemas use the SDK's 2020-12 default), and returned `structuredContent` is validated for both proxy and direct calls. Results use compact self-rendered rows by default — collapsed success output shows the call title and the first result line plus a `Ctrl+O to expand` hint — while the model still receives the full result. Set `toolResultRendering: "boxed"` for the legacy row, or `collapsedResultLines` to `2`/`3` for more collapsed text.
191Limitations: no cross-session server sharing (each Pi session runs its own server processes, unless using rmcp-mux); MCP sampling is text-only (context inclusion, tools, stop sequences, audio, and images are rejected); inline images follow Pi's image display settings; Pi still owns one separator row before self-rendered tool output, so compact mode reduces but cannot eliminate the gap.
193Subagents (`pi-subagents`) receive direct MCP tools only when listed with an `mcp:` prefix in their `tools:` frontmatter — a global `directTools: true` is not enough. See `references/pi-subagents.md`.

Behavior Notes and Limitations

npx-based servers resolve to direct binaries, avoiding an extra npm parent process. Advertised outputSchema supports JSON Schema draft-07 and 2020-12 (unstamped schemas use the SDK's 2020-12 default), and returned structuredContent is validated for both proxy and direct calls. Results use compact self-rendered rows by default — collapsed success output shows the call title and the first result line plus a Ctrl+O to expand hint — while the model still receives the full result. Set toolResultRendering: "boxed" for the legacy row, or collapsedResultLines to 2/3 for more collapsed text.

Limitations: no cross-session server sharing (each Pi session runs its own server processes, unless using rmcp-mux); MCP sampling is text-only (context inclusion, tools, stop sequences, audio, and images are rejected); inline images follow Pi's image display settings; Pi still owns one separator row before self-rendered tool output, so compact mode reduces but cannot eliminate the gap.

Subagents (pi-subagents) receive direct MCP tools only when listed with an mcp: prefix in their tools: frontmatter — a global directTools: true is not enough. See references/pi-subagents.md.

Raw lines 187 to 195
Uses an outside tool MCP :191
Starts an agent Subagents :193
Starts an agent subagents :193
196## Current admission and scripting contracts
198Project `.mcp.json` and `.pi/mcp-adapter.json` servers follow Pi trust and a separate approval tied to the effective server definition. Trusted headless sessions skip unapproved project servers unless user-global `settings.projectServers: "allow"` explicitly enables them. Project files cannot loosen that policy. `settings.ancestorConfigRoots` optionally scopes ancestor discovery to explicit trusted roots; it is off by default.
200Stdio `inheritEnv: false` limits inherited process environment; `literalEnv` avoids interpolation for explicitly literal values. `directTools: "search"` registers tools inactive until search/call activates them. Optional `strictDirectToolArguments` validates direct inputs; `directToolResultDetails: "bounded"` retains guarded raw fields (default `lean`). Namespace proxy wrappers are controlled by `namespaceProxyTools` (default true).
202`mcpScript` is **opt-in** through `settings.scriptMode: true`. Use `await tools.search({ query, server?, limit?, offset? })`, `await tools.describe({ path, server? })`, and `await tools.call(path, args, { server }?)`; calls return `{ ok: true, data }` or `{ ok: false, error: { code, message } }`, where `data` is the raw MCP result. Preserve the server from discovery to disambiguate shared names. Output shapes in describe are observed field/type hints, not schemas; with script mode enabled they may persist across sessions. The script deadline defaults to 30s. These are adapter APIs, not native Pi codemode APIs.
204The SDK `createMcpAdapter({ config })` form is an isolated snapshot. Extension loading and configuration paths were executed without contacting external MCP servers; programmatic approval, OAuth, Apps, sampling and remote protocol compatibility remain source-verified only.

Current admission and scripting contracts

Project .mcp.json and .pi/mcp-adapter.json servers follow Pi trust and a separate approval tied to the effective server definition. Trusted headless sessions skip unapproved project servers unless user-global settings.projectServers: "allow" explicitly enables them. Project files cannot loosen that policy. settings.ancestorConfigRoots optionally scopes ancestor discovery to explicit trusted roots; it is off by default.

Stdio inheritEnv: false limits inherited process environment; literalEnv avoids interpolation for explicitly literal values. directTools: "search" registers tools inactive until search/call activates them. Optional strictDirectToolArguments validates direct inputs; directToolResultDetails: "bounded" retains guarded raw fields (default lean). Namespace proxy wrappers are controlled by namespaceProxyTools (default true).

mcpScript is opt-in through settings.scriptMode: true. Use await tools.search({ query, server?, limit?, offset? }), await tools.describe({ path, server? }), and await tools.call(path, args, { server }?); calls return { ok: true, data } or { ok: false, error: { code, message } }, where data is the raw MCP result. Preserve the server from discovery to disambiguate shared names. Output shapes in describe are observed field/type hints, not schemas; with script mode enabled they may persist across sessions. The script deadline defaults to 30s. These are adapter APIs, not native Pi codemode APIs.

The SDK createMcpAdapter({ config }) form is an isolated snapshot. Extension loading and configuration paths were executed without contacting external MCP servers; programmatic approval, OAuth, Apps, sampling and remote protocol compatibility remain source-verified only.

Raw lines 196 to 204
Uses an outside tool MCP :202