pi-mcp-adapter 2.18.0 → 2.20.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 +29 -0
- package/README.md +62 -12
- package/commands.ts +2 -2
- package/config.ts +19 -12
- package/direct-tools.ts +34 -19
- package/elicitation-handler.ts +10 -11
- package/errors.ts +21 -17
- package/host-html-template.ts +9 -3
- package/index.ts +25 -13
- package/init.ts +9 -7
- package/json-schema-validator.ts +2 -2
- package/lifecycle.ts +8 -8
- package/logger.ts +1 -1
- package/mcp-auth-flow.ts +13 -5
- package/mcp-code.ts +117 -60
- package/mcp-oauth-provider.ts +18 -13
- package/mcp-output-guard.ts +15 -11
- package/mcp-panel.ts +31 -13
- package/mcp-script-worker.mjs +22 -9
- package/mcp-setup-panel.ts +7 -4
- package/mcp-trace.ts +59 -64
- package/metadata-cache.ts +33 -25
- package/npx-resolver.ts +6 -4
- package/oauth-handler.ts +1 -1
- package/onboarding-state.ts +19 -11
- package/package.json +7 -4
- package/prompts.ts +2 -2
- package/proxy-modes.ts +28 -9
- package/sampling-handler.ts +14 -11
- package/search-ranking.ts +2 -1
- package/server-manager.ts +149 -111
- package/session-recovery.ts +11 -12
- package/skills/mcp-scripting/SKILL.md +10 -8
- package/state.ts +3 -1
- package/tool-approval.ts +67 -4
- package/tool-metadata.ts +5 -4
- package/ts-shape.ts +4 -1
- package/types.ts +58 -23
- package/ui-resource-handler.ts +24 -11
- package/ui-server.ts +36 -33
- package/ui-session.ts +16 -14
- package/unix-socket-transport.ts +4 -4
- package/utils.ts +23 -9
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.20.0] - 2026-08-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Added an MCP tool approval broker event so permission extensions can allow, deny, or abstain on proxy, direct, `mcpScript`, resource, and iframe-originated MCP calls before the built-in `approveTools` prompt runs. Thanks @geshido for issue #279.
|
|
14
|
+
- Added opt-in per-server MCP protocol selection with `protocolVersion: "legacy" | "auto" | "2026-07-28"`. Legacy remains the default; auto negotiates the modern era with conservative legacy fallback, while the pinned mode fails instead of falling back. Thanks @mjfaga for PR #272.
|
|
15
|
+
- Added a strict TypeScript typecheck command and CI gate.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Migrated the MCP client from the monolithic SDK v1 package to the stable modular `@modelcontextprotocol/client` and `@modelcontextprotocol/core` v2 packages. The stable release restores conservative legacy discovery fallback and declared JSON Schema dialect support while retaining strict OAuth issuer validation.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- Renamed the MCP scripting tool to camel-case `mcpScript` because Anthropic rejects the previous underscore-form name. Thanks @ritvij14 for issue #278 and @wierdbytes for confirmation and the workaround.
|
|
22
|
+
- Pinned the Chrome DevTools setup preset and README examples to `chrome-devtools-mcp@1.6.0` instead of `@latest`, so reviewed scaffolded commands stay stable. Thanks @fitchmultz for issue #274.
|
|
23
|
+
- Removed the adapter's throwaway Streamable HTTP initialize probe. HTTP connections now initialize once on the real client and use narrowly classified SSE fallback, avoiding duplicate sessions and preventing authentication, cancellation, timeout, negotiation, and server failures from being misclassified as transport incompatibility.
|
|
24
|
+
- Stopped tokenless discovery requests, sandboxed MCP app documents, unrelated child windows, and app-opened popups from gaining session authority; discovery now serves a non-sensitive landing page, app HTML loads with a separate resource-only token, host messages accept only the app frame as their source, and the app response enforces sandboxing even when opened as a top-level page.
|
|
25
|
+
|
|
26
|
+
## [2.19.0] - 2026-08-03
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
- `mcp_script` now records each search, describe, and call with its input, outcome, and duration in result details; emitted, returned, and console values retain readable Maps, Sets, cycles, functions, symbols, and BigInts. Its docs now lead with the plain JavaScript agents write and position it as the primary MCP multi-call workflow surface.
|
|
30
|
+
- Documented how to hide the bundled `mcp-scripting` Pi skill while keeping the adapter extension installed. Thanks @aryzing for issue #267.
|
|
31
|
+
- Documented Linux revoked-keyring recovery in the OAuth guide and `_meta.ui.visibility` behavior in the MCP UI guide.
|
|
32
|
+
|
|
33
|
+
### Changed
|
|
34
|
+
- `mcp_script` is now registered by default for trusted JavaScript MCP multi-call workflows, while `mcp` remains the right tool for status, discovery, auth, and single calls. Set `settings.scriptMode` to `false` to hide the tool.
|
|
35
|
+
|
|
36
|
+
### Fixed
|
|
37
|
+
- `mcp_script` traces now include missing describe attempts, and shared acyclic values no longer render as circular in script output formatting.
|
|
38
|
+
|
|
10
39
|
## [2.18.0] - 2026-08-02
|
|
11
40
|
|
|
12
41
|
### Added
|
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ Preferred project config: `.mcp.json`
|
|
|
45
45
|
"mcpServers": {
|
|
46
46
|
"chrome-devtools": {
|
|
47
47
|
"command": "npx",
|
|
48
|
-
"args": ["-y", "chrome-devtools-mcp@
|
|
48
|
+
"args": ["-y", "chrome-devtools-mcp@1.6.0"]
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
}
|
|
@@ -191,6 +191,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
191
191
|
| `lifecycle` | `"lazy"` (default), `"eager"`, `"keep-alive"`, or `"lazy-keep-alive"` |
|
|
192
192
|
| `idleTimeout` | Minutes before idle disconnect (overrides global) |
|
|
193
193
|
| `requestTimeoutMs` | Request timeout in milliseconds for live MCP calls (overrides global; if omitted or `<= 0`, the MCP SDK default timeout is used) |
|
|
194
|
+
| `protocolVersion` | `"legacy"` (default), `"auto"`, or `"2026-07-28"`; modern negotiation is opt-in |
|
|
194
195
|
| `exposeResources` | Expose MCP resources as tools (default: true) |
|
|
195
196
|
| `directTools` | `true`, `string[]`, or `false` — register tools individually instead of through proxy |
|
|
196
197
|
| `toolPrefix` | Override global `settings.toolPrefix` for this server (`"server"`, `"short"`, `"none"`, or `"mcp"`) |
|
|
@@ -200,6 +201,16 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
200
201
|
| `trace` | Enable metadata-only JSONL protocol tracing for this server; payloads, prompts, tool arguments/results, authorization data, and URLs are never persisted |
|
|
201
202
|
| `disabled` | Keep the server visible in config and status, but prevent connections, authentication, tools, and resource calls (only literal `true` disables it) |
|
|
202
203
|
|
|
204
|
+
#### Protocol version negotiation
|
|
205
|
+
|
|
206
|
+
The adapter defaults to `protocolVersion: "legacy"`. Omitting the field uses the classic MCP initialize sequence without `server/discover` or 2026 headers, preserving compatibility with deployed 2025-era servers.
|
|
207
|
+
|
|
208
|
+
Use `"auto"` to probe for MCP 2026-07-28 and conservatively fall back to the classic handshake when the server provides legacy evidence. For stdio servers, the SDK probes with a short-lived sibling process before starting the session process, so each fresh auto connection adds one process spawn and can wait for the configured request timeout. Explicit Unix sockets are custom transports and probe in place. HTTP auto negotiation uses the actual Streamable HTTP connection; the adapter falls back to legacy SSE only when the endpoint definitively rejects Streamable HTTP (for example 404/405/406/415), never for authentication failures, cancellation, timeouts, or server errors.
|
|
209
|
+
|
|
210
|
+
Use `"2026-07-28"` to pin that revision. Pinning has no legacy or SSE fallback and fails if the server does not offer the requested version.
|
|
211
|
+
|
|
212
|
+
The stable SDK handles era-specific request envelopes, result decoding, list-changed subscriptions, cancellation, and multi-round-trip sampling/elicitation. The adapter keeps strict OAuth issuer validation in every mode. Adapter-level roots support, standard MCP logging presentation, and configuration/UI for protocol cache hints are not yet implemented.
|
|
213
|
+
|
|
203
214
|
For pre-registered browser OAuth clients, set `oauth.redirectUri` to the exact callback registered with the provider, for example `"http://localhost:3118/callback"`. Dynamic clients normally omit it and use a lazy OS-assigned localhost callback port.
|
|
204
215
|
|
|
205
216
|
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.
|
|
@@ -263,7 +274,6 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
263
274
|
"mcpFooterStatus": "full",
|
|
264
275
|
"hostConfigDiscovery": "off",
|
|
265
276
|
"approveTools": ["github_delete_*", "notion_update_*"],
|
|
266
|
-
"scriptMode": true,
|
|
267
277
|
"oauthDir": ".pi/mcp-oauth",
|
|
268
278
|
"trace": {
|
|
269
279
|
"enabled": true,
|
|
@@ -289,7 +299,7 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
289
299
|
| `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. |
|
|
290
300
|
| `directTools` | Global default for all servers (default: false). Per-server overrides this. |
|
|
291
301
|
| `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. |
|
|
292
|
-
| `scriptMode` | Register the MCP-only `
|
|
302
|
+
| `scriptMode` | Register the MCP-only `mcpScript` plain-JavaScript tool (default: true). Set to `false` to hide it. |
|
|
293
303
|
| `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
|
|
294
304
|
| `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
|
|
295
305
|
| `sampling` | Allow MCP servers to sample through Pi models, honoring `modelPreferences.hints` before current/default fallback (default: true when UI approval is available). |
|
|
@@ -318,6 +328,23 @@ Use `approveTools` when a tool should stay visible but not run without confirmat
|
|
|
318
328
|
|
|
319
329
|
When a matching tool is called from the proxy tool, a direct MCP tool, a resource call, or an MCP UI iframe, Pi asks: **Allow once**, **Allow for session**, or **Deny**. Session approvals are kept in memory only. In headless sessions, matching calls fail closed with an `approval_required` result instead of running. `excludeTools` still removes tools entirely; `approveTools` only gates visible tools at call time.
|
|
320
330
|
|
|
331
|
+
Permission extensions can broker these decisions by listening on `pi-mcp-adapter:tool-approval-request` and claiming the request synchronously:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
import {
|
|
335
|
+
MCP_TOOL_APPROVAL_REQUEST_EVENT,
|
|
336
|
+
type McpToolApprovalRequest,
|
|
337
|
+
} from "pi-mcp-adapter";
|
|
338
|
+
|
|
339
|
+
pi.events.on(MCP_TOOL_APPROVAL_REQUEST_EVENT, (request: McpToolApprovalRequest) => {
|
|
340
|
+
request.claim(async () => {
|
|
341
|
+
return "allow_once"; // "allow_for_session" | "deny" | "abstain"
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
The request includes `serverName`, `originalToolName`, `prefixedToolName`, `args`, `origin`, and optional `signal`. The first synchronous claim wins. `allow_for_session` updates the same in-memory approval cache as the built-in dialog; `deny` blocks the MCP call; `abstain` or no claim preserves the fallback behavior above. Brokered approval runs for every uncached MCP call regardless of `approveTools` configuration, across proxy, direct, `mcpScript`, resource, and iframe origins.
|
|
347
|
+
|
|
321
348
|
### Output Guard
|
|
322
349
|
|
|
323
350
|
Oversized MCP tool/resource results are guarded by default so a single huge response can't blow up the model context window or the session file:
|
|
@@ -340,19 +367,41 @@ Set `"outputGuard": false` — or the env kill switch `MCP_OUTPUT_GUARD=0` — t
|
|
|
340
367
|
|
|
341
368
|
### MCP Scripting
|
|
342
369
|
|
|
343
|
-
|
|
370
|
+
For multi-call MCP work, write ordinary JavaScript: discover, inspect, call, loop, filter, chain, or fan out, then return one result. Run that code with the default-on `mcpScript` tool. For a single MCP call, search, describe, status check, or auth action, use `mcp` instead. Set `settings.scriptMode` to `false` to hide the scripting tool.
|
|
371
|
+
|
|
372
|
+
The bundled `mcp-scripting` skill is a separate Pi package resource. To hide that skill while keeping the adapter extension installed, replace the package entry in Pi settings with the object form and disable package skills:
|
|
373
|
+
|
|
374
|
+
```json
|
|
375
|
+
{
|
|
376
|
+
"packages": [
|
|
377
|
+
{ "source": "npm:pi-mcp-adapter", "skills": [] }
|
|
378
|
+
]
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
Preserve any version pin in `source` if your existing package entry has one. You can also disable package resources through `pi config`.
|
|
383
|
+
|
|
384
|
+
For example, this is the JavaScript passed as the `code` argument to `mcpScript`:
|
|
344
385
|
|
|
345
386
|
```js
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
387
|
+
const { items } = await tools.search({ query: "search issues", server: "github" });
|
|
388
|
+
const candidate = items[0];
|
|
389
|
+
if (!candidate) return { error: "No matching tool" };
|
|
390
|
+
|
|
391
|
+
const details = await tools.describe({ path: candidate.path });
|
|
392
|
+
if (details.error) return details;
|
|
393
|
+
|
|
394
|
+
const result = await tools.call(details.path, { query: "is:open label:bug" });
|
|
395
|
+
if (!result.ok) return result;
|
|
396
|
+
emit({ tool: details.path, completed: true });
|
|
397
|
+
return result.data;
|
|
351
398
|
```
|
|
352
399
|
|
|
353
|
-
|
|
400
|
+
See the bundled `mcp-scripting` skill for the complete workflow guide. The API is `await tools.search({ query, server?, limit?, offset? })`, `await tools.describe({ path })`, `tools.call(path, args)`, direct flat calls, `emit(value)`, and a captured `console`. Use ordinary JavaScript loops and Promise utilities for composition; fluent helpers such as `tools.find(...).one()`, `tools.parallel(...)`, and `tools.retry(...)` are not provided. MCP calls return `{ ok: true, data }` or `{ ok: false, error: { code, message } }`, so a failed call does not stop the rest of the script. Result details include a concise `calls` trace with each operation, its path or query, outcome, and duration. Emitted values and console output appear before the script's final return value, and the combined result uses the normal MCP output guard. The default timeout is 30 seconds; each script runs in a worker thread that is terminated at the deadline, including for infinite loops.
|
|
401
|
+
|
|
402
|
+
For a tool-restricted subagent, launch the child Pi with its tool allowlist set to `["mcpScript"]`. Have the parent discover MCP tool names with `mcp({ search: "..." })` and include the relevant prefixed names in the child's task; the child can then loop, filter, and chain those MCP calls without filesystem, shell, or edit tools. The adapter's ordinary lazy connection, authentication, output guard, abort handling, and approval gates still apply to every call.
|
|
354
403
|
|
|
355
|
-
`
|
|
404
|
+
`mcpScript` is a trusted agent-authored MCP scripting layer, not an isolation boundary. If you need isolation, run Pi in an isolated environment. It is distinct from Pi's code-mode skill: Pi's skill batches general Pi tools, while `mcpScript` exposes MCP calls only and can be the child's sole tool.
|
|
356
405
|
|
|
357
406
|
### MCP Prompts
|
|
358
407
|
|
|
@@ -383,7 +432,7 @@ Per-server:
|
|
|
383
432
|
"mcpServers": {
|
|
384
433
|
"chrome-devtools": {
|
|
385
434
|
"command": "npx",
|
|
386
|
-
"args": ["-y", "chrome-devtools-mcp@
|
|
435
|
+
"args": ["-y", "chrome-devtools-mcp@1.6.0"],
|
|
387
436
|
"directTools": true
|
|
388
437
|
},
|
|
389
438
|
"github": {
|
|
@@ -510,6 +559,7 @@ Returns accumulated messages from UI sessions. Each message includes `type`, `se
|
|
|
510
559
|
**Technical notes:**
|
|
511
560
|
|
|
512
561
|
- Tool consent gates whether UIs can call MCP tools (never/once-per-server/always)
|
|
562
|
+
- `_meta.ui.visibility` controls audience: tools marked app-only stay out of the model tool list, and tools marked model-only cannot be called from the UI iframe.
|
|
513
563
|
- Works with both stdio and HTTP MCP servers
|
|
514
564
|
- Uses a local 408KB AppBridge bundle (MCP SDK + Zod) for browser↔server communication
|
|
515
565
|
- Enforces CSP from standard `_meta.ui.csp` and OpenAI-compatible `_meta["openai/widgetCSP"]` metadata in the response header while preserving provider HTML.
|
package/commands.ts
CHANGED
package/config.ts
CHANGED
|
@@ -56,7 +56,7 @@ export const KNOWN_SERVER_PRESETS: readonly KnownServerPreset[] = [
|
|
|
56
56
|
id: "chrome-devtools",
|
|
57
57
|
name: "Chrome DevTools",
|
|
58
58
|
summary: "Inspect and automate a local Chrome browser.",
|
|
59
|
-
entry: { command: "npx", args: ["-y", "chrome-devtools-mcp@
|
|
59
|
+
entry: { command: "npx", args: ["-y", "chrome-devtools-mcp@1.6.0"] },
|
|
60
60
|
},
|
|
61
61
|
];
|
|
62
62
|
|
|
@@ -417,10 +417,12 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
|
|
|
417
417
|
}
|
|
418
418
|
|
|
419
419
|
function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
|
|
420
|
+
const imports = mergeImports(base.imports, next.imports);
|
|
421
|
+
const settings = next.settings ? { ...base.settings, ...next.settings } : base.settings;
|
|
420
422
|
return {
|
|
421
423
|
mcpServers: mergeServerMaps(base.mcpServers, next.mcpServers),
|
|
422
|
-
imports
|
|
423
|
-
settings
|
|
424
|
+
...(imports !== undefined ? { imports } : {}),
|
|
425
|
+
...(settings !== undefined ? { settings } : {}),
|
|
424
426
|
};
|
|
425
427
|
}
|
|
426
428
|
|
|
@@ -499,7 +501,7 @@ function expandImports(config: McpConfig, cwd = process.cwd()): McpConfig {
|
|
|
499
501
|
|
|
500
502
|
return {
|
|
501
503
|
imports: config.imports,
|
|
502
|
-
settings: config.settings,
|
|
504
|
+
...(config.settings !== undefined ? { settings: config.settings } : {}),
|
|
503
505
|
mcpServers: mergeServerMaps(importedServers, config.mcpServers),
|
|
504
506
|
};
|
|
505
507
|
}
|
|
@@ -609,8 +611,8 @@ function validateConfig(raw: unknown): McpConfig {
|
|
|
609
611
|
|
|
610
612
|
return {
|
|
611
613
|
mcpServers: servers as Record<string, ServerEntry>,
|
|
612
|
-
|
|
613
|
-
settings: obj.settings as McpSettings
|
|
614
|
+
...(Array.isArray(obj.imports) ? { imports: obj.imports as ImportKind[] } : {}),
|
|
615
|
+
...(obj.settings !== undefined ? { settings: obj.settings as McpSettings } : {}),
|
|
614
616
|
};
|
|
615
617
|
}
|
|
616
618
|
|
|
@@ -708,8 +710,10 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
|
|
|
708
710
|
|
|
709
711
|
if (raw.type === "local" && Array.isArray(raw.command) && raw.command.length > 0 && raw.command.every((value): value is string => typeof value === "string")) {
|
|
710
712
|
const env = toStringRecord(raw.environment);
|
|
713
|
+
const command = raw.command[0];
|
|
714
|
+
if (command === undefined) continue;
|
|
711
715
|
const mapped: ServerEntry = {
|
|
712
|
-
command
|
|
716
|
+
command,
|
|
713
717
|
args: raw.command.slice(1),
|
|
714
718
|
...(env ? { env } : {}),
|
|
715
719
|
...(typeof raw.cwd === "string" ? { cwd: raw.cwd } : {}),
|
|
@@ -789,9 +793,12 @@ function buildUnifiedDiff(beforeText: string, afterText: string): string {
|
|
|
789
793
|
|
|
790
794
|
for (let i = rows - 1; i >= 0; i--) {
|
|
791
795
|
for (let j = cols - 1; j >= 0; j--) {
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
796
|
+
const row = lcs[i];
|
|
797
|
+
const nextRow = lcs[i + 1];
|
|
798
|
+
if (!row || !nextRow) continue;
|
|
799
|
+
row[j] = before[i] === after[j]
|
|
800
|
+
? (nextRow[j + 1] ?? 0) + 1
|
|
801
|
+
: Math.max(nextRow[j] ?? 0, row[j + 1] ?? 0);
|
|
795
802
|
}
|
|
796
803
|
}
|
|
797
804
|
|
|
@@ -805,7 +812,7 @@ function buildUnifiedDiff(beforeText: string, afterText: string): string {
|
|
|
805
812
|
j++;
|
|
806
813
|
continue;
|
|
807
814
|
}
|
|
808
|
-
if (j < cols && (i === rows || lcs[i][j + 1] >= lcs[i + 1][j])) {
|
|
815
|
+
if (j < cols && (i === rows || (lcs[i]?.[j + 1] ?? 0) >= (lcs[i + 1]?.[j] ?? 0))) {
|
|
809
816
|
lines.push(`+ ${after[j]}`);
|
|
810
817
|
j++;
|
|
811
818
|
continue;
|
|
@@ -1108,7 +1115,7 @@ export function getServerProvenance(overridePath?: string, cwd = process.cwd()):
|
|
|
1108
1115
|
provenance.set(name, {
|
|
1109
1116
|
path: source.writePath,
|
|
1110
1117
|
kind: source.kind,
|
|
1111
|
-
importKind: source.importKind,
|
|
1118
|
+
...(source.importKind !== undefined ? { importKind: source.importKind } : {}),
|
|
1112
1119
|
});
|
|
1113
1120
|
}
|
|
1114
1121
|
}
|
package/direct-tools.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { UrlElicitationRequiredError } from "@modelcontextprotocol/
|
|
2
|
+
import { UrlElicitationRequiredError, type Client } from "@modelcontextprotocol/client";
|
|
3
3
|
import type { McpExtensionState } from "./state.ts";
|
|
4
4
|
import type { DirectToolSpec, McpConfig, McpContent, ToolPrefix } from "./types.ts";
|
|
5
5
|
import type { MetadataCache } from "./metadata-cache.ts";
|
|
@@ -19,6 +19,8 @@ import { SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session
|
|
|
19
19
|
import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
|
|
20
20
|
import { ensureToolCallApproved } from "./tool-approval.ts";
|
|
21
21
|
|
|
22
|
+
type ClientCallToolResult = Awaited<ReturnType<Client["callTool"]>>;
|
|
23
|
+
|
|
22
24
|
const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
23
25
|
const INSTRUCTIONS_SNIPPET_LENGTH = 150;
|
|
24
26
|
export const DIRECT_TOOLS_ADVISORY_THRESHOLD = 75;
|
|
@@ -92,7 +94,10 @@ async function attemptDirectAutoAuth(
|
|
|
92
94
|
: { authStorageOptions: state.authStorageOptions, runtime: state.oauthRuntime },
|
|
93
95
|
);
|
|
94
96
|
} else {
|
|
95
|
-
await authenticate(serverName, serverUrl, definition, {
|
|
97
|
+
await authenticate(serverName, serverUrl, definition, {
|
|
98
|
+
...(signal ? { signal } : {}),
|
|
99
|
+
runtime: state.oauthRuntime,
|
|
100
|
+
});
|
|
96
101
|
}
|
|
97
102
|
return { status: "success" };
|
|
98
103
|
} catch (error) {
|
|
@@ -162,9 +167,9 @@ export function resolveDirectTools(
|
|
|
162
167
|
originalName: tool.name,
|
|
163
168
|
prefixedName,
|
|
164
169
|
description: tool.description ?? "",
|
|
165
|
-
inputSchema: tool.inputSchema,
|
|
166
|
-
uiResourceUri: tool.uiResourceUri,
|
|
167
|
-
uiStreamMode: tool.uiStreamMode,
|
|
170
|
+
...(tool.inputSchema !== undefined ? { inputSchema: tool.inputSchema } : {}),
|
|
171
|
+
...(tool.uiResourceUri !== undefined ? { uiResourceUri: tool.uiResourceUri } : {}),
|
|
172
|
+
...(tool.uiStreamMode !== undefined ? { uiStreamMode: tool.uiStreamMode } : {}),
|
|
168
173
|
});
|
|
169
174
|
}
|
|
170
175
|
|
|
@@ -207,7 +212,7 @@ export function buildProxyDescription(
|
|
|
207
212
|
directSpecs: DirectToolSpec[],
|
|
208
213
|
): string {
|
|
209
214
|
const prefix = config.settings?.toolPrefix ?? "server";
|
|
210
|
-
let desc = `MCP gateway
|
|
215
|
+
let desc = `MCP gateway — server status, tool search/describe, auth, and single MCP tool calls. When one request needs several MCP calls with logic between them, use mcpScript. Non-MCP Pi tools should be called directly, not through mcp.\n`;
|
|
211
216
|
|
|
212
217
|
const directByServer = new Map<string, number>();
|
|
213
218
|
for (const spec of directSpecs) {
|
|
@@ -223,7 +228,7 @@ export function buildProxyDescription(
|
|
|
223
228
|
const serverSummaries: string[] = [];
|
|
224
229
|
for (const serverName of Object.keys(config.mcpServers)) {
|
|
225
230
|
const definition = config.mcpServers[serverName];
|
|
226
|
-
if (isServerDisabled(definition)) continue;
|
|
231
|
+
if (!definition || isServerDisabled(definition)) continue;
|
|
227
232
|
const entry = cache?.servers?.[serverName];
|
|
228
233
|
const effectivePrefix = resolveToolPrefix(definition, prefix);
|
|
229
234
|
const toolCount = (entry?.tools ?? []).filter(
|
|
@@ -377,11 +382,11 @@ export function createDirectToolExecutor(
|
|
|
377
382
|
name: spec.prefixedName,
|
|
378
383
|
originalName: spec.originalName,
|
|
379
384
|
description: spec.description,
|
|
380
|
-
inputSchema: spec.inputSchema,
|
|
381
|
-
resourceUri: spec.resourceUri,
|
|
382
|
-
uiResourceUri: spec.uiResourceUri,
|
|
383
|
-
uiStreamMode: spec.uiStreamMode,
|
|
384
|
-
}, params, ownedSignal);
|
|
385
|
+
...(spec.inputSchema !== undefined ? { inputSchema: spec.inputSchema } : {}),
|
|
386
|
+
...(spec.resourceUri !== undefined ? { resourceUri: spec.resourceUri } : {}),
|
|
387
|
+
...(spec.uiResourceUri !== undefined ? { uiResourceUri: spec.uiResourceUri } : {}),
|
|
388
|
+
...(spec.uiStreamMode !== undefined ? { uiStreamMode: spec.uiStreamMode } : {}),
|
|
389
|
+
}, params, ownedSignal, spec.resourceUri ? "resource" : "direct");
|
|
385
390
|
if (approval.ok === false) {
|
|
386
391
|
const denied = approval.reason === "denied";
|
|
387
392
|
const message = denied
|
|
@@ -431,7 +436,12 @@ export function createDirectToolExecutor(
|
|
|
431
436
|
|
|
432
437
|
if (spec.resourceUri) {
|
|
433
438
|
const result = await withSessionRecovery(
|
|
434
|
-
{
|
|
439
|
+
{
|
|
440
|
+
manager: state.manager,
|
|
441
|
+
config: state.config,
|
|
442
|
+
...(ownedSignal ? { signal: ownedSignal } : {}),
|
|
443
|
+
onNeedsAuth: recoverAuthConnection,
|
|
444
|
+
},
|
|
435
445
|
spec.serverName,
|
|
436
446
|
(conn) => conn.client.readResource({ uri: spec.resourceUri! }, requestOptions),
|
|
437
447
|
);
|
|
@@ -453,22 +463,27 @@ export function createDirectToolExecutor(
|
|
|
453
463
|
toolName: spec.originalName,
|
|
454
464
|
toolArgs: params ?? {},
|
|
455
465
|
uiResourceUri: spec.uiResourceUri!,
|
|
456
|
-
streamMode: spec.uiStreamMode,
|
|
457
|
-
signal,
|
|
466
|
+
...(spec.uiStreamMode !== undefined ? { streamMode: spec.uiStreamMode } : {}),
|
|
467
|
+
...(signal ? { signal } : {}),
|
|
458
468
|
onNeedsAuth: recoverAuthConnection,
|
|
459
469
|
})
|
|
460
470
|
: null;
|
|
461
471
|
|
|
462
|
-
const result = await withSessionRecovery(
|
|
463
|
-
{
|
|
472
|
+
const result = await withSessionRecovery<ClientCallToolResult>(
|
|
473
|
+
{
|
|
474
|
+
manager: state.manager,
|
|
475
|
+
config: state.config,
|
|
476
|
+
...(ownedSignal ? { signal: ownedSignal } : {}),
|
|
477
|
+
onNeedsAuth: recoverAuthConnection,
|
|
478
|
+
},
|
|
464
479
|
spec.serverName,
|
|
465
480
|
(conn) => abortable(conn.client.callTool({
|
|
466
481
|
name: spec.originalName,
|
|
467
482
|
arguments: params ?? {},
|
|
468
483
|
_meta: uiSession?.requestMeta,
|
|
469
|
-
},
|
|
484
|
+
}, requestOptions), ownedSignal),
|
|
470
485
|
);
|
|
471
|
-
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/
|
|
486
|
+
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/client").CallToolResult);
|
|
472
487
|
|
|
473
488
|
if (result.isError) {
|
|
474
489
|
const mcpContent = (result.content ?? []) as McpContent[];
|
package/elicitation-handler.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { Client } from "@modelcontextprotocol/
|
|
2
|
+
import type { Client } from "@modelcontextprotocol/client";
|
|
3
3
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
McpError,
|
|
4
|
+
ProtocolError,
|
|
5
|
+
ProtocolErrorCode,
|
|
7
6
|
type ElicitRequest,
|
|
8
7
|
type ElicitRequestFormParams,
|
|
9
8
|
type ElicitRequestURLParams,
|
|
10
9
|
type ElicitResult,
|
|
11
|
-
} from "@modelcontextprotocol/
|
|
12
|
-
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/
|
|
13
|
-
import type { JsonSchemaType } from "@modelcontextprotocol/
|
|
10
|
+
} from "@modelcontextprotocol/client";
|
|
11
|
+
import { AjvJsonSchemaValidator } from "@modelcontextprotocol/client/validators/ajv";
|
|
12
|
+
import type { JsonSchemaType } from "@modelcontextprotocol/client";
|
|
14
13
|
import open from "open";
|
|
15
14
|
|
|
16
15
|
export type ElicitationValue = string | number | boolean | string[] | undefined;
|
|
@@ -28,7 +27,7 @@ export interface ElicitationHandlerOptions {
|
|
|
28
27
|
export type ServerElicitationConfig = Omit<ElicitationHandlerOptions, "serverName" | "onUrlAccepted">;
|
|
29
28
|
|
|
30
29
|
export function registerElicitationHandler(client: Client, options: ElicitationHandlerOptions): void {
|
|
31
|
-
client.setRequestHandler(
|
|
30
|
+
client.setRequestHandler("elicitation/create", request =>
|
|
32
31
|
handleElicitationRequest(options, request));
|
|
33
32
|
}
|
|
34
33
|
|
|
@@ -305,16 +304,16 @@ export async function handleUrlElicitation(
|
|
|
305
304
|
options: ElicitationHandlerOptions,
|
|
306
305
|
params: ElicitRequestURLParams,
|
|
307
306
|
): Promise<ElicitResult> {
|
|
308
|
-
if (!options.allowUrl) throw new
|
|
307
|
+
if (!options.allowUrl) throw new ProtocolError(ProtocolErrorCode.InvalidParams, "URL elicitation is not supported");
|
|
309
308
|
|
|
310
309
|
let parsed: URL;
|
|
311
310
|
try {
|
|
312
311
|
parsed = new URL(params.url);
|
|
313
312
|
} catch {
|
|
314
|
-
throw new
|
|
313
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "URL elicitation supplied an invalid URL");
|
|
315
314
|
}
|
|
316
315
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
317
|
-
throw new
|
|
316
|
+
throw new ProtocolError(ProtocolErrorCode.InvalidParams, "URL elicitation only supports HTTP and HTTPS URLs");
|
|
318
317
|
}
|
|
319
318
|
|
|
320
319
|
const decision = await options.ui.select([
|
package/errors.ts
CHANGED
|
@@ -17,8 +17,8 @@ export interface McpUiErrorContext {
|
|
|
17
17
|
export class McpUiError extends Error {
|
|
18
18
|
readonly code: string;
|
|
19
19
|
readonly context: McpUiErrorContext;
|
|
20
|
-
readonly recoveryHint
|
|
21
|
-
readonly cause
|
|
20
|
+
readonly recoveryHint: string | undefined;
|
|
21
|
+
readonly cause: Error | undefined;
|
|
22
22
|
|
|
23
23
|
constructor(
|
|
24
24
|
message: string,
|
|
@@ -65,9 +65,9 @@ export class ResourceFetchError extends McpUiError {
|
|
|
65
65
|
) {
|
|
66
66
|
super(`Failed to fetch UI resource "${uri}": ${reason}`, {
|
|
67
67
|
code: "RESOURCE_FETCH_ERROR",
|
|
68
|
-
context: { uri, server: options
|
|
68
|
+
context: { uri, ...(options?.server !== undefined ? { server: options.server } : {}) },
|
|
69
69
|
recoveryHint: "Check that the MCP server is connected and the resource URI is valid.",
|
|
70
|
-
cause: options
|
|
70
|
+
...(options?.cause !== undefined ? { cause: options.cause } : {}),
|
|
71
71
|
});
|
|
72
72
|
this.name = "ResourceFetchError";
|
|
73
73
|
}
|
|
@@ -84,7 +84,11 @@ export class ResourceParseError extends McpUiError {
|
|
|
84
84
|
) {
|
|
85
85
|
super(`Invalid UI resource "${uri}": ${reason}`, {
|
|
86
86
|
code: "RESOURCE_PARSE_ERROR",
|
|
87
|
-
context: {
|
|
87
|
+
context: {
|
|
88
|
+
uri,
|
|
89
|
+
...(options?.server !== undefined ? { server: options.server } : {}),
|
|
90
|
+
...(options?.mimeType !== undefined ? { mimeType: options.mimeType } : {}),
|
|
91
|
+
},
|
|
88
92
|
recoveryHint: "Ensure the resource returns valid HTML with the correct MIME type.",
|
|
89
93
|
});
|
|
90
94
|
this.name = "ResourceParseError";
|
|
@@ -98,9 +102,9 @@ export class BridgeConnectionError extends McpUiError {
|
|
|
98
102
|
constructor(reason: string, options?: { session?: string; cause?: Error }) {
|
|
99
103
|
super(`AppBridge connection failed: ${reason}`, {
|
|
100
104
|
code: "BRIDGE_CONNECTION_ERROR",
|
|
101
|
-
context: { session: options
|
|
105
|
+
context: options?.session !== undefined ? { session: options.session } : {},
|
|
102
106
|
recoveryHint: "Check browser console for detailed errors. The iframe may have failed to load.",
|
|
103
|
-
cause: options
|
|
107
|
+
...(options?.cause !== undefined ? { cause: options.cause } : {}),
|
|
104
108
|
});
|
|
105
109
|
this.name = "BridgeConnectionError";
|
|
106
110
|
}
|
|
@@ -142,9 +146,9 @@ export class SessionError extends McpUiError {
|
|
|
142
146
|
) {
|
|
143
147
|
super(`Session error: ${reason}`, {
|
|
144
148
|
code: "SESSION_ERROR",
|
|
145
|
-
context: { session: options
|
|
149
|
+
context: options?.session !== undefined ? { session: options.session } : {},
|
|
146
150
|
recoveryHint: "The session may have expired or been closed. Try opening the UI again.",
|
|
147
|
-
cause: options
|
|
151
|
+
...(options?.cause !== undefined ? { cause: options.cause } : {}),
|
|
148
152
|
});
|
|
149
153
|
this.name = "SessionError";
|
|
150
154
|
}
|
|
@@ -160,9 +164,9 @@ export class ServerError extends McpUiError {
|
|
|
160
164
|
) {
|
|
161
165
|
super(`UI server error: ${reason}`, {
|
|
162
166
|
code: "SERVER_ERROR",
|
|
163
|
-
context: { port: options
|
|
167
|
+
context: options?.port !== undefined ? { port: options.port } : {},
|
|
164
168
|
recoveryHint: "Check if the port is available. Another process may be using it.",
|
|
165
|
-
cause: options
|
|
169
|
+
...(options?.cause !== undefined ? { cause: options.cause } : {}),
|
|
166
170
|
});
|
|
167
171
|
this.name = "ServerError";
|
|
168
172
|
}
|
|
@@ -179,9 +183,9 @@ export class McpServerError extends McpUiError {
|
|
|
179
183
|
) {
|
|
180
184
|
super(`MCP server "${server}" error: ${reason}`, {
|
|
181
185
|
code: "MCP_SERVER_ERROR",
|
|
182
|
-
context: { server, tool: options
|
|
186
|
+
context: { server, ...(options?.tool !== undefined ? { tool: options.tool } : {}) },
|
|
183
187
|
recoveryHint: "Check that the MCP server is running and responsive.",
|
|
184
|
-
cause: options
|
|
188
|
+
...(options?.cause !== undefined ? { cause: options.cause } : {}),
|
|
185
189
|
});
|
|
186
190
|
this.name = "McpServerError";
|
|
187
191
|
}
|
|
@@ -196,8 +200,8 @@ export function wrapError(error: unknown, context?: McpUiErrorContext): McpUiErr
|
|
|
196
200
|
return new McpUiError(error.message, {
|
|
197
201
|
code: error.code,
|
|
198
202
|
context: { ...error.context, ...context },
|
|
199
|
-
recoveryHint: error.recoveryHint,
|
|
200
|
-
cause: error.cause,
|
|
203
|
+
...(error.recoveryHint !== undefined ? { recoveryHint: error.recoveryHint } : {}),
|
|
204
|
+
...(error.cause !== undefined ? { cause: error.cause } : {}),
|
|
201
205
|
});
|
|
202
206
|
}
|
|
203
207
|
|
|
@@ -206,8 +210,8 @@ export function wrapError(error: unknown, context?: McpUiErrorContext): McpUiErr
|
|
|
206
210
|
|
|
207
211
|
return new McpUiError(message, {
|
|
208
212
|
code: "UNKNOWN_ERROR",
|
|
209
|
-
context,
|
|
210
|
-
cause,
|
|
213
|
+
...(context !== undefined ? { context } : {}),
|
|
214
|
+
...(cause !== undefined ? { cause } : {}),
|
|
211
215
|
});
|
|
212
216
|
}
|
|
213
217
|
|
package/host-html-template.ts
CHANGED
|
@@ -2,9 +2,11 @@ import type { UiHostContext, UiResourceContent, UiResourceCsp } from "./types.ts
|
|
|
2
2
|
|
|
3
3
|
// Use locally bundled AppBridge to avoid CDN Zod bundling issues
|
|
4
4
|
const DEFAULT_APP_BRIDGE_MODULE_URL = "/app-bridge.bundle.js";
|
|
5
|
+
const APP_SANDBOX = "allow-scripts allow-forms allow-modals allow-popups allow-downloads";
|
|
5
6
|
|
|
6
7
|
export interface HostHtmlTemplateInput {
|
|
7
8
|
sessionToken: string;
|
|
9
|
+
uiResourceToken: string;
|
|
8
10
|
serverName: string;
|
|
9
11
|
toolName: string;
|
|
10
12
|
toolArgs: Record<string, unknown>;
|
|
@@ -20,6 +22,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
20
22
|
const hostContext = input.hostContext ?? {};
|
|
21
23
|
|
|
22
24
|
const sessionToken = safeInlineJSON(input.sessionToken);
|
|
25
|
+
const uiResourceToken = safeInlineJSON(input.uiResourceToken);
|
|
23
26
|
const toolArgs = safeInlineJSON(input.toolArgs);
|
|
24
27
|
const serverName = safeInlineJSON(input.serverName);
|
|
25
28
|
const toolName = safeInlineJSON(input.toolName);
|
|
@@ -109,7 +112,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
109
112
|
</div>
|
|
110
113
|
</header>
|
|
111
114
|
<main>
|
|
112
|
-
<iframe id="mcp-app" sandbox="
|
|
115
|
+
<iframe id="mcp-app" sandbox="${APP_SANDBOX}" referrerpolicy="no-referrer"></iframe>
|
|
113
116
|
</main>
|
|
114
117
|
<div class="overlay" id="error-overlay">
|
|
115
118
|
<div class="panel">
|
|
@@ -127,6 +130,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
127
130
|
import { AppBridge, PostMessageTransport } from ${moduleUrl};
|
|
128
131
|
|
|
129
132
|
const SESSION_TOKEN = ${sessionToken};
|
|
133
|
+
const UI_RESOURCE_TOKEN = ${uiResourceToken};
|
|
130
134
|
const SERVER_NAME = ${serverName};
|
|
131
135
|
const TOOL_NAME = ${toolName};
|
|
132
136
|
const TOOL_ARGS = ${toolArgs};
|
|
@@ -236,6 +240,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
236
240
|
// Also listen for raw postMessage events with custom types (notify, prompt, intent, etc.)
|
|
237
241
|
// These bypass the AppBridge protocol but are used by some MCP UI implementations
|
|
238
242
|
window.addEventListener("message", async (event) => {
|
|
243
|
+
if (event.source !== iframe.contentWindow) return;
|
|
239
244
|
const data = event.data;
|
|
240
245
|
if (!data || typeof data !== "object") return;
|
|
241
246
|
|
|
@@ -300,7 +305,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
300
305
|
|
|
301
306
|
// Connect bridge BEFORE loading iframe to ensure we're listening when the app sends ui/initialize
|
|
302
307
|
try {
|
|
303
|
-
const transport = new PostMessageTransport(iframe.contentWindow,
|
|
308
|
+
const transport = new PostMessageTransport(iframe.contentWindow, iframe.contentWindow);
|
|
304
309
|
await bridge.connect(transport);
|
|
305
310
|
} catch (error) {
|
|
306
311
|
console.error("[host] Bridge connection failed:", error);
|
|
@@ -310,7 +315,7 @@ export function buildHostHtmlTemplate(input: HostHtmlTemplateInput): string {
|
|
|
310
315
|
const iframeLoaded = new Promise((resolve) => {
|
|
311
316
|
iframe.onload = resolve;
|
|
312
317
|
});
|
|
313
|
-
iframe.src = "/ui-app?
|
|
318
|
+
iframe.src = "/ui-app?resource=" + encodeURIComponent(UI_RESOURCE_TOKEN);
|
|
314
319
|
await iframeLoaded;
|
|
315
320
|
|
|
316
321
|
const eventSource = new EventSource("/events?session=" + encodeURIComponent(SESSION_TOKEN));
|
|
@@ -399,6 +404,7 @@ export function buildCspMetaContent(csp: UiResourceCsp | undefined): string {
|
|
|
399
404
|
|
|
400
405
|
return [
|
|
401
406
|
"default-src 'none'",
|
|
407
|
+
`sandbox ${APP_SANDBOX}`,
|
|
402
408
|
toDirective("script-src", ["'self'", "'unsafe-inline'"], resourceDomains),
|
|
403
409
|
toDirective("style-src", ["'self'", "'unsafe-inline'"], resourceDomains),
|
|
404
410
|
toDirective("font-src", ["'self'"], resourceDomains),
|