pi-mcp-adapter 2.26.1 → 2.28.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 (67) hide show
  1. package/CHANGELOG.md +68 -7
  2. package/README.md +73 -4
  3. package/cli.js +163 -1
  4. package/commands.ts +106 -24
  5. package/config.ts +60 -12
  6. package/direct-tools.ts +107 -95
  7. package/dist/agent-dir.d.ts +10 -0
  8. package/dist/agent-dir.js +63 -0
  9. package/dist/agent-dir.js.map +1 -0
  10. package/dist/agent-plugin-loader.d.ts +8 -0
  11. package/dist/agent-plugin-loader.js +356 -0
  12. package/dist/agent-plugin-loader.js.map +1 -0
  13. package/dist/config.d.ts +123 -0
  14. package/dist/config.js +1077 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/metadata-cache.d.ts +19 -0
  17. package/dist/metadata-cache.js +319 -0
  18. package/dist/metadata-cache.js.map +1 -0
  19. package/dist/package-mcp-loader.d.ts +2 -0
  20. package/dist/package-mcp-loader.js +156 -0
  21. package/dist/package-mcp-loader.js.map +1 -0
  22. package/dist/resource-tools.d.ts +1 -0
  23. package/dist/resource-tools.js +15 -0
  24. package/dist/resource-tools.js.map +1 -0
  25. package/dist/types.d.ts +564 -0
  26. package/dist/types.js +287 -0
  27. package/dist/types.js.map +1 -0
  28. package/dist/ui-app-bridge-helpers.d.ts +6 -0
  29. package/dist/ui-app-bridge-helpers.js +37 -0
  30. package/dist/ui-app-bridge-helpers.js.map +1 -0
  31. package/dist/ui-stream-types.d.ts +104 -0
  32. package/dist/ui-stream-types.js +59 -0
  33. package/dist/ui-stream-types.js.map +1 -0
  34. package/dist/ui-tool-visibility.d.ts +4 -0
  35. package/dist/ui-tool-visibility.js +27 -0
  36. package/dist/ui-tool-visibility.js.map +1 -0
  37. package/dist/utils.d.ts +32 -0
  38. package/dist/utils.js +338 -0
  39. package/dist/utils.js.map +1 -0
  40. package/failure-backoff.ts +23 -0
  41. package/index.ts +325 -60
  42. package/init.ts +46 -31
  43. package/lifecycle.ts +89 -23
  44. package/mcp-auth-flow.ts +39 -6
  45. package/mcp-bearer-store.ts +387 -0
  46. package/mcp-callback-server.ts +27 -3
  47. package/mcp-code.ts +6 -0
  48. package/mcp-oauth-provider.ts +66 -16
  49. package/mcp-output-guard.ts +95 -7
  50. package/mcp-panel.ts +42 -13
  51. package/mcp-probe.ts +18 -4
  52. package/mcp-references.ts +368 -0
  53. package/mcp-status.ts +8 -19
  54. package/metadata-cache.ts +52 -33
  55. package/namespace-tools.ts +276 -0
  56. package/package-mcp-loader.ts +168 -0
  57. package/package.json +26 -6
  58. package/proxy-modes.ts +71 -14
  59. package/request-headers-command.ts +31 -37
  60. package/search-ranking.ts +94 -22
  61. package/server-manager.ts +86 -17
  62. package/state.ts +1 -3
  63. package/tool-result-renderer.ts +16 -2
  64. package/types.ts +50 -1
  65. package/ui-server.ts +21 -35
  66. package/ui-session.ts +2 -1
  67. package/utils.ts +91 -19
