pi-mcp-adapter

extensionmaintained

MCP (Model Context Protocol) adapter extension for Pi coding agent

by — · v2.37.0 · published 3d ago

$ pi install npm:pi-mcp-adapter
downloads/mo
1.1M
stars
1.5K
last push
2d ago
open issues
5

Signals

license: MITtestspi manifest: missinginstall size: —deps: 0peer deps: 0

Download trend

No downloads in the last 12 weeks.

README

pi-mcp-adapter

Pi MCP Adapter

Use MCP servers with Pi without burning your context window.

https://github.com/user-attachments/assets/4b7c66ff-e27e-4639-b195-22c3db406a5a

Why This Exists

Mario wrote about why you might not need MCP. The problem: tool definitions are verbose. A single MCP server can burn 10k+ tokens, and you're paying that cost whether you use those tools or not. Connect a few servers and you've burned half your context window before the conversation starts.

His take: skip MCP entirely, write simple CLI tools instead.

But the MCP ecosystem has useful stuff - databases, browsers, APIs. This adapter gives you access without the bloat. One proxy tool (~200 tokens) instead of hundreds. The agent discovers what it needs on-demand. Servers only start when you actually use them.

Install

pi install npm:pi-mcp-adapter

Restart Pi after installation.

DeepSeek Harness (third-party bridge): Run the unmodified adapter in DSH via pi2dsh; see the verified dsh-TUI and Web MCP guide.

What happens on first run

The adapter reads standard MCP files automatically. No extra setup needed if you already have them.

You already have...What happens
.mcp.json or ~/.config/mcp/mcp.jsonPi uses it immediately. Use .mcp.json for project/team sharing and ~/.config/mcp/mcp.json for all projects.
Host-specific configs (Cursor, Claude Code, Codex, etc.) but no standard MCP filesRun /mcp-adapter setup to adopt those host configs into Pi. The setup flow shows exactly what it found, lets you pick which ones to import, and previews the exact file changes before writing.
Nothing configured yetRun /mcp-adapter setup, choose project .mcp.json or global ~/.config/mcp/mcp.json, then scaffold a minimal config, add a curated known server, quick-add RepoPrompt, or inspect what the adapter discovered on your machine.

If you prefer the terminal, you can also run pi-mcp-adapter init after install to scan for host-specific configs and add missing compatibility imports to the adapter config (~/.pi/agent/mcp-adapter.json by default, or $PI_CODING_AGENT_DIR/mcp-adapter.json when set).

Quick Start

Preferred project config: .mcp.json

{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": ["-y", "chrome-devtools-mcp@1.6.0"]
    }
  }
}

Preferred user-global shared config: ~/.config/mcp/mcp.json (for all projects). Pi also reads the tool-agnostic global paths ~/.agents/mcp.json and ~/.agents/mcp/mcp.json as compatibility inputs.

The adapter does not read Pi's <Pi agent dir>/mcp.json or .pi/mcp.json at all. If you previously used either file with this adapter, rename it to mcp-adapter.json; the format is unchanged, so a plain mv works (merge the files if the target already exists). This leaves mcp.json exclusively to Pi's built-in MCP support, so Pi and the adapter never start the same servers.

Host-specific configs are detected and shown by /mcp-adapter setup and pi-mcp-adapter init, but they are compatibility inputs rather than normal setup paths and are not loaded automatically. The normal /mcp-adapter panel does not scan host-specific files when settings.hostConfigDiscovery is "off". To explicitly opt in to host-config fallback discovery, set settings.hostConfigDiscovery to "on" or run pi-mcp-adapter init --discover-host-configs. The default is "off"; "prompt" is available for integrations that want detection without activation. Host configs are lower precedence than every normal config source, and /mcp-adapter setup continues to offer explicit import adoption. Discovery reports source paths, provenance, and same-name conflicts; it never writes to external host files or silently launches commands from them.

Precedence is (later entries win):

  1. ~/.config/mcp/mcp.json
  2. ~/.agents/mcp.json
  3. ~/.agents/mcp/mcp.json
  4. <Pi agent dir>/mcp-adapter.json
  5. opted-in ancestors, farthest first: .mcp.json, .pi/mcp-adapter.json
  6. .mcp.json
  7. .pi/mcp-adapter.json

Ancestor discovery is off by default. To opt in, set settings.ancestorConfigRoots in a user-global source (~/.config/mcp/mcp.json, either ~/.agents MCP file, or the global mcp-adapter.json) or in the explicitly selected --mcp-config/configPath file, for example "ancestorConfigRoots": ["~/work/team"]. Each root must be an explicit absolute path or ~/... and resolve to an existing directory under $HOME. Roots that do not contain the canonical cwd are ignored. If several roots match, only the nearest (deepest) is used. Project files cannot enable discovery or extend the boundary.

Within the selected root, .mcp.json and <configDir>/mcp-adapter.json load from the root through parent(cwd), farthest first. Nearer directories override farther ones, adapter config overrides shared config within each directory, and cwd files win over ancestors. Search never goes above the configured root or $HOME; the boundary limits discovery but is not a file-ownership or symlink-target sandbox. Only configure roots whose project files you trust.

/mcp-adapter disable <server> and /mcp-adapter enable <server> persist only the disabled field in the project-local .pi/mcp-adapter.json, the highest-precedence adapter layer. The source file is never rewritten and credentials are never copied. Run /reload after changing the flag so registered tool surfaces are refreshed. Supplied in-memory createMcpAdapter({ config }) configurations are isolated and do not read or write this project override; the commands are unavailable in that mode.

Servers are lazy by default — they won't connect until you actually call one of their tools. The adapter caches tool metadata so search and describe work without live connections.

mcp({ search: "screenshot" })
chrome_devtools_take_screenshot
  Take a screenshot of the page or element.

  Parameters:
    format (enum: "png", "jpeg", "webp") [default: "png"]
    fullPage (boolean) - Full page instead of viewport
mcp({ tool: "chrome_devtools_take_screenshot", args: { format: "png" } })

args can be a JSON object or a JSON string. Prefer the object form when your model handles it reliably; the string form remains supported for providers that need simpler schemas.

Two calls instead of 26 tools cluttering the context.

Config

File Layout

Use shared MCP files when you want one setup to work across hosts, and adapter-owned files for adapter-specific overrides or settings.

FilePurpose
~/.config/mcp/mcp.jsonUser-global shared MCP config
~/.agents/mcp.jsonUser-global tool-agnostic MCP config
~/.agents/mcp/mcp.jsonUser-global tool-agnostic MCP config
.mcp.jsonProject-local shared MCP config
<Pi agent dir>/mcp.jsonPi built-in MCP config; never read by this adapter
<Pi agent dir>/mcp-adapter.jsonGlobal adapter settings, imports, and overrides (~/.pi/agent/mcp-adapter.json by default)
.pi/mcp.jsonProject Pi built-in MCP config; never read by this adapter
.pi/mcp-adapter.jsonProject adapter settings and overrides

