|
| 1 | +# mcp — MCP client extension |
| 2 | + |
| 3 | +Connects pi to [Model Context Protocol](https://modelcontextprotocol.io) servers and registers |
| 4 | +their tools as pi tools. In-house replacement for the community `pi-mcp-adapter`. |
| 5 | + |
| 6 | +## Configuration |
| 7 | + |
| 8 | +Config is merged from two `mcp.json` files (project overrides global per key; project servers |
| 9 | +replace same-named global servers wholesale): |
| 10 | + |
| 11 | +| Location | Scope | |
| 12 | +| --- | --- | |
| 13 | +| `~/.pi/agent/mcp.json` | global | |
| 14 | +| `<project>/.pi/mcp.json` | project-local — only honored when the project is trusted | |
| 15 | + |
| 16 | +```json |
| 17 | +{ |
| 18 | + "settings": { |
| 19 | + "toolPrefix": "mcp", |
| 20 | + "requestTimeoutMs": 30000, |
| 21 | + "maxRetries": 3 |
| 22 | + }, |
| 23 | + "mcpServers": { |
| 24 | + "filesystem": { |
| 25 | + "command": "npx", |
| 26 | + "args": ["-y", "@modelcontextprotocol/server-filesystem", "/workspace"], |
| 27 | + "env": { "NODE_ENV": "production" }, |
| 28 | + "lifecycle": "eager" |
| 29 | + }, |
| 30 | + "internal-api": { |
| 31 | + "transport": "streamable-http", |
| 32 | + "url": "https://mcp.example.com/mcp", |
| 33 | + "headers": { "Authorization": "Bearer <api-key>" }, |
| 34 | + "lifecycle": "lazy" |
| 35 | + }, |
| 36 | + "linear": { |
| 37 | + "transport": "streamable-http", |
| 38 | + "url": "https://mcp.linear.app/mcp", |
| 39 | + "auth": { "type": "oauth" } |
| 40 | + } |
| 41 | + } |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +### Server options |
| 46 | + |
| 47 | +| Field | Type / values | Default | Notes | |
| 48 | +| --- | --- | --- | --- | |
| 49 | +| `transport` | `"stdio" \| "streamable-http" \| "sse"` | `"stdio"` | | |
| 50 | +| `command`, `args`, `env` | | | stdio only; `command` required | |
| 51 | +| `url` | URL | | required for streamable-http / sse | |
| 52 | +| `headers` | record | | static HTTP headers (API-key auth) | |
| 53 | +| `auth` | object | | OAuth config, http/sse only (below) | |
| 54 | +| `lifecycle` | `"eager" \| "lazy"` | `"eager"` | eager starts at `session_start`; lazy via `/mcp:start` | |
| 55 | +| `requestTimeoutMs` | number | settings value | per-request timeout override | |
| 56 | +| `healthCheckIntervalMs` | number | disabled | opt-in ping; reconnects on failure | |
| 57 | + |
| 58 | +### Settings |
| 59 | + |
| 60 | +| Field | Default | Notes | |
| 61 | +| --- | --- | --- | |
| 62 | +| `toolPrefix` | `"mcp"` | tool names are `<prefix>_<server>_<tool>` (sanitized to `[a-zA-Z0-9_]`, ≤64 chars with hash suffix on truncation) | |
| 63 | +| `requestTimeoutMs` | `30000` | default per-request timeout | |
| 64 | +| `maxRetries` | `3` | reconnect attempts (fixed 1s/3s/5s/10s/30s schedule) | |
| 65 | + |
| 66 | +## OAuth (`auth`) |
| 67 | + |
| 68 | +Authorization-code + PKCE against the server's advertised authorization server: RFC 9728 / |
| 69 | +metadata discovery, dynamic client registration (or a pre-registered client), browser |
| 70 | +authorization via a loopback callback, token exchange, and silent refresh on reconnect. |
| 71 | + |
| 72 | +```json |
| 73 | +"auth": { |
| 74 | + "type": "oauth", |
| 75 | + "scope": "read write", |
| 76 | + "clientId": "pre-registered-id", |
| 77 | + "clientSecret": "…", |
| 78 | + "redirectUrl": "http://127.0.0.1:19876/callback", |
| 79 | + "clientName": "PostHog Code" |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +All fields except `type` are optional. Without `clientId`, the client is registered dynamically. |
| 84 | +Without `redirectUrl`, the callback server binds an ephemeral 127.0.0.1 port; set it only when a |
| 85 | +pre-registered client requires an exact redirect URI (must be an `http://` loopback URL with an |
| 86 | +explicit port). |
| 87 | + |
| 88 | +Credentials are stored per server under `~/.pi/agent/mcp-auth/<sha256(name)>.json` (mode 0600), |
| 89 | +scoped to the configured server URL — changing the URL invalidates them. Background reconnects |
| 90 | +attach and refresh tokens silently but never open a browser or register clients; when a fresh |
| 91 | +authorization is needed the connection fails with a hint to run `/mcp:auth <server>`. |
| 92 | + |
| 93 | +The `client_credentials` grant (machine-to-machine, no user) is not implemented — use static |
| 94 | +`headers` for that case. |
| 95 | + |
| 96 | +## Commands |
| 97 | + |
| 98 | +| Command | Purpose | |
| 99 | +| --- | --- | |
| 100 | +| `/mcp` | status summary for all servers | |
| 101 | +| `/mcp <name>` | state, retries, last error, tools, recent logs | |
| 102 | +| `/mcp:start <name>` | start a server (lazy servers, or after failure) | |
| 103 | +| `/mcp:stop <name>` | stop a server and deactivate its tools | |
| 104 | +| `/mcp:auth` | list OAuth-enabled servers with auth status | |
| 105 | +| `/mcp:auth <name> [reset]` | run the browser OAuth flow (`reset` clears stored credentials first) | |
| 106 | + |
| 107 | +The model can start the OAuth flow itself via the `mcp_auth` tool, which queues |
| 108 | +`/mcp:auth <server>` as a follow-up user message ("log in to linear for me" works). |
| 109 | + |
| 110 | +## Bundled skill |
| 111 | + |
| 112 | +The extension contributes an `mcp-servers` skill (`skills/mcp-servers/SKILL.md`) via |
| 113 | +`resources_discover`, so the model can handle "install/configure the X MCP server" requests |
| 114 | +itself: it knows the config schema, file locations and trust rules, the OAuth setup + |
| 115 | +`/mcp:auth` handoff, that `/reload` applies config changes, and how to troubleshoot via |
| 116 | +`/mcp <name>`. Only the one-line description sits in the system prompt; the full instructions |
| 117 | +load on demand (progressive disclosure). |
| 118 | + |
| 119 | +## Behavior notes |
| 120 | + |
| 121 | +- Tools are registered once and **activated/deactivated** as servers connect/disconnect, so tool |
| 122 | + identities stay stable across reconnects (no churn in pi's tool registry). |
| 123 | +- Crashed or dropped connections are detected via the client's close event: the server flips to |
| 124 | + `stopped`, its tools deactivate, and a background reconnect (re)activates them on success. |
| 125 | +- Tool-name collisions (two MCP tools sanitizing to the same pi name) are reported in |
| 126 | + `/mcp <name>`; the later definition wins. |
| 127 | +- `notifications/tools/list_changed` triggers live tool re-discovery; `tools/list` pagination is |
| 128 | + followed per spec (with a 100-page guard). |
| 129 | +- Tool annotations (`readOnlyHint`, `destructiveHint`, …) are appended to tool descriptions. |
| 130 | +- Tool-call `AbortSignal`s propagate to the SDK (`notifications/cancelled`). |
| 131 | +- MCP text/image result content passes through; audio/resource content is described as text. |
| 132 | +- Server stderr and `notifications/message` logs land in a per-server ring buffer (`/mcp <name>`). |
| 133 | + |
| 134 | +## Module map |
| 135 | + |
| 136 | +| File | Responsibility | |
| 137 | +| --- | --- | |
| 138 | +| `extension.ts` | pi wiring: lifecycle, commands, notifications | |
| 139 | +| `config.ts` | zod schemas, load + merge of the two `mcp.json` files | |
| 140 | +| `server-manager.ts` | connection lifecycle, retries, health checks, transports | |
| 141 | +| `tool-bridge.ts` | MCP tools ⇄ pi tools (naming, schema conversion, execution) | |
| 142 | +| `schema.ts` | JSON Schema → TypeBox conversion | |
| 143 | +| `render.ts` | TUI call renderer (shows tool arguments inline; full JSON when expanded) | |
| 144 | +| `auth-storage.ts` / `oauth-provider.ts` / `callback-server.ts` / `auth-flow.ts` | OAuth | |
| 145 | +| `skills/mcp-servers/` | bundled skill teaching the model to install/configure servers | |
| 146 | +| `test-support.ts` | in-memory MCP server + fake OAuth server (tests only) | |
0 commit comments