pi-mcp-adapter 2.33.0 → 2.35.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 +70 -0
- package/README.md +72 -10
- package/agent-plugin-loader.ts +69 -30
- package/agent-plugin-provenance.ts +36 -0
- package/bearer-command-resolver.ts +193 -0
- package/cli.js +56 -5
- package/commands.ts +37 -9
- package/config.ts +158 -13
- package/direct-tool-surface.ts +243 -0
- package/direct-tools.ts +21 -254
- package/dist/abort.d.ts +2 -0
- package/dist/abort.js +36 -0
- package/dist/abort.js.map +1 -0
- package/dist/agent-plugin-loader.js +74 -32
- package/dist/agent-plugin-loader.js.map +1 -1
- package/dist/agent-plugin-provenance.d.ts +1 -0
- package/dist/agent-plugin-provenance.js +31 -0
- package/dist/agent-plugin-provenance.js.map +1 -0
- package/dist/bearer-command-resolver.d.ts +6 -0
- package/dist/bearer-command-resolver.js +194 -0
- package/dist/bearer-command-resolver.js.map +1 -0
- package/dist/config.d.ts +2 -1
- package/dist/config.js +167 -12
- package/dist/config.js.map +1 -1
- package/dist/consent-manager.d.ts +18 -0
- package/dist/consent-manager.js +88 -0
- package/dist/consent-manager.js.map +1 -0
- package/dist/elicitation-handler.d.ts +17 -0
- package/dist/elicitation-handler.js +316 -0
- package/dist/elicitation-handler.js.map +1 -0
- package/dist/errors.d.ts +131 -0
- package/dist/errors.js +278 -0
- package/dist/errors.js.map +1 -0
- package/dist/http-ca.d.ts +8 -0
- package/dist/http-ca.js +88 -0
- package/dist/http-ca.js.map +1 -0
- package/dist/jev-client.d.ts +14 -0
- package/dist/jev-client.js +314 -0
- package/dist/jev-client.js.map +1 -0
- package/dist/jev-contracts.d.ts +77 -0
- package/dist/jev-contracts.js +2 -0
- package/dist/jev-contracts.js.map +1 -0
- package/dist/jev-key-store.d.ts +22 -0
- package/dist/jev-key-store.js +85 -0
- package/dist/jev-key-store.js.map +1 -0
- package/dist/json-schema-validator.d.ts +2 -0
- package/dist/json-schema-validator.js +55 -0
- package/dist/json-schema-validator.js.map +1 -0
- package/dist/lifecycle.d.ts +56 -0
- package/dist/lifecycle.js +410 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/logger.d.ts +51 -0
- package/dist/logger.js +131 -0
- package/dist/logger.js.map +1 -0
- package/dist/mcp-auth-fetch.d.ts +26 -0
- package/dist/mcp-auth-fetch.js +103 -0
- package/dist/mcp-auth-fetch.js.map +1 -0
- package/dist/mcp-auth-flow.d.ts +118 -0
- package/dist/mcp-auth-flow.js +1098 -0
- package/dist/mcp-auth-flow.js.map +1 -0
- package/dist/mcp-auth.d.ts +168 -0
- package/dist/mcp-auth.js +1117 -0
- package/dist/mcp-auth.js.map +1 -0
- package/dist/mcp-bearer-store.js +7 -140
- package/dist/mcp-bearer-store.js.map +1 -1
- package/dist/mcp-callback-server.d.ts +53 -0
- package/dist/mcp-callback-server.js +441 -0
- package/dist/mcp-callback-server.js.map +1 -0
- package/dist/mcp-oauth-provider.d.ts +148 -0
- package/dist/mcp-oauth-provider.js +689 -0
- package/dist/mcp-oauth-provider.js.map +1 -0
- package/dist/mcp-probe.d.ts +6 -0
- package/dist/mcp-probe.js +166 -0
- package/dist/mcp-probe.js.map +1 -0
- package/dist/mcp-tasks.d.ts +102 -0
- package/dist/mcp-tasks.js +369 -0
- package/dist/mcp-tasks.js.map +1 -0
- package/dist/mcp-trace.d.ts +95 -0
- package/dist/mcp-trace.js +242 -0
- package/dist/mcp-trace.js.map +1 -0
- package/dist/metadata-cache.js +6 -14
- package/dist/metadata-cache.js.map +1 -1
- package/dist/npx-resolver.d.ts +6 -0
- package/dist/npx-resolver.js +505 -0
- package/dist/npx-resolver.js.map +1 -0
- package/dist/package-mcp-loader.js +4 -16
- package/dist/package-mcp-loader.js.map +1 -1
- package/dist/request-headers-command.d.ts +10 -0
- package/dist/request-headers-command.js +312 -0
- package/dist/request-headers-command.js.map +1 -0
- package/dist/runtime-owner.d.ts +13 -0
- package/dist/runtime-owner.js +97 -0
- package/dist/runtime-owner.js.map +1 -0
- package/dist/sampling-handler.d.ts +17 -0
- package/dist/sampling-handler.js +203 -0
- package/dist/sampling-handler.js.map +1 -0
- package/dist/secure-keyring.d.ts +11 -0
- package/dist/secure-keyring.js +172 -0
- package/dist/secure-keyring.js.map +1 -0
- package/dist/server-manager.d.ts +157 -0
- package/dist/server-manager.js +1743 -0
- package/dist/server-manager.js.map +1 -0
- package/dist/session-approvals.d.ts +34 -0
- package/dist/session-approvals.js +140 -0
- package/dist/session-approvals.js.map +1 -0
- package/dist/session-recovery.d.ts +26 -0
- package/dist/session-recovery.js +142 -0
- package/dist/session-recovery.js.map +1 -0
- package/dist/state.d.ts +73 -0
- package/dist/state.js +2 -0
- package/dist/state.js.map +1 -0
- package/dist/types.d.ts +43 -1
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -1
- package/dist/ui-resource-handler.d.ts +17 -0
- package/dist/ui-resource-handler.js +219 -0
- package/dist/ui-resource-handler.js.map +1 -0
- package/dist/unix-socket-transport.d.ts +15 -0
- package/dist/unix-socket-transport.js +85 -0
- package/dist/unix-socket-transport.js.map +1 -0
- package/dist/utils.d.ts +4 -0
- package/dist/utils.js +42 -1
- package/dist/utils.js.map +1 -1
- package/elicitation-handler.ts +29 -19
- package/examples/jev-accessibility-loop.mjs +85 -0
- package/examples/jev-semantic-filter.mjs +34 -0
- package/host-html-template.ts +20 -19
- package/http-ca.ts +40 -5
- package/index.ts +489 -170
- package/init.ts +16 -26
- package/jev-client.ts +246 -0
- package/jev-contracts.ts +53 -0
- package/jev-key-store.ts +79 -0
- package/lazy-loader.ts +7 -0
- package/lifecycle.ts +14 -3
- package/mcp-auth-fetch.ts +7 -19
- package/mcp-auth-flow.ts +189 -89
- package/mcp-auth.ts +322 -75
- package/mcp-bearer-store.ts +7 -142
- package/mcp-code.ts +119 -11
- package/mcp-oauth-provider.ts +54 -59
- package/mcp-output-guard.ts +4 -0
- package/mcp-references.ts +5 -3
- package/mcp-script-worker.mjs +5 -0
- package/mcp-setup-panel.ts +4 -2
- package/mcp-tasks.ts +467 -0
- package/metadata-cache.ts +6 -16
- package/namespace-tools.ts +61 -3
- package/oauth.ts +1 -4
- package/package-mcp-loader.ts +4 -16
- package/package.json +22 -15
- package/prompts.ts +35 -3
- package/proxy-modes.ts +316 -166
- package/request-headers-command.ts +6 -3
- package/sandbox-proxy-template.ts +20 -93
- package/search-ranking.ts +38 -7
- package/secure-keyring.ts +170 -0
- package/semantic-search.ts +184 -0
- package/server-manager.ts +372 -54
- package/session-approvals.ts +14 -0
- package/skills/mcp-scripting/SKILL.md +2 -0
- package/state.ts +3 -1
- package/tool-approval.ts +26 -4
- package/tool-metadata.ts +6 -14
- package/tool-registrar.ts +8 -6
- package/tool-result-renderer.ts +4 -1
- package/types.ts +60 -1
- package/ui-server.ts +45 -36
- package/utils.ts +44 -1
- package/OAUTH.md +0 -367
- package/mcp-refresh-lock.ts +0 -62
- package/oauth-diagnostics.ts +0 -31
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,76 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.35.0] - 2026-09-20
|
|
11
|
+
|
|
12
|
+
### Highlights
|
|
13
|
+
|
|
14
|
+
- Describe what you want to do and let Jev find the MCP tools that best match your request.
|
|
15
|
+
- Run long-lived MCP Tasks with progress polling, interactive input, and cancellation.
|
|
16
|
+
- Edit shared MCP configuration without leaving Pi and approve a server for the rest of the session.
|
|
17
|
+
- Reconnect to OAuth and bearer-token servers more reliably, including after expired credentials or authorization failures.
|
|
18
|
+
- Find and use tools more accurately across CJK queries, namespaced catalogs, structured results, and ambiguous names.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Opt-in TypeSafe Jev support can understand a request, rank MCP tools by how well they match, and evaluate intermediate `mcpScript` results. Credentials stay in the OS keyring or environment, sharing MCP data requires an explicit server allowlist, and requests have configurable limits. In a live test across 95 local tools and resources, Jev chose the expected result first in 10 of 11 answerable cases and second once, compared with 5 first-place matches from regular text search. Part of [#611](https://github.com/nicobailon/pi-mcp-adapter/issues/611).
|
|
23
|
+
- `/mcp edit [project|global]` opens the shared MCP config in an editor (Ctrl+G opens `$EDITOR`), refuses text that is not a JSONC object, and reloads after a save. Closes #593. Thanks to [@turisanapo](https://github.com/turisanapo) for PR #594.
|
|
24
|
+
- Support for MCP Tasks. Long-running tool calls are polled to completion, interactive questions use the normal Pi interface, cancellation is forwarded to the server, and failures are reported like ordinary tool-call errors. Task support activates only when the server advertises it and can be disabled per server with `tasks: false`. Thanks to [@rgarcia](https://github.com/rgarcia) for PR #620.
|
|
25
|
+
- Users can grant runtime-only approval for all tools and arguments on a server for the current session. Thanks to [@derdossi](https://github.com/derdossi) for PR #618.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- Development and peer dependency coverage now includes Pi 0.86.
|
|
30
|
+
- Self-namespaced MCP tools no longer receive duplicate prefixes, and proxy calls now resolve unique canonical-name candidates while failing closed on collisions and ambiguity. Fixes [#609](https://github.com/nicobailon/pi-mcp-adapter/issues/609). Thanks to [@elkaix](https://github.com/elkaix) for the report.
|
|
31
|
+
- Namespace proxy tools can now be disabled with `settings.namespaceProxyTools: false`. Thanks to [@k03mad](https://github.com/k03mad) for PR #592.
|
|
32
|
+
- The bundled `mcp-scripting` skill is now discovered alongside the default-on `mcpScript` tool and hidden with it when `settings.scriptMode` is `false`. Thanks to [@zhangyoufu](https://github.com/zhangyoufu) for PR #583.
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
|
|
36
|
+
- HTTP streams authenticated with `requestHeadersCommand` remain cancellable after garbage collection. Thanks to [@zaini](https://github.com/zaini) for PR #619.
|
|
37
|
+
- Bearer-token and TypeSafe key storage now retry revoked Linux session-keyring operations through the packaged `keyctl` helper, without special launch commands or plaintext fallback. Set `PI_MCP_ADAPTER_DISABLE_KEYRING_RECOVERY=1` to disable recovery. Thanks to [@magoz](https://github.com/magoz) for PR #621.
|
|
38
|
+
- OAuth-enabled MCP servers now reconnect reliably after explicit OAuth, stored-token, and 401 authentication paths. Thanks to [@jaresty](https://github.com/jaresty) for PR #624.
|
|
39
|
+
- Direct tools now recover stringified array and object arguments declared through type arrays and schema unions without coercing values that are valid strings. Thanks to [@sashkachan](https://github.com/sashkachan) for issue [#606](https://github.com/nicobailon/pi-mcp-adapter/issues/606).
|
|
40
|
+
- Default tool search now supports CJK text, including unseparated mixed-script queries and configured search keywords, while retaining bounded lexical matching. Thanks to [@wjunhere](https://github.com/wjunhere) for issue [#607](https://github.com/nicobailon/pi-mcp-adapter/issues/607).
|
|
41
|
+
- Command-backed bearer tokens now refresh through a TTL cache, and keep-alive bearer connections reconnect after a 401. Thanks to [@kesor](https://github.com/kesor) for PR #608.
|
|
42
|
+
- Tool results now preserve `structuredContent` alongside ordinary content in direct and proxy calls. Thanks to [@civcode](https://github.com/civcode) for issue #588.
|
|
43
|
+
- Restored the MCP footer status during cache-backed deferred startup without eagerly loading or connecting the runtime. Thanks to [@pkulyn](https://github.com/pkulyn) for issue #586.
|
|
44
|
+
- Oversized object `structuredContent` summaries now identify themselves as omitted and account for preserved and dropped fields, so extension consumers do not mistake a partial preview for an empty payload. Thanks to [@Batchputz](https://github.com/Batchputz) for issue #585.
|
|
45
|
+
- Server-scoped tool describe and call requests now fail closed when a name exactly identifies different displayed and upstream tools. Thanks to [@sheurich](https://github.com/sheurich) for PR #587.
|
|
46
|
+
- Config writes now preserve resolvable existing symlinks by atomically replacing their targets. Thanks to [@peedrr](https://github.com/peedrr) for #597.
|
|
47
|
+
- Server-returned MCP tool errors no longer include misleading input-schema guidance, while invalid proxy arguments are rejected before dispatch. Thanks to [@jaresty](https://github.com/jaresty) for PR #596.
|
|
48
|
+
|
|
49
|
+
## [2.34.0] - 2026-09-14
|
|
50
|
+
|
|
51
|
+
### Highlights
|
|
52
|
+
|
|
53
|
+
- Start Pi faster while keeping cached MCP tools, prompts, and commands immediately available.
|
|
54
|
+
- Connect to more OAuth servers with Client ID Metadata Documents.
|
|
55
|
+
- Store OAuth credentials securely in encrypted files when Windows OpenSSH or a headless session cannot use the OS credential store.
|
|
56
|
+
- Load shared configuration and Agent Plugin MCP servers more reliably.
|
|
57
|
+
- Use MCP Apps, private HTTPS servers, and frequently changing tool catalogs with fewer connection problems.
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
|
|
61
|
+
- Windows OpenSSH/network logons can explicitly select externally keyed AES-256-GCM OAuth credential files with `settings.oauthCredentialStore: "encrypted-file"`; error 1312 now points to this option, while the OS store remains the default with no automatic fallback. Thanks to [@pierreh](https://github.com/pierreh) for issue #574.
|
|
62
|
+
- OAuth servers can explicitly opt into operator-hosted Client ID Metadata Documents (SEP-991) with `oauth.clientMetadataUrl`; URL-only/default configurations continue using Dynamic Client Registration. Existing DCR refresh credentials get their normal refresh attempt before migration to CIMD after invalidation. Thanks to [@dsluo](https://github.com/dsluo) for PR #571.
|
|
63
|
+
- User-global or explicitly selected config can opt in to bounded ancestor `.mcp.json` and `<configDir>/mcp.json` discovery with `settings.ancestorConfigRoots`. Discovery is off by default; project files cannot enable or widen it, and the deepest matching existing directory under `$HOME` bounds farthest-first loading. Thanks to [@johnhenaot](https://github.com/johnhenaot) for PR #555.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- Runtime-heavy MCP modules now load only when first needed, while cached tools, prompts, and commands remain immediately available. Thanks to [@thefakepaulgg](https://github.com/thefakepaulgg) for PR #576.
|
|
68
|
+
- OAuth dependencies now use published MCP SDK releases again, restoring normal npm installs and removing the standalone native-addon dependency. Cross-process OAuth transaction serialization remains unavailable until the official SDK exposes the required support.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- Built-in Agent Plugin MCP definitions now preserve literal values through connection and cache handling, validate manifest field types, resolve contained paths through symlinks, and expand plugin placeholders once. Thanks to [@cheetahbyte](https://github.com/cheetahbyte) for #570.
|
|
73
|
+
- Empty, whitespace-only, or comments-only optional MCP config layers are now treated as absent, allowing other precedence layers to load without a warning. Thanks to [@RobertoNegro](https://github.com/RobertoNegro) for PR #567.
|
|
74
|
+
- macOS Keychain and Linux Secret Service now keep ordinary OAuth records in one credential item and compact existing chunks on ordinary reads; Windows Credential Manager retains chunking. Thanks to [@jploskonka](https://github.com/jploskonka) for PR #560.
|
|
75
|
+
- Configured direct tools now hot-load from fresh live catalogs even when a server advertises `ttlMs: 0`, while persisted zero-TTL metadata remains non-cacheable. Thanks to [@dsluo](https://github.com/dsluo) for PR #562. (#561)
|
|
76
|
+
- Switching a server between transports (HTTP to stdio command or socket) now drops an inherited `bearerTokenStore` flag alongside the other URL-bound credential fields. Thanks to [@zhulinchng](https://github.com/zhulinchng) for PR #552.
|
|
77
|
+
- Per-origin `caFile` trust now routes same-origin requests through the bundled undici fetch so the custom CA dispatcher matches the fetch implementation on newer Node releases (Node 26 ships undici v8 while the dependency pins undici v6); previously every `caFile` connection failed with `UND_ERR_INVALID_ARG`. Thanks to [@zhulinchng](https://github.com/zhulinchng) for PR #550.
|
|
78
|
+
- MCP Apps now load provider-declared asset, connection, and frame domains through a session-bound sandbox resource navigation with response-level CSP enforcement. Thanks to [@tekumara](https://github.com/tekumara) for #548.
|
|
79
|
+
|
|
10
80
|
## [2.33.0] - 2026-09-10
|
|
11
81
|
|
|
12
82
|
### Highlights
|
package/README.md
CHANGED
|
@@ -71,6 +71,10 @@ Precedence is (later entries win):
|
|
|
71
71
|
5. `.mcp.json`
|
|
72
72
|
6. `.pi/mcp.json`
|
|
73
73
|
|
|
74
|
+
Ancestor discovery is off by default. To opt in, set `settings.ancestorConfigRoots` in a user-global config above, or in the explicitly selected `--mcp-config`/`configPath` file, for example `"ancestorConfigRoots": ["~/work/team"]`. Each root must be an explicit absolute path or `~/...`, resolve to an existing directory under `$HOME`, and contain the canonical cwd. If several roots match, only the nearest (deepest) is used. Project `.mcp.json` and `.pi/mcp.json` files cannot enable discovery or extend the boundary.
|
|
75
|
+
|
|
76
|
+
Within the selected root, existing `.mcp.json` and `<configDir>/mcp.json` (normally `.pi/mcp.json`) files load between steps 4 and 5, from the root through parent(cwd), farthest first. Nearer directories override farther ones, Pi overrides shared config within each directory, and cwd files win over ancestors. Search never goes above the configured root or `$HOME`; the boundary limits discovery but is not a file-ownership or symlink-target sandbox. Only configure roots whose project files you trust. `/mcp setup` write targets and project-local `/mcp disable` and `/mcp enable` overrides are unchanged.
|
|
77
|
+
|
|
74
78
|
`/mcp disable <server>` and `/mcp enable <server>` persist only the `disabled` field in the project-local `.pi/mcp.json`, which is the highest-precedence Pi layer. Enabling removes the project flag when lower layers are enabled, or writes `false` when needed to override a disabled lower source. This applies even when the effective server came from a shared global/project file, an imported host config, or `configPath`; the source file is never rewritten and credentials are never copied. Run `/reload` after changing the flag so registered tool surfaces are refreshed. The manual equivalent is to add `{ "disabled": true }` to a server in any normal MCP config. Supplied in-memory `createMcpAdapter({ config })` configurations are isolated and do not read or write this project override; the commands are unavailable in that mode.
|
|
75
79
|
|
|
76
80
|
Servers are **lazy by default** — they won't connect until you actually call one of their tools. The adapter caches tool metadata so search and describe work without live connections.
|
|
@@ -300,8 +304,9 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
300
304
|
| `caFile` | HTTPS HTTP servers only: local PEM CA certificate/bundle, e.g. `"caFile": "~/certs/local-ca.pem"`. Replaces (does not add to) default roots for the resolved MCP origin. Supports environment interpolation and `~`; relative paths use the process working directory. Unreadable/invalid files fail closed; hostname and certificate-expiry verification remain enabled. |
|
|
301
305
|
| `auth` | `"bearer"` or `"oauth"` |
|
|
302
306
|
| `oauth.grantType` | `"authorization_code"` (default) or `"client_credentials"` for non-interactive machine auth |
|
|
303
|
-
| `oauth.clientId` | Pre-registered OAuth client ID.
|
|
304
|
-
| `oauth.clientSecret` | OAuth client secret for confidential clients; a value beginning with `!` runs a command when OAuth authenticates, while `!!` escapes a literal leading
|
|
307
|
+
| `oauth.clientId` | Pre-registered OAuth client ID. Takes precedence over `oauth.clientMetadataUrl` when both are set. |
|
|
308
|
+
| `oauth.clientSecret` | OAuth client secret for confidential clients; a value beginning with `!` runs a command when OAuth authenticates, while `!!` escapes a literal leading `!`. Combining it with `oauth.clientMetadataUrl` requires an explicit `oauth.clientId`. |
|
|
309
|
+
| `oauth.clientMetadataUrl` | Advanced opt-in for an operator-supplied public HTTPS Client ID Metadata Document (CIMD) URL with a non-root path. Used as the `client_id` when the authorization server advertises CIMD support; otherwise the adapter falls back to Dynamic Client Registration. The adapter does not provide or host a default document. |
|
|
305
310
|
| `oauth.scope` | Requested OAuth scopes |
|
|
306
311
|
| `oauth.redirectUri` | Redirect URI for browser OAuth. Dynamic clients normally omit it and use an OS-assigned localhost callback port. Local `http://` loopback URIs accept an explicit port or `{port}` for an OS-assigned port (for example, `http://127.0.0.1:{port}/callback`). Pre-registered `https://` callbacks use manual completion by pasting the full callback URL. |
|
|
307
312
|
| `oauth.clientName` | Client display name advertised during Dynamic Client Registration fallback |
|
|
@@ -315,6 +320,7 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
315
320
|
| `idleTimeout` | Minutes before idle disconnect (overrides global) |
|
|
316
321
|
| `requestTimeoutMs` | Request timeout in milliseconds for live MCP calls (overrides global; if omitted or `<= 0`, the MCP SDK default timeout is used) |
|
|
317
322
|
| `protocolVersion` | `"legacy"` (default), `"auto"`, or `"2026-07-28"`; modern negotiation is opt-in |
|
|
323
|
+
| `tasks` | MCP Tasks extension support on 2026-07-28 connections (default: true; set `false` to opt out); see [Task-augmented tool calls](#task-augmented-tool-calls) |
|
|
318
324
|
| `exposeResources` | Expose MCP resources as tools (default: true) |
|
|
319
325
|
| `directTools` | `true`, `string[]`, or `false` — register tools individually instead of through proxy |
|
|
320
326
|
| `toolPrefix` | Override global `settings.toolPrefix` for this server (`"server"`, `"short"`, `"none"`, or `"mcp"`) |
|
|
@@ -343,12 +349,29 @@ Use `"auto"` to probe for MCP 2026-07-28 and conservatively fall back to the cla
|
|
|
343
349
|
|
|
344
350
|
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.
|
|
345
351
|
|
|
352
|
+
#### Task-augmented tool calls
|
|
353
|
+
|
|
354
|
+
The adapter supports the [MCP Tasks extension](https://modelcontextprotocol.io/extensions/tasks/overview) (`io.modelcontextprotocol/tasks`, SEP-2663), which lets long-running tools return a durable task handle instead of blocking the connection. Support is negotiated per connection and needs no configuration: the task session only activates when a 2026-07-28 connection's server advertises the extension, so nothing changes for servers without task support. Set `tasks: false` on a server to opt out and keep the plain synchronous call path. Legacy (2025-11-25) experimental tasks are not supported.
|
|
355
|
+
|
|
356
|
+
When active, tool calls keep their normal contract from the model's point of view:
|
|
357
|
+
|
|
358
|
+
- A tool that returns a task handle is transparently polled to completion, honoring the server's suggested poll interval; the final result is returned as if the call had been synchronous.
|
|
359
|
+
- If the task pauses for input (`input_required`), elicitation requests are routed through the same interactive elicitation UI as direct `elicitation/create` requests, and answers are delivered back via `tasks/update`.
|
|
360
|
+
- Cancelling the Pi tool call sends a cooperative `tasks/cancel` to the server.
|
|
361
|
+
- A task that fails with a JSON-RPC error surfaces as the same error a synchronous call would have produced; a tool result with `isError: true` is returned as a normal tool error.
|
|
362
|
+
|
|
363
|
+
Task traffic is dispatched on a dedicated raw channel below the SDK client (the published MCP SDK does not yet decode task result shapes itself), built on the official `@modelcontextprotocol/ext-tasks` requester package. The channel chains onto the connected transport's handlers without replacing the transport, and raw task frames appear in `/mcp-trace` in both directions. Task status notifications (`notifications/tasks`) are not consumed; polling is used exclusively. `requestTimeoutMs` applies per task request (the initiating call and each poll), not to the overall task duration — a task that runs for hours holds the Pi tool call for as long as the model waits for it.
|
|
364
|
+
|
|
365
|
+
One trade-off while tasks are active: every `tools/call` on that connection is dispatched through the task-aware path instead of `Client.callTool`, so the SDK's client-side output-schema validation of `structuredContent` and SEP-2243 `Mcp-Param-*` header mirroring do not run for those calls. Servers still validate their own results; only the client-side double-check is skipped.
|
|
366
|
+
|
|
346
367
|
The stable SDK handles era-specific request envelopes, result decoding, list-changed subscriptions, cancellation, and multi-round-trip sampling/elicitation. The SDK's embedded-input progress callback does not expose the originating tool or resource identity, so the adapter cannot maintain a durable per-tool waiting status row; interactive sessions keep the existing input dialog visible, and proxy calls show request progress when UI is available. 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.
|
|
347
368
|
|
|
348
369
|
If an internal authorization server publishes mismatched OAuth metadata and cannot be fixed immediately, set `oauth.skipIssuerMetadataValidation: true` on that server only. This is security-weakening. It disables the RFC 8414 issuer echo check and should not be used for public or untrusted servers.
|
|
349
370
|
|
|
350
371
|
If an MCP server does not publish usable protected-resource metadata, set `oauth.authServerMetadataUrl` to its HTTPS OAuth/OIDC authorization-server metadata document. The configured document is used authoritatively, while issuer validation remains enabled by default. This is trusted configuration; use it only for a metadata endpoint you control or explicitly trust.
|
|
351
372
|
|
|
373
|
+
URL-only/default Pi OAuth continues to use Dynamic Client Registration; there is no project-hosted default Client ID Metadata Document. To explicitly opt into CIMD as an advanced operator setting, publish the OAuth client metadata at a stable public HTTPS URL and set `oauth.clientMetadataUrl` to that exact URL. The adapter uses it as the URL-based `client_id` only when discovered authorization-server metadata contains `client_id_metadata_document_supported: true`; servers without CIMD support continue through Dynamic Client Registration. An explicit `oauth.clientId` always wins, and `oauth.clientSecret` without that explicit ID cannot be combined with `oauth.clientMetadataUrl`.
|
|
374
|
+
|
|
352
375
|
#### Stdio environment boundaries
|
|
353
376
|
|
|
354
377
|
`inheritEnv: false` applies only to the actual MCP stdio server process and, for `protocolVersion: "auto"` or `"2026-07-28"`, its disposable SDK negotiation sibling. It does not change the default for other servers: omitting the field or setting it to `true` preserves the existing full host-environment inheritance. With `false`, the SDK still supplies its platform defaults and configured `env` values remain explicit overlays; the result is not a literally empty environment and is not an OS sandbox.
|
|
@@ -371,6 +394,8 @@ Secret values in `headers`, `bearerToken`, `oauth.clientSecret`, and stdio `env`
|
|
|
371
394
|
|
|
372
395
|
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+.
|
|
373
396
|
|
|
397
|
+
On Linux, bearer-token and TypeSafe key storage also recover automatically when a native operation fails with `KeyRevoked`, including wrapped errors from a revoked inherited session keyring. Each failed read/write/remove is retried once through `keyctl session - <current runtime> <packaged helper>`, with a 10-second timeout and no plaintext fallback. This requires `keyctl` on `PATH` and a working credential store in the fresh session; other storage errors still fail closed. Set `PI_MCP_ADAPTER_DISABLE_KEYRING_RECOVERY=1` to disable this recovery. Normal Pi and token CLI launches need no special wrapper.
|
|
398
|
+
|
|
374
399
|
### Shared MCP processes with rmcp-mux
|
|
375
400
|
|
|
376
401
|
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:
|
|
@@ -405,7 +430,9 @@ Public servers are ready immediately. For OAuth servers, the same action opens t
|
|
|
405
430
|
|
|
406
431
|
If Pi is running on a remote server, `/mcp-auth <server>` shows a clickable authorization URL first. Open it in your local browser and approve access, then select **Yes** in Pi to open the callback input. The browser may fail to load the localhost callback page because localhost refers to your workstation; copy the full URL from its address bar and paste it into Pi. The authorization screen closes automatically instead when the browser can reach Pi's callback directly.
|
|
407
432
|
|
|
408
|
-
The same flow is available through the proxy tool for non-interactive clients.
|
|
433
|
+
The same flow is available through the proxy tool for non-interactive clients. By default, persistent OAuth requires an available OS credential store; on headless Linux that usually means an unlocked Secret Service/libsecret keyring. The adapter fails closed instead of falling back to plaintext credentials when the secure store is unavailable.
|
|
434
|
+
|
|
435
|
+
Windows OpenSSH network logons can return `ERROR_NO_SUCH_LOGON_SESSION` (1312) because Credential Manager is unavailable to that logon. For this case, explicitly set `settings.oauthCredentialStore` to `"encrypted-file"` and inject `PI_MCP_ADAPTER_OAUTH_FILE_KEY` as canonical base64 for 32 random bytes (`node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"`). Encrypted entries live under the Pi agent directory's `mcp-oauth-encrypted/`; keep the key separately and reauthenticate after loss or rotation. This backend never falls back to the OS store or imports legacy plaintext; see [OAuth](OAUTH.md#token-storage) for its security model.
|
|
409
436
|
|
|
410
437
|
On Linux, if credential access fails because Pi inherited a revoked session keyring, the adapter uses a best-effort recovery path through `keyctl session - node <packaged helper>` so explicit re-authentication can write fresh credentials without killing a long-lived tmux server. This path requires `keyctl` and `node` on `PATH`; missing, locked, or otherwise unavailable credential stores still fail closed.
|
|
411
438
|
|
|
@@ -475,16 +502,20 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
475
502
|
| `collapsedResultLines` | Number of result text lines to show before expansion: `1`, `2`, or `3`. Defaults to `1` in compact mode and `3` in boxed mode. |
|
|
476
503
|
| `notifyOnStartupConnect` | Show successful startup connection notices (default: `true`). Set to `false` to suppress routine `MCP: N servers connected (M tools)` notices. Connection errors and authentication warnings remain visible. |
|
|
477
504
|
| `hostConfigDiscovery` | Host-specific config policy: `"off"` (default), `"prompt"` (detect/report only), or `"on"` (explicitly load detected host configs as the lowest-precedence fallback) |
|
|
505
|
+
| `ancestorConfigRoots` | Trusted absolute or `~/...` roots for opt-in ancestor config discovery. Only user-global or explicitly selected config may set it; the deepest root containing cwd is used. |
|
|
478
506
|
| `agentPluginPaths` | Agent Plugins package directories to load MCP servers from. Relative paths resolve from the active project cwd. |
|
|
479
507
|
| `approveTools` | `true` to require approval before every MCP tool call, or an array of glob patterns such as `["github_delete_*", "notion_update_*"]`. Per-server `approveTools` overrides this. |
|
|
480
508
|
| `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. |
|
|
509
|
+
| `oauthCredentialStore` | Set explicitly to `"encrypted-file"` for externally keyed AES-256-GCM storage (notably Windows OpenSSH network logons). Requires `PI_MCP_ADAPTER_OAUTH_FILE_KEY`; absent uses the OS credential store. |
|
|
481
510
|
| `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. |
|
|
482
511
|
| `directTools` | Global default for all servers (default: false). `true`, `false`, or `"search"`. Per-server overrides this. |
|
|
512
|
+
| `namespaceProxyTools` | Register per-server `mcp__<server>` wrappers (default: true). Set to `false` to omit them from the model's tool list; `mcp`, `mcpScript`, and direct tools are unaffected. References such as `mcp:<server>` that rely on a wrapper will no longer resolve. Run `/reload` after changing this setting. |
|
|
483
513
|
| `strictDirectToolArguments` | Validate direct-tool inputs against their advertised schemas and recover one JSON string layer for object and array properties (default: false). |
|
|
484
514
|
| `directToolResultDetails` | Direct-tool result details: `"lean"` (default) or `"bounded"` to retain the guarded raw MCP result. |
|
|
485
515
|
| `warnOnLargeDirectTools` | Show the advisory when 75 or more direct tools resolve (default: `true`). Set to `false` to suppress only this advisory. |
|
|
486
516
|
| `freezeDirectTools` | Keep direct-tool registration stable after the initial sync so metadata updates and explicit reconnects do not rebuild the system prompt. Proxy/search/cache metadata still refreshes. Default: false. |
|
|
487
517
|
| `scriptMode` | Register the MCP-only `mcpScript` plain-JavaScript tool (default: true). Set to `false` to hide it. |
|
|
518
|
+
| `jev` | Optional TypeSafe Jev evaluation settings. `semanticSearch` and `scriptEvaluation` both default to `false`; `allowedServers` is an explicit MCP data-egress allowlist. |
|
|
488
519
|
| `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. Ignored while any server uses `directTools: "search"`, whose tools are registered inactive and can only be activated through `mcp({ search })`. |
|
|
489
520
|
| `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
|
|
490
521
|
| `sampling` | Allow MCP servers to sample through Pi models, honoring `modelPreferences.hints` before current/default fallback (default: true when UI approval is available). |
|
|
@@ -511,7 +542,11 @@ Use `approveTools` when a tool should stay visible but not run without confirmat
|
|
|
511
542
|
}
|
|
512
543
|
```
|
|
513
544
|
|
|
514
|
-
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**. **Allow for session** tool grants and MCP UI iframe consent decisions (including denials) persist as non-LLM custom entries on the active Pi session branch and restore on resume or branch navigation. Entries store only server/tool names and deterministic definition/argument hashes; raw arguments, results, and secrets never persist. Tool grants and iframe consent remain separate gates. In headless sessions, matching calls fail closed with an `approval_required` result; denials, abstentions, **Allow once**, and approval-required paths do not create tool grant records. `excludeTools` still removes tools entirely; `approveTools` only gates visible tools at call time.
|
|
545
|
+
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**, **Allow server for this session**, or **Deny**. **Allow for session** tool grants and MCP UI iframe consent decisions (including denials) persist as non-LLM custom entries on the active Pi session branch and restore on resume or branch navigation. Entries store only server/tool names and deterministic definition/argument hashes; raw arguments, results, and secrets never persist. Tool grants and iframe consent remain separate gates. In headless sessions, matching calls fail closed with an `approval_required` result; denials, abstentions, **Allow once**, and approval-required paths do not create tool grant records. `excludeTools` still removes tools entirely; `approveTools` only gates visible tools at call time.
|
|
546
|
+
|
|
547
|
+
**Allow server for this session** permits all tools and argument combinations on the selected server, including tools discovered later. It does not approve other servers. This broad grant stays in memory only: reload, session replacement, resume, and branch navigation clear it. A changed or replaced server configuration also invalidates it. It is never saved to session entries or configuration. Broker denials, tool exclusions, host security guards, and the separate MCP UI iframe consent gate still apply. Use **Allow for session** instead to approve only the displayed tool definition and arguments.
|
|
548
|
+
|
|
549
|
+
`pi-mcp-adapter/status/v1` is the documented, versioned public channel for cross-extension status. By contrast, `mcp-approval-v1` entries are adapter-owned persistence state, not a supported cross-extension contract; consumers should use documented package exports and event APIs instead.
|
|
515
550
|
|
|
516
551
|
Permission extensions can broker these decisions by listening on `pi-mcp-adapter:tool-approval-request` and claiming the request synchronously:
|
|
517
552
|
|
|
@@ -539,6 +574,8 @@ Oversized MCP tool/resource results are guarded by default so a single huge resp
|
|
|
539
574
|
- 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.
|
|
540
575
|
- 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.
|
|
541
576
|
|
|
577
|
+
Extensions consuming `details.mcpResult` must check for `omitted === true` on both the result and its `structuredContent` before treating either value as an original payload. For omitted object `structuredContent`, `preservedFields` is only a partial preview; `summary.keyCount` is the original cardinality, while `preservedCount` and `droppedCount` account for retention. Under tiny limits, the whole result may compact to an omission marker without spill metadata.
|
|
578
|
+
|
|
542
579
|
Tune the text and details limits with the object form:
|
|
543
580
|
|
|
544
581
|
```json
|
|
@@ -553,19 +590,43 @@ Set `"outputGuard": false` — or the env kill switch `MCP_OUTPUT_GUARD=0` — t
|
|
|
553
590
|
|
|
554
591
|
### MCP Scripting
|
|
555
592
|
|
|
556
|
-
|
|
593
|
+
#### Opt-in Jev evaluation and semantic search
|
|
594
|
+
|
|
595
|
+
Jev is disabled by default: configuring a key alone performs no credential lookup or network I/O. Enabled evaluations use pinned model `jev-1.13.0` at the fixed origin `https://api.typesafe.ai`. Review TypeSafe's current [legal terms](https://docs.typesafe.ai/legal), including privacy and retention, before opt-in; a no-training commitment does not mean zero retention.
|
|
557
596
|
|
|
558
|
-
|
|
597
|
+
On desktops, store the API key in the OS keyring (recommended):
|
|
598
|
+
|
|
599
|
+
```sh
|
|
600
|
+
pi-mcp-adapter key set typesafe
|
|
601
|
+
pi-mcp-adapter key status typesafe
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
`TYPESAFE_API_KEY` is for CI/headless use and overrides the keyring. Stdio MCP subprocesses inherit the host environment by default, so set `inheritEnv: false` where they must not receive it. The script worker receives no key, SDK, endpoint, headers, or environment.
|
|
559
605
|
|
|
560
606
|
```json
|
|
561
607
|
{
|
|
562
|
-
"
|
|
563
|
-
|
|
564
|
-
|
|
608
|
+
"settings": {
|
|
609
|
+
"jev": {
|
|
610
|
+
"semanticSearch": true,
|
|
611
|
+
"scriptEvaluation": true,
|
|
612
|
+
"allowedServers": ["github"],
|
|
613
|
+
"maxEvaluationTokensPerScript": 32768
|
|
614
|
+
}
|
|
615
|
+
}
|
|
565
616
|
}
|
|
566
617
|
```
|
|
567
618
|
|
|
568
|
-
|
|
619
|
+
Request semantic discovery explicitly with `mcp({ search: "triage customer reports", searchMode: "semantic" })` or `tools.search({ query: "triage customer reports", searchMode: "semantic" })`. Regex is incompatible. Timeout, rate-limit, and service failures return marked lexical fallback; credential, policy, configuration, and response failures do not.
|
|
620
|
+
|
|
621
|
+
Optional `jev` controls bound timeout/retries, request and script budgets, semantic candidates (at most 127), and minimum probability. The cumulative token budget uses provider-reported input plus output usage. Exact pre-response admission is unavailable without the provider tokenizer, so byte/question/state limits bound requests before dispatch; a response that exceeds the remaining token budget is discarded and exhausts it. The endpoint, headers, and SDK logging are not configurable.
|
|
622
|
+
|
|
623
|
+
`await jev.evaluate({ state, questions, sources })` returns `{ ok, data }` or `{ ok: false, error }`. `sources` must name every MCP server represented in `state`. The host also conservatively taints the whole script with every server-attributed MCP call result or error: declared and observed sources must all be enabled and in `allowedServers`, so copying data or omitting/mislabeling `sources` cannot bypass policy. The taint remains for later direct evaluations and semantic searches even when the script did not retain the call result. Direct and semantic provider attempts share the per-script count, UTF-8 request-byte, token, and deadline budgets; later `tools.call` operations still require normal authentication and approval. See `examples/jev-semantic-filter.mjs` and `examples/jev-accessibility-loop.mjs`.
|
|
624
|
+
|
|
625
|
+
Semantic search sends your request and the available tool descriptions to Jev, which works out which tools best match what you’re trying to do. In a live test with 12 everyday requests and 95 tools and resources, Jev chose the expected result first in 10 of 11 answerable cases and placed it second once. Regular text search found the expected result first in 5 cases. Jev also correctly returned no result for an unrelated request. This was a small test using one local setup, so results will vary with different tools and queries.
|
|
626
|
+
|
|
627
|
+
For multi-call MCP work, write ordinary JavaScript: discover, inspect, call, loop, filter, chain, or fan out, then return one result. Run that code with the default-on `mcpScript` tool. For a single MCP call, search, describe, status check, or auth action, use `mcp` instead. Set `settings.scriptMode` to `false` to hide both the scripting tool and its bundled skill.
|
|
628
|
+
|
|
629
|
+
The bundled `mcp-scripting` skill is manual-only by default, so its description is not added to the model's automatic skill context. Use `/skill:mcp-scripting` when you want its detailed workflow.
|
|
569
630
|
|
|
570
631
|
For example, this is the JavaScript passed as the `code` argument to `mcpScript`:
|
|
571
632
|
|
|
@@ -868,6 +929,7 @@ Servers that provide usage guidance via the MCP `instructions` field surface it
|
|
|
868
929
|
| `/mcp` | Interactive panel and first-run onboarding surface |
|
|
869
930
|
| `/pi-mcp` | Alias for `/mcp` when the host reserves `/mcp` |
|
|
870
931
|
| `/mcp setup` | Guided setup for imports, a minimal `.mcp.json`, curated known servers, RepoPrompt quick-add, and config-path inspection |
|
|
932
|
+
| `/mcp edit [project\|global]` | Open `.mcp.json` (default) or `~/.config/mcp/mcp.json` in an editor; Ctrl+G opens `$EDITOR`; saves a valid JSONC object and reloads |
|
|
871
933
|
| `/mcp tools` | List all tools |
|
|
872
934
|
| `/mcp prompts` | List all MCP prompts registered as slash commands |
|
|
873
935
|
| `/mcp reconnect` | Reconnect all servers |
|
package/agent-plugin-loader.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
3
|
import { getAgentPath } from "./agent-dir.ts";
|
|
4
|
+
import { markBuiltInAgentPlugin } from "./agent-plugin-provenance.ts";
|
|
4
5
|
import type { McpConfig, ServerEntry } from "./types.ts";
|
|
6
|
+
import { resolveRealContainedPath } from "./utils.ts";
|
|
5
7
|
|
|
6
8
|
const PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
|
|
7
9
|
const MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json";
|
|
@@ -21,6 +23,8 @@ const PLUGIN_MANIFEST_FIELDS = new Set([
|
|
|
21
23
|
const MCP_CONFIG_FIELDS = new Set(["$schema", "mcpServers"]);
|
|
22
24
|
const STDIO_FIELDS = new Set(["type", "command", "args", "env", "cwd"]);
|
|
23
25
|
const HTTP_FIELDS = new Set(["type", "url", "headers"]);
|
|
26
|
+
const STRING_MANIFEST_FIELDS = ["version", "description", "homepage", "repository", "license"] as const;
|
|
27
|
+
const AUTHOR_FIELDS = new Set(["name", "email", "url"]);
|
|
24
28
|
|
|
25
29
|
interface AgentPluginManifest {
|
|
26
30
|
name: string;
|
|
@@ -50,7 +54,7 @@ export function loadAgentPluginConfigs(paths: unknown, cwd = process.cwd()): Mcp
|
|
|
50
54
|
|
|
51
55
|
export function getAgentPluginSummaries(paths: unknown, cwd = process.cwd()): AgentPluginSummary[] {
|
|
52
56
|
return getPluginPaths(paths).map(path => {
|
|
53
|
-
const pluginRoot =
|
|
57
|
+
const pluginRoot = resolvePluginRoot(path, cwd);
|
|
54
58
|
const loaded = loadAgentPluginMcpConfig(path, cwd);
|
|
55
59
|
const manifest = loaded ? readPluginManifest(pluginRoot, false) : null;
|
|
56
60
|
return {
|
|
@@ -66,7 +70,7 @@ function getPluginPaths(paths: unknown): string[] {
|
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
function loadAgentPluginMcpConfig(path: string, cwd: string): McpConfig | null {
|
|
69
|
-
const pluginRoot =
|
|
73
|
+
const pluginRoot = resolvePluginRoot(path, cwd);
|
|
70
74
|
const manifest = readPluginManifest(pluginRoot, true);
|
|
71
75
|
if (!manifest) return null;
|
|
72
76
|
|
|
@@ -76,10 +80,15 @@ function loadAgentPluginMcpConfig(path: string, cwd: string): McpConfig | null {
|
|
|
76
80
|
console.warn(`Agent Plugin ${manifest.name} has invalid MCP config: mcp.json is not a regular file`);
|
|
77
81
|
return { mcpServers: {} };
|
|
78
82
|
}
|
|
83
|
+
const resolvedMcpPath = resolveRealContainedPath(pluginRoot, mcpPath);
|
|
84
|
+
if (!resolvedMcpPath) {
|
|
85
|
+
console.warn(`Agent Plugin ${manifest.name} has invalid MCP config: mcp.json must stay inside the plugin directory`);
|
|
86
|
+
return { mcpServers: {} };
|
|
87
|
+
}
|
|
79
88
|
|
|
80
89
|
let raw: unknown;
|
|
81
90
|
try {
|
|
82
|
-
raw = JSON.parse(readFileSync(
|
|
91
|
+
raw = JSON.parse(readFileSync(resolvedMcpPath, "utf8"));
|
|
83
92
|
} catch (error) {
|
|
84
93
|
console.warn(`Agent Plugin ${manifest.name} has invalid MCP config: failed to parse mcp.json`, error);
|
|
85
94
|
return { mcpServers: {} };
|
|
@@ -98,10 +107,15 @@ function readPluginManifest(pluginRoot: string, report: boolean): AgentPluginMan
|
|
|
98
107
|
if (report) console.warn(`Agent Plugin at ${pluginRoot} is invalid: plugin.json is not a regular file`);
|
|
99
108
|
return null;
|
|
100
109
|
}
|
|
110
|
+
const resolvedManifestPath = resolveRealContainedPath(pluginRoot, manifestPath);
|
|
111
|
+
if (!resolvedManifestPath) {
|
|
112
|
+
if (report) console.warn(`Agent Plugin at ${pluginRoot} is invalid: plugin.json must stay inside the plugin directory`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
101
115
|
|
|
102
116
|
let raw: unknown;
|
|
103
117
|
try {
|
|
104
|
-
raw = JSON.parse(readFileSync(
|
|
118
|
+
raw = JSON.parse(readFileSync(resolvedManifestPath, "utf8"));
|
|
105
119
|
} catch (error) {
|
|
106
120
|
if (report) console.warn(`Agent Plugin at ${pluginRoot} is invalid: failed to parse plugin.json`, error);
|
|
107
121
|
return null;
|
|
@@ -125,6 +139,20 @@ function readPluginManifest(pluginRoot: string, report: boolean): AgentPluginMan
|
|
|
125
139
|
if (report) console.warn(`Agent Plugin at ${pluginRoot} is invalid: plugin.json name is invalid`);
|
|
126
140
|
return null;
|
|
127
141
|
}
|
|
142
|
+
for (const field of STRING_MANIFEST_FIELDS) {
|
|
143
|
+
if (manifest[field] !== undefined && typeof manifest[field] !== "string") {
|
|
144
|
+
if (report) console.warn(`Agent Plugin ${manifest.name} is invalid: plugin.json ${field} must be a string`);
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (manifest.keywords !== undefined && (!Array.isArray(manifest.keywords) || manifest.keywords.some(value => typeof value !== "string"))) {
|
|
149
|
+
if (report) console.warn(`Agent Plugin ${manifest.name} is invalid: plugin.json keywords must be an array of strings`);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
if (manifest.author !== undefined && !isValidManifestAuthor(manifest.author)) {
|
|
153
|
+
if (report) console.warn(`Agent Plugin ${manifest.name} is invalid: plugin.json author is invalid`);
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
128
156
|
if (manifest.extensions !== undefined && (!manifest.extensions || typeof manifest.extensions !== "object" || Array.isArray(manifest.extensions))) {
|
|
129
157
|
if (report) console.warn(`Agent Plugin ${manifest.name} ignores non-object plugin.json extensions`);
|
|
130
158
|
}
|
|
@@ -205,14 +233,14 @@ function translateStdioServer(
|
|
|
205
233
|
const env = translateEnv(raw.env, manifest, serverName);
|
|
206
234
|
if (env === null) return null;
|
|
207
235
|
|
|
208
|
-
const command = raw.command.startsWith("./") ?
|
|
209
|
-
if (command === null) return skipServer(manifest, serverName, "command must
|
|
236
|
+
const command = raw.command.startsWith("./") ? resolveRealContainedPath(pluginRoot, resolve(pluginRoot, raw.command)) : raw.command;
|
|
237
|
+
if (command === null) return skipServer(manifest, serverName, "command must resolve to an accessible path inside the plugin directory");
|
|
210
238
|
|
|
211
239
|
const pluginDataDir = getAgentPath("agent-plugin-data", manifest.name);
|
|
212
240
|
const cwd = resolvePluginCwd(raw.cwd, pluginRoot, pluginDataDir);
|
|
213
|
-
if (cwd === null) return skipServer(manifest, serverName, "cwd must
|
|
241
|
+
if (cwd === null) return skipServer(manifest, serverName, "cwd must resolve from an allowed root and stay contained");
|
|
214
242
|
|
|
215
|
-
return {
|
|
243
|
+
return markBuiltInAgentPlugin({
|
|
216
244
|
command,
|
|
217
245
|
args: args.map(value => expandPluginPlaceholders(value, pluginRoot, pluginDataDir)),
|
|
218
246
|
env: {
|
|
@@ -223,7 +251,7 @@ function translateStdioServer(
|
|
|
223
251
|
cwd,
|
|
224
252
|
pluginDataDir,
|
|
225
253
|
literalEnv: true,
|
|
226
|
-
};
|
|
254
|
+
}, ["args", "env", "cwd"]);
|
|
227
255
|
}
|
|
228
256
|
|
|
229
257
|
function translateHttpServer(
|
|
@@ -240,11 +268,11 @@ function translateHttpServer(
|
|
|
240
268
|
const headers = translateHeaders(raw.headers, manifest, serverName);
|
|
241
269
|
if (headers === null) return null;
|
|
242
270
|
|
|
243
|
-
return {
|
|
271
|
+
return markBuiltInAgentPlugin({
|
|
244
272
|
url: raw.url,
|
|
245
273
|
httpTransport: type,
|
|
246
274
|
...(headers ? { headers } : {}),
|
|
247
|
-
};
|
|
275
|
+
}, headers ? ["headers"] : []);
|
|
248
276
|
}
|
|
249
277
|
|
|
250
278
|
function formatAgentPluginServerName(pluginName: string, serverName: string): string {
|
|
@@ -273,7 +301,7 @@ function translateEnv(value: unknown, manifest: AgentPluginManifest, serverName:
|
|
|
273
301
|
console.warn(`Agent Plugin ${manifest.name} skips invalid MCP server ${serverName}: env must be an object of strings`);
|
|
274
302
|
return null;
|
|
275
303
|
}
|
|
276
|
-
const
|
|
304
|
+
const entries: Array<[string, string]> = [];
|
|
277
305
|
for (const [key, entry] of Object.entries(value)) {
|
|
278
306
|
if (key === "PLUGIN_ROOT" || key === "PLUGIN_DATA") {
|
|
279
307
|
console.warn(`Agent Plugin ${manifest.name} skips invalid MCP server ${serverName}: env must not define ${key}`);
|
|
@@ -283,9 +311,9 @@ function translateEnv(value: unknown, manifest: AgentPluginManifest, serverName:
|
|
|
283
311
|
console.warn(`Agent Plugin ${manifest.name} skips invalid MCP server ${serverName}: env values must be strings`);
|
|
284
312
|
return null;
|
|
285
313
|
}
|
|
286
|
-
|
|
314
|
+
entries.push([key, entry]);
|
|
287
315
|
}
|
|
288
|
-
return
|
|
316
|
+
return Object.fromEntries(entries);
|
|
289
317
|
}
|
|
290
318
|
|
|
291
319
|
function translateHeaders(value: unknown, manifest: AgentPluginManifest, serverName: string): Record<string, string> | undefined | null {
|
|
@@ -294,7 +322,7 @@ function translateHeaders(value: unknown, manifest: AgentPluginManifest, serverN
|
|
|
294
322
|
console.warn(`Agent Plugin ${manifest.name} skips invalid MCP server ${serverName}: headers must be an object of strings`);
|
|
295
323
|
return null;
|
|
296
324
|
}
|
|
297
|
-
const
|
|
325
|
+
const entries: Array<[string, string]> = [];
|
|
298
326
|
const seen = new Set<string>();
|
|
299
327
|
for (const [key, entry] of Object.entries(value)) {
|
|
300
328
|
if (typeof entry !== "string") {
|
|
@@ -307,8 +335,9 @@ function translateHeaders(value: unknown, manifest: AgentPluginManifest, serverN
|
|
|
307
335
|
return null;
|
|
308
336
|
}
|
|
309
337
|
seen.add(normalized);
|
|
310
|
-
|
|
338
|
+
entries.push([key, entry]);
|
|
311
339
|
}
|
|
340
|
+
const headers = Object.fromEntries(entries);
|
|
312
341
|
try {
|
|
313
342
|
new Headers(headers);
|
|
314
343
|
} catch {
|
|
@@ -324,6 +353,20 @@ function resolvePluginPath(path: string, cwd: string): string {
|
|
|
324
353
|
return isAbsolute(path) ? resolve(path) : resolve(cwd, path);
|
|
325
354
|
}
|
|
326
355
|
|
|
356
|
+
function resolvePluginRoot(path: string, cwd: string): string {
|
|
357
|
+
const resolved = resolvePluginPath(path, cwd);
|
|
358
|
+
try {
|
|
359
|
+
return realpathSync(resolved);
|
|
360
|
+
} catch {
|
|
361
|
+
return resolved;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function isValidManifestAuthor(value: unknown): boolean {
|
|
366
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
367
|
+
return Object.entries(value).every(([key, entry]) => AUTHOR_FIELDS.has(key) && typeof entry === "string");
|
|
368
|
+
}
|
|
369
|
+
|
|
327
370
|
function isBareCommand(command: string): boolean {
|
|
328
371
|
return !command.includes("/") && !command.includes("\\") && !command.includes("${PLUGIN_ROOT}") && !command.includes("${PLUGIN_DATA}");
|
|
329
372
|
}
|
|
@@ -331,27 +374,23 @@ function isBareCommand(command: string): boolean {
|
|
|
331
374
|
function resolvePluginCwd(value: unknown, pluginRoot: string, pluginDataDir: string): string | null {
|
|
332
375
|
if (value === undefined) return pluginRoot;
|
|
333
376
|
if (typeof value !== "string") return null;
|
|
334
|
-
|
|
335
|
-
if (value === "${PLUGIN_ROOT}" || value.startsWith("${PLUGIN_ROOT}/")) {
|
|
336
|
-
return
|
|
377
|
+
const expanded = expandPluginPlaceholders(value, pluginRoot, pluginDataDir);
|
|
378
|
+
if (value.startsWith("./") || value === "${PLUGIN_ROOT}" || value.startsWith("${PLUGIN_ROOT}/")) {
|
|
379
|
+
return resolveRealContainedPath(pluginRoot, resolve(pluginRoot, expanded));
|
|
337
380
|
}
|
|
338
381
|
if (value === "${PLUGIN_DATA}" || value.startsWith("${PLUGIN_DATA}/")) {
|
|
339
|
-
return
|
|
382
|
+
return resolvePluginDataCwd(pluginDataDir, expanded);
|
|
340
383
|
}
|
|
341
384
|
return null;
|
|
342
385
|
}
|
|
343
386
|
|
|
344
|
-
function
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (rel === "" || (!rel.startsWith("..") && !rel.startsWith(sep) && !isAbsolute(rel))) return resolved;
|
|
348
|
-
return null;
|
|
387
|
+
function resolvePluginDataCwd(pluginDataDir: string, expanded: string): string | null {
|
|
388
|
+
if (existsSync(pluginDataDir) && !resolveRealContainedPath(dirname(pluginDataDir), pluginDataDir)) return null;
|
|
389
|
+
return resolveRealContainedPath(pluginDataDir, resolve(pluginDataDir, expanded), true);
|
|
349
390
|
}
|
|
350
391
|
|
|
351
392
|
function expandPluginPlaceholders(value: string, pluginRoot: string, pluginDataDir: string): string {
|
|
352
|
-
return value
|
|
353
|
-
.replaceAll("${PLUGIN_ROOT}", pluginRoot)
|
|
354
|
-
.replaceAll("${PLUGIN_DATA}", pluginDataDir);
|
|
393
|
+
return value.replace(/\$\{PLUGIN_(ROOT|DATA)\}/g, (_, name: string) => name === "ROOT" ? pluginRoot : pluginDataDir);
|
|
355
394
|
}
|
|
356
395
|
|
|
357
396
|
function isValidAgentPluginUrl(value: string): boolean {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ServerEntry } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
const LITERAL_PLUGIN_FIELDS = ["args", "env", "cwd", "headers"] as const;
|
|
4
|
+
type LiteralPluginField = typeof LITERAL_PLUGIN_FIELDS[number];
|
|
5
|
+
const BUILT_IN_AGENT_PLUGIN = Symbol("built-in-agent-plugin");
|
|
6
|
+
type BuiltInAgentPluginEntry = ServerEntry & { [BUILT_IN_AGENT_PLUGIN]?: ReadonlySet<LiteralPluginField> };
|
|
7
|
+
|
|
8
|
+
/** @internal */
|
|
9
|
+
export function markBuiltInAgentPlugin(definition: ServerEntry, fields: LiteralPluginField[]): ServerEntry {
|
|
10
|
+
(definition as BuiltInAgentPluginEntry)[BUILT_IN_AGENT_PLUGIN] = new Set(fields);
|
|
11
|
+
return definition;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** @internal */
|
|
15
|
+
export function isBuiltInAgentPlugin(definition: ServerEntry, field: LiteralPluginField): boolean {
|
|
16
|
+
return (definition as BuiltInAgentPluginEntry)[BUILT_IN_AGENT_PLUGIN]?.has(field) === true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @internal */
|
|
20
|
+
export function cloneBuiltInAgentPluginEntry(source: ServerEntry): ServerEntry | undefined {
|
|
21
|
+
const fields = LITERAL_PLUGIN_FIELDS.filter(field => Object.hasOwn(source, field) && isBuiltInAgentPlugin(source, field));
|
|
22
|
+
if (fields.length === 0) return undefined;
|
|
23
|
+
return markBuiltInAgentPlugin(structuredClone(source), fields);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** @internal */
|
|
27
|
+
export function mergeBuiltInAgentPluginEntries(base: ServerEntry, next: ServerEntry): ServerEntry {
|
|
28
|
+
const merged = { ...base, ...next };
|
|
29
|
+
delete (merged as BuiltInAgentPluginEntry)[BUILT_IN_AGENT_PLUGIN];
|
|
30
|
+
const fields = LITERAL_PLUGIN_FIELDS.filter(field => {
|
|
31
|
+
const owner = Object.hasOwn(next, field) ? next : base;
|
|
32
|
+
return Object.hasOwn(owner, field) && isBuiltInAgentPlugin(owner, field);
|
|
33
|
+
});
|
|
34
|
+
if (fields.length > 0) markBuiltInAgentPlugin(merged, fields);
|
|
35
|
+
return merged;
|
|
36
|
+
}
|