@herbertgao/pi-extensions 2026.9.3 → 2026.9.5
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/README.md +2 -1
- package/THIRD_PARTY_NOTICES.md +25 -0
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/README.es.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +33 -2
- package/node_modules/@dietrichgebert/ponytail/README.md +49 -5
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +4 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +0 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/cursor-hooks.json +17 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +22 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +27 -2
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +61 -2
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -1
- package/node_modules/@dietrichgebert/ponytail/scripts/cursor-hooks.js +135 -0
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -4
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/reference/index.ts +33 -15
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +3 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +16 -11
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +6 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +23 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +88 -19
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +12 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +98 -0
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/economics.ts +4 -4
- package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +41 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/events.ts +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/normalize-params.ts +56 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/option-list-view.ts +1 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/tab-bar.ts +1 -2
- package/node_modules/@pi-plugins/fast-mode/README.md +31 -44
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20914 -30
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +5 -7
- package/node_modules/@tifan/pi-copy-response/README.md +2 -2
- package/node_modules/@tifan/pi-copy-response/package.json +3 -2
- package/node_modules/@tifan/pi-inline-skills/README.md +2 -2
- package/node_modules/@tifan/pi-inline-skills/package.json +3 -2
- package/node_modules/@tifan/pi-mermaid-open/README.md +1 -1
- package/node_modules/@tifan/pi-mermaid-open/package.json +3 -2
- package/node_modules/@tifan/pi-preferred-thinking/package.json +3 -2
- package/node_modules/@tifan/pi-recap/package.json +3 -2
- package/node_modules/pi-antigravity/LICENSE +21 -0
- package/node_modules/pi-antigravity/README.md +194 -0
- package/node_modules/pi-antigravity/package.json +67 -0
- package/node_modules/pi-antigravity/src/auth/index.ts +14 -0
- package/node_modules/pi-antigravity/src/auth/oauth.ts +442 -0
- package/node_modules/pi-antigravity/src/client/client.ts +561 -0
- package/node_modules/pi-antigravity/src/client/index.ts +1 -0
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/node_modules/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/node_modules/pi-antigravity/src/image/image.ts +336 -0
- package/node_modules/pi-antigravity/src/image/index.ts +1 -0
- package/node_modules/pi-antigravity/src/index.ts +280 -0
- package/node_modules/pi-antigravity/src/models/discovery.ts +154 -0
- package/node_modules/pi-antigravity/src/models/grouping.ts +424 -0
- package/node_modules/pi-antigravity/src/models/index.ts +3 -0
- package/node_modules/pi-antigravity/src/models/models.ts +500 -0
- package/node_modules/pi-antigravity/src/stream/index.ts +1 -0
- package/node_modules/pi-antigravity/src/stream/stream.ts +1460 -0
- package/node_modules/pi-antigravity/src/types/enums.ts +42 -0
- package/node_modules/pi-antigravity/src/types/index.ts +2 -0
- package/node_modules/pi-antigravity/src/types/types.ts +292 -0
- package/node_modules/pi-antigravity/src/usage/index.ts +1 -0
- package/node_modules/pi-antigravity/src/usage/usage.ts +371 -0
- package/node_modules/pi-antigravity/src/utils/http.ts +91 -0
- package/node_modules/pi-antigravity/src/utils/index.ts +3 -0
- package/node_modules/pi-antigravity/src/utils/security.ts +73 -0
- package/node_modules/pi-antigravity/src/utils/util.ts +132 -0
- package/node_modules/pi-antigravity/tsconfig.json +21 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +78 -0
- package/node_modules/pi-mcp-adapter/README.md +125 -19
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +69 -30
- package/node_modules/pi-mcp-adapter/agent-plugin-provenance.ts +36 -0
- package/node_modules/pi-mcp-adapter/claude-plugin-loader.ts +365 -0
- package/node_modules/pi-mcp-adapter/commands.ts +13 -15
- package/node_modules/pi-mcp-adapter/config.ts +198 -30
- package/node_modules/pi-mcp-adapter/consent-manager.ts +40 -9
- package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +250 -0
- package/node_modules/pi-mcp-adapter/direct-tools.ts +9 -228
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js +74 -32
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.d.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js +31 -0
- package/node_modules/pi-mcp-adapter/dist/agent-plugin-provenance.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.d.ts +9 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js +362 -0
- package/node_modules/pi-mcp-adapter/dist/claude-plugin-loader.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/config.js +189 -27
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +7 -16
- package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js +5 -17
- package/node_modules/pi-mcp-adapter/dist/package-mcp-loader.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +30 -4
- package/node_modules/pi-mcp-adapter/dist/types.js +26 -0
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/utils.d.ts +6 -0
- package/node_modules/pi-mcp-adapter/dist/utils.js +65 -6
- package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
- package/node_modules/pi-mcp-adapter/host-html-template.ts +20 -19
- package/node_modules/pi-mcp-adapter/http-ca.ts +84 -0
- package/node_modules/pi-mcp-adapter/index.ts +827 -164
- package/node_modules/pi-mcp-adapter/init.ts +48 -10
- package/node_modules/pi-mcp-adapter/lazy-loader.ts +7 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-fetch.ts +113 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +256 -77
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +316 -54
- package/node_modules/pi-mcp-adapter/mcp-code.ts +45 -18
- package/node_modules/pi-mcp-adapter/mcp-install.ts +60 -0
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +70 -6
- package/node_modules/pi-mcp-adapter/mcp-output-guard.ts +53 -42
- package/node_modules/pi-mcp-adapter/mcp-panel-theme.ts +104 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +366 -332
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -14
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +3 -1
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +383 -341
- package/node_modules/pi-mcp-adapter/mcp-status.ts +5 -0
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +7 -16
- package/node_modules/pi-mcp-adapter/namespace-tools.ts +61 -3
- package/node_modules/pi-mcp-adapter/oauth.ts +6 -2
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +5 -17
- package/node_modules/pi-mcp-adapter/package.json +13 -3
- package/node_modules/pi-mcp-adapter/prompts.ts +35 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -7
- package/node_modules/pi-mcp-adapter/runtime-owner.ts +16 -1
- package/node_modules/pi-mcp-adapter/sampling-handler.ts +9 -32
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +20 -93
- package/node_modules/pi-mcp-adapter/server-manager.ts +227 -40
- package/node_modules/pi-mcp-adapter/session-approvals.ts +187 -0
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +9 -1
- package/node_modules/pi-mcp-adapter/state.ts +10 -1
- package/node_modules/pi-mcp-adapter/tool-approval.ts +10 -22
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +14 -0
- package/node_modules/pi-mcp-adapter/types.ts +57 -4
- package/node_modules/pi-mcp-adapter/ui-server.ts +36 -28
- package/node_modules/pi-mcp-adapter/utils.ts +64 -6
- package/package.json +15 -15
|
@@ -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.
|
|
@@ -126,8 +130,37 @@ The adapter can load MCP servers from [Agent Plugins](https://agent-plugins.org/
|
|
|
126
130
|
|
|
127
131
|
Each directory must contain a valid Agent Plugins 1.0 `plugin.json`. If it also has a root `mcp.json`, the adapter loads its `mcpServers` entries and prefixes them as `<plugin>__<server>`. The loader uses the Agent Plugins transport declared by each server `type` and skips invalid entries without blocking other servers. For stdio plugin servers, `${PLUGIN_ROOT}` and `${PLUGIN_DATA}` are expanded only in `args`, `env`, and `cwd`; the adapter sets both variables for the child process and stores plugin data under the Pi agent directory.
|
|
128
132
|
|
|
133
|
+
`inheritEnv` is an adapter-specific Pi field, not an Agent Plugins or OpenCode schema field. Do not add it to a plugin's strict `mcp.json`; to opt a plugin stdio server out of host-environment inheritance, set `inheritEnv: false` in a normal Pi override using the translated `<plugin>__<server>` name:
|
|
134
|
+
|
|
135
|
+
```json
|
|
136
|
+
{
|
|
137
|
+
"mcpServers": {
|
|
138
|
+
"acme_tools__local": { "inheritEnv": false }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
129
143
|
Agent Plugins is a portable package format. Native Pi MCP config remains `.mcp.json`, `~/.config/mcp/mcp.json`, and Pi-owned overrides.
|
|
130
144
|
|
|
145
|
+
### Local Claude plugin bundles
|
|
146
|
+
|
|
147
|
+
The adapter can opt into MCP servers and Pi skills from explicitly configured local [Claude plugin](https://docs.anthropic.com/en/docs/claude-code/plugins) directories:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"claudePlugins": [
|
|
152
|
+
{ "path": "./plugins/acme-tools", "mcp": true, "skills": true }
|
|
153
|
+
],
|
|
154
|
+
"mcpServers": {}
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Each entry needs a non-empty `path` and must enable `mcp`, `skills`, or both. The root-level field can be set in any normal adapter config source; normal config-source precedence applies, and a higher-precedence `claudePlugins` array replaces a lower one. Relative paths in file-based config resolve from the active project cwd. For `createMcpAdapter({ config })`, relative plugin paths are normalized against `process.cwd()` when the factory is created; this explicit API-boundary snapshot keeps early registration and session startup on the same local bundle even when the host's context cwd differs. `mcp: true` reads only the plugin's root `.mcp.json`; `skills: true` discovers `skills/**/SKILL.md` inside the plugin and passes those files through Pi's normal resource discovery, including startup and `/reload`. A `.claude-plugin/plugin.json` manifest is optional, matching Claude's plugin format, but when present it must be valid JSON with a kebab-case `name` and valid standard field types. Manifest path overrides are intentionally not followed.
|
|
159
|
+
|
|
160
|
+
Claude plugin MCP server names are used as written. The first explicitly listed plugin wins same-name conflicts between plugin bundles, while every normal Pi MCP config source overrides these plugin defaults. `${CLAUDE_PLUGIN_ROOT}` is expanded in plugin MCP server fields, and stdio servers receive it in their environment. Skills use Pi's existing skill parsing and conflict handling.
|
|
161
|
+
|
|
162
|
+
This is an explicit local trust boundary: the adapter does not discover, download, install, or update plugins; execute plugin hooks; or fetch skills from MCP instructions. It resolves plugin components inside each configured directory and rejects component symlinks that escape it. Config and skills are read during discovery, but MCP commands are still lazy and run only when normal adapter lifecycle/tool use connects that server. Enable `mcp` only for plugin directories whose commands and configuration you trust.
|
|
163
|
+
|
|
131
164
|
### Pi package manifests
|
|
132
165
|
|
|
133
166
|
A Pi package can ship MCP servers for the installed adapter without requiring a separate MCP config file. Declare a package-relative config in its `package.json`:
|
|
@@ -208,7 +241,7 @@ const extension = createMcpAdapter({
|
|
|
208
241
|
|
|
209
242
|
The package ships TypeScript source for Pi's source-loader and SDK integrations. Use a TypeScript-capable loader/toolchain (for example `node --import tsx`) when importing the package from a standalone Node process; raw Node ESM does not execute the `.ts` entry directly.
|
|
210
243
|
|
|
211
|
-
A supplied `config` is a complete, isolated snapshot. It is not merged with files, imports, global config, project config, or `--mcp-config`, and it is never mutated. Each adapter factory and session receives its own clone, so separate integrations can use different servers and settings safely. In this mode, server status, reconnect, explicit `/mcp-auth <server>`, proxy calls, and direct tools continue to work; setup and no-argument auth/status panels report the limitation instead of discovering or writing ambient config.
|
|
244
|
+
A supplied `config` is a complete, isolated snapshot. It is not merged with files, imports, global config, project config, or `--mcp-config`, and it is never mutated. Explicit `claudePlugins` entries are the sole exception to file isolation: their configured local directories are read because they are part of that supplied snapshot. Relative programmatic plugin paths are normalized against `process.cwd()` when `createMcpAdapter` creates the factory, so the early model-facing surface, load-time initialization, and later session runtime all use the same bundle. Each adapter factory and session receives its own clone, so separate integrations can use different servers and settings safely. In this mode, server status, reconnect, explicit `/mcp-auth <server>`, proxy calls, and direct tools continue to work; setup and no-argument auth/status panels report the limitation instead of discovering or writing ambient config.
|
|
212
245
|
|
|
213
246
|
With `configPath` and no `config`, the adapter keeps normal file merge behavior, and that path takes precedence over argv and `--mcp-config`. The default export keeps the normal file-based behavior. OAuth credentials are stored in the operating system credential store and keyed by the configured server name; URL binding prevents credentials from being accepted for a different server URL. `settings.oauthDir` and `MCP_OAUTH_DIR` are used only as legacy plaintext import locations for older `tokens.json` files, not as credential namespaces. CSRF state and PKCE verifiers are flow-local, so concurrent authorization flows do not share transient secrets.
|
|
214
247
|
|
|
@@ -218,10 +251,10 @@ Cooperating Pi extensions can use `pi-mcp-adapter/oauth` to reuse URL-bound OAut
|
|
|
218
251
|
import { getMcpOAuthTokensForUrl, updateMcpOAuthTokensForUrl } from "pi-mcp-adapter/oauth";
|
|
219
252
|
|
|
220
253
|
const tokens = await getMcpOAuthTokensForUrl("jira", "https://jira.example.com/mcp");
|
|
221
|
-
updateMcpOAuthTokensForUrl("jira", "https://jira.example.com/mcp", { accessToken: "..." });
|
|
254
|
+
await updateMcpOAuthTokensForUrl("jira", "https://jira.example.com/mcp", { accessToken: "..." });
|
|
222
255
|
```
|
|
223
256
|
|
|
224
|
-
The public subpath exposes only token read/update helpers plus a status helper. The async read path uses the adapter's refresh logic before it returns tokens. The helpers keep secure-store storage, URL binding, refresh persistence, chunk handling, legacy import, and fail-closed credential-store errors. They do not expose client registration secrets, PKCE verifiers, or OAuth state.
|
|
257
|
+
The public subpath exposes only token read/update helpers plus a status helper. The async read path uses the adapter's refresh logic before it returns tokens. For a service-protected endpoint or a pre-registered OAuth client, pass the explicit refresh configuration as `getMcpOAuthTokensForUrl(name, url, { definition: { headers, oauth } })`. This optional configuration is never loaded from ambient config or stored with the tokens; headers are bound to the supplied MCP URL's origin. The helpers keep secure-store storage, URL binding, refresh persistence, chunk handling, legacy import, and fail-closed credential-store errors. They do not expose client registration secrets, PKCE verifiers, or OAuth state.
|
|
225
258
|
|
|
226
259
|
### Runtime status snapshots
|
|
227
260
|
|
|
@@ -237,7 +270,7 @@ pi.events.on(MCP_STATUS_EVENT, (snapshot) => {
|
|
|
237
270
|
});
|
|
238
271
|
```
|
|
239
272
|
|
|
240
|
-
The snapshot is read-only machine-readable data with copied per-server entries. It includes `totalTools`, `totalResources`, `connectedCount`, and `disabledCount`; each server includes `name`, `status`, `toolCount`, and `disabled`, with `resourceCount` when known and `failedAgoSeconds` only for an active failure. Reading status never connects a lazy server, starts authentication, or exposes SDK clients, transports, credentials, or server definitions. An initial snapshot is emitted after initialization, updates are emitted for status and metadata changes, and an empty snapshot is emitted when the session shuts down. Initialization withholds that first snapshot until authoritative metadata has been reconciled into Pi's active direct-tool registry. A `connected` snapshot therefore follows model-facing tool-surface synchronization, including removal of stale cached tools when the authoritative catalog is empty.
|
|
273
|
+
The snapshot is read-only machine-readable data with copied per-server entries. It includes `totalTools`, `totalResources`, `connectedCount`, and `disabledCount`; each server includes `name`, `status`, `toolCount`, `directToolCount` (the number of tools currently registered directly with Pi, including direct resource tools), and `disabled`, with `resourceCount` when known and `failedAgoSeconds` only for an active failure. Reading status never connects a lazy server, starts authentication, or exposes SDK clients, transports, credentials, or server definitions. An initial snapshot is emitted after initialization, updates are emitted for status and metadata changes, and an empty snapshot is emitted when the session shuts down. Initialization withholds that first snapshot until authoritative metadata has been reconciled into Pi's active direct-tool registry. A `connected` snapshot therefore follows model-facing tool-surface synchronization, including removal of stale cached tools when the authoritative catalog is empty.
|
|
241
274
|
|
|
242
275
|
In the configuration examples below, `30000` is illustrative only. If `requestTimeoutMs` is omitted or set to `<= 0`, the MCP SDK default timeout is used.
|
|
243
276
|
|
|
@@ -263,16 +296,19 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
263
296
|
| `args` | Command arguments |
|
|
264
297
|
| `socket` | Explicit `rmcp-mux` Unix-domain socket path; supports `${VAR}`, `$env:VAR`, and `~` expansion and is mutually exclusive with `command` and `url` |
|
|
265
298
|
| `env` | Environment variables; supports `${VAR}` and `$env:VAR` interpolation. A value beginning with `!` runs a command when the stdio server connects; use `!!` for a literal leading `!`. |
|
|
299
|
+
| `inheritEnv` | Stdio only; defaults to `true` and preserves full host-environment inheritance. Set to `false` to exclude arbitrary host variables from the MCP child and SDK negotiation sibling while retaining SDK platform defaults and explicit `env` overlays. This is not an empty environment or an OS sandbox. |
|
|
266
300
|
| `cwd` | Working directory; supports `${VAR}`, `$env:VAR`, and `~` expansion |
|
|
267
301
|
| `url` | HTTP endpoint (StreamableHTTP with SSE fallback); supports raw `${VAR}` and `$env:VAR` interpolation, and missing URL variables fail before any request is sent |
|
|
268
302
|
| `headers` | HTTP headers; supports `${VAR}` and `$env:VAR` interpolation. A value beginning with `!` runs a command when the HTTP server connects or OAuth authenticates; use `!!` for a literal leading `!`. |
|
|
269
303
|
| `requestHeadersCommand` | Trusted executable run for every HTTP request. It receives a versioned JSON envelope containing `method`, `url`, and the exact `bodyBase64` on stdin, and must return a JSON object of headers on stdout. `command`, `args`, and `env` support environment interpolation. Use for caller-bound request signatures; failures stop the request. |
|
|
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. |
|
|
270
305
|
| `auth` | `"bearer"` or `"oauth"` |
|
|
271
306
|
| `oauth.grantType` | `"authorization_code"` (default) or `"client_credentials"` for non-interactive machine auth |
|
|
272
|
-
| `oauth.clientId` | Pre-registered OAuth client ID.
|
|
273
|
-
| `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. |
|
|
274
310
|
| `oauth.scope` | Requested OAuth scopes |
|
|
275
|
-
| `oauth.redirectUri` |
|
|
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. |
|
|
276
312
|
| `oauth.clientName` | Client display name advertised during Dynamic Client Registration fallback |
|
|
277
313
|
| `oauth.clientUri` | Client homepage URI advertised during Dynamic Client Registration fallback. Defaults to `piConfig.clientUri` from the host's manifest when set, and is omitted rather than guessed under a rebranded host |
|
|
278
314
|
| `oauth.logoUri` | Client logo URL advertised during Dynamic Client Registration fallback (RFC 7591 `logo_uri`). Must be an absolute `http(s)` URL — consent screens fetch it server-side, so local paths render nothing. Omitted from the registration request when unset |
|
|
@@ -294,6 +330,16 @@ In the configuration examples below, `30000` is illustrative only. If `requestTi
|
|
|
294
330
|
| `trace` | Enable metadata-only JSONL protocol tracing for this server; payloads, prompts, tool arguments/results, authorization data, and URLs are never persisted |
|
|
295
331
|
| `disabled` | Keep the server visible in config and status, but prevent connections, authentication, tools, and resource calls (only literal `true` disables it) |
|
|
296
332
|
|
|
333
|
+
#### Custom HTTPS trust
|
|
334
|
+
|
|
335
|
+
`caFile` works with Streamable HTTP, SSE, and per-request header commands. Requests using this trust reject all redirects; configure the final HTTPS endpoint directly. Other origins and servers retain default trust. Layered configuration drops inherited trust when replacing the URL or switching away from HTTP. This option covers the MCP origin, including connection-owned OAuth requests to that exact origin, but not the separate interactive OAuth flow or private-CA authorization servers on other origins. Thanks to [@desmonna](https://github.com/desmonna) for #527.
|
|
336
|
+
|
|
337
|
+
#### macOS local-network access
|
|
338
|
+
|
|
339
|
+
On macOS 15+, Local Network Privacy may deny access to a LAN MCP server depending on the app responsible for hosting Pi. For HTTP URLs with literal private/link-local IPv4 or IPv6 addresses, the adapter adds a hint to `EHOSTUNREACH`, `ENETUNREACH`, or `EACCES` connection errors while retaining the original cause. These codes can also mean routing or firewall trouble; the hint is not proof of a privacy denial. Hostnames are not resolved for this diagnostic.
|
|
340
|
+
|
|
341
|
+
Check **System Settings > Privacy & Security > Local Network** for the hosting app, enable access if listed, then restart that app and Pi. If it is absent or access still fails, try launching Pi directly from Apple Terminal.app or over SSH (contexts Apple documents as exempt). Permission is attributed to responsible code, not necessarily Node or Pi; signing an unsigned CLI alone does not guarantee a permission prompt or fix host attribution. See [Apple TN3179](https://developer.apple.com/documentation/technotes/tn3179-understanding-local-network-privacy).
|
|
342
|
+
|
|
297
343
|
#### Protocol version negotiation
|
|
298
344
|
|
|
299
345
|
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.
|
|
@@ -304,13 +350,31 @@ Use `"2026-07-28"` to pin that revision. Pinning has no legacy or SSE fallback a
|
|
|
304
350
|
|
|
305
351
|
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.
|
|
306
352
|
|
|
307
|
-
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. A configured `https://` callback runs in manual mode because the adapter cannot receive a callback on another host. After authorization, copy the full callback URL from the browser address bar and paste it into `/mcp-auth` or `mcp({ action: "auth-complete", ... })`.
|
|
308
|
-
|
|
309
353
|
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.
|
|
310
354
|
|
|
311
355
|
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.
|
|
312
356
|
|
|
313
|
-
|
|
357
|
+
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`.
|
|
358
|
+
|
|
359
|
+
#### Stdio environment boundaries
|
|
360
|
+
|
|
361
|
+
`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.
|
|
362
|
+
|
|
363
|
+
Environment interpolation remains intentional. `${VAR}`, `$env:VAR`, and `{env:VAR}` values still read selected host variables and can place those values in the child. `literalEnv: true` keeps its existing behavior by treating configured stdio `env` values as literals. The following helper boundaries are unchanged and still retain the full host environment even when a server uses `inheritEnv: false`:
|
|
364
|
+
|
|
365
|
+
- npm/npx cache resolution and cache-population subprocesses;
|
|
366
|
+
- `!command` secret helpers used by stdio `env` (and other secret fields); and
|
|
367
|
+
- the HTTP `requestHeadersCommand` helper.
|
|
368
|
+
|
|
369
|
+
For tighter use, configure a direct executable instead of npm/npx and avoid `!command` secret helpers. This option limits stdio child inheritance only; it does not provide complete multi-agent or helper-process isolation.
|
|
370
|
+
|
|
371
|
+
With explicit `auth: "oauth"`, configured HTTP `headers` also accompany native OAuth metadata discovery (including `oauth.authServerMetadataUrl`), dynamic registration, code exchange, and refresh, **only at the configured MCP URL's origin** (scheme, host, and port). Discovered or explicitly configured cross-origin OAuth endpoints receive no configured service headers. SDK-owned headers such as OAuth `Authorization` and content types take precedence over configured `headers`. Requests carrying configured service headers reject all HTTP redirects, including same-origin redirects; configure the final endpoint directly. Browser authorization navigation and loopback callbacks do not use these headers. Missing or empty header credentials fail closed.
|
|
372
|
+
|
|
373
|
+
`requestHeadersCommand` follows the fetch path, not the URL path: during a server connection, it wraps the SDK transport fetch (`requestFetch`), so it runs for MCP requests and SDK-owned OAuth requests using that fetch, including discovery, dynamic registration, token exchange (including `client_credentials`), and refresh. It also runs for cross-origin OAuth endpoints: unlike configured `headers`, command-produced headers are **not origin-scoped**. The command receives each request's exact method, URL, and body and must decide where its credentials belong. Its returned headers are applied last, overriding even SDK `Authorization` and content types on name collisions; avoid those names unless intentional.
|
|
374
|
+
|
|
375
|
+
Provider-owned metadata loading through `authFetch` (notably `oauth.authServerMetadataUrl`) bypasses the command, even during a connection. Standalone OAuth start/complete/refresh helpers use their own OAuth fetch, not the transport wrapper, and also bypass it. Browser authorization navigation and loopback callbacks never invoke the command. Thus this is transport-fetch signing, not a hook for every OAuth interaction.
|
|
376
|
+
|
|
377
|
+
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 the preliminary MCP OAuth challenge probe or while reading, merging, previewing, hashing, or rendering configuration. OAuth header commands resolve lazily for the actual SDK backchannel requests, once per authentication leg or connection; the preliminary probe omits command headers. Use `!!` to escape a literal leading `!`; ordinary and escaped values retain environment interpolation.
|
|
314
378
|
|
|
315
379
|
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+.
|
|
316
380
|
|
|
@@ -330,11 +394,27 @@ To share one stdio MCP server across Pi sessions, run it under [`rmcp-mux`](http
|
|
|
330
394
|
|
|
331
395
|
The adapter owns only its client socket and closes that connection when the Pi runtime stops. `rmcp-mux` owns the upstream process, request routing, initialization cache, restart policy, client limits, and socket permissions. Start and configure the mux separately; the adapter never discovers, starts, adopts, or stops its daemon. A socket is an explicit trusted local endpoint, so do not point unrelated projects or users at a mux service unless its tools, state, credentials, and filesystem access are intended to be shared.
|
|
332
396
|
|
|
397
|
+
### Install from one URL
|
|
398
|
+
|
|
399
|
+
Install an MCP endpoint without editing configuration:
|
|
400
|
+
|
|
401
|
+
```js
|
|
402
|
+
mcp({ action: "install", url: "https://example.com/mcp" })
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Install validates and connects the endpoint. New entries use a name derived from the hostname and are saved to Pi's global MCP config; existing URL entries are reused without rewriting. Pass `server` to choose a name or `target: "project"` to save to the project's `.mcp.json`. Unsafe URLs, name collisions, and failed connections are not persisted.
|
|
406
|
+
|
|
407
|
+
In exclusive config mode, a project target must be the active config path; otherwise use the global target. URL install cannot promote runtime-registered servers: save their complete definitions manually so required headers and transport/auth settings are retained.
|
|
408
|
+
|
|
409
|
+
Public servers are ready immediately. For OAuth servers, the same action opens the authorization page and watches a reachable loopback callback. After the user grants consent, an `mcp-oauth-status` message returns the agent to connect the server and verify its discovered tools. Remote/headless callbacks retain the manual completion fallback below.
|
|
410
|
+
|
|
333
411
|
### Remote/headless OAuth
|
|
334
412
|
|
|
335
413
|
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.
|
|
336
414
|
|
|
337
|
-
The same flow is available through the proxy tool for non-interactive clients.
|
|
415
|
+
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.
|
|
416
|
+
|
|
417
|
+
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.
|
|
338
418
|
|
|
339
419
|
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.
|
|
340
420
|
|
|
@@ -342,7 +422,7 @@ On Linux, if credential access fails because Pi inherited a revoked session keyr
|
|
|
342
422
|
mcp({ action: "auth-start", server: "linear-server" })
|
|
343
423
|
```
|
|
344
424
|
|
|
345
|
-
|
|
425
|
+
For a loopback redirect, the adapter attempts to open the returned authorization URL, watches the callback, completes token exchange, and sends an `mcp-oauth-status` event when authentication finishes. If Pi is remote or cannot open a browser, open the returned URL locally. If the browser cannot reach Pi's callback, copy the full localhost URL from the address bar and complete the flow in the same Pi session:
|
|
346
426
|
|
|
347
427
|
```js
|
|
348
428
|
mcp({
|
|
@@ -404,17 +484,19 @@ When any enabled server uses `eager` or `keep-alive`, initialization also starts
|
|
|
404
484
|
| `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. |
|
|
405
485
|
| `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. |
|
|
406
486
|
| `hostConfigDiscovery` | Host-specific config policy: `"off"` (default), `"prompt"` (detect/report only), or `"on"` (explicitly load detected host configs as the lowest-precedence fallback) |
|
|
487
|
+
| `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. |
|
|
407
488
|
| `agentPluginPaths` | Agent Plugins package directories to load MCP servers from. Relative paths resolve from the active project cwd. |
|
|
408
489
|
| `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. |
|
|
409
490
|
| `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. |
|
|
491
|
+
| `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. |
|
|
410
492
|
| `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. |
|
|
411
|
-
| `directTools` | Global default for all servers (default: false). Per-server overrides this. |
|
|
493
|
+
| `directTools` | Global default for all servers (default: false). `true`, `false`, or `"search"`. Per-server overrides this. |
|
|
412
494
|
| `strictDirectToolArguments` | Validate direct-tool inputs against their advertised schemas and recover one JSON string layer for object and array properties (default: false). |
|
|
413
495
|
| `directToolResultDetails` | Direct-tool result details: `"lean"` (default) or `"bounded"` to retain the guarded raw MCP result. |
|
|
414
496
|
| `warnOnLargeDirectTools` | Show the advisory when 75 or more direct tools resolve (default: `true`). Set to `false` to suppress only this advisory. |
|
|
415
497
|
| `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. |
|
|
416
498
|
| `scriptMode` | Register the MCP-only `mcpScript` plain-JavaScript tool (default: true). Set to `false` to hide it. |
|
|
417
|
-
| `disableProxyTool` | Hide the `mcp` proxy tool once configured direct tools are fully available from cache. |
|
|
499
|
+
| `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 })`. |
|
|
418
500
|
| `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
|
|
419
501
|
| `sampling` | Allow MCP servers to sample through Pi models, honoring `modelPreferences.hints` before current/default fallback (default: true when UI approval is available). |
|
|
420
502
|
| `samplingAutoApprove` | Skip sampling confirmation prompts. Required for sampling in non-UI sessions (default: false). |
|
|
@@ -440,7 +522,7 @@ Use `approveTools` when a tool should stay visible but not run without confirmat
|
|
|
440
522
|
}
|
|
441
523
|
```
|
|
442
524
|
|
|
443
|
-
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**.
|
|
525
|
+
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.
|
|
444
526
|
|
|
445
527
|
Permission extensions can broker these decisions by listening on `pi-mcp-adapter:tool-approval-request` and claiming the request synchronously:
|
|
446
528
|
|
|
@@ -457,7 +539,7 @@ pi.events.on(MCP_TOOL_APPROVAL_REQUEST_EVENT, (request: McpToolApprovalRequest)
|
|
|
457
539
|
});
|
|
458
540
|
```
|
|
459
541
|
|
|
460
|
-
The request includes `serverName`, `originalToolName`, `prefixedToolName`, `args`, `origin`, and optional `signal`. The first synchronous claim wins. `allow_for_session` updates the same
|
|
542
|
+
The request includes `serverName`, `originalToolName`, `prefixedToolName`, `args`, `origin`, and optional `signal`. The first synchronous claim wins. Brokered approval runs for every resolved MCP call reaching the approval gate, including calls matching session grants restored from the active branch, regardless of `approveTools` configuration, across proxy, direct, `mcpScript`, resource, and iframe origins. `allow_once` permits only the current call; `allow_for_session` updates the same session-scoped approval cache and persistence path as the built-in dialog; `deny` blocks the current MCP call even if cached, without revoking its grant. Only `abstain` or no claim consults the cache, then the configured approval/UI fallback above if no matching grant exists. With no broker listener, fallback behavior is unchanged.
|
|
461
543
|
|
|
462
544
|
### Output Guard
|
|
463
545
|
|
|
@@ -512,9 +594,15 @@ emit({ tool: details.path, completed: true });
|
|
|
512
594
|
return result.data;
|
|
513
595
|
```
|
|
514
596
|
|
|
597
|
+
Depending on the server, successful `result.data` may be the raw MCP `CallToolResult` envelope rather than the domain payload. Check `result.data.structuredContent` for the fields your script expects; if they are absent, inspect text blocks in `result.data.content` too. If neither shape is understood, return the envelope for inspection instead of coercing it to an empty collection.
|
|
598
|
+
|
|
515
599
|
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.
|
|
516
600
|
|
|
517
|
-
|
|
601
|
+
Successful intermediate results reach the script without presentation truncation, details summaries, or output-guard spill files. Each script has a fixed **16 MiB cumulative UTF-8 JSON transfer budget** for successful intermediate data, shared by sequential and parallel calls. A result that cannot fit returns `{ ok: false, error: { code: "intermediate_result_too_large", message } }` and a failed call trace; rejected bytes do not consume the budget, and the script can continue. Request less data or start a new script; there is no configuration option for this cap. Resource calls retain their text-result semantics. Only script-selected output (`emit`, captured console, and `return`) reaches the final output guard; ordinary MCP calls remain guarded as before.
|
|
602
|
+
|
|
603
|
+
The upstream tool executes before this check and may already have side effects. This is a transfer budget, not a total-memory limit: SDK responses, JSON serialization (including rejected results), copies, concurrent responses, and script-created values still allocate memory. Synchronous serialization can delay deadline handling.
|
|
604
|
+
|
|
605
|
+
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, abort handling, and approval gates still apply to every call.
|
|
518
606
|
|
|
519
607
|
`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.
|
|
520
608
|
|
|
@@ -586,6 +674,24 @@ To set a global default for all servers:
|
|
|
586
674
|
|
|
587
675
|
Per-server `directTools` overrides the global setting. The example above registers direct tools for every server except `huge-server`.
|
|
588
676
|
|
|
677
|
+
### Search-activated direct tools
|
|
678
|
+
|
|
679
|
+
`directTools: true` puts every tool's definition in front of the model on every turn. Past a few dozen tools that costs context and, on smaller models, accuracy — the advisory at 75 exists for that reason. `directTools: "search"` is the middle path: the tools are registered as real direct tools with real schemas, but **inactive**, and `mcp({ search })` activates the matches additively.
|
|
680
|
+
|
|
681
|
+
```json
|
|
682
|
+
{
|
|
683
|
+
"mcpServers": {
|
|
684
|
+
"github": {
|
|
685
|
+
"command": "npx",
|
|
686
|
+
"args": ["-y", "@modelcontextprotocol/server-github"],
|
|
687
|
+
"directTools": "search"
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
A successful `mcp({ search })` activates matching search-mode tools additively for the process lifetime and reports newly activated names in `addedToolNames`; no other operation activates them. A restart or resumed session starts with them inactive again. Selecting `directTools: true` activates held tools, while switching back to `"search"` holds them again. Search-mode tools do not count toward the 75-tool advisory.
|
|
694
|
+
|
|
589
695
|
To expose only a subset of a noisy server, add `includeTools` on the server. Values can be exact original names, generated resource names such as `read_<resource>`, prefixed names, or simple glob patterns:
|
|
590
696
|
|
|
591
697
|
```json
|
|
@@ -618,7 +724,7 @@ To hide specific tools while still using `directTools: true`, add `excludeTools`
|
|
|
618
724
|
|
|
619
725
|
Each direct tool costs ~150-300 tokens in the system prompt (name + description + schema). Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy or pick specific tools with a `string[]`. If 75+ direct tools resolve, the adapter prints an advisory but still registers the tools you configured. Set `settings.warnOnLargeDirectTools` to `false` to suppress this advisory.
|
|
620
726
|
|
|
621
|
-
Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/mcp-cache.json` by default, or `$PI_CODING_AGENT_DIR/mcp-cache.json` when set), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only while the cache populates, then the extension hot-loads the refreshed direct tools into the current session. Servers that advertise MCP list-change notifications refresh the current session when their tool or resource list changes. On Pi versions that expose `pi.unregisterTool()`, stale direct tools are removed from the registry during refresh; older Pi versions still deactivate them from the active tool set. To force a refresh: `/mcp reconnect <server>`.
|
|
727
|
+
Direct tools register from the metadata cache in the Pi agent dir (`~/.pi/agent/mcp-cache.json` by default, or `$PI_CODING_AGENT_DIR/mcp-cache.json` when set), so no server connections are needed at startup. On the first session after adding `directTools` to a new server, the cache won't exist yet — tools fall back to proxy-only while the cache populates, then the extension hot-loads the refreshed direct tools into the current session. When `mcp({ connect: "<server>" })` is what discovers them, the connect result lists the new tools in `addedToolNames`, so Pi can load their definitions from that point in the transcript instead of rewriting the active tool list. Servers that advertise MCP list-change notifications refresh the current session when their tool or resource list changes. On Pi versions that expose `pi.unregisterTool()`, stale direct tools are removed from the registry during refresh; older Pi versions still deactivate them from the active tool set. To force a refresh: `/mcp reconnect <server>`.
|
|
622
728
|
|
|
623
729
|
Models sometimes encode an object or array argument as a JSON string. Set `settings.strictDirectToolArguments` to `true` to recover one such layer for schema-declared object and array properties, then validate the complete input against the advertised schema before execution.
|
|
624
730
|
|
|
@@ -785,7 +891,7 @@ Servers that provide usage guidance via the MCP `instructions` field surface it
|
|
|
785
891
|
|
|
786
892
|
If `settings.autoAuth` is `true`, `mcp({ connect: ... })`, `mcp({ tool: ... })`, and direct tool calls automatically run OAuth when needed and retry once.
|
|
787
893
|
|
|
788
|
-
In interactive sessions, you can also authenticate from `/mcp` with `ctrl+a` or Enter on a server that needs auth.
|
|
894
|
+
In interactive sessions, you can also authenticate from `/mcp` with `ctrl+a` or Enter on a server that needs auth. `/mcp-auth` without a server only opens a picker in the interactive UI. For gateway authorization and manual callback completion, see [Remote/headless OAuth](#remoteheadless-oauth).
|
|
789
895
|
|
|
790
896
|
### MCP output schemas
|
|
791
897
|
|
|
@@ -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
|
+
}
|