For local stdio servers, a leading ~/ is expanded to the current user's home directory in command, args, and cwd. On Windows, the equivalent ~\\ form is supported too; on POSIX, backslashes remain literal filename characters. Bare commands such as node, bunx, or git continue to resolve through PATH. Built-in Agent Plugin arguments remain literal; this path expansion applies to native and shared MCP configuration.

Pi-specific files are the write targets for imported or shared global servers when Pi needs to persist adapter-only settings such as directTools.

Agent Plugins

The adapter can load MCP servers from Agent Plugins packages when you list plugin directories in settings.agentPluginPaths:

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

Each directory must contain a valid Agent Plugins 1.0 plugin.json. If it also has a root mcp.json, the adapter loads its mcpServers entries and prefixes them as <plugin>__<server>. The loader uses the Agent Plugins transport declared by each server type and skips invalid entries without blocking other servers. For stdio plugin servers, ${PLUGIN_ROOT} and ${PLUGIN_DATA} are expanded only in args, env, and cwd; the adapter sets both variables for the child process and stores plugin data under the Pi agent directory.

inheritEnv is an adapter-specific field, not an Agent Plugins or OpenCode schema field. Do not add it to a plugin's strict mcp.json; to opt a plugin stdio server out of host-environment inheritance, set inheritEnv: false in an mcp-adapter.json override using the translated <plugin>__<server> name:

{
  "mcpServers": {
    "acme_tools__local": { "inheritEnv": false }
  }
}

Agent Plugins is a portable package format. Native adapter config remains .mcp.json, ~/.config/mcp/mcp.json, and mcp-adapter.json overrides.

Local Claude plugin bundles

The adapter can opt into MCP servers and Pi skills from explicitly configured local Claude plugin directories:

{
  "claudePlugins": [
    { "path": "./plugins/acme-tools", "mcp": true, "skills": true }
  ],
  "mcpServers": {}
}

Each entry needs a non-empty path and must enable mcp, skills, or both. The root-level field can be set in any normal adapter config source; normal config-source precedence applies, and a higher-precedence claudePlugins array replaces a lower one. Relative paths in file-based config resolve from the active project cwd. For createMcpAdapter({ config }), relative plugin paths are normalized against process.cwd() when the factory is created; this explicit API-boundary snapshot keeps early registration and session startup on the same local bundle even when the host's context cwd differs. mcp: true reads only the plugin's root .mcp.json; skills: true discovers skills/**/SKILL.md inside the plugin and passes those files through Pi's normal resource discovery, including startup and /reload. A .claude-plugin/plugin.json manifest is optional, matching Claude's plugin format, but when present it must be valid JSON with a kebab-case name and valid standard field types. Manifest path overrides are intentionally not followed.

Claude plugin MCP server names are used as written. The first explicitly listed plugin wins same-name conflicts between plugin bundles, while every normal Pi MCP config source overrides these plugin defaults. ${CLAUDE_PLUGIN_ROOT} is expanded in plugin MCP server fields, and stdio servers receive it in their environment. Skills use Pi's existing skill parsing and conflict handling.

This is an explicit local trust boundary: the adapter does not discover, download, install, or update plugins; execute plugin hooks; or fetch skills from MCP instructions. It resolves plugin components inside each configured directory and rejects component symlinks that escape it. Config and skills are read during discovery, but MCP commands are still lazy and run only when normal adapter lifecycle/tool use connects that server. Enable mcp only for plugin directories whose commands and configuration you trust.

Pi package manifests

A Pi package can ship MCP servers for the installed adapter without requiring a separate MCP config file. Declare a package-relative config in its package.json:

{
  "pi": {
    "mcp": "./mcp.json"
  }
}

pi.mcp can also be an array of package-relative paths. Each file uses the normal mcpServers object shape, but package manifests load only server entries: package settings and imports are ignored. Server names are prefixed with the sanitized package name, such as acme_tools__docs, and user/global/project MCP config has higher precedence. The adapter loads only Pi packages listed in Pi settings; it does not scan node_modules.

Runtime registration from other extensions

An extension can register MCP servers with the installed adapter at runtime, for example a plugin host that discovers plugins after load:

const MCP_RUNTIME_REGISTER_EVENT = "pi-mcp-adapter:runtime-register:v1";
type RuntimeRegistrationRequest = {
  version: 1;
  name: string;
  definition: { url: string };
  result?:
    | { ok: true; registration: { dispose(): Promise<void> } }
    | { ok: false; error: Error };
};

export default function pluginHost(pi) {
  let registration: { dispose(): Promise<void> } | undefined;

  pi.on("session_start", () => {
    if (registration) return;
    const request: RuntimeRegistrationRequest = {
      version: 1,
      name: "acme__docs",
      definition: { url: "https://mcp.example.com/mcp" },
    };
    pi.events.emit(MCP_RUNTIME_REGISTER_EVENT, request);
    if (!request.result) throw new Error("pi-mcp-adapter is not installed");
    if (!request.result.ok) throw request.result.error;
    registration = request.result.registration;
  });

  pi.on("session_shutdown", async () => {
    const current = registration;
    registration = undefined;
    await current?.dispose();
  });
}

Cross-extension registration uses Pi's shared event bus and does not require a runtime import from pi-mcp-adapter. Emit during session_start or later so the adapter listener is installed. The adapter writes request.result synchronously; the first adapter listener to respond wins.

Runtime registrations are session scoped and never written to config files. Duplicate server names fail closed against configured servers and other registrations. Registered servers use the normal lazy connection, OAuth, approval, and shutdown behavior, but they are proxy-tool-only and their tools become visible at the next tool sync. To change a definition, dispose the registration and register again.

SDK configuration

Use createMcpAdapter when an SDK or server integration already owns its MCP configuration:

import { createMcpAdapter } from "pi-mcp-adapter";

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

// Register `extension` with the host SDK.

The package ships TypeScript source for Pi's source-loader and SDK integrations. Use a TypeScript-capable loader/toolchain (for example node --import tsx) when importing the package from a standalone Node process; raw Node ESM does not execute the .ts entry directly.

A supplied config is a complete, isolated snapshot. It is not merged with files, imports, global config, project config, or --mcp-config, and it is never mutated. Explicit claudePlugins entries are the sole exception to file isolation: their configured local directories are read because they are part of that supplied snapshot. Relative programmatic plugin paths are normalized against process.cwd() when createMcpAdapter creates the factory, so the early model-facing surface, load-time initialization, and later session runtime all use the same bundle. Each adapter factory and session receives its own clone, so separate integrations can use different servers and settings safely. In this mode, server status, reconnect, explicit /mcp-auth <server>, proxy calls, and direct tools continue to work; setup and no-argument auth/status panels report the limitation instead of discovering or writing ambient config.

