pi-mcp-adapter 3.3.0 → 4.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,57 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.0.0] - 2026-09-30
11
+
12
+ ### Highlights
13
+
14
+ - Figma now works with Pi through the Figma desktop app. `/mcp-adapter setup` offers it when the app is installed and tells you how to turn its server on.
15
+ - `/mcp-adapter setup` is easier to use: it keeps one size, groups actions into sections, shows details beside the list, and scrolls long previews.
16
+ - Other extensions can call your MCP tools directly, with the same approval rules the model gets.
17
+ - `mcpScript` is now opt-in. When you turn it on, scripts pick the right tool when servers share tool names and remember what each tool returns across sessions.
18
+ - Errors now say what to do when an OAuth server refuses Pi or a local MCP server isn't running, and Pi 0.99 and git-installed Pi packages work again.
19
+
20
+ ### Breaking
21
+
22
+ - `mcpScript` is off by default. To keep using it, set `settings.scriptMode` to `true` in `mcp-adapter.json`. Its bundled skill, the pointer to it in the `mcp` tool description, and the large-result hint (see Changed) follow the same setting.
23
+ - `tools.describe` in `mcpScript` now looks tools up the same way as `mcp({ describe })`. Where it used to return the first server's match, it can now return `error: { code, message }`: `ambiguous_tool` when equally good matches exist on more than one server, and `server_disabled` or `server_backoff` when only a disabled or backed-off server has the tool. A clearly closer match, such as an exact name against a normalized one, is still picked without an error. Pass the `server` from the search result when the target matters: `tools.describe({ path, server })`.
24
+ - `observedOutput.target` from `tools.describe` is now the full expression that reads the JSON, such as `(await tools.call("github_list_issues", args)).data.structuredContent`, instead of `data.structuredContent`. Models misread the short form.
25
+ - `observedOutput` no longer has `calls`, and `describe` no longer says how many calls a shape came from, since saved shapes can come from earlier sessions.
26
+
27
+ ### Added
28
+
29
+ - Other extensions can call a configured MCP tool by emitting `pi-mcp-adapter:runtime-tool-call:v1` on Pi's event bus. The call goes through the same tool lookup and approval as `mcp({ tool })`, and `request.result` resolves to `{ ok: true, result }` or `{ ok: false, error }`. Thanks to [@Djarid](https://github.com/Djarid) for [PR #735](https://github.com/nicobailon/pi-mcp-adapter/pull/735).
30
+ - `/mcp-adapter setup` offers Figma (desktop) when the Figma app is installed. It adds the app's local server at `http://127.0.0.1:3845/mcp` and, if nothing answers there yet, says how to turn it on in Figma. Figma's remote server only accepts clients Figma has approved, and Pi isn't one yet.
31
+ - `tools.describe` and `tools.call` in `mcpScript` accept the `server` from a search result: `tools.describe({ path, server })` and `tools.call(path, args, { server })`. This makes a tool usable when two servers share its name, as with `toolPrefix: "none"`. Before, describe returned the first server's tool and call failed as ambiguous. The README has a new "Composable tool search" section showing search, filter, describe, and call in one script.
32
+ - With `mcpScript` on, the output shapes seen from tools are saved in `mcp-cache.json`, so a later session can script a tool without first calling it to see its fields. Saved shapes hold field names and types only, and are dropped when the tool's description, input schema, or server config changes.
33
+ - When an `mcpScript` run throws, times out, or returns nothing useful (`[]`, `{}`, `null`, `""`, or no value), the result ends with the output shapes of the tools it called, so a wrong field guess can be fixed without another call.
34
+ - `settings.scriptSkill: "model"` adds the `mcp-scripting` skill's path to the `mcpScript` description so the model reads it before writing a script. The default, `"manual"`, leaves the skill to `/skill:mcp-scripting`.
35
+
36
+ ### Changed
37
+
38
+ - `/mcp-adapter setup` keeps one size as you move through it, groups actions into sections, and shows details beside the list. PgUp/PgDn scroll long details. The `Close` row is gone; press Esc to close.
39
+ - When a server refuses Pi's OAuth client registration, the error now says what to do: for Figma's remote server, use the desktop app's server through `/mcp-adapter setup`; for other servers, set `oauth.clientId` if the provider gave you one. Before, it only said `Dynamic Client Registration rejected (HTTP 403): Forbidden`.
40
+ - When nothing is listening at a `localhost`, `127.0.0.1`, or `[::1]` server URL, the error now adds `Nothing is listening at <url>. Start the app or local process that serves this MCP server.`
41
+ - `tools.search` in `mcpScript` now behaves like `mcp({ search })`. An empty query with a `server` lists that server's tools, and a search that can't run returns `error: { code, message }` next to `items: []`, with the same codes as `mcp({ search })`, instead of a bare empty result.
42
+ - With `mcpScript` on, MCP tool results of 8 KiB or more end with a one-line hint to use `mcpScript` when passing them to another call, so models stop retyping large results by hand.
43
+ - The `mcpScript` description is shorter and now explains that a tool call's `data` is the raw MCP result and how to read JSON from it.
44
+ - Output shapes list a wide object that repeats as a named type, so a GitHub-style issue list shape is about 40% shorter with every field still shown.
45
+ - The `mcp-scripting` skill is about half as long, with `jev.evaluate` details moved to `references/jev.md`. It now says to write the real script first, check the first item's fields before a loop that writes, and fix the script from the listed fields when it fails or finds nothing.
46
+
47
+ ### Fixed
48
+
49
+ - Pi 0.99's built-in MCP extension is detected again. Pi 0.99 renamed it `builtin:mcp`, so the adapter took over `/mcp` and warned about `mcp.json` on every start even though the built-in extension owns that file. Thanks to [@Sebastianlopez-dev](https://github.com/Sebastianlopez-dev) for reporting it in [#736](https://github.com/nicobailon/pi-mcp-adapter/issues/736).
50
+ - Installing next to Pi 0.99 no longer prints an npm peer dependency warning. The optional `@earendil-works/pi-ai` peer range now includes `^0.99.0`, and the adapter is tested against Pi 0.99.1. Thanks to [@fl4pj4ck](https://github.com/fl4pj4ck) for reporting it in [#741](https://github.com/nicobailon/pi-mcp-adapter/issues/741).
51
+ - MCP servers from a Pi package installed from a git URL with a port or a user other than `git`, such as `ssh://git@gitlab.example.com:2235/acme/tools.git`, now load. Before, they were skipped without a warning. Thanks to [@shura-v](https://github.com/shura-v) for [#746](https://github.com/nicobailon/pi-mcp-adapter/pull/746).
52
+ - MCP servers from a Pi package installed at a ref that contains a slash, such as `https://gitlab.example.com/acme/tools.git@release/v2`, now load.
53
+ - MCP metadata marked `private` is no longer reused from the cache in a later session, including output shapes saved for `mcpScript`. It is still used within the session that fetched it. Thanks to [@quifox](https://github.com/quifox) for [#743](https://github.com/nicobailon/pi-mcp-adapter/pull/743).
54
+ - When a server's tool list changes and saving the refreshed metadata fails, the update is retried instead of lost. Thanks to [@quifox](https://github.com/quifox) for [#742](https://github.com/nicobailon/pi-mcp-adapter/pull/742).
55
+ - `regex: true` in `mcpScript`'s `tools.search` was ignored: scripts got ranked word matches, and an invalid pattern gave no error. It now runs the same checked regex search as `mcp({ search })`.
56
+ - Models no longer call doubled names such as `tracker_tracker_list_issues` in `mcpScript`. The `code` parameter's example now uses the tool name exactly as `mcp` lists it: `tools.call("github_search_issues", args)`.
57
+ - When two servers share a tool name, the `tools.call` examples in `observedOutput.target` and in the fields listed after a failed or empty script include the server, such as `tools.call("echo", args, { server: "other" })`, so copying them works. A script that called the shared name on both servers now lists each server's fields, not just the last one's. Closes [#751](https://github.com/nicobailon/pi-mcp-adapter/issues/751).
58
+ - Output shapes in `describe` no longer drop nested fields such as `labels: { name: string }[]` on GitHub-style lists.
59
+ - The `mcp` tool description mentions `mcpScript` only when it is available. Before, it pointed to `mcpScript` even with `settings.scriptMode` set to `false`.
60
+
10
61
  ## [3.3.0] - 2026-09-29
11
62
 
12
63
  ### Highlights
package/OAUTH.md CHANGED
@@ -314,9 +314,11 @@ The SDK automatically discovers OAuth endpoints from the MCP server. If discover
314
314
  }
315
315
  ```
316
316
 
317
- ### "Dynamic client registration not supported"
317
+ ### "Dynamic client registration not supported" or "Dynamic Client Registration rejected (HTTP 403)"
318
318
 
319
- Some servers require pre-registered clients. Obtain a client ID from your OAuth provider and add it to the config.
319
+ Some servers only accept pre-registered clients. Obtain a client ID from your OAuth provider and set `oauth.clientId` (and `oauth.clientSecret` if the provider issued one).
320
+
321
+ Figma's remote server (`https://mcp.figma.com/mcp`) only accepts clients Figma has approved, and Pi isn't one yet. Use the Figma desktop app's local server instead: run `/mcp-adapter setup` and add Figma (desktop).
320
322
 
321
323
  ### Callback server already in use
322
324
 
package/README.md CHANGED
@@ -225,6 +225,32 @@ Cross-extension registration uses Pi's shared event bus and does not require a r
225
225
 
226
226
  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.
227
227
 
228
+ ### Runtime tool calls from other extensions
229
+
230
+ An extension can call a configured MCP tool from its own code, without a model turn:
231
+
232
+ ```ts
233
+ const MCP_RUNTIME_TOOL_CALL_EVENT = "pi-mcp-adapter:runtime-tool-call:v1";
234
+ type RuntimeToolCallRequest = {
235
+ version: 1;
236
+ tool: string;
237
+ args?: Record<string, unknown>;
238
+ server?: string;
239
+ result?: Promise<
240
+ | { ok: true; result: { content: unknown[]; details?: unknown } }
241
+ | { ok: false; error: Error }
242
+ >;
243
+ };
244
+
245
+ const request: RuntimeToolCallRequest = { version: 1, tool: "search", args: { query: "mcp" } };
246
+ pi.events.emit(MCP_RUNTIME_TOOL_CALL_EVENT, request);
247
+ if (!request.result) throw new Error("pi-mcp-adapter is not installed");
248
+ const outcome = await request.result;
249
+ if (!outcome.ok) throw outcome.error;
250
+ ```
251
+
252
+ `tool` and `server` resolve the same way as `mcp({ tool, server })`, and approval settings and disabled servers apply as they do for that call. The adapter sets `request.result` to a promise during `emit()`; await it. A tool error or denied approval resolves with `ok: false`. Emit during `session_start` or later; calls fail after the session shuts down.
253
+
228
254
  ### SDK configuration
229
255
 
230
256
  Use `createMcpAdapter` when an SDK or server integration already owns its MCP configuration:
@@ -581,7 +607,8 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
581
607
  | `directToolResultDetails` | Direct-tool result details: `"lean"` (default) or `"bounded"` to retain the guarded raw MCP result. |
582
608
  | `warnOnLargeDirectTools` | Show the advisory when 75 or more direct tools resolve (default: `true`). Set to `false` to suppress only this advisory. |
583
609
  | `freezeDirectTools` | Keep 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. |
584
- | `scriptMode` | Register the MCP-only `mcpScript` plain-JavaScript tool (default: true). Set to `false` to hide it. |
610
+ | `scriptMode` | Register the MCP-only `mcpScript` plain-JavaScript tool and its bundled skill (default: false). Read when Pi loads the adapter; run `/reload` after changing it. |
611
+ | `scriptSkill` | How the model finds the bundled `mcp-scripting` skill when `scriptMode` is on: `"manual"` (default) keeps it to `/skill:mcp-scripting`; `"model"` adds its path to the `mcpScript` description so the model reads it before writing a script. |
585
612
  | `exposeResources` | Expose MCP resources as tools (default: `true`). Set to `false` to disable globally across all servers. Per-server `exposeResources` overrides this. |
586
613
  | `jev` | Optional 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. |
587
614
  | `disableProxyTool` | Hide 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). |
@@ -733,9 +760,9 @@ Optional `jev` controls bound timeout/retries, request and script budgets, seman
733
760
 
734
761
  Semantic search sends your request and the available tool descriptions to Jev, which works out which tools best match what you’re trying to do. In a live test with 12 everyday requests and 95 tools and resources, Jev chose the expected result first in 10 of 11 answerable cases and placed it second once. Regular text search found the expected result first in 5 cases. Jev also correctly returned no result for an unrelated request. This was a small test using one local setup, so results will vary with different tools and queries.
735
762
 
736
- 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 `mcpScript` tool. For a single MCP call, search, describe, status check, or auth action, use `mcp` instead. Set `settings.scriptMode` to `false` to hide both the scripting tool and its bundled skill.
763
+ 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 `mcpScript` tool, which is off by default; set `settings.scriptMode` to `true` to register it and its bundled skill. For a single MCP call, search, describe, status check, or auth action, use `mcp` instead.
737
764
 
738
- The bundled `mcp-scripting` skill is manual-only by default, so its description is not added to the model's automatic skill context. Use `/skill:mcp-scripting` when you want its detailed workflow.
765
+ The bundled `mcp-scripting` skill is manual-only: use `/skill:mcp-scripting`, or set `settings.scriptSkill` to `"model"` so the `mcpScript` description tells the model where to read it.
739
766
 
740
767
  For example, this is the JavaScript passed as the `code` argument to `mcpScript`:
741
768
 
@@ -753,9 +780,30 @@ emit({ tool: details.path, completed: true });
753
780
  return result.data;
754
781
  ```
755
782
 
756
- Depending on the server, successful `result.data` may be the raw MCP `CallToolResult` envelope rather than the domain payload. Check `result.data.structuredContent` for the fields your script expects; if they are absent, inspect text blocks in `result.data.content` too. If neither shape is understood, return the envelope for inspection instead of coercing it to an empty collection.
783
+ For tool calls, successful `result.data` is the raw MCP `CallToolResult`, not the domain payload; resource reads return text. Use `result.data.structuredContent` when present. Otherwise most JSON APIs return their payload as text, so parse `result.data.content[0].text`. If neither shape is understood, return the envelope for inspection instead of coercing it to an empty collection.
784
+
785
+ #### Composable tool search
786
+
787
+ Use JavaScript to filter, sort, or combine search results before describing or calling tools. This adds no model context until a script runs.
788
+
789
+ ```js
790
+ const found = await tools.search({ query: "issue", server: "github", limit: 50 });
791
+ if (found.error) return found.error;
792
+
793
+ const readOnly = [];
794
+ for (const hit of found.items) {
795
+ const details = await tools.describe({ path: hit.path, server: hit.server });
796
+ if (details.annotations?.readOnlyHint) readOnly.push(hit.path);
797
+ }
798
+ return readOnly;
799
+ ```
800
+
801
+ - `tools.search` runs the same search as `mcp({ search })`: ranked words by default, `regex: true` for a pattern, or `searchMode: "semantic"` for Jev. An empty `query` with a `server` lists that server's tools. Results come one page at a time (`limit` defaults to 12); follow `nextOffset` for the rest.
802
+ - Every hit carries its `server`. Pass it on with `tools.describe({ path, server })` and `tools.call(path, args, { server })`, so the script reaches the tool it found even when two servers expose the same name, as with `toolPrefix: "none"`.
803
+ - A search that cannot run returns `items: []` with `error: { code, message }`, using the same codes as `mcp({ search })`, such as `empty_query`, `server_disabled`, `server_backoff`, and `invalid_pattern`.
804
+ - Annotations are the server's own hints, not guarantees. Script searches never activate `directTools: "search"` tools, and every call still goes through the normal approval gate.
757
805
 
758
- 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; each script runs in a worker thread that is terminated at the deadline, including for infinite loops.
806
+ See the bundled `mcp-scripting` skill for the complete workflow guide. The API is `await tools.search({ query, server?, regex?, searchMode?, limit?, offset? })`, `await tools.describe({ path, server? })`, `tools.call(path, args, { server }?)`, 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; each script runs in a worker thread that is terminated at the deadline, including for infinite loops.
759
807
 
760
808
  Successful intermediate results reach the script without presentation truncation, details summaries, or output-guard spill files. Each script has a fixed **16 MiB cumulative UTF-8 JSON transfer budget** for successful intermediate data, shared by sequential and parallel calls. A result that cannot fit returns `{ ok: false, error: { code: "intermediate_result_too_large", message } }` and a failed call trace; rejected bytes do not consume the budget, and the script can continue. Request less data or start a new script; there is no configuration option for this cap. Resource calls retain their text-result semantics. Only script-selected output (`emit`, captured console, and `return`) reaches the final output guard; ordinary MCP calls remain guarded as before.
761
809
 
@@ -873,7 +921,7 @@ To hide specific tools while still using `directTools: true`, add `excludeTools`
873
921
  {
874
922
  "mcpServers": {
875
923
  "figma": {
876
- "url": "http://localhost:3845/mcp",
924
+ "url": "http://127.0.0.1:3845/mcp",
877
925
  "directTools": true,
878
926
  "excludeTools": ["read_figjam", "figma_get_code_connect_map"]
879
927
  }
@@ -901,7 +949,9 @@ When you change direct-tool toggles in `/mcp-adapter`, the extension updates dir
901
949
 
902
950
  **Interactive configuration:** Run `/mcp-adapter` to open an interactive panel showing all servers with connection status, tools, and direct/proxy toggles. `/mcp` is also available as an alias when Pi's built-in MCP extension is not installed. You can reconnect servers, toggle tools between direct and proxy, and enable or disable servers (`ctrl+d`) from the same overlay. For OAuth, press Enter on a server that needs auth or `ctrl+a` on any OAuth server. The Save action defaults to `ctrl+s` and can be remapped with the `mcp.panel.save` keybinding.
903
951
 
904
- **Guided first-run setup:** Run `/mcp-adapter setup` to choose the normal write target for new shared servers — project `.mcp.json` or global `~/.config/mcp/mcp.json` — 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 selected config, add a curated known server (DeepWiki, Context7, Notion, GitHub, or Chrome DevTools), or quick-add RepoPrompt into a standard/shared MCP file.
952
+ **Guided first-run setup:** Run `/mcp-adapter setup` to choose the normal write target for new shared servers — project `.mcp.json` or global `~/.config/mcp/mcp.json` — 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 selected config, add a curated known server (DeepWiki, Context7, Notion, GitHub, Chrome DevTools, or Figma (desktop) when the Figma app is installed), or quick-add RepoPrompt into a standard/shared MCP file.
953
+
954
+ **Figma:** Figma's remote server, `https://mcp.figma.com/mcp`, only accepts approved clients, and Pi isn't approved yet. Use the Figma desktop app's local server instead: run `/mcp-adapter setup` and add Figma (desktop). It needs a Dev or Full seat on a paid Figma plan; to turn it on, open a Design file, switch to Dev Mode (Shift+D), and click "Enable desktop MCP server" in the inspect panel.
905
955
 
906
956
  **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.
907
957
 
@@ -1004,7 +1054,7 @@ Prefer `.mcp.json` for project-local shared MCP config and `~/.config/mcp/mcp.js
1004
1054
 
1005
1055
  MCP proxy and direct-tool results use compact self-rendered rows by default. Collapsed success output shows the call title, a bounded one-line input preview when arguments exist, and the first result line, with a `Ctrl+O to expand` hint when more text is hidden. The full result remains available when expanded and is still returned unchanged to the model. Set `settings.toolResultRendering` to `"boxed"` to restore the legacy boxed Pi row, or set `settings.collapsedResultLines` to `2` or `3` when you want more collapsed text.
1006
1056
 
1007
- Search includes both MCP tools and Pi tools (from extensions). Pi tools appear first with `[pi tool]` prefix. Space-separated words are ranked by weighted matches across name, server, description, and any configured `searchKeywords`, then returned one page at a time (`limit` defaults to 12). Use `details.nextOffset` for the next page. Regex search is still available with `regex: true`, but regex results are paginated without ranking.
1057
+ Search covers MCP tools only. Space-separated words are ranked by weighted matches across name, server, description, and any configured `searchKeywords`, then returned one page at a time (`limit` defaults to 12). Use `details.nextOffset` for the next page. Regex search is still available with `regex: true`, but regex results are paginated without ranking.
1008
1058
 
1009
1059
  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.
1010
1060
 
@@ -1035,7 +1085,9 @@ Keys match a tool's original name, prefixed name, or a glob (`*` applies to ever
1035
1085
 
1036
1086
  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.
1037
1087
 
1038
- Most tools declare no output schema. After such a tool returns structured content or a JSON text result, `describe` (and `tools.describe` in `mcpScript`) also shows the output shape seen so far this session: field names and types only, never values. It lives in memory only and is labeled as observed, not a contract.
1088
+ Most tools declare no output schema. After such a tool returns structured content or a JSON text result, `describe` (and `tools.describe` in `mcpScript`) also shows the output shape seen so far: field names and types only, never values, labeled as observed, not a contract. A wide object that appears more than once, such as a user under `user`, `assignee`, and `assignees`, is written once as a named type. With `scriptMode` on, shapes are also saved in `mcp-cache.json`, so later sessions can write a script without calling the tool first. A shape is used only while the tool's description and input schema stay the same, and a saved one is dropped when they or the server's config change.
1089
+
1090
+ When an `mcpScript` run throws, times out, or returns `[]`, `{}`, `null`, `""`, or nothing, its result ends with the shapes seen from the schemaless tools it called, so the model can fix a wrong field guess without a separate look at the data.
1039
1091
 
1040
1092
  For HTTP servers, Pi reports HTTP 503 as temporary unavailability and does not add another immediate retry loop. Keep-alive servers keep cached metadata available and retry after 30 seconds, backing off to 5 minutes. Other failed connects run a one-request shape probe that can turn opaque transport errors into setup hints such as `endpoint returned HTML (200) — this URL does not appear to speak MCP`. Healthy connections are not probed.
1041
1093
 
package/commands.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { createConnection } from "node:net";
2
3
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
4
  import type { OverlayHandle } from "@earendil-works/pi-tui";
4
5
  import type { McpExtensionState } from "./state.ts";
@@ -587,6 +588,21 @@ function buildSharedConfigNoticeLines(configOverridePath: string | undefined, cw
587
588
  };
588
589
  }
589
590
 
591
+ // Any accepted TCP connection counts as reachable; failures resolve false so the add still succeeds.
592
+ function isLocalServerReachable(url: string, timeoutMs = 1_500): Promise<boolean> {
593
+ const { hostname, port } = new URL(url);
594
+ return new Promise((resolve) => {
595
+ const socket = createConnection({ host: hostname, port: Number(port) });
596
+ const finish = (reachable: boolean) => {
597
+ socket.destroy();
598
+ resolve(reachable);
599
+ };
600
+ socket.setTimeout(timeoutMs, () => finish(false));
601
+ socket.once("connect", () => finish(true));
602
+ socket.once("error", () => finish(false));
603
+ });
604
+ }
605
+
590
606
  export async function openMcpSetup(
591
607
  state: McpExtensionState,
592
608
  pi: ExtensionAPI,
@@ -641,7 +657,8 @@ export async function openMcpSetup(
641
657
  addKnownServer: async (preset: KnownServerPreset, target: SharedConfigTarget) => {
642
658
  const path = writeSharedServerEntry(getSharedConfigPath(target, ctx.cwd), preset.id, preset.entry);
643
659
  configChanged = true;
644
- return { path, serverName: preset.name };
660
+ if (!preset.desktopApp) return { path, serverName: preset.name };
661
+ return { path, serverName: preset.name, reachable: await isLocalServerReachable(preset.entry.url!) };
645
662
  },
646
663
  openPath: async (targetPath: string) => {
647
664
  await openPath(pi, targetPath);
@@ -659,7 +676,8 @@ export async function openMcpSetup(
659
676
  resolve({ configChanged });
660
677
  });
661
678
  },
662
- { overlay: true, overlayOptions: { anchor: "center", width: 92 } },
679
+ // The panel sizes its height from tui.terminal.rows minus this margin, so it always fits.
680
+ { overlay: true, overlayOptions: { anchor: "center", width: 92, maxHeight: "100%", margin: { top: 1, bottom: 1 } } },
663
681
  );
664
682
  });
665
683
  }
package/config.ts CHANGED
@@ -31,6 +31,8 @@ export interface KnownServerPreset {
31
31
  name: string;
32
32
  summary: string;
33
33
  entry: ServerEntry;
34
+ /** Offered only when one of these app paths exists; the local server is probed after adding. */
35
+ desktopApp?: { paths: readonly string[]; enableSteps: string };
34
36
  }
35
37
 
36
38
  export const KNOWN_SERVER_PRESETS: readonly KnownServerPreset[] = [
@@ -74,6 +76,20 @@ export const KNOWN_SERVER_PRESETS: readonly KnownServerPreset[] = [
74
76
  summary: "Inspect and automate a local Chrome browser.",
75
77
  entry: { command: "npx", args: ["-y", "chrome-devtools-mcp@1.6.0"] },
76
78
  },
79
+ {
80
+ id: "figma",
81
+ name: "Figma (desktop)",
82
+ summary: "Read designs through the Figma desktop app. Needs a Dev or Full seat on a paid Figma plan.",
83
+ entry: { url: "http://127.0.0.1:3845/mcp", protocolVersion: "auto" },
84
+ desktopApp: {
85
+ paths: [
86
+ "/Applications/Figma.app",
87
+ join(homedir(), "Applications", "Figma.app"),
88
+ join(homedir(), "AppData", "Local", "Figma", "Figma.exe"),
89
+ ],
90
+ enableSteps: "To enable it, open a Design file in Figma, switch to Dev Mode (Shift+D), and click 'Enable desktop MCP server' in the inspect panel.",
91
+ },
92
+ },
77
93
  ];
78
94
 
79
95
  const IMPORT_PATHS: Record<ImportKind, string[]> = {
@@ -162,6 +178,7 @@ export interface McpDiscoverySummary {
162
178
  totalServerCount: number;
163
179
  fingerprint: string;
164
180
  repoPrompt: RepoPromptDiscovery;
181
+ knownServerPresets: readonly KnownServerPreset[];
165
182
  }
166
183
 
167
184
  export interface McpStandardConfigSummary {
@@ -360,6 +377,7 @@ export function getMcpDiscoverySummary(
360
377
  ...summaryWithoutRepoPrompt,
361
378
  fingerprint,
362
379
  repoPrompt: detectRepoPrompt(summaryWithoutRepoPrompt, cwd),
380
+ knownServerPresets: KNOWN_SERVER_PRESETS.filter(({ desktopApp }) => !desktopApp || desktopApp.paths.some((path) => existsSync(path))),
363
381
  };
364
382
  }
365
383
 
@@ -1531,7 +1549,7 @@ function buildRepoPromptEntry(executablePath: string): ServerEntry {
1531
1549
  };
1532
1550
  }
1533
1551
 
1534
- function detectRepoPrompt(summary: Omit<McpDiscoverySummary, "fingerprint" | "repoPrompt">, cwd = process.cwd()): RepoPromptDiscovery {
1552
+ function detectRepoPrompt(summary: Omit<McpDiscoverySummary, "fingerprint" | "repoPrompt" | "knownServerPresets">, cwd = process.cwd()): RepoPromptDiscovery {
1535
1553
  for (const source of summary.sources) {
1536
1554
  if (source.kind !== "shared" || source.serverCount === 0) continue;
1537
1555
  const config = readValidatedConfig(source.path, `MCP config from ${source.path}`);
@@ -199,8 +199,9 @@ export function resolveDirectTools(
199
199
  * Live counts/status belong to `mcp({ })`, full instructions to
200
200
  * `mcp({ instructions })`.
201
201
  */
202
- export function buildProxyDescription(config: McpConfig): string {
203
- let desc = `MCP gateway — URL installation, server status, tool search/describe, auth, and single MCP tool calls. When a user supplies an MCP endpoint URL, install it with the install action. When one request needs several MCP calls with logic between them, use mcpScript. Non-MCP Pi tools should be called directly, not through mcp.\n`;
202
+ export function buildProxyDescription(config: McpConfig, scriptTool = false): string {
203
+ const scriptHint = scriptTool ? " Use mcpScript for several MCP calls with logic between them." : "";
204
+ let desc = `MCP gateway — URL installation, server status, tool search/describe, auth, and single MCP tool calls. When a user supplies an MCP endpoint URL, install it with the install action.${scriptHint} Non-MCP Pi tools should be called directly, not through mcp.\n`;
204
205
 
205
206
  const serverNames = Object.keys(config.mcpServers)
206
207
  .filter((serverName) => !isServerDisabled(config.mcpServers[serverName]));
package/direct-tools.ts CHANGED
@@ -8,7 +8,7 @@ import { describeFailure } from "./failure-backoff.ts";
8
8
  import { abortable, throwIfAborted } from "./abort.ts";
9
9
  import { formatSchema } from "./tool-metadata.ts";
10
10
  import { resolveMcpResultContent, transformMcpResourceContents } from "./tool-registrar.ts";
11
- import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
11
+ import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions, scriptPipeHint } from "./mcp-output-guard.ts";
12
12
  import { maybeStartUiSession, summarizeUiSessionResult, type UiSessionRuntime } from "./ui-session.ts";
13
13
  import { isServerDisabled } from "./types.ts";
14
14
  import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
@@ -17,7 +17,7 @@ import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session
17
17
  import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
18
18
  import { callToolViaTaskSession } from "./mcp-tasks.ts";
19
19
  import { callToolPausingForElicitation } from "./elicitation-handler.ts";
20
- import { recordObservedOutput } from "./output-shape.ts";
20
+ import { observedOutputRecorder } from "./output-shape.ts";
21
21
  import { ensureToolCallApproved } from "./tool-approval.ts";
22
22
  import { getInputRequiredNeedsUiDetails } from "./errors.ts";
23
23
 
@@ -312,6 +312,7 @@ export function createDirectToolExecutor(
312
312
  : null;
313
313
 
314
314
  const requestMeta = withToolCallIdMeta(uiSession?.requestMeta, toolCallId);
315
+ const recordOutput = observedOutputRecorder(state, spec.serverName, spec.originalName);
315
316
  const result = await withSessionRecovery<ClientCallToolResult>(
316
317
  {
317
318
  manager: state.manager,
@@ -339,7 +340,7 @@ export function createDirectToolExecutor(
339
340
  },
340
341
  );
341
342
  uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/client").CallToolResult);
342
- if (!result.isError) recordObservedOutput(state, spec.serverName, spec.originalName, result as Record<string, unknown>);
343
+ if (!result.isError) recordOutput(result as Record<string, unknown>);
343
344
 
344
345
  if (result.isError) {
345
346
  const content = resolveMcpResultContent(result as Record<string, unknown>, state.owner?.signal);
@@ -380,6 +381,7 @@ export function createDirectToolExecutor(
380
381
 
381
382
  const guarded = await guardMcpOutput(outputContent, {
382
383
  ...outputGuardOptions,
384
+ ...scriptPipeHint(state.scriptTool, outputContent),
383
385
  ...(state.config.settings?.directToolResultDetails === "bounded" ? { rawMcpResult: result } : {}),
384
386
  });
385
387
  return {
package/dist/config.d.ts CHANGED
@@ -5,6 +5,11 @@ export interface KnownServerPreset {
5
5
  name: string;
6
6
  summary: string;
7
7
  entry: ServerEntry;
8
+ /** Offered only when one of these app paths exists; the local server is probed after adding. */
9
+ desktopApp?: {
10
+ paths: readonly string[];
11
+ enableSteps: string;
12
+ };
8
13
  }
9
14
  export declare const KNOWN_SERVER_PRESETS: readonly KnownServerPreset[];
10
15
  interface ConfigSourceSpec {
@@ -71,6 +76,7 @@ export interface McpDiscoverySummary {
71
76
  totalServerCount: number;
72
77
  fingerprint: string;
73
78
  repoPrompt: RepoPromptDiscovery;
79
+ knownServerPresets: readonly KnownServerPreset[];
74
80
  }
75
81
  export interface McpStandardConfigSummary {
76
82
  sources: ConfigDiscoverySource[];
package/dist/config.js CHANGED
@@ -65,6 +65,20 @@ export const KNOWN_SERVER_PRESETS = [
65
65
  summary: "Inspect and automate a local Chrome browser.",
66
66
  entry: { command: "npx", args: ["-y", "chrome-devtools-mcp@1.6.0"] },
67
67
  },
68
+ {
69
+ id: "figma",
70
+ name: "Figma (desktop)",
71
+ summary: "Read designs through the Figma desktop app. Needs a Dev or Full seat on a paid Figma plan.",
72
+ entry: { url: "http://127.0.0.1:3845/mcp", protocolVersion: "auto" },
73
+ desktopApp: {
74
+ paths: [
75
+ "/Applications/Figma.app",
76
+ join(homedir(), "Applications", "Figma.app"),
77
+ join(homedir(), "AppData", "Local", "Figma", "Figma.exe"),
78
+ ],
79
+ enableSteps: "To enable it, open a Design file in Figma, switch to Dev Mode (Shift+D), and click 'Enable desktop MCP server' in the inspect panel.",
80
+ },
81
+ },
68
82
  ];
69
83
  const IMPORT_PATHS = {
70
84
  cursor: [join(homedir(), ".cursor", "mcp.json")],
@@ -247,6 +261,7 @@ export function getMcpDiscoverySummary(overridePath, cwd = process.cwd(), option
247
261
  ...summaryWithoutRepoPrompt,
248
262
  fingerprint,
249
263
  repoPrompt: detectRepoPrompt(summaryWithoutRepoPrompt, cwd),
264
+ knownServerPresets: KNOWN_SERVER_PRESETS.filter(({ desktopApp }) => !desktopApp || desktopApp.paths.some((path) => existsSync(path))),
250
265
  };
251
266
  }
252
267
  export function cloneMcpConfig(config) {