package/CHANGELOG.md CHANGED
@@ -7,14 +7,75 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
- ## [2.26.1] - 2026-08-18
10
+ ## [2.28.0] - 2026-08-26
11
+
12
+ ### Highlights
13
+ - MCP connections are less fragile when servers fail, recover, move slowly, or refresh their catalogs.
14
+ - Direct MCP tools are safer to expose, with stricter input checks and bounded result details when hosts opt in.
15
+ - Other Pi extensions can register MCP servers at runtime without sharing module state.
16
+ - Proxy calls now show live server progress in the interactive UI.
17
+ - Package installs and public helper imports are easier to use from downstream hosts.
18
+
19
+ ### Added
20
+ - Persistent metadata cache entries now honor server `ttlMs` hints without extending the default max age. Thanks to [@Seinra](https://github.com/Seinra) for #431.
21
+ - Proxy tool calls now forward server progress notifications to the interactive UI. Thanks to [@Seinra](https://github.com/Seinra) for PR #440 and for mapping the area in #431.
22
+ - Added a pure `mcp:` reference resolver API for consumers that validate adapter tool names from explicit config and cache inputs. Thanks to [@abdwhb-png](https://github.com/abdwhb-png) for PR #420.
23
+ - Direct tools can opt into strict advertised-schema validation with one-layer JSON recovery for object and array properties. Thanks to [@4ndr3wxh1ll](https://github.com/4ndr3wxh1ll) for PR #430.
24
+ - Direct tools can opt into guarded raw MCP result details, retaining bounded structured fields while summarizing oversized values.
25
+ - Embedding hosts can import the configuration loader and metadata cache helpers from public package subpaths, and can validate cached metadata against an explicit private process environment.
26
+
27
+ ### Fixed
28
+ - Runtime MCP registration now works across separately loaded Pi extensions through a versioned shared event contract. Thanks to [@fmoda3](https://github.com/fmoda3) for #443.
29
+ - Stdio MCP startup errors now identify a configured missing or non-directory `cwd` instead of blaming the executable. Thanks to [@SoyElf](https://github.com/SoyElf) for #442.
30
+ - Package installs with `--omit=dev` no longer run the public helper build during `prepare`; Git installs and package tarballs still include the built public exports. Thanks to [@KripaMishra](https://github.com/KripaMishra) for #441.
31
+ - MCP gateway descriptions now stay stable across metadata-only refreshes and keep live counts behind `mcp({})`. Thanks to [@voidfreud](https://github.com/voidfreud) for PR #432.
32
+ - Failed MCP servers in active backoff no longer remain advertised through cached direct tools, gateway list/search/describe results, or status tool counts. Thanks to [@voidfreud](https://github.com/voidfreud) for PR #434.
33
+ - OAuth token invalidation now preserves credentials replaced by another Pi process instead of letting a stale refresh delete newly authorized shared credentials. Thanks to [@mjlbach](https://github.com/mjlbach) for PR #422.
34
+ - Failed first-time MCP initialization no longer leaves the session permanently stuck with only `MCP not initialized`; the gateway keeps the failure reason and retries initialization on the next `mcp(...)` call. Thanks to [@hara-seihun](https://github.com/hara-seihun) for #428.
35
+ - HTTP 202 and unauthenticated HTTP 401 endpoint probes now report ambiguous endpoint shape instead of claiming the URL is not MCP. Thanks to [@jayshah5696](https://github.com/jayshah5696) for #415.
36
+ - Expanded `mcpScript` calls now show bounded submitted code. Thanks to [@DenisBalan](https://github.com/DenisBalan) for #413.
37
+ - Gateway parameters nested inside `args` now fail with top-level guidance instead of dispatching inconsistently. Thanks to [@voidfreud](https://github.com/voidfreud) for PR #417.
38
+ - Large direct-tool advisories now explain how to hide them with `settings.warnOnLargeDirectTools: false`. Thanks to [@afrodao2394](https://github.com/afrodao2394) for #412.
39
+ - Namespace proxy tools now use valid cached metadata, include resource-only proxy servers, clean up only prior namespace registrations, follow `MCP_DIRECT_TOOLS` selection, and skip ambiguous normalized server names. Thanks to [@abdwhb-png](https://github.com/abdwhb-png) for PR #414.
40
+ - Kept transient HTTP 503 connection failures as availability errors without multiplying gateway retries or misdiagnosing the endpoint as non-MCP. Thanks to [@elkaix](https://github.com/elkaix) for PR #411.
41
+ - Preserved cached keep-alive catalogs during transient 503 refresh outages while deferred recovery continues with bounded backoff.
42
+ - Startup connections that fail with a transient HTTP 503 now surface one quiet "temporarily unavailable; retry later" warning instead of the full hard failure; documented lifecycle behavior is unchanged, and keep-alive health checks continue their existing self-healing path. Thanks to [@elkaix](https://github.com/elkaix) for PR #424.
43
+
44
+ ## [2.27.0] - 2026-08-20
45
+
46
+ ### Highlights
47
+ - MCP servers can now come from packages, runtime extension APIs, or the usual config files.
48
+ - Static bearer tokens can use the OS credential store, with a stdin-only CLI for safer token management.
49
+ - Long-lived sessions recover better when remote MCP servers are slow, refreshed, or reconnected.
50
+ - OAuth flows clean up their callback listener when idle and work better inside nested terminal UI prompts.
51
+ - Search, metadata cache writes, direct-tool counts, and UI stream pruning now do less repeated work.
52
+
53
+ ### Added
54
+ - `/pi-mcp` now works as an alias for `/mcp` when a host reserves `/mcp`. Thanks to [@inxeoz](https://github.com/inxeoz) for #391.
55
+ - `registerMcpServer({ pi, name, definition })` lets other extensions register and dispose session-scoped MCP servers at runtime. Registrations are proxy-tool-only, never persisted, and duplicate names fail closed. Thanks to [@bendavis78](https://github.com/bendavis78) and [@fmoda3](https://github.com/fmoda3) for the runtime API request in #376/#382.
56
+ - Pi packages can ship prefixed MCP server definitions with `pi.mcp` manifest entries, without asking users to edit MCP config files. Thanks to [@bendavis78](https://github.com/bendavis78) for #376 and [@fmoda3](https://github.com/fmoda3) for the manifest design.
57
+ - Static bearer tokens can opt into OS credential-store lookup with URL-bound records by setting `bearerTokenStore: true`. The new `pi-mcp-adapter token set|status|remove <server>` CLI reads tokens from stdin and never accepts a token as an argument. Thanks to [@AlexanderBartash](https://github.com/AlexanderBartash) for issue #366.
11
58
 
12
- ### Fixed
13
- - Scoped “Allow for session” tool approvals to the approved arguments, so one approval no longer applies to later calls with different inputs. Thanks to [@spaceshipmike](https://github.com/spaceshipmike) for #367.
14
- - Stopped MCP panel commands from hanging in RPC, JSON, and print modes when terminal-only UI is unavailable. Thanks to [@shixin-guo](https://github.com/shixin-guo) for PR #365.
15
- - Kept compact MCP result rows useful by showing a short input preview and skipping leading blank output in collapsed previews.
16
- - Recovered MCP gateway requests nested inside proxy `args` instead of showing status, and now rejects invalid nested gateway requests with guidance. Thanks to [@ibrmora](https://github.com/ibrmora) for #363.
17
- - Kept remote keep-alive tool catalogs fresh across server restarts, so long-lived Pi sessions can discover replacement tools without restarting. Thanks to [@dmorn](https://github.com/dmorn) for #369 and PR #370.
59
+ ### Changed
60
+ - Search ranking now reuses normalized MCP tool fields and keyword tokens per catalog.
61
+ - Per-request header commands collect process cleanup data in one snapshot per pass, reducing cold HTTP connect overhead.
62
+ - Metadata cache saves write compact JSON while preserving atomic replacement and cross-process merges.
63
+ - High-frequency UI stream event-log pruning tracks the latest checkpoint event ID instead of rescanning retained patches.
64
+ - The MCP panel reuses direct-tool counts and token totals across renders while keeping toggle and reconnect updates immediate.
65
+
66
+ ### Fixed
67
+ - Short-lived `mcpScript` workers no longer emit false unmanaged file-descriptor warnings on Node 24. Thanks to [@blalor](https://github.com/blalor) for PR #407.
68
+ - OAuth callback listeners are released after idle auth flows, and MCP pickers stay hidden while nested OAuth input is active. Thanks to [@trevorleibert-mixpanel](https://github.com/trevorleibert-mixpanel) for PRs #403 and #404.
69
+ - Request-header command cleanup can scan large process lists without overflowing `spawnSync`'s 1 MiB default buffer, which previously caused spurious `HTTP request headers command cleanup failed: ps exited with code unknown` refresh failures on busy hosts. Thanks to [@rtfpessoa](https://github.com/rtfpessoa) for PR #399.
70
+ - Slow but healthy remote keep-alive servers are no longer marked failed when a bounded tools/list refresh times out. Thanks to [@brightmeowso](https://github.com/brightmeowso) for #400.
71
+ - Cached metadata reconstruction now reuses one selector candidate index, avoiding repeated scans of large cached catalogs at startup.
72
+ - MCP status consumers no longer see a connected catalog before Pi's model-facing tool surface is current. The first connected status snapshot now waits for direct-tool synchronization. Thanks to [@dmorn](https://github.com/dmorn) for PR #380.
73
+ - JSON-string MCP tool-call arguments are normalized before approval and transport, preserving all fields and embedded quotes. Thanks to [@sebbean](https://github.com/sebbean) for PR #377.
74
+ - Session tool approvals are scoped to the approved argument payload instead of every later call to the same tool. Thanks to [@spaceshipmike](https://github.com/spaceshipmike) for #367.
75
+ - MCP panel commands no longer hang in RPC, JSON, and print modes when terminal-only custom UI is unavailable. Thanks to [@shixin-guo](https://github.com/shixin-guo) for PR #365.
76
+ - Compact MCP rows now show a bounded tool-input preview and skip leading blank output lines in collapsed result previews.
77
+ - MCP gateway requests nested inside proxy `args` are recovered instead of silently showing status, and invalid nested gateway requests now fail with guidance. Thanks to [@ibrmora](https://github.com/ibrmora) for #363.
78
+ - Remote keep-alive tool catalogs refresh before user input, adapter-triggered turns, and health checks. Expired Streamable HTTP sessions reconnect so long-lived Pi sessions can discover replacement catalogs without restarting. Thanks to [@dmorn](https://github.com/dmorn) for #369 and PR #370.
18
79
 
19
80
  ## [2.26.0] - 2026-08-14
20
81
 
package/README.md CHANGED
@@ -24,6 +24,8 @@ pi install npm:pi-mcp-adapter
24
24
 
25
25
  Restart Pi after installation.
26
26
 
27
+ > **DeepSeek Harness (third-party bridge):** Run the unmodified adapter in DSH via [pi2dsh](https://github.com/weijiafu14/pi2dsh); see the [verified dsh-TUI and Web MCP guide](https://github.com/weijiafu14/pi2dsh/tree/main/examples/tui-mcp).
28
+
27
29
  ## What happens on first run
28
30
 
29
31
  The adapter reads standard MCP files automatically. No extra setup needed if you already have them.
@@ -126,6 +128,63 @@ Each directory must contain a valid Agent Plugins 1.0 `plugin.json`. If it also
126
128
 
127
129
  Agent Plugins is a portable package format. Native Pi MCP config remains `.mcp.json`, `~/.config/mcp/mcp.json`, and Pi-owned overrides.
128
130
 
131
+ ### Pi package manifests
132
+
133
+ A Pi package can ship MCP servers for the installed adapter without requiring a separate MCP config file. Declare a package-relative config in its `package.json`:
134
+
135
+ ```json
136
+ {
137
+ "pi": {
138
+ "mcp": "./mcp.json"
139
+ }
140
+ }
141
+ ```
142
+
143
+ `pi.mcp` can also be an array of package-relative paths. Each file uses the normal `mcpServers` object shape, but package manifests load only server entries: package `settings` and `imports` are ignored. Server names are prefixed with the sanitized package name, such as `acme_tools__docs`, and user/global/project MCP config has higher precedence. The adapter loads only Pi packages listed in Pi settings; it does not scan `node_modules`.
144
+
145
+ ### Runtime registration from other extensions
146
+
147
+ An extension can register MCP servers with the installed adapter at runtime, for example a plugin host that discovers plugins after load:
148
+
149
+ ```ts
150
+ const MCP_RUNTIME_REGISTER_EVENT = "pi-mcp-adapter:runtime-register:v1";
151
+ type RuntimeRegistrationRequest = {
152
+ version: 1;
153
+ name: string;
154
+ definition: { url: string };
155
+ result?:
156
+ | { ok: true; registration: { dispose(): Promise<void> } }
157
+ | { ok: false; error: Error };
158
+ };
159
+
160
+ export default function pluginHost(pi) {
161
+ let registration: { dispose(): Promise<void> } | undefined;
162
+
163
+ pi.on("session_start", () => {
164
+ if (registration) return;
165
+ const request: RuntimeRegistrationRequest = {
166
+ version: 1,
167
+ name: "acme__docs",
168
+ definition: { url: "https://mcp.example.com/mcp" },
169
+ };
170
+ pi.events.emit(MCP_RUNTIME_REGISTER_EVENT, request);
171
+ if (!request.result) throw new Error("pi-mcp-adapter is not installed");
172
+ if (!request.result.ok) throw request.result.error;
173
+ registration = request.result.registration;
174
+ });
175
+
176
+ pi.on("session_shutdown", async () => {
177
+ const current = registration;
178
+ registration = undefined;
179
+ await current?.dispose();
180
+ });
181
+ }
182
+ ```
183
+
184
+ Cross-extension registration uses Pi's shared event bus and does not require a runtime import from `pi-mcp-adapter`. Emit during `session_start` or later so the adapter listener is installed. The adapter writes `request.result` synchronously; the first adapter listener to respond wins.
185
+
186
+ 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.
187
+
129
188
  ### SDK configuration
130
189
 
131
190
  Use `createMcpAdapter` when an SDK or server integration already owns its MCP configuration:
@@ -178,7 +237,7 @@ pi.events.on(MCP_STATUS_EVENT, (snapshot) => {
178
237
  });
179
238
  ```
180
239
 
181
- 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.
240
+ 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. Initialization withholds that first snapshot until authoritative metadata has been reconciled into Pi's active direct-tool registry. A `connected` snapshot therefore follows model-facing tool-surface synchronization, including removal of stale cached tools when the authoritative catalog is empty.
182
241
 
183
242
  In the configuration examples below, `30000` is illustrative only. If `requestTimeoutMs` is omitted or set to `<= 0`, the MCP SDK default timeout is used.
184
243
 
@@ -219,6 +278,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
219
278
  | `oauth.logoUri` | Client logo URL advertised during Dynamic Client Registration fallback (RFC 7591 `logo_uri`). Must be an absolute `http(s)` URL — consent screens fetch it server-side, so local paths render nothing. Omitted from the registration request when unset |
220
279
  | `oauth.skipIssuerMetadataValidation` | `true` disables the OAuth authorization-server metadata issuer check for this server. This weakens OAuth mix-up protection and should only be used for known-misconfigured internal servers while their metadata is being fixed. |
221
280
  | `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 `!`. |
281
+ | `bearerTokenStore` | Set to `true` to read a static bearer token from the adapter-owned OS credential store when `auth` is `"bearer"` and no `bearerToken` or `bearerTokenEnv` is configured. Stored records are keyed only by the server name, bind to the resolved server URL, and are never named by config. Store a token with `pi-mcp-adapter token set <server>`, which reads it from a masked prompt or stdin pipe and never from an argument. `/mcp token status <server>` and `/mcp token remove <server>` manage non-secret state inside Pi; `/mcp token set` stays disabled until Pi exposes masked secret input. |
222
282
  | `lifecycle` | `"lazy"` (default), `"eager"`, `"keep-alive"`, or `"lazy-keep-alive"` |
223
283
  | `idleTimeout` | Minutes before idle disconnect (overrides global) |
224
284
  | `requestTimeoutMs` | Request timeout in milliseconds for live MCP calls (overrides global; if omitted or `<= 0`, the MCP SDK default timeout is used) |
@@ -249,6 +309,8 @@ If an internal authorization server publishes mismatched OAuth metadata and cann
249
309
 
250
310
  Secret values in `headers`, `bearerToken`, `oauth.clientSecret`, and stdio `env` may use a leading `!command` to obtain their value at connection or authentication time. The command runs with stdin and stderr suppressed, stdout is limited to 1 MiB and trimmed, and it must finish within 10 seconds with non-empty output; failures stop the connection or authentication flow. Commands are not run during OAuth discovery or while reading, merging, previewing, hashing, or rendering configuration. Use `!!` to escape a literal leading `!`; ordinary and escaped values retain environment interpolation.
251
311
 
312
+ For local desktop bearer tokens, `bearerTokenStore: true` can opt in to the adapter-owned credential-store namespace. It never falls back to plaintext if the store is unavailable, if the stored record is malformed, or if the stored URL differs from the effective server URL. Literal tokens, command tokens, and environment tokens keep precedence so existing configs do not change. Create or rotate a stored token with `pi-mcp-adapter token set <server>` (masked prompt on a terminal, or piped stdin such as `security find-generic-password -s my-token -w | pi-mcp-adapter token set <server>`); the record binds to the effective configured URL at write time. Token commands need Node 22.18+.
313
+
252
314
  ### Shared MCP processes with rmcp-mux
253
315
 
254
316
  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:
@@ -344,6 +406,8 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
344
406
  | `oauthDir` | Legacy OAuth `tokens.json` import directory for this MCP config. Relative paths resolve from the active project cwd. `MCP_OAUTH_DIR` still wins when set. Persistent OAuth credentials are stored in the OS credential store, not this directory. |
345
407
  | `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. |
346
408
  | `directTools` | Global default for all servers (default: false). Per-server overrides this. |
409
+ | `strictDirectToolArguments` | Validate direct-tool inputs against their advertised schemas and recover one JSON string layer for object and array properties (default: false). |
410
+ | `directToolResultDetails` | Direct-tool result details: `"lean"` (default) or `"bounded"` to retain the guarded raw MCP result. |
347
411
  | `warnOnLargeDirectTools` | Show the advisory when 75 or more direct tools resolve (default: `true`). Set to `false` to suppress only this advisory. |
348
412
  | `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. |
349
413
  | `scriptMode` | Register the MCP-only `mcpScript` plain-JavaScript tool (default: true). Set to `false` to hide it. |
@@ -399,7 +463,7 @@ Oversized MCP tool/resource results are guarded by default so a single huge resp
399
463
  - 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.
400
464
  - **Image content blocks pass through unchanged** — only text output is guarded. Images are delivered to the provider as native image content.
401
465
  - Binary resource blobs up to **10 MiB** are decoded to private temp files and replaced with file references. Each session is limited to **100 MiB** and **10,000 files**. The files are removed at session teardown.
402
- - In proxy mode, `details.mcpResult` is kept raw when its JSON is **≤ 16 KiB**; larger results are replaced with a compact summary (block counts, sizes, key previews) and the raw JSON is saved to a temp file. Direct tools keep their lean details and never carry `mcpResult`.
466
+ - In proxy mode, `details.mcpResult` is kept raw when its JSON is **≤ 16 KiB**; larger results are replaced with a compact summary (block counts, sizes, key previews) and the raw JSON is saved to a temp file. Direct tools keep lean details unless `settings.directToolResultDetails` is set to `"bounded"`, which applies the same guarded `mcpResult` limit.
403
467
 
404
468
  Tune the text and details limits with the object form:
405
469
 
@@ -553,6 +617,10 @@ Each direct tool costs ~150-300 tokens in the system prompt (name + description
553
617
 
554
618
  Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/mcp-cache.json` by default, or `$PI_CODING_AGENT_DIR/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. On Pi versions that expose `pi.unregisterTool()`, stale direct tools are removed from the registry during refresh; older Pi versions still deactivate them from the active tool set. To force a refresh: `/mcp reconnect <server>`.
555
619
 
620
+ Models sometimes encode an object or array argument as a JSON string. Set `settings.strictDirectToolArguments` to `true` to recover one such layer for schema-declared object and array properties, then validate the complete input against the advertised schema before execution.
621
+
622
+ Set `settings.directToolResultDetails` to `"bounded"` when an extension needs structured MCP result fields in Pi's direct-tool result details. The same output guard limits apply. Small leading structured fields stay available, while large fields receive bounded summaries and the complete guarded result follows the output guard's spill-file policy. The default `"lean"` mode keeps the existing server and tool metadata only.
623
+
556
624
  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.
557
625
 
558
626
  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.
@@ -691,15 +759,16 @@ Keys match a tool's original name, prefixed name, or a glob (`*` applies to ever
691
759
 
692
760
  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.
693
761
 
694
- For HTTP servers, 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.
762
+ 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.
695
763
 
696
- Servers that provide usage guidance via the MCP `instructions` field surface it at three levels: a truncated head in the `mcp` proxy tool description itself (so the model sees it without any call), a longer preview at the end of `mcp({ server: "name" })` listings, and the full text via `mcp({ instructions: "name" })`. Instructions are captured at connect time and cached alongside tool metadata, so they stay available without a live connection.
764
+ Servers that provide usage guidance via the MCP `instructions` field surface it through discovery paths: `mcp({ server: "name" })` includes a preview, and `mcp({ instructions: "name" })` returns the full text. Instructions are captured at connect time and cached alongside tool metadata, so they stay available without a live connection.
697
765
 
698
766
  ## Commands
699
767
 
700
768
  | Command | What it does |
701
769
  |---------|--------------|
702
770
  | `/mcp` | Interactive panel and first-run onboarding surface |
771
+ | `/pi-mcp` | Alias for `/mcp` when the host reserves `/mcp` |
703
772
  | `/mcp setup` | Guided setup for imports, a minimal `.mcp.json`, curated known servers, RepoPrompt quick-add, and config-path inspection |
704
773
  | `/mcp tools` | List all tools |
705
774
  | `/mcp prompts` | List all MCP prompts registered as slash commands |
package/cli.js CHANGED
@@ -73,6 +73,11 @@ function printHelp(log = console.log) {
73
73
  log(" pi-mcp-adapter init Detect host configs and scaffold Pi imports");
74
74
  log(" pi-mcp-adapter init --dry-run");
75
75
  log(" pi-mcp-adapter init --discover-host-configs Opt in to host config fallback discovery");
76
+ log("");
77
+ log("Bearer token storage (servers configured with auth: \"bearer\" and bearerTokenStore: true):");
78
+ log(" pi-mcp-adapter token set <server> Store a token read from stdin (masked prompt or pipe; never argv)");
79
+ log(" pi-mcp-adapter token status <server> Report whether a stored token matches the configured URL");
80
+ log(" pi-mcp-adapter token remove <server> Remove the stored token");
76
81
  }
77
82
 
78
83
  function readJsonFile(filePath) {
@@ -194,7 +199,160 @@ async function runInit(argv, log = console.log) {
194
199
  return 0;
195
200
  }
196
201
 
197
- export async function main(argv = process.argv.slice(2), log = console.log, error = console.error) {
202
+ async function importTokenModules(error) {
203
+ try {
204
+ const [store, config, utils] = await Promise.all([
205
+ import("./mcp-bearer-store.ts"),
206
+ import("./config.ts"),
207
+ import("./utils.ts"),
208
+ ]);
209
+ return { store, config, utils };
210
+ } catch (err) {
211
+ error("Token commands need a Node.js version that can load TypeScript modules (Node 22.18+ or 23+).");
212
+ error(`Import failed: ${err instanceof Error ? err.message : String(err)}`);
213
+ return undefined;
214
+ }
215
+ }
216
+
217
+ function readTokenFromStdin(stdin) {
218
+ if (stdin.isTTY) return readTokenMasked(stdin);
219
+ return new Promise((resolve, reject) => {
220
+ let data = "";
221
+ stdin.setEncoding("utf8");
222
+ stdin.on("data", (chunk) => {
223
+ data += chunk;
224
+ });
225
+ stdin.on("end", () => resolve(data.replace(/[\r\n]+$/, "")));
226
+ stdin.on("error", reject);
227
+ });
228
+ }
229
+
230
+ // Raw-mode masked prompt: the token never echoes to the terminal.
231
+ function readTokenMasked(stdin) {
232
+ return new Promise((resolve, reject) => {
233
+ process.stderr.write("Enter bearer token (input hidden): ");
234
+ stdin.setRawMode(true);
235
+ stdin.resume();
236
+ stdin.setEncoding("utf8");
237
+ let token = "";
238
+ const onData = (chunk) => {
239
+ for (const char of chunk) {
240
+ if (char === "\u0003") {
241
+ cleanup();
242
+ reject(new Error("Token entry cancelled"));
243
+ return;
244
+ }
245
+ if (char === "\r" || char === "\n") {
246
+ cleanup();
247
+ resolve(token);
248
+ return;
249
+ }
250
+ if (char === "\u007f" || char === "\b") {
251
+ token = token.slice(0, -1);
252
+ continue;
253
+ }
254
+ token += char;
255
+ }
256
+ };
257
+ const cleanup = () => {
258
+ stdin.setRawMode(false);
259
+ stdin.pause();
260
+ stdin.off("data", onData);
261
+ process.stderr.write("\n");
262
+ };
263
+ stdin.on("data", onData);
264
+ });
265
+ }
266
+
267
+ async function runToken(argv, log, error, stdin) {
268
+ const [action, serverName, ...extra] = argv;
269
+ if (!["set", "status", "remove"].includes(action) || !serverName) {
270
+ error("Usage: pi-mcp-adapter token <set|status|remove> <server>");
271
+ error("`token set` reads the token from stdin only. Never pass the token as an argument.");
272
+ return 1;
273
+ }
274
+ if (extra.length > 0) {
275
+ error("Unexpected extra arguments. The token must not be passed on the command line; pipe it on stdin or use the interactive prompt.");
276
+ return 1;
277
+ }
278
+
279
+ const modules = await importTokenModules(error);
280
+ if (!modules) return 1;
281
+
282
+ // Argv-derived text is sanitized before terminal output, and the resolved
283
+ // URL is never printed: it can embed userinfo or interpolated secrets.
284
+ const safeName = modules.utils.sanitizeTerminalText(serverName);
285
+ const definition = modules.config.loadMcpConfig(undefined, process.cwd()).mcpServers[serverName];
286
+ if (!definition) {
287
+ error(`Server "${safeName}" not found in the effective MCP config.`);
288
+ return 1;
289
+ }
290
+ if (definition.auth !== "bearer" || definition.bearerTokenStore !== true) {
291
+ error(`Server "${safeName}" is not configured for bearerTokenStore. Set "auth": "bearer" and "bearerTokenStore": true.`);
292
+ return 1;
293
+ }
294
+ // resolveServerUrl embeds the interpolated URL in its exceptions, so catch
295
+ // and redact: the URL can carry userinfo or interpolated secrets.
296
+ let serverUrl;
297
+ try {
298
+ serverUrl = modules.utils.resolveServerUrl(definition);
299
+ } catch {
300
+ error(`Server "${safeName}" has an invalid or unresolvable URL. Fix the server's url in the MCP config and retry.`);
301
+ return 1;
302
+ }
303
+ if (!serverUrl) {
304
+ error(`Server "${safeName}" has no URL configured.`);
305
+ return 1;
306
+ }
307
+
308
+ if (action === "set") {
309
+ const token = await readTokenFromStdin(stdin);
310
+ if (!token) {
311
+ error("No token provided on stdin.");
312
+ return 1;
313
+ }
314
+ try {
315
+ modules.store.saveBearerTokenForUrl(serverName, token, serverUrl);
316
+ } catch (err) {
317
+ error(modules.utils.sanitizeTerminalText(err instanceof Error ? err.message : String(err)));
318
+ return 1;
319
+ }
320
+ log(`Bearer token stored for "${safeName}" and bound to the configured server URL.`);
321
+ if (definition.bearerToken !== undefined || definition.bearerTokenEnv !== undefined) {
322
+ log("Note: this server also configures bearerToken or bearerTokenEnv, which take precedence over the stored token.");
323
+ }
324
+ return 0;
325
+ }
326
+
327
+ if (action === "status") {
328
+ const status = modules.store.inspectBearerTokenForUrl(serverName, serverUrl);
329
+ if (status.status === "present") {
330
+ log(`Bearer token is stored for "${safeName}".`);
331
+ return 0;
332
+ }
333
+ if (status.status === "url-mismatch") {
334
+ log(`Bearer token is stored for "${safeName}", but its URL does not match the current server URL. Run \`pi-mcp-adapter token set ${safeName}\` to rebind it.`);
335
+ return 1;
336
+ }
337
+ if (status.status === "unavailable") {
338
+ error(status.message);
339
+ return 1;
340
+ }
341
+ log(`No bearer token is stored for "${safeName}".`);
342
+ return 1;
343
+ }
344
+
345
+ try {
346
+ modules.store.removeBearerToken(serverName);
347
+ } catch (err) {
348
+ error(modules.utils.sanitizeTerminalText(err instanceof Error ? err.message : String(err)));
349
+ return 1;
350
+ }
351
+ log(`Bearer token removed for "${safeName}".`);
352
+ return 0;
353
+ }
354
+
355
+ export async function main(argv = process.argv.slice(2), log = console.log, error = console.error, stdin = process.stdin) {
198
356
  const [command, ...rest] = argv;
199
357
 
200
358
  if (!command || command === "help" || command === "--help" || command === "-h") {
@@ -202,6 +360,10 @@ export async function main(argv = process.argv.slice(2), log = console.log, erro
202
360
  return 0;
203
361
  }
204
362
 
363
+ if (command === "token") {
364
+ return runToken(rest, log, error, stdin);
365
+ }
366
+
205
367
  if (command === "install") {
206
368
  error("The custom downloader has been retired.");
207
369
  error("Use `pi install npm:pi-mcp-adapter` instead, then optionally run `pi-mcp-adapter init`.");
package/commands.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { OverlayHandle } from "@earendil-works/pi-tui";
2
3
  import type { McpExtensionState } from "./state.ts";
3
4
  import { isServerDisabled, type McpAuthResult, type McpConfig, type McpPanelCallbacks, type McpPanelResult, type ImportKind } from "./types.ts";
4
5
  import {
@@ -16,10 +17,12 @@ import {
16
17
  writeStarterProjectConfig,
17
18
  } from "./config.ts";
18
19
  import { markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateMetadataCache, updateStatusBar, getFailureAgeSeconds, getFailureMessage, clearFailure, recordFailure } from "./init.ts";
20
+ import { isServerInActiveFailureBackoff } from "./failure-backoff.ts";
19
21
  import { loadMetadataCache, reconstructPromptMetadata } from "./metadata-cache.ts";
20
22
  import { buildToolMetadata } from "./tool-metadata.ts";
21
23
  import { supportsOAuth, authenticate, removeAuth, type McpOAuthRuntime } from "./mcp-auth-flow.ts";
22
24
  import { getAuthStorageOptions, inspectAuthForUrl } from "./mcp-auth.ts";
25
+ import { inspectBearerTokenForUrl, removeBearerToken } from "./mcp-bearer-store.ts";
23
26
  import { loadOnboardingState, markSetupCompleted as persistSetupCompleted, markSharedConfigHintShown } from "./onboarding-state.ts";
24
27
  import { openPath, resolveServerUrl, sanitizeTerminalText } from "./utils.ts";
25
28
  import { isAbortError } from "./runtime-owner.ts";
@@ -129,6 +132,7 @@ export async function showTools(state: McpExtensionState, ctx: ExtensionContext)
129
132
 
130
133
  const allTools = [...state.toolMetadata.entries()]
131
134
  .filter(([serverName]) => !isServerDisabled(state.config.mcpServers[serverName]))
135
+ .filter(([serverName]) => !isServerInActiveFailureBackoff(state, serverName))
132
136
  .flatMap(([, metadata]) => metadata.map(m => m.name));
133
137
 
134
138
  if (allTools.length === 0) {
@@ -194,9 +198,9 @@ export async function reconnectServer(
194
198
  state.serverInstructions.delete(name);
195
199
  }
196
200
  updateMetadataCache(state, name);
197
- notifyToolMetadataUpdated(state, name, "command-reconnect");
201
+ const restored = clearFailure(state, name, "command-reconnect");
202
+ if (!restored) notifyToolMetadataUpdated(state, name, "command-reconnect");
198
203
  markKeepAliveAfterConnect(state, name);
199
- clearFailure(state, name);
200
204
 
201
205
  if (ui) {
202
206
  ui.notify(
@@ -287,25 +291,14 @@ export async function authenticateServer(
287
291
  const authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd);
288
292
  const status = await authenticate(serverName, serverUrl, definition, {
289
293
  ...(authStorageOptions.baseDir ? { authStorageOptions } : {}),
290
- onAuthorizationUrl: (authorizationUrl) => {
291
- ui.notify(
292
- `Open this URL to authenticate ${serverName}:\n\n${terminalHyperlink(authorizationUrl, authorizationUrl)}\n\n` +
293
- "After approving, Pi will complete automatically if the browser can reach its localhost callback. " +
294
- "On a remote machine, copy the full localhost URL from the browser address bar and paste it into Pi.",
295
- "info"
296
- );
297
- },
294
+ onAuthorizationUrl: () => {},
298
295
  onAuthorizationInput: async (authorizationUrl, inputSignal) => {
299
- const readyToPaste = await ui.confirm(
300
- `Authorize ${serverName}`,
301
- `Open this link in your browser:\n${terminalHyperlink(authorizationUrl, authorizationUrl)}\n\n` +
302
- "After approving access, select Yes to paste the callback URL.",
303
- { signal: inputSignal },
304
- );
305
- if (!readyToPaste || inputSignal.aborted) return undefined;
296
+ if (inputSignal.aborted) return undefined;
306
297
  return ui.input(
307
- `Complete ${serverName} OAuth`,
308
- "Paste the full callback URL",
298
+ `Complete ${serverName} OAuth\n\n` +
299
+ `${terminalHyperlink("Open authorization page", authorizationUrl)}\n${authorizationUrl}\n\n` +
300
+ "Approve access, then paste the full localhost callback URL below.",
301
+ undefined,
309
302
  { signal: inputSignal },
310
303
  );
311
304
  },
@@ -381,6 +374,75 @@ export async function logoutServer(
381
374
  return { ok: true, message };
382
375
  }
383
376
 
377
+ function validateBearerTokenStoreServer(
378
+ serverName: string,
379
+ state: McpExtensionState,
380
+ ): { ok: true; serverUrl: string } | { ok: false; message: string; type: "error" | "warning" } {
381
+ const safeName = sanitizeTerminalText(serverName);
382
+ const definition = state.config.mcpServers[serverName];
383
+ if (!definition) return { ok: false, message: `Server "${safeName}" not found in config`, type: "error" };
384
+ if (isServerDisabled(definition)) return { ok: false, message: `Server "${safeName}" is disabled. Run /mcp enable ${safeName}, then /reload.`, type: "warning" };
385
+ if (definition.auth !== "bearer" || definition.bearerTokenStore !== true) {
386
+ return { ok: false, message: `Server "${safeName}" is not configured for bearerTokenStore.`, type: "error" };
387
+ }
388
+ // resolveServerUrl embeds the interpolated URL in its exceptions; redact it
389
+ // because the URL can carry userinfo or interpolated secrets.
390
+ let serverUrl: string | undefined;
391
+ try {
392
+ serverUrl = resolveServerUrl(definition);
393
+ } catch {
394
+ return { ok: false, message: `Server "${safeName}" has an invalid or unresolvable URL.`, type: "error" };
395
+ }
396
+ if (!serverUrl) return { ok: false, message: `Server "${safeName}" has no URL configured.`, type: "error" };
397
+ return { ok: true, serverUrl };
398
+ }
399
+
400
+ export async function manageBearerToken(
401
+ action: "set" | "remove" | "status",
402
+ serverName: string,
403
+ state: McpExtensionState,
404
+ ctx: ExtensionContext,
405
+ ): Promise<{ ok: boolean; message: string }> {
406
+ const ui = ctx.hasUI ? ctx.ui : undefined;
407
+ const safeName = sanitizeTerminalText(serverName);
408
+ const validation = validateBearerTokenStoreServer(serverName, state);
409
+ if (!validation.ok) {
410
+ if (ui) ui.notify(validation.message, validation.type);
411
+ return { ok: false, message: validation.message };
412
+ }
413
+
414
+ if (action === "set") {
415
+ const message = `Cannot store bearer token here: Pi extension UI has no masked secret input primitive. Run \`pi-mcp-adapter token set ${safeName}\` in a terminal; it reads the token from stdin only.`;
416
+ if (ui) ui.notify(message, "error");
417
+ return { ok: false, message };
418
+ }
419
+
420
+ if (action === "status") {
421
+ const status = inspectBearerTokenForUrl(serverName, validation.serverUrl);
422
+ const message = status.status === "present"
423
+ ? `Bearer token is stored for "${safeName}".`
424
+ : status.status === "url-mismatch"
425
+ ? `Bearer token is stored for "${safeName}", but its URL does not match the current server URL.`
426
+ : status.status === "unavailable"
427
+ ? status.message
428
+ : `No bearer token is stored for "${safeName}".`;
429
+ if (ui) ui.notify(message, status.status === "unavailable" ? "error" : "info");
430
+ return { ok: status.status !== "unavailable", message };
431
+ }
432
+
433
+ try {
434
+ removeBearerToken(serverName);
435
+ } catch (error) {
436
+ const message = error instanceof Error ? error.message : String(error);
437
+ if (ui) ui.notify(`Failed to remove bearer token for "${safeName}": ${sanitizeTerminalText(message)}`, "error");
438
+ return { ok: false, message };
439
+ }
440
+
441
+ const message = `Bearer token removed for "${safeName}".`;
442
+ if (ui) ui.notify(message, "info");
443
+ return { ok: true, message };
444
+ }
445
+
384
446
  export interface PanelFlowResult {
385
447
  configChanged: boolean;
386
448
  }
@@ -484,6 +546,7 @@ function buildMcpPanelCallbacks(
484
546
  state: McpExtensionState,
485
547
  config: McpConfig,
486
548
  ctx: ExtensionContext,
549
+ getOverlayHandle?: () => OverlayHandle | undefined,
487
550
  ): McpPanelCallbacks {
488
551
  // Panel-only diagnostics keep status inspection from mutating connection
489
552
  // failure state while allowing the existing panel failure UI to show why the
@@ -496,7 +559,16 @@ function buildMcpPanelCallbacks(
496
559
  const definition = config.mcpServers[serverName];
497
560
  return definition ? !isServerDisabled(definition) && supportsOAuth(definition) : false;
498
561
  },
499
- authenticate: (serverName: string) => authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime),
562
+ authenticate: async (serverName: string) => {
563
+ const overlay = getOverlayHandle?.();
564
+ overlay?.setHidden(true);
565
+ try {
566
+ return await authenticateServer(serverName, config, ctx, state.owner?.signal, state.oauthRuntime);
567
+ } finally {
568
+ overlay?.setHidden(false);
569
+ overlay?.focus();
570
+ }
571
+ },
500
572
  getConnectionStatus: (serverName: string) => {
501
573
  authStatusFailures.delete(serverName);
502
574
  const definition = config.mcpServers[serverName];
@@ -565,7 +637,8 @@ export async function openMcpPanel(
565
637
  const provenanceMap = getServerProvenance(configPath, ctx.cwd);
566
638
  const { lines: noticeLines, fingerprint } = buildSharedConfigNoticeLines(configPath, ctx.cwd);
567
639
 
568
- const callbacks = buildMcpPanelCallbacks(state, config, ctx);
640
+ let overlayHandle: OverlayHandle | undefined;
641
+ const callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);
569
642
 
570
643
  const { createMcpPanel } = await import("./mcp-panel.ts");
571
644
  let configChanged = false;
@@ -591,7 +664,11 @@ export async function openMcpPanel(
591
664
  });
592
665
  }, { noticeLines, keybindings });
593
666
  },
594
- { overlay: true, overlayOptions: { anchor: "center", width: 82 } },
667
+ {
668
+ overlay: true,
669
+ overlayOptions: { anchor: "center", width: 82 },
670
+ onHandle: (handle) => { overlayHandle = handle; },
671
+ },
595
672
  );
596
673
  });
597
674
 
@@ -630,7 +707,8 @@ export async function openMcpAuthPanel(
630
707
  const cache = loadMetadataCache();
631
708
  const configPath = pi.getFlag("mcp-config") as string | undefined ?? configOverridePath;
632
709
  const provenanceMap = getServerProvenance(configPath, ctx.cwd);
633
- const callbacks = buildMcpPanelCallbacks(state, config, ctx);
710
+ let overlayHandle: OverlayHandle | undefined;
711
+ const callbacks = buildMcpPanelCallbacks(state, config, ctx, () => overlayHandle);
634
712
  const { createMcpPanel } = await import("./mcp-panel.ts");
635
713
 
636
714
  await new Promise<void>((resolve) => {
@@ -645,7 +723,11 @@ export async function openMcpAuthPanel(
645
723
  noticeLines: ["Select an OAuth MCP server and press Enter or ctrl+a to authenticate."],
646
724
  });
647
725
  },
648
- { overlay: true, overlayOptions: { anchor: "center", width: 82 } },
726
+ {
727
+ overlay: true,
728
+ overlayOptions: { anchor: "center", width: 82 },
729
+ onHandle: (handle) => { overlayHandle = handle; },
730
+ },
649
731
  );
650
732
  });
651
733