With configPath and no config, the adapter keeps normal file merge behavior, and that path takes precedence over argv and --mcp-config. The default export keeps the normal file-based behavior. OAuth credentials are stored in the operating system credential store and keyed by the configured server name; URL binding prevents credentials from being accepted for a different server URL. settings.oauthDir and MCP_OAUTH_DIR are used only as legacy plaintext import locations for older tokens.json files, not as credential namespaces. CSRF state and PKCE verifiers are flow-local, so concurrent authorization flows do not share transient secrets.

Cooperating Pi extensions can use pi-mcp-adapter/oauth to reuse URL-bound OAuth tokens without deep-importing private files:

import { getMcpOAuthTokensForUrl, updateMcpOAuthTokensForUrl } from "pi-mcp-adapter/oauth";

const tokens = await getMcpOAuthTokensForUrl("jira", "https://jira.example.com/mcp");
await updateMcpOAuthTokensForUrl("jira", "https://jira.example.com/mcp", { accessToken: "..." });

The public subpath exposes only token read/update helpers plus a status helper. The async read path uses the adapter's refresh logic before it returns tokens. For a service-protected endpoint or a pre-registered OAuth client, pass the explicit refresh configuration as getMcpOAuthTokensForUrl(name, url, { definition: { headers, oauth } }). This optional configuration is never loaded from ambient config or stored with the tokens; headers are bound to the supplied MCP URL's origin. The helpers keep secure-store storage, URL binding, refresh persistence, chunk handling, legacy import, and fail-closed credential-store errors. They do not expose client registration secrets, PKCE verifiers, or OAuth state.

Runtime status snapshots

Extensions can subscribe to the adapter's versioned shared event-bus channel instead of parsing /mcp-adapter or mcp({}) output:

import { MCP_STATUS_EVENT, type McpStatusSnapshot } from "pi-mcp-adapter";

pi.events.on(MCP_STATUS_EVENT, (snapshot) => {
  const status = snapshot as McpStatusSnapshot;
  // status.servers contains connected, cached, failed, needs-auth,
  // not-connected, or disabled entries.
});

The snapshot is read-only machine-readable data with copied per-server entries. It includes totalTools, totalResources, connectedCount, and disabledCount; each server includes name, status, toolCount, directToolCount (the number of tools currently registered directly with Pi, including direct resource tools), and disabled, with resourceCount when known and failedAgoSeconds only for an active failure. Reading status never connects a lazy server, starts authentication, or exposes SDK clients, transports, credentials, or server definitions. An initial snapshot is emitted after initialization, updates are emitted for status and metadata changes, and an empty snapshot is emitted when the session shuts down. Initialization withholds that first snapshot until authoritative metadata has been reconciled into Pi's active direct-tool registry. A connected snapshot therefore follows model-facing tool-surface synchronization, including removal of stale cached tools when the authoritative catalog is empty.

In the configuration examples below, 30000 is illustrative only. If requestTimeoutMs is omitted or set to <= 0, the MCP SDK default timeout is used.

Server Options

{
  "mcpServers": {
    "my-server": {
      "command": "npx",
      "args": ["-y", "some-mcp-server"],
      "lifecycle": "lazy",
      "idleTimeout": 10,
      "requestTimeoutMs": 30000
    }
  }
}
FieldDescription
commandExecutable for stdio transport; mutually exclusive with url and socket
argsCommand arguments
socketExplicit rmcp-mux Unix-domain socket path; supports ${VAR}, $env:VAR, and ~ expansion and is mutually exclusive with command and url
envEnvironment variables; supports ${VAR} and $env:VAR interpolation. A value beginning with ! runs a command when the stdio server connects; use !! for a literal leading !.
inheritEnvStdio only; defaults to true and preserves full host-environment inheritance. Set to false to exclude arbitrary host variables from the MCP child and SDK negotiation sibling while retaining SDK platform defaults and explicit env overlays. This is not an empty environment or an OS sandbox.
cwdWorking directory; supports ${VAR}, $env:VAR, and ~ expansion
urlHTTP endpoint (StreamableHTTP with SSE fallback); supports raw ${VAR} and $env:VAR interpolation, and missing URL variables fail before any request is sent
headersHTTP headers; supports ${VAR} and $env:VAR interpolation. A value beginning with ! runs a command when the HTTP server connects or OAuth authenticates; use !! for a literal leading !.
requestHeadersCommandTrusted executable run for every HTTP request. It receives a versioned JSON envelope containing method, url, and the exact bodyBase64 on stdin, and must return a JSON object of headers on stdout. command, args, and env support environment interpolation. Use for caller-bound request signatures; failures stop the request.
caFileHTTPS HTTP servers only: local PEM CA certificate/bundle, e.g. "caFile": "~/certs/local-ca.pem". Replaces (does not add to) default roots for the resolved MCP origin. Supports environment interpolation and ~; relative paths use the process working directory. Unreadable/invalid files fail closed; hostname and certificate-expiry verification remain enabled.
auth"bearer" or "oauth"
oauth.grantType"authorization_code" (default) or "client_credentials" for non-interactive machine auth
oauth.clientIdPre-registered OAuth client ID. Takes precedence over oauth.clientMetadataUrl when both are set.
oauth.clientSecretOAuth client secret for confidential clients; a value beginning with ! runs a command when OAuth authenticates, while !! escapes a literal leading !. Combining it with oauth.clientMetadataUrl requires an explicit oauth.clientId.
oauth.clientMetadataUrlAdvanced opt-in for an operator-supplied public HTTPS Client ID Metadata Document (CIMD) URL with a non-root path. Used as the client_id when the authorization server advertises CIMD support; otherwise the adapter falls back to Dynamic Client Registration. The adapter does not provide or host a default document.
oauth.scopeRequested OAuth scopes
oauth.redirectUriRedirect URI for browser OAuth. Dynamic clients normally omit it and use an OS-assigned localhost callback port. Local http:// loopback URIs accept an explicit port or {port} for an OS-assigned port (for example, http://127.0.0.1:{port}/callback). Pre-registered https:// callbacks use manual completion by pasting the full callback URL.
oauth.clientNameClient display name advertised during Dynamic Client Registration fallback
oauth.clientUriClient homepage URI advertised during Dynamic Client Registration fallback. Defaults to piConfig.clientUri from the host's manifest when set, and is omitted rather than guessed under a rebranded host
oauth.logoUriClient logo URL advertised during Dynamic Client Registration fallback (RFC 7591 logo_uri). Must be an absolute http(s) URL — consent screens fetch it server-side, so local paths render nothing. Omitted from the registration request when unset
oauth.authServerMetadataUrlHTTPS URL of an OAuth/OIDC authorization-server metadata document. When set, this document is authoritative instead of MCP protected-resource discovery; its issuer remains validated by default
oauth.skipIssuerMetadataValidationtrue disables the OAuth authorization-server metadata issuer check for this server. This weakens OAuth mix-up protection and should only be used for known-misconfigured internal servers while their metadata is being fixed.
bearerToken / bearerTokenEnvToken or env var name; bearerToken supports ${VAR} and $env:VAR interpolation. A leading ! in bearerToken runs a command when the HTTP server connects; use !! for a literal leading !.
bearerTokenStoreSet to true to read a static bearer token from the adapter-owned OS credential store when auth is "bearer" and no bearerToken or bearerTokenEnv is configured. Stored records are keyed only by the server name, bind to the resolved server URL, and are never named by config. Store a token with pi-mcp-adapter token set <server>, which reads it from a masked prompt or stdin pipe and never from an argument. /mcp-adapter token status <server> and /mcp-adapter token remove <server> manage non-secret state inside Pi; /mcp-adapter token set stays disabled until Pi exposes masked secret input.
lifecycle"lazy" (default), "eager", "keep-alive", or "lazy-keep-alive"
idleTimeoutMinutes before idle disconnect (overrides global)
requestTimeoutMsRequest timeout in milliseconds for live MCP calls (overrides global; if omitted or <= 0, the MCP SDK default timeout is used)
protocolVersion"legacy" (default), "auto", or "2026-07-28"; modern negotiation is opt-in
tasksMCP Tasks extension support on 2026-07-28 connections (default: true; set false to opt out); see Task-augmented tool calls
exposeResourcesExpose MCP resources as tools (default: true)
directToolstrue, string[], or false — register tools individually instead of through proxy
toolPrefixOverride global settings.toolPrefix for this server ("server", "short", "none", or "mcp")
includeToolsstring[] of tool names or glob patterns to expose (matches original names like get_screenshot, generated resource names like read_figjam, and prefixed names like figma_get_screenshot)
excludeToolsstring[] of tool names or glob patterns to hide (applied after includeTools)
searchKeywords{ "tool-or-glob": ["keyword", ...] } — extra keywords that boost mcp({ search }) ranking for matching tools; never shown to the model
debugShow server stderr (default: false)
traceEnable metadata-only JSONL protocol tracing for this server; payloads, prompts, tool arguments/results, authorization data, and URLs are never persisted
disabledKeep the server visible in config and status, but prevent connections, authentication, tools, and resource calls (only literal true disables it)

