@fitchmultz/pi-mcp-adapter 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +750 -0
  2. package/LICENSE +21 -0
  3. package/OAUTH.md +462 -0
  4. package/README.md +820 -0
  5. package/abort.ts +34 -0
  6. package/agent-dir.ts +24 -0
  7. package/app-bridge.bundle.js +67 -0
  8. package/banner.png +0 -0
  9. package/checkpoint.ts +55 -0
  10. package/cli.js +222 -0
  11. package/commands.ts +619 -0
  12. package/config.ts +1204 -0
  13. package/consent-manager.ts +25 -0
  14. package/direct-tools.ts +408 -0
  15. package/dist/abort.d.ts +2 -0
  16. package/dist/abort.js +35 -0
  17. package/dist/agent-dir.d.ts +3 -0
  18. package/dist/agent-dir.js +21 -0
  19. package/dist/app-bridge.bundle.js +67 -0
  20. package/dist/checkpoint.d.ts +18 -0
  21. package/dist/checkpoint.js +53 -0
  22. package/dist/commands.d.ts +20 -0
  23. package/dist/commands.js +521 -0
  24. package/dist/config.d.ts +109 -0
  25. package/dist/config.js +1013 -0
  26. package/dist/consent-manager.d.ts +8 -0
  27. package/dist/consent-manager.js +21 -0
  28. package/dist/direct-tools.d.ts +10 -0
  29. package/dist/direct-tools.js +350 -0
  30. package/dist/docs/client-metadata.json +14 -0
  31. package/dist/elicitation-handler.d.ts +17 -0
  32. package/dist/elicitation-handler.js +308 -0
  33. package/dist/error-signal.d.ts +15 -0
  34. package/dist/error-signal.js +21 -0
  35. package/dist/glimpse-ui.d.ts +9 -0
  36. package/dist/glimpse-ui.js +75 -0
  37. package/dist/host-html-template.d.ts +15 -0
  38. package/dist/host-html-template.js +429 -0
  39. package/dist/index.d.ts +7 -0
  40. package/dist/index.js +840 -0
  41. package/dist/init.d.ts +25 -0
  42. package/dist/init.js +562 -0
  43. package/dist/legacy-migration.d.ts +18 -0
  44. package/dist/legacy-migration.js +45 -0
  45. package/dist/lifecycle.d.ts +40 -0
  46. package/dist/lifecycle.js +167 -0
  47. package/dist/logger.d.ts +38 -0
  48. package/dist/logger.js +102 -0
  49. package/dist/mcp-auth-flow.d.ts +113 -0
  50. package/dist/mcp-auth-flow.js +834 -0
  51. package/dist/mcp-auth.d.ts +127 -0
  52. package/dist/mcp-auth.js +658 -0
  53. package/dist/mcp-callback-server.d.ts +56 -0
  54. package/dist/mcp-callback-server.js +371 -0
  55. package/dist/mcp-code.d.ts +51 -0
  56. package/dist/mcp-code.js +366 -0
  57. package/dist/mcp-keyring-helper.cjs +89 -0
  58. package/dist/mcp-oauth-provider.d.ts +154 -0
  59. package/dist/mcp-oauth-provider.js +672 -0
  60. package/dist/mcp-output-guard.d.ts +59 -0
  61. package/dist/mcp-output-guard.js +332 -0
  62. package/dist/mcp-panel.d.ts +66 -0
  63. package/dist/mcp-panel.js +889 -0
  64. package/dist/mcp-script-worker.mjs +128 -0
  65. package/dist/mcp-setup-panel.d.ts +84 -0
  66. package/dist/mcp-setup-panel.js +561 -0
  67. package/dist/mcp-status.d.ts +11 -0
  68. package/dist/mcp-status.js +96 -0
  69. package/dist/mcp-trace.d.ts +95 -0
  70. package/dist/mcp-trace.js +233 -0
  71. package/dist/metadata-cache.d.ts +18 -0
  72. package/dist/metadata-cache.js +277 -0
  73. package/dist/npx-resolver.d.ts +6 -0
  74. package/dist/npx-resolver.js +448 -0
  75. package/dist/onboarding-state.d.ts +11 -0
  76. package/dist/onboarding-state.js +47 -0
  77. package/dist/panel-keys.d.ts +16 -0
  78. package/dist/panel-keys.js +15 -0
  79. package/dist/prompts.d.ts +56 -0
  80. package/dist/prompts.js +284 -0
  81. package/dist/proxy-modes.d.ts +38 -0
  82. package/dist/proxy-modes.js +1235 -0
  83. package/dist/resource-tools.d.ts +1 -0
  84. package/dist/resource-tools.js +14 -0
  85. package/dist/runtime-owner.d.ts +18 -0
  86. package/dist/runtime-owner.js +117 -0
  87. package/dist/sampling-handler.d.ts +15 -0
  88. package/dist/sampling-handler.js +211 -0
  89. package/dist/search-ranking.d.ts +20 -0
  90. package/dist/search-ranking.js +180 -0
  91. package/dist/server-manager.d.ts +114 -0
  92. package/dist/server-manager.js +1175 -0
  93. package/dist/session-recovery.d.ts +31 -0
  94. package/dist/session-recovery.js +310 -0
  95. package/dist/state.d.ts +63 -0
  96. package/dist/state.js +1 -0
  97. package/dist/tool-approval.d.ts +10 -0
  98. package/dist/tool-approval.js +42 -0
  99. package/dist/tool-metadata.d.ts +9 -0
  100. package/dist/tool-metadata.js +193 -0
  101. package/dist/tool-registrar.d.ts +10 -0
  102. package/dist/tool-registrar.js +64 -0
  103. package/dist/tool-result-renderer.d.ts +45 -0
  104. package/dist/tool-result-renderer.js +224 -0
  105. package/dist/ts-shape.d.ts +2 -0
  106. package/dist/ts-shape.js +147 -0
  107. package/dist/types.d.ts +516 -0
  108. package/dist/types.js +168 -0
  109. package/dist/ui-resource-handler.d.ts +17 -0
  110. package/dist/ui-resource-handler.js +209 -0
  111. package/dist/ui-server.d.ts +51 -0
  112. package/dist/ui-server.js +669 -0
  113. package/dist/ui-session.d.ts +39 -0
  114. package/dist/ui-session.js +463 -0
  115. package/dist/ui-stream-types.d.ts +104 -0
  116. package/dist/ui-stream-types.js +58 -0
  117. package/dist/ui-tool-visibility.d.ts +4 -0
  118. package/dist/ui-tool-visibility.js +26 -0
  119. package/dist/unix-socket-transport.d.ts +15 -0
  120. package/dist/unix-socket-transport.js +84 -0
  121. package/dist/utils.d.ts +30 -0
  122. package/dist/utils.js +276 -0
  123. package/docs/client-metadata.json +14 -0
  124. package/elicitation-handler.ts +349 -0
  125. package/error-signal.ts +21 -0
  126. package/glimpse-ui.ts +80 -0
  127. package/host-html-template.ts +456 -0
  128. package/index.ts +900 -0
  129. package/init.ts +627 -0
  130. package/legacy-migration.ts +47 -0
  131. package/lifecycle.ts +165 -0
  132. package/logger.ts +127 -0
  133. package/mcp-auth-flow.ts +975 -0
  134. package/mcp-auth.ts +793 -0
  135. package/mcp-callback-server.ts +450 -0
  136. package/mcp-code.ts +386 -0
  137. package/mcp-keyring-helper.cjs +89 -0
  138. package/mcp-oauth-provider.ts +774 -0
  139. package/mcp-output-guard.ts +412 -0
  140. package/mcp-panel.ts +978 -0
  141. package/mcp-script-worker.mjs +128 -0
  142. package/mcp-setup-panel.ts +666 -0
  143. package/mcp-status.ts +109 -0
  144. package/mcp-trace.ts +295 -0
  145. package/metadata-cache.ts +333 -0
  146. package/npx-resolver.ts +488 -0
  147. package/onboarding-state.ts +58 -0
  148. package/package.json +182 -0
  149. package/panel-keys.ts +37 -0
  150. package/prompts.ts +337 -0
  151. package/proxy-modes.ts +1361 -0
  152. package/resource-tools.ts +17 -0
  153. package/runtime-owner.ts +123 -0
  154. package/sampling-handler.ts +273 -0
  155. package/scripts/build.mjs +131 -0
  156. package/scripts/prepare.mjs +102 -0
  157. package/search-ranking.ts +196 -0
  158. package/server-manager.ts +1320 -0
  159. package/session-recovery.ts +316 -0
  160. package/skills/mcp-scripting/SKILL.md +38 -0
  161. package/state.ts +64 -0
  162. package/tool-approval.ts +78 -0
  163. package/tool-metadata.ts +235 -0
  164. package/tool-registrar.ts +69 -0
  165. package/tool-result-renderer.ts +286 -0
  166. package/ts-shape.ts +145 -0
  167. package/tsconfig.build.json +13 -0
  168. package/tsconfig.json +16 -0
  169. package/types.ts +810 -0
  170. package/ui-resource-handler.ts +249 -0
  171. package/ui-server.ts +808 -0
  172. package/ui-session.ts +568 -0
  173. package/ui-stream-types.ts +89 -0
  174. package/ui-tool-visibility.ts +25 -0
  175. package/unix-socket-transport.ts +85 -0
  176. package/utils.ts +309 -0
