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.
- package/CHANGELOG.md +68 -7
- package/README.md +73 -4
- package/cli.js +163 -1
- package/commands.ts +106 -24
- package/config.ts +60 -12
- package/direct-tools.ts +107 -95
- package/dist/agent-dir.d.ts +10 -0
- package/dist/agent-dir.js +63 -0
- package/dist/agent-dir.js.map +1 -0
- package/dist/agent-plugin-loader.d.ts +8 -0
- package/dist/agent-plugin-loader.js +356 -0
- package/dist/agent-plugin-loader.js.map +1 -0
- package/dist/config.d.ts +123 -0
- package/dist/config.js +1077 -0
- package/dist/config.js.map +1 -0
- package/dist/metadata-cache.d.ts +19 -0
- package/dist/metadata-cache.js +319 -0
- package/dist/metadata-cache.js.map +1 -0
- package/dist/package-mcp-loader.d.ts +2 -0
- package/dist/package-mcp-loader.js +156 -0
- package/dist/package-mcp-loader.js.map +1 -0
- package/dist/resource-tools.d.ts +1 -0
- package/dist/resource-tools.js +15 -0
- package/dist/resource-tools.js.map +1 -0
- package/dist/types.d.ts +564 -0
- package/dist/types.js +287 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-app-bridge-helpers.d.ts +6 -0
- package/dist/ui-app-bridge-helpers.js +37 -0
- package/dist/ui-app-bridge-helpers.js.map +1 -0
- package/dist/ui-stream-types.d.ts +104 -0
- package/dist/ui-stream-types.js +59 -0
- package/dist/ui-stream-types.js.map +1 -0
- package/dist/ui-tool-visibility.d.ts +4 -0
- package/dist/ui-tool-visibility.js +27 -0
- package/dist/ui-tool-visibility.js.map +1 -0
- package/dist/utils.d.ts +32 -0
- package/dist/utils.js +338 -0
- package/dist/utils.js.map +1 -0
- package/failure-backoff.ts +23 -0
- package/index.ts +325 -60
- package/init.ts +46 -31
- package/lifecycle.ts +89 -23
- package/mcp-auth-flow.ts +39 -6
- package/mcp-bearer-store.ts +387 -0
- package/mcp-callback-server.ts +27 -3
- package/mcp-code.ts +6 -0
- package/mcp-oauth-provider.ts +66 -16
- package/mcp-output-guard.ts +95 -7
- package/mcp-panel.ts +42 -13
- package/mcp-probe.ts +18 -4
- package/mcp-references.ts +368 -0
- package/mcp-status.ts +8 -19
- package/metadata-cache.ts +52 -33
- package/namespace-tools.ts +276 -0
- package/package-mcp-loader.ts +168 -0
- package/package.json +26 -6
- package/proxy-modes.ts +71 -14
- package/request-headers-command.ts +31 -37
- package/search-ranking.ts +94 -22
- package/server-manager.ts +86 -17
- package/state.ts +1 -3
- package/tool-result-renderer.ts +16 -2
- package/types.ts +50 -1
- package/ui-server.ts +21 -35
- package/ui-session.ts +2 -1
- 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.
|
|
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
|
-
###
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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: (
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
-
{
|
|
726
|
+
{
|
|
727
|
+
overlay: true,
|
|
728
|
+
overlayOptions: { anchor: "center", width: 82 },
|
|
729
|
+
onHandle: (handle) => { overlayHandle = handle; },
|
|
730
|
+
},
|
|
649
731
|
);
|
|
650
732
|
});
|
|
651
733
|
|