Custom HTTPS trust

caFile works with Streamable HTTP, SSE, and per-request header commands. Requests using this trust reject all redirects; configure the final HTTPS endpoint directly. Other origins and servers retain default trust. Layered configuration drops inherited trust when replacing the URL or switching away from HTTP. This option covers the MCP origin, including connection-owned OAuth requests to that exact origin, but not the separate interactive OAuth flow or private-CA authorization servers on other origins. Thanks to @desmonna for #527.

macOS local-network access

On macOS 15+, Local Network Privacy may deny access to a LAN MCP server depending on the app responsible for hosting Pi. For HTTP URLs with literal private/link-local IPv4 or IPv6 addresses, the adapter adds a hint to EHOSTUNREACH, ENETUNREACH, or EACCES connection errors while retaining the original cause. These codes can also mean routing or firewall trouble; the hint is not proof of a privacy denial. Hostnames are not resolved for this diagnostic.

Check System Settings > Privacy & Security > Local Network for the hosting app, enable access if listed, then restart that app and Pi. If it is absent or access still fails, try launching Pi directly from Apple Terminal.app or over SSH (contexts Apple documents as exempt). Permission is attributed to responsible code, not necessarily Node or Pi; signing an unsigned CLI alone does not guarantee a permission prompt or fix host attribution. See Apple TN3179.

Protocol version negotiation

The adapter defaults to protocolVersion: "legacy". Omitting the field uses the classic MCP initialize sequence without server/discover or 2026 headers, preserving compatibility with deployed 2025-era servers.

Use "auto" to probe for MCP 2026-07-28 and conservatively fall back to the classic handshake when the server provides legacy evidence. Set it for Cloudflare Workers createMcpHandler and other MCP SDK v2 stateless servers. The adapter keeps "legacy" as the global default for compatibility. For stdio servers, the SDK probes with a short-lived sibling process before starting the session process, so each fresh auto connection adds one process spawn and can wait for the configured request timeout. Explicit Unix sockets are custom transports and probe in place. HTTP auto negotiation uses the actual Streamable HTTP connection; the adapter falls back to legacy SSE only when the endpoint definitively rejects Streamable HTTP (for example 404/405/406/415), never for authentication failures, cancellation, timeouts, or server errors.

Use "2026-07-28" to pin that revision. Pinning has no legacy or SSE fallback and fails if the server does not offer the requested version.

Task-augmented tool calls

The adapter supports the MCP Tasks extension (io.modelcontextprotocol/tasks, SEP-2663), which lets long-running tools return a durable task handle instead of blocking the connection. Support is negotiated per connection and needs no configuration: the task session only activates when a 2026-07-28 connection's server advertises the extension, so nothing changes for servers without task support. Set tasks: false on a server to opt out and keep the plain synchronous call path. Legacy (2025-11-25) experimental tasks are not supported.

When active, tool calls keep their normal contract from the model's point of view:

  • A tool that returns a task handle is transparently polled to completion, honoring the server's suggested poll interval; the final result is returned as if the call had been synchronous.
  • If the task pauses for input (input_required), elicitation requests are routed through the same interactive elicitation UI as direct elicitation/create requests, and answers are delivered back via tasks/update.
  • Cancelling the Pi tool call sends a cooperative tasks/cancel to the server.
  • A task that fails with a JSON-RPC error surfaces as the same error a synchronous call would have produced; a tool result with isError: true is returned as a normal tool error.

Task traffic is dispatched on a dedicated raw channel below the SDK client (the published MCP SDK does not yet decode task result shapes itself), built on the official @modelcontextprotocol/ext-tasks requester package. The channel chains onto the connected transport's handlers without replacing the transport, and raw task frames appear in /mcp-trace in both directions. Task status notifications (notifications/tasks) are not consumed; polling is used exclusively. requestTimeoutMs applies per task request (the initiating call and each poll), not to the overall task duration — a task that runs for hours holds the Pi tool call for as long as the model waits for it.

One trade-off while tasks are active: every tools/call on that connection is dispatched through the task-aware path instead of Client.callTool, so the SDK's client-side output-schema validation of structuredContent and SEP-2243 Mcp-Param-* header mirroring do not run for those calls. Servers still validate their own results; only the client-side double-check is skipped.