package/README.md ADDED
@@ -0,0 +1,820 @@
1
+ <p>
2
+ <img src="banner.png" alt="pi-mcp-adapter" width="1100">
3
+ </p>
4
+
5
+ # Fitch MCP Adapter
6
+
7
+ Use MCP servers with [Pi](https://github.com/badlogic/pi-mono/) without burning your context window.
8
+
9
+ An independently maintained distribution of [Nico's Pi MCP Adapter](https://github.com/nicobailon/pi-mcp-adapter), under the original MIT license.
10
+
11
+ https://github.com/user-attachments/assets/4b7c66ff-e27e-4639-b195-22c3db406a5a
12
+
13
+ ## Why This Exists
14
+
15
+ Mario wrote about [why you might not need MCP](https://mariozechner.at/posts/2025-11-02-what-if-you-dont-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.
16
+
17
+ His take: skip MCP entirely, write simple CLI tools instead.
18
+
19
+ 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.
20
+
21
+ ## Install
22
+
23
+ Requires Pi 0.84.0 or later and Node.js 22.19.0 or later.
24
+
25
+ ```bash
26
+ pi install npm:@fitchmultz/pi-mcp-adapter
27
+ ```
28
+
29
+ Or install from Git: `pi install git:github.com/fitchmultz/pi-mcp-adapter`.
30
+
31
+ Restart Pi after installation. Existing v4 users should migrate first.
32
+
33
+ ### Upgrading from v4
34
+
35
+ Version 5 uses its own package, state paths and OAuth credential namespace. Remove the previous adapter package entry from Pi settings (for example, `pi remove npm:pi-mcp-adapter` for the unscoped npm source), then install the distribution above. Keep only one adapter loaded in a Pi host.
36
+
37
+ After installation, **before restarting Pi**, run from your project directory:
38
+
39
+ ```bash
40
+ fitch-mcp-adapter migrate --dry-run
41
+ fitch-mcp-adapter migrate
42
+ ```
43
+
44
+ If Pi's package bin is not on your `PATH`, use `npx --package @fitchmultz/pi-mcp-adapter fitch-mcp-adapter migrate` (append `--dry-run` to preview).
45
+
46
+ Migration copies v4's global `mcp.json`, `mcp-cache.json`, `mcp-onboarding.json` and `mcp-npx-cache.json` into `<Pi agent dir>/fitch-mcp-adapter/`, and the current project's `.pi/mcp.json` into `.pi/fitch-mcp-adapter/mcp.json`. Existing destinations are reported and left unchanged, without merging. Repeat in each project that has an old override. Use the same `PI_CODING_AGENT_DIR` as your Pi installation.
47
+
48
+ For configured HTTP servers, migration copies an existing OAuth entry from the old OS credential namespace or legacy token file only when the new namespace has no entry. It preserves URL/issuer bindings, client registration and tokens, verifies the copy, and never prints plaintext credentials. All old sources remain untouched. A configured `settings.oauthDir` uses a `fitch-mcp-adapter/` child directory during normal operation, keeping the original import files separate. Dry-run lists file actions without reading the keychain or writing anything; repeated migration skips already-copied destinations.
49
+
50
+ Normal startup never falls back to old adapter-owned defaults or the old credential namespace, so logout cannot resurrect old credentials. Migration copies an existing provider grant; it does not create an independent grant. To use both distributions with independent provider grants, sign in separately. Shared standard MCP files and explicit `configPath`/imports retain their behavior. See [OAuth storage](OAUTH.md#token-storage).
51
+
52
+ ## What happens on first run
53
+
54
+ The adapter reads standard MCP files automatically. No extra setup needed if you already have them.
55
+
56
+ | You already have... | What happens |
57
+ |---------------------|--------------|
58
+ | `.mcp.json` or `~/.config/mcp/mcp.json` | Pi uses it immediately. The first time you open `/mcp`, you'll see a short heads-up explaining which file Pi detected and that Pi only writes adapter-specific overrides to its own files. |
59
+ | Host-specific configs (Cursor, Claude Code, Codex, etc.) but no standard MCP files | Run `/mcp 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. |
60
+ | Nothing configured yet | Run `/mcp setup` to scaffold a minimal `.mcp.json`, add a curated known server, quick-add RepoPrompt, or inspect what the adapter discovered on your machine. |
61
+
62
+ If you prefer the terminal, run `fitch-mcp-adapter init` after install to scan for host-specific configs and add missing compatibility imports to `~/.pi/agent/fitch-mcp-adapter/mcp.json` (or `$PI_CODING_AGENT_DIR/fitch-mcp-adapter/mcp.json` when set).
63
+
64
+ ## Quick Start
65
+
66
+ Preferred project config: `.mcp.json`
67
+
68
+ ```json
69
+ {
70
+ "mcpServers": {
71
+ "chrome-devtools": {
72
+ "command": "npx",
73
+ "args": ["-y", "chrome-devtools-mcp@latest"]
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ Preferred user-global shared config: `~/.config/mcp/mcp.json`. Pi also reads the tool-agnostic global paths `~/.agents/mcp.json` and `~/.agents/mcp/mcp.json`.
80
+
81
+ Pi also reads Pi-owned override files for settings and host-specific compatibility:
82
+
83
+ - `<Pi agent dir>/fitch-mcp-adapter/mcp.json` — adapter global override (`~/.pi/agent/fitch-mcp-adapter/mcp.json` by default)
84
+ - `.pi/fitch-mcp-adapter/mcp.json` — adapter project override
85
+
86
+ Host-specific configs are detected and shown by `/mcp setup` and `fitch-mcp-adapter init`, but they are not loaded automatically. To explicitly opt in to host-config fallback discovery, set `settings.hostConfigDiscovery` to `"on"` or run `fitch-mcp-adapter init --discover-host-configs`. The default is `"off"`. Host configs are lower precedence than every shared and Pi-owned source, and `/mcp 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.
87
+
88
+ Precedence is:
89
+
90
+ 1. `~/.config/mcp/mcp.json`
91
+ 2. `~/.agents/mcp.json`
92
+ 3. `~/.agents/mcp/mcp.json`
93
+ 4. `<Pi agent dir>/fitch-mcp-adapter/mcp.json`
94
+ 5. `.mcp.json`
95
+ 6. `.pi/fitch-mcp-adapter/mcp.json`
96
+
97
+ Project layers and project-local host imports are read only after Pi marks the project trusted. Until then, the adapter uses global configuration only, does not start project-defined servers, and blocks project configuration panels and writes.
98
+
99
+ `/mcp disable <server>` and `/mcp enable <server>` persist only the `disabled` field in the project-local `.pi/fitch-mcp-adapter/mcp.json`, which is the highest-precedence Pi layer. Enabling removes the project flag when lower layers are enabled, or writes `false` when needed to override a disabled lower source. This applies even when the effective server came from a shared global/project file, an imported host config, or `configPath`; the source file is never rewritten and credentials are never copied. Run `/reload` after changing the flag so registered tool surfaces are refreshed. The manual equivalent is to add `{ "disabled": true }` to a server in any normal MCP config. Supplied in-memory `createMcpAdapter({ config })` configurations are isolated and do not read or write this project override; the commands are unavailable in that mode.
100
+
101
+ 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.
102
+
103
+ ```
104
+ mcp({ search: "screenshot" })
105
+ ```
106
+ ```
107
+ chrome_devtools_take_screenshot
108
+ Take a screenshot of the page or element.
109
+
110
+ Parameters:
111
+ format (enum: "png", "jpeg", "webp") [default: "png"]
112
+ fullPage (boolean) - Full page instead of viewport
113
+ ```
114
+ ```
115
+ mcp({ tool: "chrome_devtools_take_screenshot", args: { format: "png" } })
116
+ ```
117
+
118
+ `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.
119
+
120
+ Two calls instead of 26 tools cluttering the context.
121
+
122
+ ## Config
123
+
124
+ ### File Layout
125
+
126
+ Use the shared MCP files when you want one setup to work across hosts, and Pi-owned files when you need Pi-specific overrides or settings.
127
+
128
+ | File | Purpose |
129
+ |------|---------|
130
+ | `~/.config/mcp/mcp.json` | User-global shared MCP config |
131
+ | `~/.agents/mcp.json` | User-global tool-agnostic MCP config |
132
+ | `~/.agents/mcp/mcp.json` | User-global tool-agnostic MCP config |
133
+ | `.mcp.json` | Project-local shared MCP config |
134
+ | `<Pi agent dir>/fitch-mcp-adapter/mcp.json` | Adapter global override and compatibility imports |
135
+ | `.pi/fitch-mcp-adapter/mcp.json` | Adapter project override |
136
+
137
+ The Pi agent dir defaults to `~/.pi/agent` and honors `PI_CODING_AGENT_DIR`. The owned `fitch-mcp-adapter/` root also contains `mcp-cache.json`, `mcp-onboarding.json`, `mcp-npx-cache.json`, and the `mcp-oauth/` legacy import directory.
138
+
139
+ For imported or shared global servers, Pi saves only `directTools` selections in its own config, without copying connection details or credentials from the source file.
140
+
141
+ ### SDK configuration
142
+
143
+ Use `createMcpAdapter` when an SDK or server integration already owns its MCP configuration:
144
+
145
+ ```ts
146
+ import { createMcpAdapter } from "@fitchmultz/pi-mcp-adapter";
147
+
148
+ const extension = createMcpAdapter({
149
+ config: {
150
+ mcpServers: {
151
+ docs: {
152
+ url: "https://mcp.example.com/mcp",
153
+ lifecycle: "eager",
154
+ },
155
+ },
156
+ },
157
+ });
158
+
159
+ // Register `extension` with the host SDK.
160
+ ```
161
+
162
+ 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.
163
+
164
+ 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. 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.
165
+
166
+ 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` selects the parent of a `fitch-mcp-adapter/` legacy plaintext import directory; `FITCH_MCP_OAUTH_DIR` explicitly overrides the final import path. Neither changes the OS credential namespace. CSRF state and PKCE verifiers are flow-local, so concurrent authorization flows do not share transient secrets.
167
+
168
+ Set `createMcpAdapter({ outputDirectory: "/workspace/internal/mcp-output" })` to keep oversized tool/resource text, raw MCP JSON, and final `mcp_script` output beneath a host-owned directory. The directory is created on the first spill; each file still uses a random subdirectory/name and mode `0600`. Relative paths resolve from the process working directory when written; prefer an absolute path for hosts that change directories. This runtime option works with either configuration mode and does not change live results, output limits, or cleanup. The host owns retention, access, and any redaction of saved copies; files may contain sensitive data. Omitting it keeps the system temp directory.
169
+
170
+ Set `createMcpAdapter({ defaultScriptTimeoutMs: null })` to remove the default overall `mcp_script` deadline for that host only. Omitting the option keeps 30,000ms; a numeric default must be an integer from 1 to 2,147,483,647 milliseconds. Each script's explicit `timeoutMs` takes precedence. This is a factory option, not an MCP config or global Pi setting. It does not change initialization waits, individual MCP request deadlines, Stop/AbortSignal, session-owner shutdown, or unfinished-call cleanup. Without an overall deadline, a script can run until it completes or is stopped. Script result `details.timeoutMs` is `null` when no overall deadline applies.
171
+
172
+ ### Session configuration
173
+
174
+ Use `createMcpAdapter({ transformConfig: (config, ctx) => ... })` to add session-specific servers or settings in memory. The synchronous hook runs once per `session_start`, including reloads and new sessions, after normal config merging and project trust checks, before tool/metadata registration and initialization. It receives a fresh config clone and the current `ExtensionContext`; its result is cloned for session ownership. No config files are rewritten. Without an explicit `config`, normal status, setup, disable/enable, and auth panels remain available. With `config`, the hook transforms that isolated snapshot and its panel limitations still apply.
175
+
176
+ ```ts
177
+ const extension = createMcpAdapter({
178
+ transformConfig(config, ctx) {
179
+ config.mcpServers.session = {
180
+ command: "my-mcp-server",
181
+ lifecycle: "lazy",
182
+ env: { SESSION_ID: ctx.sessionManager.getSessionId() },
183
+ };
184
+ config.settings = { ...config.settings, autoAuth: false };
185
+ return config;
186
+ },
187
+ });
188
+ ```
189
+
190
+ Hosts that share a server within a root session can supply their root-session ID in `env` instead. Load this factory instead of also loading the default adapter.
191
+
192
+ ### Execution checkpoints and call capture
193
+
194
+ `createMcpAdapter({ beforeExecute: async (toolCallId, ctx, operation) => { ... } })` lets a host finish its workspace/native-session checkpoint before the next MCP action. For direct tools, actual `mcp` calls and every resolved `mcp_script` inner call, it runs after target resolution, connection, approval and UI preparation but **before the individual service deadline** and `onToolCall` capture. Any configured overall script deadline still applies. A second awaited script call reaches this boundary after the first call's output files have finished writing. The callback receives the native Pi call ID and that invocation's `ExtensionContext`, with its caller/owner/script cancellation signal in `ctx.signal`. Pass it to checkpoint I/O and throw if the checkpoint cannot complete. Rejection prevents that call: direct/proxy execution throws, while scripts keep their failed-call envelope so they can handle preparation failures. Existing two-argument callbacks remain supported.
195
+
196
+ The optional `McpOperationContext` (exported from the package and `/types`) carries the same `server`, `tool`, resolved `args`, optional `annotations`/`resourceUri`, and outer/inner IDs as the native before event, plus `annotationsTrusted`. Live connection annotations take precedence over frozen direct-tool hints, including when live hints are absent. `annotationsTrusted` reports only the existing server `retryOnTransportFailure: true` opt-in; it is not a new permission or configuration gate. No server configuration, headers, credentials, or SDK clients are exposed. Non-call proxy modes (including status, discovery, connect and auth actions) still invoke the callback without an operation; unresolved or denied tool calls do not dispatch.
197
+
198
+ A host can skip an expensive before-effect save for a native resource read (`operation.resourceUri`) or for `operation.annotationsTrusted && operation.annotations?.readOnlyHint === true`. Missing/false hints and `idempotentHint: true` alone are not read-only: an idempotent write still changes state. Keep unknown operations conservative and retain original intent/outcome capture. The adapter does not choose the host's save policy.
199
+
200
+ Supplying `beforeExecute` marks the adapter's tools `executionMode: "sequential"`, so Pi finishes earlier sibling tools before entering this boundary. Omitting it keeps normal scheduling. It does not serialize `Promise.all` inside a script, join detached writers, or implement checkpoint storage. The host still owns coherent capture and a final completion checkpoint, including final script output files. These callbacks do not change request or script timeout defaults.
201
+
202
+ `createMcpAdapter({ onToolCall: async (event) => { ... } })` supplies one optional, session-scoped callback for `mcp`, direct tools, and each resolved `mcp_script` call. Load this factory instead of also loading the default adapter. The callback is passed directly, so separate Pi/Jiti module instances do not need to share a singleton or event bus.
203
+
204
+ `McpToolCallEvent` (exported from `@fitchmultz/pi-mcp-adapter/types`) contains `server`, `tool`, the actual resolved `args`, optional SDK `annotations` and `resourceUri`, and a cancellation/deadline `signal`. `toolCallId` is the outer native Pi ID; scripts additionally carry their existing numeric `innerCallId`. These IDs are correlation only, not provider idempotency keys.
205
+
206
+ - `phase: "before"`: awaited after approval, before dispatch. Rejection prevents the call.
207
+ - `phase: "after"`: contains either the raw `result` (including MCP error results) or the thrown `error`. Awaited before delivering the outcome to Pi or dependent script work. It runs once after native recovery/retry settles, not once per HTTP subrequest. Output spill files are written **after** this callback; they are complete when the tool or awaited script call resolves, not at the after event.
208
+ - Capture failure returns `call_capture_failed` with the original event, excluding `signal`, in `details.recovery`. A completed result stays there; the adapter never repeats it. A script stops before consuming that result or starting dependent work. Cancellation remains `aborted`.
209
+
210
+ The two callbacks serve different boundaries: `beforeExecute` saves completed workspace writes; `onToolCall` captures the resolved operation and its raw outcome. The host owns `pi.appendEntry()` and the awaited checkpoint of that same native JSONL. Exclude `signal`, serialize thrown errors explicitly, redact sensitive checkpoint bytes, and honor the signal and session/attempt fence. `pi.appendEntry()` alone only persists locally; `pi.events.emit()` is not an awaited barrier. Custom entries do not enter model context automatically: the host must provide truthful recovered results or readback instructions when resuming. The adapter stores no separate transcript or receipts, does not replay scripts, and does not invent provider operation keys.
211
+
212
+ ### Native working-session checkpoints
213
+
214
+ On hosts that emit the optional awaited `session_checkpoint` event, the adapter can acknowledge an **idle, stateless Streamable HTTP** runtime with its **default sampling and elicitation capabilities intact**. Older Pi hosts continue normally and never invoke this hook. The host still owns native session capture, coherent filesystem capture, private credential retention, and commit-before-sleep.
215
+
216
+ Sampling/elicitation handlers are owned through their actual UI/model/handler promises. A pending handler returns a named veto; after it finishes, an otherwise reconstructible runtime can qualify. Cancelling an SDK response or closing a transport does not discard an unfinished handler: connection cleanup drains those callbacks. Native sends that outlive per-server cancellation remain owned and veto readiness; final shutdown joins them. SDK-generated callback replies remain owned through HTTP/auth completion even after the handler returns. Unfinished callbacks also prevent automatic idle closure. Configured `beforeExecute`/`onToolCall` hooks likewise do not block idle checkpoints. Their underlying promises stay counted even if the caller's abortable wait has ended. Callbacks must return/await their work and persist any logical state they need on resume; arbitrary unreturned detached work and closure memory are not serialized.
217
+
218
+ Ordinary logging, argument completion, empty experimental metadata, and the SDK's auto-opened catalog notification subscription are reconstructible: requests are owned and notification ingress is fenced. Explicit consumer resource/listen subscriptions, negotiated remote tasks, opaque protocol capabilities, HTTP session IDs, legacy SSE, stdio/Unix servers, pending initialization/requests/refresh/health checks, browser OAuth flows, UI sessions/messages, active scripts, and unpersisted session approvals return a named `sleepReady: false`. Session grants are not silently reset to obtain readiness. Advertising subscription support alone is not an active subscription. A resource-subscribe attempt with a cancelled/lost response remains unresolved until successful unsubscribe or connection disposal; it is not silently replayed. Completed URL elicitation clears only its accepted ID; other pending IDs still veto. Failed cleanup remains owned; an explicit later close may retry only if the native client still owns its transport. Unsupported work remains usable while compute stays running; the hook does not cancel accepted operations to manufacture readiness.
219
+
220
+ Before any asynchronous flush, the adapter fences owned activity and pauses its existing health-check timer. New requests, background HTTP GET/reconnect/auth work, or incoming callbacks invalidate the host's hold **before** dispatch or mutation. Native provider promises and HTTP headers/body reads remain owned through settlement, including cancellation tails; final shutdown joins them while per-server cancellation stays prompt. An idle inbound SSE stream is not a perpetual active request. Completed refreshes use the existing native OS credential store; no token file or second auth store is introduced. Server OAuth challenges reconstruct through normal re-challenge, with granted scopes and issuer bindings in native credentials. Required metadata persistence errors reject capture; optional diagnostic tracing stays best-effort and never prevents recovery or clean exit. Release/cancellation resumes the same clients and timer, without replaying tools, reauthenticating, or running shutdown. Cold startup uses normal discovery and native credentials; only a new explicit tool invocation sends a new tool request.
221
+
222
+ `session_shutdown` separately performs best-effort cleanup and propagates persistence/cleanup failures through the host's ordinary extension error contract. A host callback still running after cleanup also prevents clean-shutdown success. A clean-exit host must not infer success after a failed handler. No callback/process memory recovery is promised.
223
+
224
+ ### Runtime status snapshots
225
+
226
+ Extensions can subscribe to the adapter's versioned shared event-bus channel instead of parsing `/mcp` or `mcp({})` output:
227
+
228
+ ```ts
229
+ import { MCP_STATUS_EVENT, type McpStatusSnapshot } from "@fitchmultz/pi-mcp-adapter";
230
+
231
+ pi.events.on(MCP_STATUS_EVENT, (snapshot) => {
232
+ const status = snapshot as McpStatusSnapshot;
233
+ // status.servers contains connected, cached, failed, needs-auth,
234
+ // not-connected, or disabled entries.
235
+ });
236
+ ```
237
+
238
+ `MCP_STATUS_EVENT` is `fitch-mcp-adapter/status/v1`. 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`, 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.
239
+
240
+ In the configuration examples below, `30000` is illustrative only. If `requestTimeoutMs` is omitted or set to `<= 0`, the MCP SDK default timeout is used.
241
+
242
+ ### Transport compatibility
243
+
244
+ The adapter uses official split MCP SDK 2.0.0. HTTP connections automatically negotiate protocol `2026-07-28` and fall back to older revisions using the SDK's native rules. The SDK generates protocol metadata, method/name headers, and schema-driven `Mcp-Param-*` headers. Modern HTTP supports streamed POST responses without opening a legacy standalone GET stream. Deprecated SSE is tried only when the Streamable HTTP handshake rejects the endpoint with HTTP 404, 405, 406, or 415—not after a timeout, network failure, or authentication failure.
245
+
246
+ For known legacy servers, set `protocolVersion: "legacy"`. This is also the workaround for servers that answer `server/discover` with an HTTP 200 JSON-RPC error whose `id` is `null`: SDK 2.0.0 cannot negotiate that response. The adapter reports the native failure rather than inventing authentication guidance or overriding the SDK's fallback classifier. Stdio and Unix sockets remain legacy by default. Explicit `"auto"` on native stdio uses the SDK's disposable probe process before starting the session process.
247
+
248
+ ```json
249
+ {
250
+ "mcpServers": {
251
+ "modern": { "url": "https://mcp.example.com/mcp" },
252
+ "older": { "url": "https://legacy.example.com/mcp", "protocolVersion": "legacy" },
253
+ "read-only": { "url": "https://reports.example.com/mcp", "retryOnTransportFailure": true }
254
+ }
255
+ }
256
+ ```
257
+
258
+ `retryOnTransportFailure` is off by default. Enabling it trusts the configured server's explicit `readOnlyHint: true` or `idempotentHint: true` tool annotations for at most one new `callTool` request on the same modern HTTP client. Annotations are retained in live/cached metadata and describe output; the live catalog takes precedence over frozen direct-tool metadata. Eligible failures are a rejected tool POST fetch before response headers arrive, an HTTP 5xx tool response without a JSON-RPC error, or an SSE response body disconnecting before its JSON-RPC response arrives. The first attempt may already have run. Fresh SDK request IDs are not idempotency keys.
259
+
260
+ Unknown or non-idempotent tools remain callable but are never automatically redispatched after those failures. An unresolved transport failure returns internal `ambiguous_outcome` details with server/tool/call identity and readback instructions. Use the original saved arguments and the provider's supported operation/resource identity to read the prior result, then continue the remaining work; do not blindly repeat a write or rerun its whole script. Missing annotations are not permission denial. Confirmed pre-dispatch legacy-session and authentication recovery remain available.
261
+
262
+ A broken SSE body fails only the affected request, even when retries are disabled. Completed responses and later stream notifications remain intact. Native SSE resumption stays SDK-owned: a server-provided event ID lets the SDK resume the existing stream before any adapter retry.
263
+
264
+ This transport-failure option does not retry OAuth failures, JSON-RPC errors, tool error results, invalid responses, cancellation, or expired deadlines. Permission recovery uses the separate existing `autoAuth` gate, not this option. JSON response-body failures, clean SSE endings without a response, and streams that merely stop making progress are not replayed. The original tool deadline covers SDK subrequests and retries, and modern retry never stacks with legacy expired-session recovery. Native header-schema refresh and multi-round-trip input handling remain SDK-owned.
265
+
266
+ ### Server Options
267
+
268
+ ```json
269
+ {
270
+ "mcpServers": {
271
+ "my-server": {
272
+ "command": "npx",
273
+ "args": ["-y", "some-mcp-server"],
274
+ "lifecycle": "lazy",
275
+ "idleTimeout": 10,
276
+ "requestTimeoutMs": 30000
277
+ }
278
+ }
279
+ }
280
+ ```
281
+
282
+ | Field | Description |
283
+ |-------|-------------|
284
+ | `command` | Executable for stdio transport; mutually exclusive with `url` and `socket` |
285
+ | `args` | Command arguments |
286
+ | `socket` | Explicit `rmcp-mux` Unix-domain socket path; supports `${VAR}`, `$env:VAR`, and `~` expansion and is mutually exclusive with `command` and `url` |
287
+ | `env` | Environment variables; supports `${VAR}` and `$env:VAR` interpolation. A value beginning with `!` runs a command when the stdio server connects; use `!!` for a literal leading `!`. |
288
+ | `cwd` | Working directory; supports `${VAR}`, `$env:VAR`, and `~` expansion |
289
+ | `protocolVersion` | `"auto"` or `"legacy"`. HTTP defaults to `"auto"`; stdio and Unix sockets default to `"legacy"`. |
290
+ | `retryOnTransportFailure` | Trust explicit read-only/idempotent annotations for one modern transport retry (default: `false`); see [Transport compatibility](#transport-compatibility) before enabling. |
291
+ | `url` | HTTP endpoint (StreamableHTTP with SSE fallback); supports raw `${VAR}` and `$env:VAR` interpolation, and missing URL variables fail before any request is sent |
292
+ | `headers` | HTTP 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 `!`. |
293
+ | `auth` | `"bearer"` or `"oauth"` |
294
+ | `oauth.grantType` | `"authorization_code"` (default) or `"client_credentials"` for non-interactive machine auth |
295
+ | `oauth.clientId` | Pre-registered OAuth client ID; takes priority over stored clients and automatic registration |
296
+ | `oauth.clientSecret` | OAuth client secret for confidential clients; a value beginning with `!` runs a command when OAuth authenticates, while `!!` escapes a literal leading `!` |
297
+ | `oauth.privateKeyJwt` | Native private-key authentication: `privateKey` (PKCS#8 PEM, JWK object/JSON, environment or `!command` source), `algorithm`, optional `audience`, `lifetimeSeconds`, `claims`; requires a configured client ID or custom metadata document, not `clientSecret` |
298
+ | `oauth.crossAppAccess` | Native enterprise OIDC exchange: `idpUrl`, IdP `clientId`, `idToken`, optional IdP `clientSecret`; all support lazy environment/`!command` sources. Omit `grantType`. |
299
+ | `oauth.scope` | Requested OAuth scopes |
300
+ | `oauth.redirectUri` | Exact localhost redirect URI for browser OAuth, including port and path, for providers that pre-register callbacks |
301
+ | `oauth.clientName` | Client display name advertised during dynamic registration |
302
+ | `oauth.clientUri` | Client homepage URI advertised during dynamic registration |
303
+ | `oauth.clientMetadataUrl` | Custom HTTPS Client ID Metadata Document URL, or `false` to disable automatic CIMD for new registrations; supports environment interpolation |
304
+ | `oauth.skipIssuerMetadataValidation` | Skip only the authorization-server metadata issuer check for a known incompatible provider (default: `false`). Stored issuer and callback checks remain enforced. |
305
+ | `bearerToken` / `bearerTokenEnv` | Token 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 `!`. |
306
+ | `lifecycle` | `"lazy"` (default), `"eager"`, `"keep-alive"`, or `"lazy-keep-alive"` |
307
+ | `idleTimeout` | Minutes before idle disconnect (overrides global) |
308
+ | `requestTimeoutMs` | Request timeout in milliseconds for live MCP calls (overrides global; if omitted or `<= 0`, the MCP SDK default timeout is used) |
309
+ | `exposeResources` | Expose MCP resources as tools (default: true) |
310
+ | `directTools` | `true`, `string[]`, or `false` — register tools individually instead of through proxy |
311
+ | `toolPrefix` | Override global `settings.toolPrefix` for this server (`"server"`, `"short"`, `"none"`, or `"mcp"`) |
312
+ | `includeTools` | `string[]` 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`) |
313
+ | `excludeTools` | `string[]` of tool names or glob patterns to hide (applied after `includeTools`) |
314
+ | `debug` | Show server stderr (default: false) |
315
+ | `trace` | Enable metadata-only JSONL protocol tracing for this server; payloads, prompts, tool arguments/results, authorization data, and URLs are never persisted |
316
+ | `disabled` | Keep the server visible in config and status, but prevent connections, authentication, tools, and resource calls (only literal `true` disables it) |
317
+
318
+ For pre-registered browser OAuth clients, set `oauth.redirectUri` to the exact callback registered with the provider, for example `"http://localhost:3118/callback"`. Dynamic clients normally omit it and use a lazy OS-assigned localhost callback port.
319
+
320
+ Secret values in `headers`, `bearerToken`, `oauth.clientSecret`, `oauth.privateKeyJwt.privateKey`, `oauth.crossAppAccess` fields, 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 OAuth discovery or while reading, merging, previewing, hashing, or rendering configuration. Use `!!` to escape a literal leading `!`; ordinary and escaped values retain environment interpolation.
321
+
322
+ ### OAuth client registration
323
+
324
+ Eligible browser clients automatically use the [Fitch MCP Adapter client metadata document](https://fitchmultz.github.io/pi-mcp-adapter/client-metadata.json). The SDK keeps configured `clientId` and usable stored registrations first, then uses the document when the authorization server advertises `client_id_metadata_document_supported: true`. Otherwise it uses dynamic client registration (DCR), if available. An authorization server rejecting or failing to fetch a document does **not** guarantee a fallback to DCR.
325
+
326
+ The shared public identity covers `/callback` on HTTP `localhost`, `127.0.0.1`, or `[::1]`, with a variable loopback port. Normal callbacks still use an OS-assigned port. A different `clientName`, `clientUri`, callback path or query keeps DCR; explicitly matching identity values remain eligible. Set `oauth.clientMetadataUrl` to your own document URL for a custom identity/callback, or `false` to opt out. Documents cannot use shared secrets; the shared browser identity is never used for `client_credentials`, cross-app or private-key authentication.
327
+
328
+ Changing this option does not replace a usable stored login. Saved CIMD registrations allow only a loopback-port change during browser auth; host, scheme, path, query and issuer bindings remain enforced. To deliberately register again, use `/mcp logout <server>`. An authorization-server issuer change still requires clearing credentials; it is not silently migrated. See [OAuth configuration](OAUTH.md#client-registration-order) for details.
329
+
330
+ ### Private-key OAuth
331
+
332
+ Use `oauth.privateKeyJwt` with an existing registered client ID, or a custom metadata document containing the matching public verification key:
333
+
334
+ ```json
335
+ {
336
+ "url": "https://api.example.com/mcp",
337
+ "oauth": {
338
+ "grantType": "client_credentials",
339
+ "clientId": "registered-service-client",
340
+ "privateKeyJwt": {
341
+ "privateKey": "${MCP_PRIVATE_KEY}",
342
+ "algorithm": "ES256"
343
+ }
344
+ }
345
+ }
346
+ ```
347
+
348
+ The SDK signs a fresh assertion for each token request. Keys are resolved at authentication time, never cached or added to the credential store. PEM strings must be PKCS#8 with an RS/PS/ES algorithm; EdDSA / Ed25519 use a JWK. `!command` may return PEM or JWK JSON. The same hook authenticates browser code and refresh requests when `grantType` is omitted. No keys are generated, registered or hosted for you. See [private-key options and identity rules](OAUTH.md#private-key-jwt) for details.
349
+
350
+ ### Enterprise cross-app authorization
351
+
352
+ Use an existing enterprise OIDC ID token to obtain an MCP access token without opening an MCP browser flow:
353
+
354
+ ```json
355
+ {
356
+ "url": "https://api.example.com/mcp",
357
+ "auth": "oauth",
358
+ "oauth": {
359
+ "clientId": "registered-mcp-client",
360
+ "clientSecret": "${MCP_CLIENT_SECRET}",
361
+ "crossAppAccess": {
362
+ "idpUrl": "https://idp.example.com",
363
+ "clientId": "registered-idp-client",
364
+ "idToken": "!your-id-token-command",
365
+ "clientSecret": "${IDP_CLIENT_SECRET}"
366
+ }
367
+ }
368
+ }
369
+ ```
370
+
371
+ The nested credentials belong to the IdP, not the MCP authorization server. Omit `grantType`: this option selects the JWT-bearer grant. The SDK discovers both servers, exchanges the ID token for an ID-JAG (JWT authorization grant), then exchanges that grant for the MCP access token using the outer client identity. Public custom metadata documents and `privateKeyJwt` work instead of the outer shared secret. The shared browser identity is not eligible.
372
+
373
+ Initial enterprise sign-in, ID-token renewal, IdP client registration and keys remain user-managed. The token source is re-read when a new grant is needed; no IdP tokens, secrets or grants are added to storage. The IdP supports public or `client_secret_post` authentication, not IdP private-key JWT, SAML or a built-in login/refresh flow. Headless proxy/direct tools and manual auth use the same noninteractive path. See [cross-app configuration and limits](OAUTH.md#enterprise-cross-app-authorization).
374
+
375
+ ### Shared MCP processes with rmcp-mux
376
+
377
+ To share one stdio MCP server across Pi sessions, run it under [`rmcp-mux`](https://github.com/VetCoders/rmcp-mux) and point each session at the service socket:
378
+
379
+ ```json
380
+ {
381
+ "mcpServers": {
382
+ "memory": {
383
+ "socket": "~/.rmcp-servers/rmcp-mux/sockets/memory.sock"
384
+ }
385
+ }
386
+ }
387
+ ```
388
+
389
+ 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.
390
+
391
+ ### Remote/headless OAuth
392
+
393
+ If Pi is running on a remote server and cannot open a local browser, start OAuth through the proxy tool. Persistent OAuth still 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.
394
+
395
+ 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.
396
+
397
+ ```js
398
+ mcp({ action: "auth-start", server: "linear-server" })
399
+ ```
400
+
401
+ Open the returned authorization URL in an authenticated browser. This can be a headless browser that can reach Pi's localhost callback; the adapter does not open a browser for `auth-start`. After approval reaches the callback, complete the flow in the same Pi session without copying the callback URL or code:
402
+
403
+ ```js
404
+ mcp({ action: "auth-complete", server: "linear-server" })
405
+ ```
406
+
407
+ The validated callback stays in memory until explicit completion or the pending flow's five-minute timeout. Calling `auth-complete` before the callback arrives gives guidance and leaves the flow available. A denied callback reports failure without exchanging a code.
408
+
409
+ If the browser cannot reach Pi's callback (for example, Pi runs on a remote server), copy the full redirected localhost URL from the address bar even if the page fails to load:
410
+
411
+ ```js
412
+ mcp({
413
+ action: "auth-complete",
414
+ server: "linear-server",
415
+ args: { redirectUrl: "http://localhost:19876/callback?code=...&state=..." }
416
+ })
417
+ ```
418
+
419
+ 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. Callback state and issuer are checked before provider error text is displayed, as well as before code exchange.
420
+
421
+ New scope requirements are retained within the current runtime for the next permitted OAuth flow. URL-only servers also activate OAuth when their first protected catalog or tool is reached; valid saved tokens do not require another sign-in. Token scopes remain exactly what the authorization server issued. See [OAuth permission recovery](OAUTH.md#newly-required-permissions) for consent, cancellation, and transport limits.
422
+
423
+ Persistent OAuth is unsupported out of the box on Android/Termux because `@napi-rs/keyring` 1.3.0 ships no Android native binding. Use a supported platform with an OS credential store for OAuth; there is no plaintext fallback. Unauthenticated, header-token, and stdio servers remain available. See [Android/Termux OAuth limits](OAUTH.md#android--termux).
424
+
425
+ ### Lifecycle Modes
426
+
427
+ - **`lazy`** (default) — Don't connect at startup. Connect on first tool call. Disconnect after idle timeout. Cached metadata keeps search/list working without connections.
428
+ - **`eager`** — Connect at startup but don't auto-reconnect if the connection drops. No idle timeout by default (set `idleTimeout` explicitly to enable).
429
+ - **`keep-alive`** — Connect at startup. Auto-reconnect via health checks. No idle timeout. Use for servers you always need available.
430
+ - **`lazy-keep-alive`** — Don't connect at startup. Connect on first tool call (like `lazy`). Once spawned, never idle-shut down and auto-reconnect via health checks if the process dies (like `keep-alive`). Use for servers that are expensive to start but should stay resident after their first use.
431
+
432
+ ### Settings
433
+
434
+ ```json
435
+ {
436
+ "settings": {
437
+ "toolPrefix": "server",
438
+ "idleTimeout": 10,
439
+ "requestTimeoutMs": 30000,
440
+ "showStatusIcon": true,
441
+ "mcpFooterStatus": "compact",
442
+ "hostConfigDiscovery": "off",
443
+ "approveTools": ["github_delete_*", "notion_update_*"],
444
+ "oauthDir": ".pi/mcp-oauth",
445
+ "trace": {
446
+ "enabled": true,
447
+ "file": ".pi/fitch-mcp-adapter/traces/mcp.jsonl",
448
+ "maxBytes": 262144,
449
+ "maxEvents": 10000
450
+ }
451
+ },
452
+ "mcpServers": { }
453
+ }
454
+ ```
455
+
456
+ | Setting | Description |
457
+ |---------|-------------|
458
+ | `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. |
459
+ | `idleTimeout` | Global idle timeout in minutes (default: 10, 0 to disable) |
460
+ | `requestTimeoutMs` | Global request timeout in milliseconds for live MCP calls (if omitted or `<= 0`, the MCP SDK default timeout is used) |
461
+ | `showStatusIcon` | Show the plug icon in MCP status and connection text (default: `true`). Set to `false` for plain `MCP: ...` text. |
462
+ | `mcpFooterStatus` | MCP footer verbosity: `"compact"` (default) for `MCP connected/enabled`, `"full"` for enabled/connected/disabled detail, or `"off"` to clear the persistent footer status. `/mcp status` remains available. |
463
+ | `hostConfigDiscovery` | Host-specific config policy: `"off"` (default) or `"on"` (explicitly load detected host configs as the lowest-precedence fallback) |
464
+ | `approveTools` | `true` 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. |
465
+ | `oauthDir` | Parent directory for legacy OAuth imports. Runtime uses its `fitch-mcp-adapter/` child; explicit v4 migration reads the original directory. Relative paths resolve from the active project cwd. `FITCH_MCP_OAUTH_DIR` overrides the final runtime import path; the old `MCP_OAUTH_DIR` is read only by explicit migration. Persistent credentials live in the OS credential store. |
466
+ | `mcpServers.<name>.oauth.authorizationParams` | Extra 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. |
467
+ | `directTools` | Global default for all servers (default: false). Per-server overrides this. |
468
+ | `freezeDirectTools` | Keep direct-tool registration stable after the initial sync so automatic reconnects and list-change notifications do not rebuild the system prompt. Use `mcp({ connect: "server" })` or `/mcp reconnect <server>` to refresh deliberately. Default: false. |
469
+ | `scriptMode` | Register the MCP-only `mcp_script` plain-JavaScript tool (default: true). Set to `false` to hide it. |
470
+ | `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
471
+ | `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
472
+ | `sampling` | Allow MCP servers to sample through Pi models, honoring `modelPreferences.hints` before current/default fallback (default: true when UI approval is available). |
473
+ | `samplingAutoApprove` | Skip sampling confirmation prompts. Required for sampling in non-UI sessions (default: false). |
474
+ | `elicitation` | Allow MCP servers to request user input through Pi dialogs (default: true when Pi UI is available). |
475
+ | `outputGuard` | Guard oversized MCP output: `true` (default), `false`, or `{ maxBytes, maxLines, detailsMaxBytes }`. See [Output Guard](#output-guard). |
476
+ | `trace` | Opt-in metadata-only protocol tracing. Set `{ enabled: true }` globally or `trace: true` on a server. The per-session JSONL file defaults to `.pi/fitch-mcp-adapter/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. |
477
+
478
+ Per-server `idleTimeout`, `requestTimeoutMs`, and `approveTools` override the global settings. `debug` remains stderr display and is unrelated to protocol tracing.
479
+
480
+ ### Tool Approval
481
+
482
+ 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.
483
+
484
+ ```json
485
+ {
486
+ "settings": {
487
+ "approveTools": ["github_delete_*", "notion_update_*"]
488
+ },
489
+ "mcpServers": {
490
+ "github": { "approveTools": ["delete_*", "merge_pull_request"] },
491
+ "docs": { "approveTools": false }
492
+ }
493
+ }
494
+ ```
495
+
496
+ 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**, or **Deny**. Session approvals are kept in memory only. In headless sessions, matching calls fail closed with an `approval_required` result instead of running. `excludeTools` still removes tools entirely; `approveTools` only gates visible tools at call time.
497
+
498
+ ### Output Guard
499
+
500
+ 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:
501
+
502
+ - 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.
503
+ - **Image content blocks pass through unchanged** — only text output is guarded. Images are delivered to the provider as native image content.
504
+ - In proxy and direct modes, `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 file.
505
+
506
+ Tune the limits with the object form:
507
+
508
+ ```json
509
+ {
510
+ "settings": {
511
+ "outputGuard": { "maxBytes": 51200, "maxLines": 2000, "detailsMaxBytes": 16384 }
512
+ }
513
+ }
514
+ ```
515
+
516
+ Set `"outputGuard": false` — or the env kill switch `MCP_OUTPUT_GUARD=0` — to disable the guard and restore raw output behavior. Saved files are created with mode `0600` under the system temp directory (or the SDK host's `outputDirectory`) and are not cleaned up automatically; note that spilled MCP output may contain sensitive data.
517
+
518
+ ### MCP Scripting
519
+
520
+ For multi-call MCP work, write ordinary JavaScript: discover, inspect, call, loop, filter, chain, or fan out, then return one result. Run that code with the default-on `mcp_script` tool. For a single MCP call, search, describe, status check, or auth action, use `mcp` instead. Set `settings.scriptMode` to `false` to hide the scripting tool.
521
+
522
+ The bundled `mcp-scripting` skill is a separate Pi package resource. To hide that skill while keeping the adapter extension installed, replace the package entry in Pi settings with the object form and disable package skills:
523
+
524
+ ```json
525
+ {
526
+ "packages": [
527
+ { "source": "npm:@fitchmultz/pi-mcp-adapter", "skills": [] }
528
+ ]
529
+ }
530
+ ```
531
+
532
+ Preserve any version pin in `source` if your existing package entry has one. You can also disable package resources through `pi config`.
533
+
534
+ For example, this is the JavaScript passed as the `code` argument to `mcp_script`:
535
+
536
+ ```js
537
+ const { items } = await tools.search({ query: "search issues", server: "github" });
538
+ const candidate = items[0];
539
+ if (!candidate) return { error: "No matching tool" };
540
+
541
+ const details = await tools.describe({ path: candidate.path });
542
+ if (details.error) return details;
543
+
544
+ const result = await tools.call(details.path, { query: "is:open label:bug" });
545
+ if (!result.ok) return result;
546
+ emit({ tool: details.path, completed: true });
547
+ return result.data;
548
+ ```
549
+
550
+ `tools.describe` returns argument information as `inputTypeScript` when representable, otherwise as the original JSON Schema in `inputSchema`. Only one is included; inspect it before constructing arguments.
551
+
552
+ See the bundled `mcp-scripting` skill for the complete workflow guide. The API is `await tools.search({ query, server?, limit?, offset? })`, `await tools.describe({ path })`, `tools.call(path, args)`, direct flat calls, `emit(value)`, and a captured `console`. Use ordinary JavaScript loops and Promise utilities for composition; fluent helpers such as `tools.find(...).one()`, `tools.parallel(...)`, and `tools.retry(...)` are not provided. MCP calls return `{ ok: true, data }` or `{ ok: false, error: { code, message } }`, so a failed call does not stop the rest of the script. Result details include a concise `calls` trace with each operation, its path or query, outcome, and duration. Emitted values and console output appear before the script's final return value, and the combined result uses the normal MCP output guard. The default timeout is 30 seconds unless the SDK host overrides it with `defaultScriptTimeoutMs`; an explicit per-script `timeoutMs` always wins. Each script runs in a worker thread that is terminated on timeout or cancellation, including for infinite loops.
553
+
554
+ For a tool-restricted subagent, launch the child Pi with its tool allowlist set to `["mcp_script"]`. Have the parent discover MCP tool names with `mcp({ search: "..." })` and include the relevant prefixed names in the child's task; the child can then loop, filter, and chain those MCP calls without filesystem, shell, or edit tools. The adapter's ordinary lazy connection, authentication, output guard, abort handling, and approval gates still apply to every call.
555
+
556
+ `mcp_script` is a trusted agent-authored MCP scripting layer, not an isolation boundary. If you need isolation, run Pi in an isolated environment. It is distinct from Pi's code-mode skill: Pi's skill batches general Pi tools, while `mcp_script` exposes MCP calls only and can be the child's sole tool.
557
+
558
+ ### MCP Prompts
559
+
560
+ MCP servers can advertise prompt templates alongside tools and resources. The adapter registers cached prompt definitions as Pi slash commands under `/mcp__<server>__<prompt>`, and refreshes their metadata whenever a server connects. Arguments support positional and `key=value` forms with quoting; required arguments are validated before `prompts/get` is called.
561
+
562
+ ```text
563
+ /mcp__agent_board__create_plan "harden retry policy"
564
+ /mcp__agent_board__review_pipeline status=paused
565
+ /mcp prompts
566
+ ```
567
+
568
+ Prompt results are flattened into one user message, preserving `[user]` and `[assistant]` role markers for multi-message results. Servers without the `prompts` capability are not probed.
569
+
570
+ ### MCP Elicitation
571
+
572
+ When Pi exposes dialog-capable UI, the adapter advertises form elicitation support. Forms use Pi's stock `select()` and `input()` dialogs, validate the response, and provide a review/edit step before submission. Explicit refusal maps to MCP `decline`; dismissing a dialog maps to `cancel`.
573
+
574
+ URL mode is advertised only in TUI mode. The adapter displays the requesting server, target host, and full URL, and always requires consent before opening the browser. It also handles URL-required tool errors (`-32042`) and completion notifications; after completing the browser interaction, retry the original tool call.
575
+
576
+ ### Direct Tools
577
+
578
+ By default, all MCP tools are accessed through the single `mcp` proxy tool. This keeps context small but means the LLM has to discover MCP tools via proxy search. If you want specific tools to show up directly in the agent's tool list — alongside `read`, `bash`, `edit`, etc. — add `directTools` to your config.
579
+
580
+ Per-server:
581
+
582
+ ```json
583
+ {
584
+ "mcpServers": {
585
+ "chrome-devtools": {
586
+ "command": "npx",
587
+ "args": ["-y", "chrome-devtools-mcp@latest"],
588
+ "directTools": true
589
+ },
590
+ "github": {
591
+ "command": "npx",
592
+ "args": ["-y", "@modelcontextprotocol/server-github"],
593
+ "directTools": ["search_repositories", "get_file_contents"]
594
+ },
595
+ "huge-server": {
596
+ "command": "npx",
597
+ "args": ["-y", "mega-mcp@latest"]
598
+ }
599
+ }
600
+ }
601
+ ```
602
+
603
+ | Value | Behavior |
604
+ |-------|----------|
605
+ | `true` | Register all tools from this server as individual Pi tools |
606
+ | `["tool_a", "tool_b"]` | Register only these tools (use original MCP names) |
607
+ | Omitted or `false` | Proxy only (default) |
608
+
609
+ To set a global default for all servers:
610
+
611
+ ```json
612
+ {
613
+ "settings": {
614
+ "directTools": true
615
+ },
616
+ "mcpServers": {
617
+ "huge-server": {
618
+ "directTools": false
619
+ }
620
+ }
621
+ }
622
+ ```
623
+
624
+ Per-server `directTools` overrides the global setting. The example above registers direct tools for every server except `huge-server`.
625
+
626
+ Set `MCP_DIRECT_TOOLS=__none__` before loading the adapter to suppress all direct-tool registration, including after lazy connections and metadata updates. With default scripting enabled, only `mcp` and `mcp_script` are exposed by the adapter. Configured servers remain available through those tools; normal tool filters and disabled-server settings still apply.
627
+
628
+ To expose only a subset of a noisy server, add `includeTools` on the server. Values can be exact original names, generated resource names such as `read_<resource>`, prefixed names, or simple glob patterns:
629
+
630
+ ```json
631
+ {
632
+ "mcpServers": {
633
+ "dokploy": {
634
+ "url": "http://localhost:3845/mcp",
635
+ "directTools": true,
636
+ "includeTools": ["get_*", "dokploy_list_apps"]
637
+ }
638
+ }
639
+ }
640
+ ```
641
+
642
+ To hide specific tools while still using `directTools: true`, add `excludeTools` on the server. `excludeTools` is applied after `includeTools`:
643
+
644
+ ```json
645
+ {
646
+ "mcpServers": {
647
+ "figma": {
648
+ "url": "http://localhost:3845/mcp",
649
+ "directTools": true,
650
+ "excludeTools": ["read_figjam", "figma_get_code_connect_map"]
651
+ }
652
+ }
653
+ }
654
+ ```
655
+
656
+ `includeTools` and `excludeTools` filter direct tools, proxy search/list/describe, and the `/mcp` panel view.
657
+
658
+ Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`. If 75+ direct tools resolve, the adapter prints a warning but still registers the tools you configured.
659
+
660
+ Direct tools register from the metadata cache (`~/.pi/agent/fitch-mcp-adapter/mcp-cache.json` by default, or `$PI_CODING_AGENT_DIR/fitch-mcp-adapter/mcp-cache.json` when set), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only while the cache populates, then the extension hot-loads the refreshed direct tools into the current session. Servers that advertise MCP list-change notifications refresh the current session when their tool or resource list changes. Stale direct tools are deactivated from the active tool set and reactivated if a later refresh restores them. To force a refresh: `/mcp reconnect <server>`.
661
+
662
+ If prompt-cache stability matters more than automatic direct-tool hot-loading, set `settings.freezeDirectTools` to `true`. The initial direct-tool sync still runs, but later automatic reconnects, lazy-connects, and list-change notifications keep the registered tool surface unchanged. Deliberate refreshes through `mcp({ connect: "server" })` or `/mcp reconnect <server>` still update direct tools.
663
+
664
+ When you change direct-tool toggles in `/mcp`, the extension updates direct tool registration in the current session. Broader setup writes from `/mcp setup` still use Pi's normal reload flow because they can add or restructure MCP config files.
665
+
666
+ **Interactive configuration:** Run `/mcp` to open an interactive panel showing all servers with connection status, tools, and direct/proxy toggles. You can reconnect servers and toggle tools between direct and proxy from the same overlay. For OAuth, press Enter on a server that needs auth or `ctrl+a` on any OAuth server.
667
+
668
+ **Guided first-run setup:** Run `/mcp setup` to inspect detected shared MCP files, adopt compatibility imports from other hosts, open discovered config paths, preview exact before/after file diffs for writes, scaffold a minimal project `.mcp.json`, add a curated known server (DeepWiki, Context7, Notion, GitHub, or Chrome DevTools), or quick-add RepoPrompt into a standard/shared MCP file.
669
+
670
+ **Subagent integration:** If you use the subagent extension, agents can request direct MCP tools in their frontmatter with `mcp:server-name` syntax. See the subagent README for details.
671
+
672
+ ### MCP UI Integration
673
+
674
+ MCP servers can ship interactive UIs via the [MCP UI](https://github.com/MCP-UI-Org/mcp-ui) standard. When you call a tool that has a UI resource, the adapter opens it in a native macOS window via [Glimpse](https://github.com/hazat/glimpse) if available, otherwise falls back to the browser.
675
+
676
+ **How it works:**
677
+
678
+ 1. Agent calls a tool like `launch_dashboard`
679
+ 2. The tool's metadata includes `_meta.ui.resourceUri` pointing to a UI resource
680
+ 3. The adapter fetches the UI HTML and opens it in an iframe
681
+ 4. The UI can call MCP tools and send messages back to the agent
682
+
683
+ **Native rendering:** On macOS, if [Glimpse](https://github.com/hazat/glimpse) is installed (`pi install npm:glimpseui`), UIs open in a native WKWebView window instead of a browser tab. Set `MCP_UI_VIEWER=browser` to force the browser, `MCP_UI_VIEWER=glimpse` to require native rendering, or `MCP_UI_VIEWER=none` (also accepts `off` / `disabled`) to suppress the window entirely — the tool still runs and its inline result is returned to the agent, but no browser or native window opens. This is useful for headless setups, CI, or users who want the tool output delivered inline as text only. When suppressed, a one-line info notification shows the UI URL so it can still be opened manually if needed.
684
+
685
+ **Bidirectional communication:** The UI talks back. When it sends a prompt or intent, the message is stored and `triggerTurn()` wakes the agent. The agent retrieves messages via `mcp({ action: "ui-messages" })` and responds, enabling conversational UIs where the app and agent collaborate in real-time.
686
+
687
+ **Session reuse:** When the agent calls the same tool again while its UI is already open, the adapter pushes the new result to the existing window instead of replacing it. This enables live updates — the agent can refine a chart, add data, or respond to user input without losing the current view. Different tools still replace the session as before.
688
+
689
+ **Message types from UI:**
690
+
691
+ | Type | Purpose |
692
+ |------|---------|
693
+ | `prompt` | User message that triggers an agent response |
694
+ | `intent` | Structured action with name + params |
695
+ | `notify` | Fire-and-forget notification |
696
+ | `message` | Generic message payload |
697
+ | (custom) | Any other type forwarded as intent |
698
+
699
+ **Retrieving UI messages:**
700
+
701
+ ```
702
+ mcp({ action: "ui-messages" })
703
+ ```
704
+
705
+ Returns accumulated messages from UI sessions. Each message includes `type`, `sessionId`, `serverName`, `toolName`, and `timestamp`. Prompt messages include `prompt`, intent messages include `intent` and `params`.
706
+
707
+ **Browser controls:**
708
+
709
+ - **Cmd/Ctrl+Enter** — Complete and close
710
+ - **Escape** — Cancel and close
711
+ - **Done/Cancel buttons** — Same as keyboard shortcuts
712
+
713
+ **Technical notes:**
714
+
715
+ - Tool consent gates whether UIs can call MCP tools (never/once-per-server/always)
716
+ - `_meta.ui.visibility` controls audience: tools marked app-only stay out of the model tool list, and tools marked model-only cannot be called from the UI iframe.
717
+ - Works with both stdio and HTTP MCP servers
718
+ - Uses a local 408KB AppBridge bundle (MCP SDK + Zod) for browser↔server communication
719
+ - Enforces CSP from standard `_meta.ui.csp` and OpenAI-compatible `_meta["openai/widgetCSP"]` metadata in the response header while preserving provider HTML.
720
+
721
+ ### Local Example: Interactive Visualizer
722
+
723
+ A minimal MCP UI example at `examples/interactive-visualizer` demonstrating charts, bidirectional messaging, and streaming. From that directory:
724
+
725
+ ```bash
726
+ npm install
727
+ npm run build
728
+ npm run install-local
729
+ ```
730
+
731
+ Restart pi, then ask the agent to show a chart — it calls `show_chart` and opens the UI in Glimpse (macOS) or the browser. Use `npm run uninstall-local` to remove the MCP entry.
732
+
733
+ ### Import Existing Configs
734
+
735
+ Shared MCP files are loaded automatically. Use `imports` only for host-specific config formats that are not already covered by `.mcp.json` or `~/.config/mcp/mcp.json`.
736
+
737
+ ```json
738
+ {
739
+ "imports": ["cursor", "claude-code", "claude-desktop", "opencode"],
740
+ "mcpServers": { }
741
+ }
742
+ ```
743
+
744
+ Supported compatibility imports: `cursor`, `claude-code`, `claude-desktop`, `opencode`, `vscode`, `windsurf`, `codex`
745
+
746
+ `fitch-mcp-adapter init` detects these host-specific configs and adds missing imports to the adapter-owned global config for you. The `opencode` import reads OpenCode V1 `mcp` entries from both `~/.config/opencode/opencode.json` and the project `opencode.json`, with project fields taking precedence. It is explicit-import only; OpenCode V2, inline content, managed configs, and remote discovery are not supported.
747
+
748
+ ### Project Config
749
+
750
+ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/fitch-mcp-adapter/mcp.json` only when you need an adapter-specific project override. Project files override both user-global shared MCP config and Pi global overrides.
751
+
752
+ ## Usage
753
+
754
+ | Mode | Example |
755
+ |------|---------|
756
+ | Status | `mcp({ })` |
757
+ | List server | `mcp({ server: "name", limit: 12, offset: 0 })` |
758
+ | Search | `mcp({ search: "screenshot navigate", limit: 12, offset: 0 })` |
759
+ | Describe | `mcp({ describe: "tool_name" })` |
760
+ | Instructions | `mcp({ instructions: "name" })` |
761
+ | Call | `mcp({ tool: "...", args: { key: "value" } })` |
762
+ | Connect | `mcp({ connect: "server-name", limit: 12, offset: 0 })` |
763
+ | UI messages | `mcp({ action: "ui-messages" })` |
764
+ | Auth start | `mcp({ action: "auth-start", server: "name" })` |
765
+ | Auth complete | `mcp({ action: "auth-complete", server: "name" })` after the browser callback, or supply `args: { redirectUrl: "..." }` |
766
+
767
+ `mcp({ connect: "server-name" })` refreshes an already connected server, so new tools, resources, prompts, and instructions can load without restarting Pi. Its returned server listing accepts the same `limit` and `offset` options as list mode.
768
+
769
+ MCP proxy and direct-tool results render compactly by default: long text shows the first three terminal-wrapped lines plus Pi's configured `app.tools.expand` keybinding hint, while the full result remains available when expanded and is still returned unchanged to the model.
770
+
771
+ Search covers MCP tools. Space-separated words are ranked by weighted matches across name, server, and description. Search and server listings return one page at a time (`limit` defaults to 12 and is capped at 100); use `details.nextOffset` for the next page.
772
+
773
+ Tool names are fuzzy-matched on hyphens and underscores — `context7_resolve_library_id` finds `context7_resolve-library-id`. When `describe` or `tool` cannot resolve a name, the result includes top suggestions so the agent can correct a typo or missing prefix in the same turn.
774
+
775
+ When `includeSchemas` is enabled, search and describe render common JSON Schema parameters as compact TypeScript shapes like `{ query: string; limit?: number; }`, with the older schema formatter retained as a fallback for unsupported schemas.
776
+
777
+ The `mcp` tool description contains usage guidance and configured server names, not changing catalog counts or server-instruction previews. Connecting, refreshing metadata, or renewing credentials therefore does not rewrite that description. Status, search, describe, and connect results still reflect current metadata; explicitly configured direct tools keep their normal refresh behavior.
778
+
779
+ Servers that provide usage guidance via the MCP `instructions` field surface it as a preview at the end of `mcp({ server: "name" })` listings and in full via `mcp({ instructions: "name" })`. Instructions are captured at connect time and cached alongside tool metadata, so they stay available without a live connection. No extra discovery call is required when the needed details are already in the conversation.
780
+
781
+ ## Commands
782
+
783
+ | Command | What it does |
784
+ |---------|--------------|
785
+ | `/mcp` | Interactive panel and first-run onboarding surface |
786
+ | `/mcp setup` | Guided setup for imports, a minimal `.mcp.json`, curated known servers, RepoPrompt quick-add, and config-path inspection |
787
+ | `/mcp tools` | List all tools |
788
+ | `/mcp prompts` | List all MCP prompts registered as slash commands |
789
+ | `/mcp reconnect` | Reconnect all servers |
790
+ | `/mcp reconnect <server>` | Connect or reconnect a single server |
791
+ | `/mcp disable <server>` | Disable a server in the project-local `.pi/fitch-mcp-adapter/mcp.json` (requires `/reload` to apply) |
792
+ | `/mcp enable <server>` | Enable through the project-local override layer (requires `/reload` to apply) |
793
+ | `/mcp logout <server>` | Clear stored OAuth credentials for a server and disconnect it |
794
+ | `/mcp-auth` | Open an OAuth server picker in interactive UI sessions |
795
+ | `/mcp-auth <server>` | OAuth setup for a specific server |
796
+
797
+ If `settings.autoAuth` is `true`, `mcp({ connect: ... })`, `mcp({ tool: ... })`, and direct/script tool calls may run OAuth when needed, with at most one automatic auth attempt per invocation and one post-auth retry. Browser authorization requires an interactive host. Auth-only replacement preserves accepted work on the old client; ordinary `/mcp reconnect` and panel `ctrl+r` remain hard resets.
798
+
799
+ In interactive sessions, you can also authenticate from `/mcp` with `ctrl+a` or Enter on a server that needs auth. In remote/headless sessions, use the proxy tool's `auth-start` and `auth-complete` actions. Complete without arguments when the browser reaches Pi's callback, or paste the redirect URL when it cannot. `/mcp-auth` without a server only opens a picker in the interactive UI.
800
+
801
+ ### MCP output schemas
802
+
803
+ Advertised tool `outputSchema` values support JSON Schema draft-07 and 2020-12. Unstamped schemas use the SDK's 2020-12 default. Returned `structuredContent` is validated against the advertised schema for both proxy and direct-tool calls. When content blocks are empty, any JSON structured value—including `null`, `false`, and `0`—is rendered as text. Non-empty content, including images, takes precedence.
804
+
805
+ ## How It Works
806
+
807
+ - One `mcp` tool in context (~200 tokens) instead of hundreds
808
+ - Servers are lazy by default — they connect on first tool call, not at startup
809
+ - Tool metadata is cached to disk so search/list/describe work without live connections
810
+ - Idle servers disconnect after 10 minutes (configurable), reconnect automatically on next use
811
+ - npx-based servers resolve to direct binary paths, skipping the ~143 MB npm parent process
812
+ - MCP server validates arguments, not the adapter
813
+ - Keep-alive servers get health checks and auto-reconnect
814
+ - Specific tools can be promoted from the proxy to first-class Pi tools via `directTools` config, so the LLM sees them directly instead of having to search
815
+
816
+ ## Limitations
817
+
818
+ - Cross-session server sharing not yet implemented (each Pi session runs its own server processes)
819
+ - Compact MCP result rendering summarizes text, but inline images are still controlled by Pi's image display settings and may render below the compact text summary.
820
+ - MCP sampling support is text-only; context inclusion, tools, stop sequences, audio, and image content are rejected with explicit errors.