The stable SDK handles era-specific request envelopes, result decoding, list-changed subscriptions, cancellation, and multi-round-trip sampling/elicitation. The SDK's embedded-input progress callback does not expose the originating tool or resource identity, so the adapter cannot maintain a durable per-tool waiting status row; interactive sessions keep the existing input dialog visible, and proxy calls show request progress when UI is available. The adapter keeps strict OAuth issuer validation in every mode. Adapter-level roots support, standard MCP logging presentation, and configuration/UI for protocol cache hints are not yet implemented.

If an internal authorization server publishes mismatched OAuth metadata and cannot be fixed immediately, set oauth.skipIssuerMetadataValidation: true on that server only. This is security-weakening. It disables the RFC 8414 issuer echo check and should not be used for public or untrusted servers.

If an MCP server does not publish usable protected-resource metadata, set oauth.authServerMetadataUrl to its HTTPS OAuth/OIDC authorization-server metadata document. The configured document is used authoritatively, while issuer validation remains enabled by default. This is trusted configuration; use it only for a metadata endpoint you control or explicitly trust.

URL-only/default Pi OAuth continues to use Dynamic Client Registration; there is no project-hosted default Client ID Metadata Document. To explicitly opt into CIMD as an advanced operator setting, publish the OAuth client metadata at a stable public HTTPS URL and set oauth.clientMetadataUrl to that exact URL. The adapter uses it as the URL-based client_id only when discovered authorization-server metadata contains client_id_metadata_document_supported: true; servers without CIMD support continue through Dynamic Client Registration. An explicit oauth.clientId always wins, and oauth.clientSecret without that explicit ID cannot be combined with oauth.clientMetadataUrl.

Stdio environment boundaries

inheritEnv: false applies only to the actual MCP stdio server process and, for protocolVersion: "auto" or "2026-07-28", its disposable SDK negotiation sibling. It does not change the default for other servers: omitting the field or setting it to true preserves the existing full host-environment inheritance. With false, the SDK still supplies its platform defaults and configured env values remain explicit overlays; the result is not a literally empty environment and is not an OS sandbox.

Environment interpolation remains intentional. ${VAR}, $env:VAR, and {env:VAR} values still read selected host variables and can place those values in the child. literalEnv: true keeps its existing behavior by treating configured stdio env values as literals. The following helper boundaries are unchanged and still retain the full host environment even when a server uses inheritEnv: false:

  • npm/npx cache resolution and cache-population subprocesses;
  • !command secret helpers used by stdio env (and other secret fields); and
  • the HTTP requestHeadersCommand helper.

For tighter use, configure a direct executable instead of npm/npx and avoid !command secret helpers. This option limits stdio child inheritance only; it does not provide complete multi-agent or helper-process isolation.

With explicit auth: "oauth", configured HTTP headers also accompany native OAuth metadata discovery (including oauth.authServerMetadataUrl), dynamic registration, code exchange, and refresh, only at the configured MCP URL's origin (scheme, host, and port). Discovered or explicitly configured cross-origin OAuth endpoints receive no configured service headers. SDK-owned headers such as OAuth Authorization and content types take precedence over configured headers. Requests carrying configured service headers reject all HTTP redirects, including same-origin redirects; configure the final endpoint directly. Browser authorization navigation and loopback callbacks do not use these headers. Missing or empty header credentials fail closed.

requestHeadersCommand follows the fetch path, not the URL path: during a server connection, it wraps the SDK transport fetch (requestFetch), so it runs for MCP requests and SDK-owned OAuth requests using that fetch, including discovery, dynamic registration, token exchange (including client_credentials), and refresh. It also runs for cross-origin OAuth endpoints: unlike configured headers, command-produced headers are not origin-scoped. The command receives each request's exact method, URL, and body and must decide where its credentials belong. Its returned headers are applied last, overriding even SDK Authorization and content types on name collisions; avoid those names unless intentional.

Provider-owned metadata loading through authFetch (notably oauth.authServerMetadataUrl) bypasses the command, even during a connection. Standalone OAuth start/complete/refresh helpers use their own OAuth fetch, not the transport wrapper, and also bypass it. Browser authorization navigation and loopback callbacks never invoke the command. Thus this is transport-fetch signing, not a hook for every OAuth interaction.

Secret values in headers, bearerToken, oauth.clientSecret, and stdio env may use a leading !command to obtain their value at connection or authentication time. The command runs with stdin and stderr suppressed, stdout is limited to 1 MiB and trimmed, and it must finish within 10 seconds with non-empty output; failures stop the connection or authentication flow. Commands are not run during the preliminary MCP OAuth challenge probe or while reading, merging, previewing, hashing, or rendering configuration. OAuth header commands resolve lazily for the actual SDK backchannel requests, once per authentication leg or connection; the preliminary probe omits command headers. Use !! to escape a literal leading !; ordinary and escaped values retain environment interpolation.

For local desktop bearer tokens, bearerTokenStore: true can opt in to the adapter-owned credential-store namespace. It never falls back to plaintext if the store is unavailable, if the stored record is malformed, or if the stored URL differs from the effective server URL. Literal tokens, command tokens, and environment tokens keep precedence so existing configs do not change. Create or rotate a stored token with pi-mcp-adapter token set <server> (masked prompt on a terminal, or piped stdin such as security find-generic-password -s my-token -w | pi-mcp-adapter token set <server>); the record binds to the effective configured URL at write time. Token commands need Node 22.18+.

On Linux, bearer-token and System One key storage also recover automatically when a native operation fails with KeyRevoked, including wrapped errors from a revoked inherited session keyring. Each failed read/write/remove is retried once through keyctl session - <current runtime> <packaged helper>, with a 10-second timeout and no plaintext fallback. This requires keyctl on PATH and a working credential store in the fresh session; other storage errors still fail closed. Set PI_MCP_ADAPTER_DISABLE_KEYRING_RECOVERY=1 to disable this recovery. Normal Pi and token CLI launches need no special wrapper.

Shared MCP processes with rmcp-mux

To share one stdio MCP server across Pi sessions, run it under rmcp-mux and point each session at the service socket:

{
  "mcpServers": {
    "memory": {
      "socket": "~/.rmcp-servers/rmcp-mux/sockets/memory.sock"
    }
  }
}

The adapter owns only its client socket and closes that connection when the Pi runtime stops. rmcp-mux owns the upstream process, request routing, initialization cache, restart policy, client limits, and socket permissions. Start and configure the mux separately; the adapter never discovers, starts, adopts, or stops its daemon. A socket is an explicit trusted local endpoint, so do not point unrelated projects or users at a mux service unless its tools, state, credentials, and filesystem access are intended to be shared.

Install from one URL

Install an MCP endpoint without editing configuration:

mcp({ action: "install", url: "https://example.com/mcp" })

Install validates and connects the endpoint. New entries use a name derived from the hostname and are saved to the global mcp-adapter.json; existing URL entries are reused without rewriting. Pass server to choose a name or target: "project" to save to the project's .mcp.json. Unsafe URLs, name collisions, and failed connections are not persisted.

Set settings.allowInstall to false in mcp-adapter.json or another adapter config source (not Pi's mcp.json or settings.json) to block mcp({ action: "install" }) for constrained or headless agents. Connect, search, tool calls, authentication, runtime registration, and interactive setup are unaffected.

In exclusive config mode, a project target must be the active config path; otherwise use the global target. URL install cannot promote runtime-registered servers: save their complete definitions manually so required headers and transport/auth settings are retained.

Public servers are ready immediately. For OAuth servers, the same action opens the authorization page and watches a reachable loopback callback. After the user grants consent, an mcp-oauth-status message returns the agent to connect the server and verify its discovered tools. Remote/headless callbacks retain the manual completion fallback below.

Remote/headless OAuth

If Pi is running on a remote server, /mcp-auth <server> shows a clickable authorization URL first. Open it in your local browser and approve access, then select Yes in Pi to open the callback input. The browser may fail to load the localhost callback page because localhost refers to your workstation; copy the full URL from its address bar and paste it into Pi. The authorization screen closes automatically instead when the browser can reach Pi's callback directly.

The same flow is available through the proxy tool for non-interactive clients. By default, persistent OAuth requires an available OS credential store; on headless Linux that usually means an unlocked Secret Service/libsecret keyring. The adapter fails closed instead of falling back to plaintext credentials when the secure store is unavailable.

Windows OpenSSH network logons can return ERROR_NO_SUCH_LOGON_SESSION (1312) because Credential Manager is unavailable to that logon. For this case, explicitly set settings.oauthCredentialStore to "encrypted-file" and inject PI_MCP_ADAPTER_OAUTH_FILE_KEY as canonical base64 for 32 random bytes (node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"). Encrypted entries live under the Pi agent directory's mcp-oauth-encrypted/; keep the key separately and reauthenticate after loss or rotation. This backend never falls back to the OS store or imports legacy plaintext; see OAuth for its security model.

On Linux, if credential access fails because Pi inherited a revoked session keyring, the adapter uses a best-effort recovery path through keyctl session - node <packaged helper> so explicit re-authentication can write fresh credentials without killing a long-lived tmux server. This path requires keyctl and node on PATH; missing, locked, or otherwise unavailable credential stores still fail closed.

mcp({ action: "auth-start", server: "linear-server" })

For a loopback redirect, the adapter attempts to open the returned authorization URL, watches the callback, completes token exchange, and sends an mcp-oauth-status event when authentication finishes. If Pi is remote or cannot open a browser, open the returned URL locally. If the browser cannot reach Pi's callback, copy the full localhost URL from the address bar and complete the flow in the same Pi session:

mcp({
  action: "auth-complete",
  server: "linear-server",
  args: { redirectUrl: "http://localhost:19876/callback?code=...&state=..." }
})

You can also pass only the code query parameter with args: { code: "..." }. Treat authorization URLs and codes as sensitive; they can grant access to the MCP server until the flow expires or completes.

Project server trust

Servers defined or changed by project-scoped MCP files (.mcp.json, .pi/mcp-adapter.json, and opted-in ancestor project files) do not run merely because a repository was opened. This includes servers brought in through project imports, claudePlugins, settings.agentPluginPaths, repo-local host config files (even when imports or discovery are enabled globally), or Pi packages listed in project .pi/settings.json. If Pi reports the project as untrusted, the adapter blocks them. In a trusted interactive session, the adapter shows the source file and command or URL and asks once before the first connection. The approval is stored under the Pi agent directory and is tied to the canonical project path, server name, and complete effective definition; changing the definition requires a new approval. Blocked servers are identified in the panel, while MCP status includes the required trust or approval action.

In print, JSON, and RPC sessions, an unapproved project server is skipped. To intentionally allow project servers in trusted headless sessions, set "projectServers": "allow" in the user-global settings object. The default is "ask"; project files cannot change this policy. Explicit config files, programmatic configuration, global/import/plugin servers, and runtime registrations retain their existing behavior. Project servers are always excluded from extension-load initialization and are admitted only after session_start supplies Pi's trust context.

Lifecycle Modes

  • lazy (default) — Don't connect at startup. Connect on first tool call. Disconnect after idle timeout. Cached metadata keeps search/list working without connections.
  • eager — Connect at startup but don't auto-reconnect if the connection drops. No idle timeout by default (set idleTimeout explicitly to enable).
  • keep-alive — Connect at startup. Remote HTTP servers refresh their tool catalog during health checks, before user input, and before adapter-triggered turns, reconnecting when the server reports that the session expired. No idle timeout. Use for servers you always need available.
  • lazy-keep-alive — Don't connect at startup. Connect on first tool call (like lazy). Once spawned, never idle-shut down and use the same catalog refresh and reconnect checks as keep-alive. Use for servers that are expensive to start but should stay resident after their first use.

For remote HTTP keep-alive servers, the authoritative tools/list refresh is also the fallback when list_changed notifications are unavailable or their stream is lost. Each tools/list or ping request is capped at 5 seconds, up to 10 servers are checked concurrently, and transient failures use bounded backoff. A successful refresh updates metadata without reconnecting; a response proving that the HTTP session expired triggers a full reconnect and reinstalls the notification handlers. Dynamic direct-tool registration follows the refreshed metadata unless freezeDirectTools is enabled.

When any enabled server uses eager or keep-alive, initialization also starts when the extension loads. This supports hosts that embed Pi programmatically and never emit session_start; if a session does start later, the session-owned runtime supersedes the load-time runtime.

Settings

{
  "settings": {
    "toolPrefix": "server",
    "allowInstall": false,
    "idleTimeout": 10,
    "requestTimeoutMs": 30000,
    "deferWithMissingMetadata": false,
    "showStatusIcon": true,
    "mcpFooterStatus": "full",
    "toolResultRendering": "compact",
    "collapsedResultLines": 1,
    "notifyOnStartupConnect": true,
    "warnOnLargeDirectTools": true,
    "hostConfigDiscovery": "off",
    "projectServers": "ask",
    "approveTools": ["github_delete_*", "notion_update_*"],
    "oauthDir": ".pi/mcp-oauth",
    "trace": {
      "enabled": true,
      "file": ".pi/mcp-traces/mcp.jsonl",
      "maxBytes": 262144,
      "maxEvents": 10000
    }
  },
  "mcpServers": { }
}
SettingDescription
toolPrefix"server" (default), "short" (strips -mcp suffix), "none", or "mcp" (prefixes with mcp__, using server-mode normalization). Per-server toolPrefix overrides this for that server.
allowInstallAllow URL installation through the mcp tool (default: true). Set to false to block it.
idleTimeoutGlobal idle timeout in minutes (default: 10, 0 to disable)
requestTimeoutMsGlobal request timeout in milliseconds for live MCP calls (if omitted or <= 0, the MCP SDK default timeout is used)
deferWithMissingMetadataAllow lazy startup to defer when persisted metadata is missing or invalid (default: false). See Direct Tools for the startup tradeoff.
showStatusIconShow the plug icon in MCP status and connection text (default: true). Set to false for plain MCP: ... text.
mcpFooterStatusMCP footer verbosity: "full" (default), "compact" for MCP connected/enabled, or "off" to clear the persistent footer status. /mcp-adapter status remains available.
toolResultRenderingMCP tool result row style: "compact" (default) uses self-rendered rows, or "boxed" restores the legacy Pi boxed tool row.
collapsedResultLinesNumber of result text lines to show before expansion: 1, 2, or 3. Defaults to 1 in compact mode and 3 in boxed mode.
notifyOnStartupConnectShow successful startup connection notices (default: true). Set to false to suppress routine MCP: N servers connected (M tools) notices. Connection errors and authentication warnings remain visible.
hostConfigDiscoveryHost-specific config policy: "off" (default), "prompt" (detect/report only), or "on" (explicitly load detected host configs as the lowest-precedence fallback)
projectServersProject-server admission policy for trusted headless sessions: "ask" (default, skip unapproved servers) or "allow". Only user-global or explicitly selected config may set it; project files are ignored.
ancestorConfigRootsTrusted absolute or ~/... roots for opt-in ancestor config discovery. Only user-global or explicitly selected config may set it; roots outside cwd are ignored and the deepest matching root is used.
agentPluginPathsAgent Plugins package directories to load MCP servers from. Relative paths resolve from the active project cwd.
approveToolstrue to require approval before every MCP tool call, or an array of glob patterns such as ["github_delete_*", "notion_update_*"]. Per-server approveTools overrides this.
oauthDirLegacy OAuth tokens.json import directory for this MCP config. Relative paths resolve from the active project cwd. MCP_OAUTH_DIR still wins when set. Persistent OAuth credentials are stored in the OS credential store, not this directory.
oauthCredentialStoreSet explicitly to "encrypted-file" for externally keyed AES-256-GCM storage (notably Windows OpenSSH network logons). Requires PI_MCP_ADAPTER_OAUTH_FILE_KEY; absent uses the OS credential store.
mcpServers.<name>.oauth.authorizationParamsExtra authorization URL parameters for provider-specific OAuth extensions. Flow-owned parameters such as client_id, redirect_uri, scope, state, code_challenge, response_type, and resource cannot be overridden.
directToolsGlobal default for all servers (default: false). true, false, or "search". Per-server overrides this.
namespaceProxyToolsRegister per-server mcp__<server> wrappers (default: true). Set to false to omit them from the model's tool list; mcp, mcpScript, and direct tools are unaffected. References such as mcp:<server> that rely on a wrapper will no longer resolve. Run /reload after changing this setting.
strictDirectToolArgumentsValidate direct-tool inputs against their advertised schemas and recover one JSON string layer for object and array properties (default: false).
directToolResultDetailsDirect-tool result details: "lean" (default) or "bounded" to retain the guarded raw MCP result.
warnOnLargeDirectToolsShow the advisory when 75 or more direct tools resolve (default: true). Set to false to suppress only this advisory.
freezeDirectToolsKeep direct-tool registration stable after the initial sync so metadata updates and explicit reconnects do not rebuild the system prompt. Proxy/search/cache metadata still refreshes. Default: false.
scriptModeRegister the MCP-only mcpScript plain-JavaScript tool (default: true). Set to false to hide it.
exposeResourcesExpose MCP resources as tools (default: true). Set to false to disable globally across all servers. Per-server exposeResources overrides this.
jevOptional System One Jev settings. A valid System One key enables semantic search across every enabled MCP server by default; semanticSearch: false disables it. scriptEvaluation remains disabled by default and requires an allowedServers source allowlist when enabled. jev: false disables both. Run /mcp-adapter jev setup for guided configuration.
disableProxyToolHide the mcp proxy tool once configured direct tools are fully available from cache. Ignored while any server uses directTools: "search", whose tools are registered inactive and can only be activated through the gateway (mcp({ search }) or a successful mcp({ tool }) call).
autoAuthAuto-run OAuth on connect/tool calls when a server needs auth, then retry once (default: false).
samplingAllow MCP servers to sample through Pi models, honoring modelPreferences.hints before current/default fallback (default: true when UI approval is available).
samplingAutoApproveSkip sampling confirmation prompts. Required for sampling in non-UI sessions (default: false).
elicitationAllow MCP servers to request user input through Pi dialogs (default: true when Pi UI is available).
outputGuardGuard oversized MCP output: true (default), false, or { maxBytes, maxLines, detailsMaxBytes }. See Output Guard.
traceOpt-in metadata-only protocol tracing. Set { enabled: true } globally or trace: true on a server. The per-session JSONL file defaults to .pi/mcp-traces/; file, maxBytes (default 262144), and maxEvents (default 10000) can be set. Raw MCP payloads, prompts, tool arguments/results, auth data, and URLs are never persisted.

Per-server idleTimeout, requestTimeoutMs, approveTools, and exposeResources override the global settings. debug remains stderr display and is unrelated to protocol tracing.

Tool Approval

Use approveTools when a tool should stay visible but not run without confirmation. This is useful for destructive or high-cost actions where hiding the tool would make planning harder, but running it silently is too risky.

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

When a matching tool is called from the proxy tool, a direct MCP tool, a resource call, or an MCP UI iframe, Pi asks: Allow once, Allow for session, Allow server for this session, or Deny. Allow for session tool grants and MCP UI iframe consent decisions (including denials) persist as non-LLM custom entries on the active Pi session branch and restore on resume or branch navigation. Entries store only server/tool names and deterministic definition/argument hashes; raw arguments, results, and secrets never persist. Tool grants and iframe consent remain separate gates. In headless sessions, matching calls fail closed with an approval_required result; denials, abstentions, Allow once, and approval-required paths do not create tool grant records. excludeTools still removes tools entirely; approveTools only gates visible tools at call time.

Allow server for this session permits all tools and argument combinations on the selected server, including tools discovered later. It does not approve other servers. This broad grant stays in memory only: reload, session replacement, resume, and branch navigation clear it. A changed or replaced server configuration also invalidates it. It is never saved to session entries or configuration. Broker denials, tool exclusions, host security guards, and the separate MCP UI iframe consent gate still apply. Use Allow for session instead to approve only the displayed tool definition and arguments.

pi-mcp-adapter/status/v1 is the documented, versioned public channel for cross-extension status. By contrast, mcp-approval-v1 entries are adapter-owned persistence state, not a supported cross-extension contract; consumers should use documented package exports and event APIs instead.

Permission extensions can broker these decisions by listening on pi-mcp-adapter:tool-approval-request and claiming the request synchronously:

import {
  MCP_TOOL_APPROVAL_REQUEST_EVENT,
  type McpToolApprovalRequest,
} from "pi-mcp-adapter";

pi.events.on(MCP_TOOL_APPROVAL_REQUEST_EVENT, (request: McpToolApprovalRequest) => {
  request.claim(async () => {
    return "allow_once"; // "allow_for_session" | "deny" | "abstain"
  });
});

The request includes serverName, originalToolName, prefixedToolName, args, origin, and optional signal. The first synchronous claim wins. Brokered approval runs for every resolved MCP call reaching the approval gate, including calls matching session grants restored from the active branch, regardless of approveTools configuration, across proxy, direct, mcpScript, resource, and iframe origins. allow_once permits only the current call; allow_for_session updates the same session-scoped approval cache and persistence path as the built-in dialog; deny blocks the current MCP call even if cached, without revoking its grant. Only abstain or no claim consults the cache, then the configured approval/UI fallback above if no matching grant exists. With no broker listener, fallback behavior is unchanged.

Output Guard

Oversized MCP tool/resource results are guarded by default so a single huge response can't blow up the model context window or the session file:

  • Inline text output is capped at 50 KiB / 2,000 lines (matching Pi's built-in bash guard). Larger output is truncated to a head preview and the full text is saved to a temp file whose path is included in the result, so the agent can read/grep it.
  • Image content blocks pass through unchanged — only text output is guarded. Images are delivered to the provider as native image content.
  • Binary resource blobs up to 10 MiB are decoded to private temp files and replaced with file references. Each session is limited to 100 MiB and 10,000 files. The files are removed at session teardown.
  • In proxy mode, details.mcpResult is kept raw when its JSON is ≤ 16 KiB; larger results are replaced with a compact summary (block counts, sizes, key previews) and the raw JSON is saved to a temp file. Direct tools keep lean details unless settings.directToolResultDetails is set to "bounded", which applies the same guarded mcpResult limit.

Extensions consuming details.mcpResult must check for omitted === true on both the result and its structuredContent before treating either value as an original payload. For omitted object structuredContent, preservedFields is only a partial preview; summary.keyCount is the original cardinality, while preservedCount and droppedCount account for retention. Under tiny limits, the whole result may compact to an omission marker without spill metadata.

Tune the text and details limits with the object form:

{
  "settings": {
    "outputGuard": { "maxBytes": 51200, "maxLines": 2000, "detailsMaxBytes": 16384 }
  }
}

Set "outputGuard": false — or the env kill switch MCP_OUTPUT_GUARD=0 — to disable text and details guarding. Binary resource materialization and its safety limits remain active. Output-guard spill files are created with mode 0600 under the system temp directory and are not cleaned up automatically; note that spilled MCP output may contain sensitive data.

MCP Scripting

Jev semantic search and opt-in script evaluation

A valid System One key makes semantic search available across every enabled MCP server; it does not run Jev searches automatically. A search uses Jev only when searchMode: "semantic" is explicitly requested. Jev ranks matching tools but never executes them. Script evaluation remains disabled until scriptEvaluation: true is configured. Requests use the pinned model from settings.jev.model (jev-1.13.0 by default) against the endpoint in SYSTEMONE_ENDPOINT, which defaults to TypeSafe at https://api.typesafe.ai/v1/systemone. Review your provider's current legal terms — for TypeSafe, legal terms, including privacy and retention; a no-training commitment does not mean zero retention.

Normal search
mcp({ search: "calendar" })
        │
        └── local lexical search
            no Jev request

Explicit semantic search
mcp({ search: "calendar", searchMode: "semantic" })
        │
        └── Jev ranks matching tools
            no tool is executed

The quickest desktop setup is:

pi-mcp-adapter key set systemone

That is enough to use semantic search across all enabled MCP tools. Run /mcp-adapter jev setup in Pi when you want to restrict which enabled servers may share semantic-search data. The command saves a project-scoped allowlist and reloads Pi automatically. Verify the stored credential at any time with pi-mcp-adapter key status systemone.

SYSTEMONE_API_KEY is for CI/headless use and overrides the keyring. Stdio MCP subprocesses inherit the host environment by default, so set inheritEnv: false where they must not receive it. The script worker receives no key, SDK, endpoint, headers, or environment.

Choosing a provider endpoint

System One decisions are the same API at different origins, so pointing at another provider needs an endpoint and, usually, a model:

ProviderSYSTEMONE_ENDPOINTModel
TypeSafe (default)https://api.typesafe.ai/v1/systemonejev-1.13.0
OpenCode Zenhttps://opencode.ai/zen/v1/systemonejev-1.13
Command Codehttps://api.commandcode.ai/provider/v1/systemonetypesafe/jev
OpenRouterhttps://openrouter.ai/api/alpha/decisionstypesafe/jev-1.13

These are example configurations, subject to each provider's current documentation (OpenCode Zen, Command Code, TypeSafe).

The endpoint must be an absolute https URL with a path. A set-but-invalid SYSTEMONE_ENDPOINT disables Jev instead of falling back to the default. Treat the endpoint as trusted configuration: it receives the API key and the judgment payload. Credentials are stored per endpoint, so switching endpoints does not overwrite a saved key. Set the model with:

{ "settings": { "jev": { "model": "jev-1.13" } } }

The older TYPESAFE_API_KEY variable still works for the default TypeSafe endpoint and is never sent to any other endpoint.

Semantic search sends the query text, server names, normalized and original tool names, tool paths, and descriptions to the configured endpoint. It does not send tool results. allowedServers restricts semantic search to named servers. scriptEvaluation is a separate opt-in that may send the state and MCP-derived results declared in each evaluation; when enabled, it requires an explicit source allowlist.

{
  "settings": {
    "jev": {
      "scriptEvaluation": true,
      "allowedServers": ["github"],
      "maxEvaluationTokensPerScript": 32768
    }
  }
}

Request semantic discovery explicitly with mcp({ search: "triage customer reports", searchMode: "semantic" }) or tools.search({ query: "triage customer reports", searchMode: "semantic" }). Regex is incompatible. Timeout, rate-limit, and service failures return marked lexical fallback; credential, policy, configuration, and response failures do not. If no allowed server has cached tools, search explains how to connect a server or update the allowlist; if Jev decides no tool fits, the result says that Jev abstained.

Optional jev controls bound timeout/retries, request and script budgets, semantic candidates (at most 127), and minimum probability. The cumulative token budget uses provider-reported input plus output usage. Exact pre-response admission is unavailable without the provider tokenizer, so byte/question/state limits bound requests before dispatch; a response that exceeds the remaining token budget is discarded and exhausts it. The endpoint is set by `SYS