@fitchmultz/pi-mcp-adapter 7.0.0 → 7.1.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 +7 -0
- package/OAUTH.md +1 -1
- package/README.md +15 -10
- package/dist/direct-tools.js +1 -1
- package/dist/index.js +38 -59
- package/dist/mcp-output-guard.js +1 -1
- package/dist/proxy-modes.js +11 -10
- package/dist/tool-loader.d.ts +7 -2
- package/dist/tool-loader.js +56 -5
- package/dist/ui-server.js +1 -1
- package/package.json +1 -1
- package/skills/mcp-scripting/SKILL.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [7.1.0] - 2026-09-27
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- Keep full MCP gateway/script schemas inactive until selected with `mcp_search({ enable: ["gateway", "script"] })`. Typed discovery still loads matches in one call. Selections persist on the current session branch; configured pins, legacy resource pins, configuration restrictions, and loader-excluded host allowlists retain their behavior.
|
|
15
|
+
- `npm run typecheck` now also checks the test suite under the same strict settings as the source.
|
|
16
|
+
|
|
10
17
|
## [7.0.0] - 2026-09-26
|
|
11
18
|
|
|
12
19
|
### Breaking changes
|
package/OAUTH.md
CHANGED
|
@@ -207,7 +207,7 @@ For browser authorization, this will:
|
|
|
207
207
|
|
|
208
208
|
### Remote/headless authentication
|
|
209
209
|
|
|
210
|
-
When Pi runs over SSH or in a headless environment, use the proxy tool to retrieve the authorization URL instead of relying on OS browser launch:
|
|
210
|
+
When Pi runs over SSH or in a headless environment, use the proxy tool to retrieve the authorization URL instead of relying on OS browser launch. If `mcp` is inactive, first call `mcp_search({ enable: ["gateway"] })` (no query needed):
|
|
211
211
|
|
|
212
212
|
```
|
|
213
213
|
mcp({ action: "auth-start", server: "my-oauth-server" })
|
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Mario wrote about [why you might not need MCP](https://mariozechner.at/posts/202
|
|
|
16
16
|
|
|
17
17
|
His take: skip MCP entirely, write simple CLI tools instead.
|
|
18
18
|
|
|
19
|
-
The MCP ecosystem has useful databases, browsers, and APIs. This adapter keeps their full tool schemas out of context until needed. `mcp_search` discovers and loads typed tools for the next request; `mcp`
|
|
19
|
+
The MCP ecosystem has useful databases, browsers, and APIs. This adapter keeps their full tool schemas out of context until needed. Only `mcp_search` and configured pins start active. `mcp_search` discovers and loads typed tools for the next request; `mcp_search({ enable: ["gateway"] })` loads `mcp` for gateway actions, and `mcp_search({ enable: ["script"] })` loads `mcp_script` for JavaScript composition. Lazy servers stay disconnected until selected discovery or a call needs them.
|
|
20
20
|
|
|
21
21
|
## Pi release qualification
|
|
22
22
|
|
|
@@ -124,7 +124,9 @@ mcp_search({ query: "take screenshot", server: "chrome-devtools" })
|
|
|
124
124
|
|
|
125
125
|
The returned exact tool references become available with their full argument schemas on the next model request. The agent then calls the chosen typed tool normally. Discovery does not run it.
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
To use resources, authentication, UI messages, saved-result readback, or fallback calls, first load the gateway with `mcp_search({ enable: ["gateway"] })`. No query is required and enabling alone performs no integration action or remote connection. Use `enable: ["script"]` for composition, or enable both together. Normal typed search still takes just one discovery call before a typed call.
|
|
128
|
+
|
|
129
|
+
If the host only permits `mcp` (and excludes `mcp_search`), it stays eager. Use explicit gateway actions instead:
|
|
128
130
|
|
|
129
131
|
```js
|
|
130
132
|
mcp({ action: "search", query: "take screenshot", server: "chrome-devtools" })
|
|
@@ -409,6 +411,8 @@ If Pi is running on a remote server and cannot open a local browser, start OAuth
|
|
|
409
411
|
|
|
410
412
|
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
413
|
|
|
414
|
+
If the gateway is inactive, first call `mcp_search({ enable: ["gateway"] })`.
|
|
415
|
+
|
|
412
416
|
```js
|
|
413
417
|
mcp({ action: "auth-start", server: "linear-server" })
|
|
414
418
|
```
|
|
@@ -481,7 +485,7 @@ Persistent OAuth is unsupported out of the box on Android/Termux because `@napi-
|
|
|
481
485
|
| `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
486
|
| `directTools` | Global default for all servers (default: false). Per-server overrides this. |
|
|
483
487
|
| `freezeDirectTools` | Keep direct-tool registration stable after the initial sync so automatic reconnects and list-change notifications do not rebuild the system prompt. Use `mcp({ action: "connect", server: "server" })` or `/mcp reconnect <server>` to refresh deliberately. Default: false. |
|
|
484
|
-
| `scriptMode` |
|
|
488
|
+
| `scriptMode` | Make the MCP-only `mcp_script` plain-JavaScript tool loadable (default: true). Set to `false` to hide it. |
|
|
485
489
|
| `disableProxyTool` | Hide `mcp` once configured pinned tools are available. Legacy resource pins keep it available for resource access. |
|
|
486
490
|
| `autoAuth` | Auto-run OAuth on `connect`/tool calls when a server needs auth, then retry once (default: false). |
|
|
487
491
|
| `sampling` | Allow MCP servers to sample through Pi models, honoring `modelPreferences.hints` before current/default fallback (default: true when UI approval is available). |
|
|
@@ -531,7 +535,7 @@ Tune the limits with the object form:
|
|
|
531
535
|
|
|
532
536
|
Set `"outputGuard": false` — or the env kill switch `MCP_OUTPUT_GUARD=0` — to disable the guard and restore raw output behavior. Recovery notices remain visible even when a configured cap is too small to fit them. Saved files are created with mode `0600` under the system temp directory (or the SDK host's `outputDirectory`) and are not cleaned up automatically; note that spilled MCP output may contain sensitive data.
|
|
533
537
|
|
|
534
|
-
Read a retained result without repeating the MCP operation:
|
|
538
|
+
Read a retained result without repeating the MCP operation. Load the gateway first if inactive with `mcp_search({ enable: ["gateway"] })`:
|
|
535
539
|
|
|
536
540
|
```js
|
|
537
541
|
mcp({ action: "read-result", ref: "/returned/result/path", path: "/structuredContent/rows", fields: ["id", "title"], offset: 0, limit: 12000 })
|
|
@@ -541,7 +545,7 @@ mcp({ action: "read-result", ref: "/returned/result/path", path: "/structuredCon
|
|
|
541
545
|
|
|
542
546
|
### MCP Scripting
|
|
543
547
|
|
|
544
|
-
For multi-call MCP work, write ordinary JavaScript: discover, inspect, call, loop, filter, chain, or fan out, then return one result.
|
|
548
|
+
For multi-call MCP work, write ordinary JavaScript: discover, inspect, call, loop, filter, chain, or fan out, then return one result. Load the script tool with `mcp_search({ enable: ["script"] })`, then run that code with `mcp_script`. Use `mcp_search` for typed discovery, a loaded typed tool for a single call, and `mcp` for explicit gateway actions. Set `settings.scriptMode` to `false` to hide the scripting tool.
|
|
545
549
|
|
|
546
550
|
The bundled `mcp-scripting` skill is a separate Pi package resource. To hide that skill while keeping the adapter extension installed, replace the package entry in Pi settings with the object form and disable package skills:
|
|
547
551
|
|
|
@@ -606,7 +610,7 @@ See the bundled `mcp-scripting` skill for the workflow and exact return shapes.
|
|
|
606
610
|
|
|
607
611
|
### MCP Resources
|
|
608
612
|
|
|
609
|
-
Resources are catalog entries addressed by URI, separate from tools:
|
|
613
|
+
Resources are catalog entries addressed by URI, separate from tools. Load the gateway first if inactive with `mcp_search({ enable: ["gateway"] })`:
|
|
610
614
|
|
|
611
615
|
```js
|
|
612
616
|
mcp({ action: "resources", server: "docs", limit: 12, offset: 0 })
|
|
@@ -637,7 +641,7 @@ URL mode is advertised only in TUI mode. The adapter displays the requesting ser
|
|
|
637
641
|
|
|
638
642
|
`mcp_search({ query, server?, limit?, offset? })` searches real tools and loads the matching typed functions for the next request. It defaults to five matches, with a maximum of 100 and zero-based pagination. Use a specific query, or an empty query with `server` to browse that server. It does not execute a hit or rewrite dynamic instructions into the system prompt.
|
|
639
643
|
|
|
640
|
-
Cached eligible schemas register inactive; only pins and tools selected for the current branch are active. The adapter persists canonical `{ server, tool }` selections in native Pi session entries and restores them across resume, reload, branch navigation, and working-directory changes. Host tool allowlists remain binding.
|
|
644
|
+
Cached eligible schemas and the full gateway/script definitions register inactive; only pins and tools selected for the current branch are active. Legacy resource pins keep the gateway eager. Allowed gateway/script tools also stay eager when the host excludes the loader, so restricted hosts remain usable. The adapter persists canonical `{ server, tool }` selections and enabled gateway/script features in native Pi session entries and restores them across resume, reload, branch navigation, and working-directory changes. Host tool allowlists remain binding. Manual deselection survives reload. Loaded schemas remain available rather than unloading each turn; activation changes declarations and can invalidate the prompt-cache prefix.
|
|
641
645
|
|
|
642
646
|
On hosts with native tool-search support, lazy tools use exact references in namespace `mcp_<server>` with the original tool name as the leaf. Existing direct pins retain their flat prefixed names. On official Pi 0.87.1, which has no native tool search, the ordinary `mcp_search` loader activates flat tools with the same discover-then-call workflow. Always use the exact reference returned by discovery rather than constructing names.
|
|
643
647
|
|
|
@@ -748,7 +752,7 @@ MCP servers can ship interactive UIs via [MCP Apps](https://github.com/modelcont
|
|
|
748
752
|
|
|
749
753
|
**Native rendering:** On macOS, if [Glimpse](https://github.com/hazat/glimpse) is installed (`pi install npm:glimpseui`), UIs open in a native WKWebView window instead of a browser tab. Set `MCP_UI_VIEWER=browser` to force the browser, `MCP_UI_VIEWER=glimpse` to require native rendering, or `MCP_UI_VIEWER=none` (also accepts `off` / `disabled`) to suppress the window entirely — the tool still runs and its inline result is returned to the agent, but no browser or native window opens. This is useful for headless setups, CI, or users who want the tool output delivered inline as text only. When suppressed, a one-line info notification shows the UI URL so it can still be opened manually if needed.
|
|
750
754
|
|
|
751
|
-
**Bidirectional communication:** The UI talks back. When it sends a prompt or intent, the message is stored and `triggerTurn()` wakes the agent. The agent retrieves messages via `mcp({ action: "ui-messages" })` and responds, enabling conversational UIs where the app and agent collaborate in real-time.
|
|
755
|
+
**Bidirectional communication:** The UI talks back. When it sends a prompt or intent, the message is stored and `triggerTurn()` wakes the agent. The agent loads the gateway if inactive with `mcp_search({ enable: ["gateway"] })`, then retrieves messages via `mcp({ action: "ui-messages" })` and responds, enabling conversational UIs where the app and agent collaborate in real-time.
|
|
752
756
|
|
|
753
757
|
**Session reuse:** When the agent calls the same tool again while its UI is already open, the adapter pushes the new result to the existing window instead of replacing it. This enables live updates — the agent can refine a chart, add data, or respond to user input without losing the current view. Different tools still replace the session as before.
|
|
754
758
|
|
|
@@ -820,6 +824,7 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/fitch-mcp-adapt
|
|
|
820
824
|
| Action | Example |
|
|
821
825
|
|--------|---------|
|
|
822
826
|
| Discover and load typed tools | `mcp_search({ query: "screenshot", server: "chrome-devtools", limit: 5 })` |
|
|
827
|
+
| Load gateway / script | `mcp_search({ enable: ["gateway", "script"] })` |
|
|
823
828
|
| Status | `mcp({ action: "status" })` |
|
|
824
829
|
| List server tools | `mcp({ action: "list", server: "name", limit: 12, offset: 0 })` |
|
|
825
830
|
| Search without activation | `mcp({ action: "search", query: "screenshot navigate", limit: 12 })` |
|
|
@@ -834,7 +839,7 @@ Prefer `.mcp.json` for project-local shared MCP config. Use `.pi/fitch-mcp-adapt
|
|
|
834
839
|
| Auth start | `mcp({ action: "auth-start", server: "name" })` |
|
|
835
840
|
| Auth complete | `mcp({ action: "auth-complete", server: "name" })` after the browser callback, or supply `args: { redirectUrl: "..." }` |
|
|
836
841
|
|
|
837
|
-
`action` is required. Search requires `query`; describe/call require `tool`; list/connect/instructions/resources/auth require `server`; resource reads require `server` and `uri`; saved-result reads require `ref`. Pass object `args`. Optional `server` disambiguates tool calls and descriptions.
|
|
842
|
+
Load `mcp` first if inactive using the row above. `action` is required. Search requires `query`; describe/call require `tool`; list/connect/instructions/resources/auth require `server`; resource reads require `server` and `uri`; saved-result reads require `ref`. Pass object `args`. Optional `server` disambiguates tool calls and descriptions.
|
|
838
843
|
|
|
839
844
|
`connect` refreshes an already connected server, including tools, resources, prompts, and instructions. Server-scoped discovery connects that server if its catalog is unknown. Global search uses known catalogs and returns `coverage: { complete, knownServers, unknownServers }`; partial coverage is never presented as an exhaustive result. Gateway search provides schemas without activating functions, so it works for gateway-only hosts.
|
|
840
845
|
|
|
@@ -875,7 +880,7 @@ Advertised tool `outputSchema` values support JSON Schema draft-07 and 2020-12.
|
|
|
875
880
|
|
|
876
881
|
## How It Works
|
|
877
882
|
|
|
878
|
-
-
|
|
883
|
+
- Eager `mcp_search`; full `mcp`, optional `mcp_script`, and typed schemas load when selected
|
|
879
884
|
- Lazy servers connect on selected discovery or calls; configured pins and eager/keep-alive servers can bootstrap intentionally
|
|
880
885
|
- Tool metadata is cached to disk so search/list/describe work without live connections
|
|
881
886
|
- Idle servers disconnect after 10 minutes (configurable), reconnect automatically on next use
|
package/dist/direct-tools.js
CHANGED
|
@@ -15,7 +15,7 @@ export function directToolSelection(config, serverName, envOverride) {
|
|
|
15
15
|
export function buildProxyDescription(config) {
|
|
16
16
|
const servers = Object.entries(config.mcpServers).filter(([, definition]) => !isServerDisabled(definition)).map(([name]) => name).sort();
|
|
17
17
|
return [
|
|
18
|
-
|
|
18
|
+
'MCP gateway. Use mcp_search to discover typed tools; enable mcp_script with mcp_search({ enable: ["script"] }) for multi-call composition. Native Pi tools are called directly.',
|
|
19
19
|
`Configured servers: ${servers.join(", ") || "none"}. Catalog discovery is lazy.`,
|
|
20
20
|
"Actions: status; list/search/describe; call (tool,args,server?); connect (server); instructions (server); resources (server); read-resource (server,uri); read-result (ref,path?,fields?,offset?,limit?); auth-start/auth-complete (server,args?); ui-messages.",
|
|
21
21
|
"Use call when a discovered direct tool is unavailable in this host. read-result reads a retained response without repeating the remote operation.",
|
package/dist/index.js
CHANGED
|
@@ -19,7 +19,7 @@ import { createToolLoader, supportsNativeAsync } from "./tool-loader.js";
|
|
|
19
19
|
import { gatewayParameters, prepareGatewayArguments } from "./gateway-arguments.js";
|
|
20
20
|
import { executeResourceList, executeResourceRead } from "./resource-tools.js";
|
|
21
21
|
import { guardMcpOutput, guardedMcpDetails, readMcpResult, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
|
|
22
|
-
import { abortable } from "./abort.js";
|
|
22
|
+
import { abortable, throwIfAborted } from "./abort.js";
|
|
23
23
|
import { onSessionCheckpoint, prepareMcpCheckpoint } from "./checkpoint.js";
|
|
24
24
|
export { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION, } from "./types.js";
|
|
25
25
|
const INIT_WAIT_TIMEOUT_MS = 30_000;
|
|
@@ -90,29 +90,15 @@ function installMcpAdapter(pi, options) {
|
|
|
90
90
|
? cloneMcpConfig(sessionConfig)
|
|
91
91
|
: { mcpServers: {} };
|
|
92
92
|
const earlyCache = null;
|
|
93
|
+
let currentConfig = earlyConfig;
|
|
93
94
|
const envRaw = process.env.MCP_DIRECT_TOOLS;
|
|
94
95
|
const envDirectToolOverride = envRaw?.split(",").map(s => s.trim()).filter(Boolean);
|
|
95
96
|
const toolLoader = createToolLoader(pi, () => state, () => initPromise, beforeExecute);
|
|
96
|
-
const deactivatedTools = new Set();
|
|
97
97
|
let runtimeCwd;
|
|
98
98
|
let nativeAsyncSupported = false;
|
|
99
99
|
let proxyToolRegistered = false;
|
|
100
100
|
let proxyToolDescription = null;
|
|
101
101
|
let directToolsFrozen = false;
|
|
102
|
-
function deactivateTools(toolNames) {
|
|
103
|
-
if (toolNames.length === 0)
|
|
104
|
-
return;
|
|
105
|
-
const remove = new Set(toolNames);
|
|
106
|
-
const activeTools = pi.getActiveTools();
|
|
107
|
-
for (const toolName of toolNames) {
|
|
108
|
-
if (activeTools.includes(toolName))
|
|
109
|
-
deactivatedTools.add(toolName);
|
|
110
|
-
}
|
|
111
|
-
const nextActiveTools = activeTools.filter((name) => !remove.has(name));
|
|
112
|
-
if (nextActiveTools.length !== activeTools.length) {
|
|
113
|
-
pi.setActiveTools(nextActiveTools);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
102
|
function applyDirectToolConfigChanges(changes) {
|
|
117
103
|
if (!state)
|
|
118
104
|
return;
|
|
@@ -124,12 +110,9 @@ function installMcpAdapter(pi, options) {
|
|
|
124
110
|
}
|
|
125
111
|
}
|
|
126
112
|
function syncToolSurface(ctx) {
|
|
127
|
-
const config = state?.config ??
|
|
113
|
+
const config = state?.config ?? currentConfig;
|
|
128
114
|
const cache = loadMetadataCache(state?.metadataCacheEnabled ?? false);
|
|
129
|
-
toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs =>
|
|
130
|
-
syncProxyTool(config, cache, specs);
|
|
131
|
-
syncScriptTool(config);
|
|
132
|
-
});
|
|
115
|
+
toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => syncGateways(config, cache, specs));
|
|
133
116
|
}
|
|
134
117
|
const registeredPromptCommands = new Set();
|
|
135
118
|
function registerPromptCommands(specs) {
|
|
@@ -260,15 +243,13 @@ function installMcpAdapter(pi, options) {
|
|
|
260
243
|
if (options.transformConfig) {
|
|
261
244
|
runtimeConfig = cloneMcpConfig(options.transformConfig(cloneMcpConfig(runtimeConfig), ctx));
|
|
262
245
|
}
|
|
246
|
+
currentConfig = runtimeConfig;
|
|
263
247
|
const metadataCacheEnabled = ctx.isProjectTrusted()
|
|
264
248
|
|| !isPathInsideProject(getMetadataCachePath(), ctx.cwd);
|
|
265
249
|
toolLoader.restore(ctx);
|
|
266
250
|
registerPromptCommands(resolveCachedPrompts(runtimeConfig, metadataCacheEnabled));
|
|
267
251
|
const runtimeCache = loadMetadataCache(metadataCacheEnabled);
|
|
268
|
-
toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs =>
|
|
269
|
-
syncProxyTool(runtimeConfig, runtimeCache, specs);
|
|
270
|
-
syncScriptTool(runtimeConfig);
|
|
271
|
-
});
|
|
252
|
+
toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => syncGateways(runtimeConfig, runtimeCache, specs));
|
|
272
253
|
const initialization = startInitialization(ctx, owner, oauthRuntime, runtimeConfig, generation);
|
|
273
254
|
if (envRaw !== undefined && envRaw !== "__none__") {
|
|
274
255
|
const missingEnvDirectTools = getMissingConfiguredDirectToolServers(runtimeConfig, runtimeCache, envDirectToolOverride);
|
|
@@ -538,7 +519,17 @@ function installMcpAdapter(pi, options) {
|
|
|
538
519
|
owner?.throwIfInactive();
|
|
539
520
|
return state ?? { content: [{ type: "text", text: "MCP not initialized" }], details: { error: "not_initialized" } };
|
|
540
521
|
}
|
|
522
|
+
const unavailableFeatures = (enable = [], tools) => enable.filter(feature => !tools.some(tool => tool.name === (feature === "gateway" ? "mcp" : "mcp_script") && !tool.namespace));
|
|
541
523
|
async function searchAndLoad(params, signal, ctx) {
|
|
524
|
+
throwIfAborted(signal);
|
|
525
|
+
if (params.query === undefined && !params.enable?.length)
|
|
526
|
+
throw new Error("Provide query or enable.");
|
|
527
|
+
// Loading front doors does not require initialization or connect to a server.
|
|
528
|
+
if (params.query === undefined) {
|
|
529
|
+
const tools = toolLoader.activate([], params.enable);
|
|
530
|
+
const unavailable = unavailableFeatures(params.enable, tools);
|
|
531
|
+
return { content: [{ type: "text", text: `Loaded for the next request: ${JSON.stringify(tools)}${unavailable.length ? `. Unavailable under configuration or host policy: ${unavailable.join(", ")}` : ""}` }], details: { loaded: tools, unavailable }, tools };
|
|
532
|
+
}
|
|
542
533
|
const ready = await getToolState(signal);
|
|
543
534
|
if ("content" in ready)
|
|
544
535
|
return { ...ready, tools: [] };
|
|
@@ -551,23 +542,27 @@ function installMcpAdapter(pi, options) {
|
|
|
551
542
|
const matches = params.query.trim() ? rankToolMatches(ready, params.query, params.server)
|
|
552
543
|
: params.server ? (ready.toolMetadata.get(params.server) ?? []).filter(tool => !tool.resourceUri).map(tool => ({ server: params.server, tool })).sort((a, b) => a.tool.name.localeCompare(b.tool.name)) : [];
|
|
553
544
|
const page = paginate(matches, params.offset ?? 0, params.limit ?? 5);
|
|
554
|
-
const tools = toolLoader.activate(page.items);
|
|
555
|
-
const
|
|
545
|
+
const tools = toolLoader.activate(page.items, params.enable);
|
|
546
|
+
const unavailable = unavailableFeatures(params.enable, tools);
|
|
547
|
+
const typedCount = tools.filter(tool => tool.namespace || (tool.name !== "mcp" && tool.name !== "mcp_script")).length;
|
|
548
|
+
const result = executeSearch(ready, params.query, params.server, typedCount < page.items.length, params.limit ?? 5, params.offset, "mcp_search");
|
|
556
549
|
const guarded = await guardMcpOutput([
|
|
557
550
|
...result.content,
|
|
558
|
-
{ type: "text", text: tools.length ? `Loaded for the next request: ${JSON.stringify(tools)}` :
|
|
551
|
+
{ type: "text", text: tools.length ? `Loaded for the next request: ${JSON.stringify(tools)}` : 'No direct tools loaded. Enable gateway or script with mcp_search({ enable: ["gateway"] }) or mcp_search({ enable: ["script"] }), then use mcp action call or tools.call with the returned exact names.' },
|
|
552
|
+
...(unavailable.length ? [{ type: "text", text: `Unavailable under configuration or host policy: ${unavailable.join(", ")}` }] : []),
|
|
559
553
|
], resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
|
|
560
|
-
return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded), loaded: tools }, tools };
|
|
554
|
+
return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded), loaded: tools, ...(unavailable.length ? { unavailable } : {}) }, tools };
|
|
561
555
|
}
|
|
562
556
|
const searchDefinition = {
|
|
563
557
|
name: "mcp_search", label: "MCP Search",
|
|
564
|
-
description: "Discover
|
|
558
|
+
description: "Discover and load typed MCP tools for the next request. Use query; server discovers an uncached server (empty query browses it). Results report incomplete coverage. Or enable gateway for mcp status/list/search/describe/call/connect/instructions, resources/read-resource, read-result, auth-start/auth-complete and ui-messages; enable script for JavaScript composition, resources and saved-result readback. Enable alone performs no integration action. Loaded schemas stay available in this session/branch.",
|
|
565
559
|
parameters: Type.Object({
|
|
566
|
-
query: Type.String(),
|
|
560
|
+
query: Type.Optional(Type.String()),
|
|
561
|
+
enable: Type.Optional(Type.Array(Type.Union([Type.Literal("gateway"), Type.Literal("script")]), { minItems: 1, uniqueItems: true })),
|
|
567
562
|
server: Type.Optional(Type.String()),
|
|
568
563
|
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Tools to load (default 5)." })),
|
|
569
564
|
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
570
|
-
}),
|
|
565
|
+
}, { anyOf: [{ required: ["query"] }, { required: ["enable"] }] }),
|
|
571
566
|
renderResult: renderMcpToolResult,
|
|
572
567
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
573
568
|
if (beforeExecute) {
|
|
@@ -587,7 +582,7 @@ function installMcpAdapter(pi, options) {
|
|
|
587
582
|
function registerScriptTool() {
|
|
588
583
|
if (scriptToolRegistered)
|
|
589
584
|
return;
|
|
590
|
-
|
|
585
|
+
toolLoader.registerFeature("script", {
|
|
591
586
|
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
592
587
|
name: "mcp_script",
|
|
593
588
|
label: "MCP Script",
|
|
@@ -609,21 +604,8 @@ function installMcpAdapter(pi, options) {
|
|
|
609
604
|
});
|
|
610
605
|
scriptToolRegistered = true;
|
|
611
606
|
}
|
|
612
|
-
function syncScriptTool(config) {
|
|
613
|
-
if (config.settings?.scriptMode === false) {
|
|
614
|
-
if (scriptToolRegistered)
|
|
615
|
-
deactivateTools(["mcp_script"]);
|
|
616
|
-
return;
|
|
617
|
-
}
|
|
618
|
-
registerScriptTool();
|
|
619
|
-
if (deactivatedTools.delete("mcp_script")) {
|
|
620
|
-
const activeTools = pi.getActiveTools();
|
|
621
|
-
if (!activeTools.includes("mcp_script"))
|
|
622
|
-
pi.setActiveTools([...activeTools, "mcp_script"]);
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
607
|
function registerProxyTool(description) {
|
|
626
|
-
|
|
608
|
+
toolLoader.registerFeature("gateway", {
|
|
627
609
|
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
628
610
|
name: "mcp", label: "MCP", description,
|
|
629
611
|
promptSnippet: "MCP server operations, resources, saved result readback, and fallback tool calls",
|
|
@@ -684,7 +666,10 @@ function installMcpAdapter(pi, options) {
|
|
|
684
666
|
proxyToolRegistered = true;
|
|
685
667
|
proxyToolDescription = description;
|
|
686
668
|
}
|
|
687
|
-
function
|
|
669
|
+
function syncGateways(config, cache, directSpecs) {
|
|
670
|
+
const policy = {};
|
|
671
|
+
if (config.settings?.scriptMode !== false)
|
|
672
|
+
policy.script = {};
|
|
688
673
|
const missingConfiguredDirectToolServers = getMissingConfiguredDirectToolServers(config, cache, envRaw === undefined || envRaw === "__none__" ? undefined : envDirectToolOverride);
|
|
689
674
|
const hasPinnedResources = envRaw !== "__none__" && Object.entries(config.mcpServers).some(([server, definition]) => {
|
|
690
675
|
if (definition.disabled === true)
|
|
@@ -706,21 +691,15 @@ function installMcpAdapter(pi, options) {
|
|
|
706
691
|
if (!proxyToolRegistered || proxyToolDescription !== description) {
|
|
707
692
|
registerProxyTool(description);
|
|
708
693
|
}
|
|
709
|
-
|
|
710
|
-
const activeTools = pi.getActiveTools();
|
|
711
|
-
if (!activeTools.includes("mcp")) {
|
|
712
|
-
pi.setActiveTools([...activeTools, "mcp"]);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
return;
|
|
694
|
+
policy.gateway = { eager: hasPinnedResources };
|
|
716
695
|
}
|
|
717
|
-
if (
|
|
718
|
-
|
|
696
|
+
if (policy.script)
|
|
697
|
+
registerScriptTool();
|
|
698
|
+
return policy;
|
|
719
699
|
}
|
|
720
700
|
// A transform needs the session context before any configured tool surface is registered.
|
|
721
701
|
if (!options.transformConfig) {
|
|
722
|
-
|
|
723
|
-
syncScriptTool(earlyConfig);
|
|
702
|
+
syncGateways(earlyConfig, earlyCache, []);
|
|
724
703
|
}
|
|
725
704
|
}
|
|
726
705
|
export function createMcpAdapter(options = {}) {
|
package/dist/mcp-output-guard.js
CHANGED
|
@@ -223,7 +223,7 @@ export function formatMcpPayloadFile(file) {
|
|
|
223
223
|
return `[MCP ${file.kind === "audio" ? "audio" : "binary resource"}${file.uri ? ` (${file.uri})` : ""}: ${file.mimeType ?? "unknown MIME type"}; not rendered. ${file.path ? `Saved file: ${file.path}` : `Could not save: ${file.error}`}]`;
|
|
224
224
|
}
|
|
225
225
|
export function formatMcpResultReference(ref) {
|
|
226
|
-
return `[MCP result saved: ${JSON.stringify(ref)}. Inspect without repeating the call using mcp action "read-result" or tools.readResult with this ref. Optionally select a JSON Pointer with path.]`;
|
|
226
|
+
return `[MCP result saved: ${JSON.stringify(ref)}. Inspect without repeating the call using mcp action "read-result" or tools.readResult with this ref. If inactive, first mcp_search({ enable: ["gateway"] }) or mcp_search({ enable: ["script"] }). Optionally select a JSON Pointer with path.]`;
|
|
227
227
|
}
|
|
228
228
|
function summarizeMcpResult(result, rawBytes, artifact) {
|
|
229
229
|
const { path: fullResultPath, error: resultWriteError } = artifact;
|
package/dist/proxy-modes.js
CHANGED
|
@@ -21,7 +21,7 @@ function disabledResult(mode, serverName) {
|
|
|
21
21
|
details: { mode, error: "server_disabled", server: serverName, message },
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
-
function getAuthRequiredMessage(state, serverName, defaultMessage = `Server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`) {
|
|
24
|
+
function getAuthRequiredMessage(state, serverName, defaultMessage = `Server "${serverName}" requires OAuth authentication. If mcp is inactive, first mcp_search({ enable: ["gateway"] }). Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`) {
|
|
25
25
|
const oauth = state.config.mcpServers[serverName]?.oauth;
|
|
26
26
|
if (oauth && oauth.crossAppAccess)
|
|
27
27
|
defaultMessage = `Server "${serverName}" requires enterprise authorization. Check oauth.crossAppAccess IdP configuration and ID-token source, then retry.`;
|
|
@@ -33,7 +33,7 @@ function getAuthFailedMessage(state, serverName, message) {
|
|
|
33
33
|
if (customGuidance || (oauth && oauth.crossAppAccess)) {
|
|
34
34
|
return `OAuth authentication failed for "${serverName}": ${message}. ${getAuthRequiredMessage(state, serverName)}`;
|
|
35
35
|
}
|
|
36
|
-
return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
|
|
36
|
+
return `OAuth authentication failed for "${serverName}": ${message}. If mcp is inactive, first mcp_search({ enable: ["gateway"] }). Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
|
|
37
37
|
}
|
|
38
38
|
function getRedirectPort(authorizationUrl) {
|
|
39
39
|
try {
|
|
@@ -91,7 +91,7 @@ async function attemptAutoAuth(state, serverName, signal) {
|
|
|
91
91
|
if (!state.ui && !isNonInteractiveOAuth(definition.oauth)) {
|
|
92
92
|
return {
|
|
93
93
|
status: "failed",
|
|
94
|
-
message: getAuthRequiredMessage(state, serverName, `Server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`),
|
|
94
|
+
message: getAuthRequiredMessage(state, serverName, `Server "${serverName}" requires OAuth authentication. If mcp is inactive, first mcp_search({ enable: ["gateway"] }). Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`),
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
97
|
try {
|
|
@@ -268,7 +268,7 @@ export async function executeAuthStart(state, serverName, signal) {
|
|
|
268
268
|
const definition = state.config.mcpServers[serverName];
|
|
269
269
|
if (!definition) {
|
|
270
270
|
return {
|
|
271
|
-
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
271
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
|
|
272
272
|
details: { mode: "auth-start", error: "not_found", server: serverName },
|
|
273
273
|
};
|
|
274
274
|
}
|
|
@@ -314,7 +314,7 @@ export async function executeAuthComplete(state, serverName, input, signal) {
|
|
|
314
314
|
const definition = state.config.mcpServers[serverName];
|
|
315
315
|
if (!definition) {
|
|
316
316
|
return {
|
|
317
|
-
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
317
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
|
|
318
318
|
details: { mode: "auth-complete", error: "not_found", server: serverName },
|
|
319
319
|
};
|
|
320
320
|
}
|
|
@@ -372,7 +372,7 @@ export function executeDescribe(state, toolName, serverFilter) {
|
|
|
372
372
|
return disabledResult("describe", disabled[0]);
|
|
373
373
|
const suggestions = rankSuggestions(state, toolName, 5, serverFilter);
|
|
374
374
|
const hint = suggestions.length > 0
|
|
375
|
-
? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: "${suggestions[0]}" }).`
|
|
375
|
+
? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: "${suggestions[0]}" }). Load if inactive: mcp_search({ enable: ["gateway"] }).`
|
|
376
376
|
: `Use mcp_search({ query: "..." }) to search.`;
|
|
377
377
|
return {
|
|
378
378
|
content: [{ type: "text", text: `Tool "${toolName}" not found. ${hint}` }],
|
|
@@ -461,7 +461,7 @@ export function executeList(state, server, limit = 12, offset = 0) {
|
|
|
461
461
|
const definition = state.config.mcpServers[server];
|
|
462
462
|
if (!definition) {
|
|
463
463
|
return {
|
|
464
|
-
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
464
|
+
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
|
|
465
465
|
details: { mode: "list", server, tools: [], count: 0, error: "not_found" },
|
|
466
466
|
};
|
|
467
467
|
}
|
|
@@ -534,7 +534,7 @@ export function executeInstructions(state, server) {
|
|
|
534
534
|
const definition = state.config.mcpServers[server];
|
|
535
535
|
if (!definition) {
|
|
536
536
|
return {
|
|
537
|
-
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
537
|
+
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
|
|
538
538
|
details: { mode: "instructions", server, error: "not_found" },
|
|
539
539
|
};
|
|
540
540
|
}
|
|
@@ -565,7 +565,7 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
|
565
565
|
const definition = state.config.mcpServers[serverName];
|
|
566
566
|
if (!definition) {
|
|
567
567
|
return {
|
|
568
|
-
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
568
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }],
|
|
569
569
|
details: { mode: "connect", error: "not_found", server: serverName },
|
|
570
570
|
};
|
|
571
571
|
}
|
|
@@ -962,6 +962,7 @@ export async function executeCall(state, toolName, args, serverOverride, getPiTo
|
|
|
962
962
|
let hint = suggestions.length
|
|
963
963
|
? ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: ${JSON.stringify(suggestions[0])} }).`
|
|
964
964
|
: serverName ? ` Search with mcp({ action: "search", query: ${JSON.stringify(toolName)}, server: ${JSON.stringify(serverName)} }).` : ' Use mcp({ action: "search", query: "..." }) to search.';
|
|
965
|
+
hint += ' If mcp is inactive, load it with mcp_search({ enable: ["gateway"] }).';
|
|
965
966
|
if (serverName && state.toolMetadata.has(serverName) && state.manager.getConnection(serverName)?.status !== "connected")
|
|
966
967
|
hint += ` Refresh a stale catalog with mcp({ action: "connect", server: ${JSON.stringify(serverName)} }).`;
|
|
967
968
|
return {
|
|
@@ -973,7 +974,7 @@ export async function executeCall(state, toolName, args, serverOverride, getPiTo
|
|
|
973
974
|
return missing();
|
|
974
975
|
const definition = state.config.mcpServers[serverName];
|
|
975
976
|
if (!definition)
|
|
976
|
-
return { content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }], details: { mode: "call", error: "server_not_found", server: serverName, requestedTool: toolName } };
|
|
977
|
+
return { content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).` }], details: { mode: "call", error: "server_not_found", server: serverName, requestedTool: toolName } };
|
|
977
978
|
if (isServerDisabled(definition))
|
|
978
979
|
return disabledResult("call", serverName);
|
|
979
980
|
const cached = state.toolMetadata.get(serverName);
|
package/dist/tool-loader.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ type Reference = {
|
|
|
7
7
|
name: string;
|
|
8
8
|
namespace?: string;
|
|
9
9
|
};
|
|
10
|
+
export type Feature = "gateway" | "script";
|
|
11
|
+
export type FeaturePolicy = Partial<Record<Feature, {
|
|
12
|
+
eager?: boolean;
|
|
13
|
+
}>>;
|
|
10
14
|
type SearchDefinition<TParams extends TSchema, TDetails> = Omit<ToolDefinition<TParams, TDetails>, "execute"> & {
|
|
11
15
|
execute: (...args: Parameters<ToolDefinition<TParams, TDetails>["execute"]>) => Promise<AgentToolResult<TDetails> & {
|
|
12
16
|
tools: Reference[];
|
|
@@ -20,12 +24,13 @@ export type SearchAPI = ExtensionAPI & {
|
|
|
20
24
|
export declare function supportsNativeAsync(ctx: ExtensionContext): boolean;
|
|
21
25
|
export declare function createToolLoader(pi: SearchAPI, getState: () => McpExtensionState | null, getInitPromise: () => Promise<McpExtensionState> | null, beforeExecute?: McpAdapterOptions["beforeExecute"]): {
|
|
22
26
|
native: boolean;
|
|
27
|
+
registerFeature: <TParams extends TSchema, TDetails>(feature: Feature, definition: ToolDefinition<TParams, TDetails>) => void;
|
|
23
28
|
restore: (ctx: ExtensionContext) => void;
|
|
24
|
-
sync: (config: McpConfig, cache: MetadataCache | null, env?: string[] | null, asyncSupported?: boolean, registerGateways?: (pins: DirectToolSpec[]) =>
|
|
29
|
+
sync: (config: McpConfig, cache: MetadataCache | null, env?: string[] | null, asyncSupported?: boolean, registerGateways?: (pins: DirectToolSpec[]) => FeaturePolicy) => DirectToolSpec[];
|
|
25
30
|
activate: (matches: Array<{
|
|
26
31
|
server: string;
|
|
27
32
|
tool: ToolMetadata;
|
|
28
|
-
}
|
|
33
|
+
}>, enable?: Feature[]) => Reference[];
|
|
29
34
|
persist: () => void;
|
|
30
35
|
selectedServers: () => string[];
|
|
31
36
|
};
|
package/dist/tool-loader.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createMcpDirectToolCallRenderer, renderMcpToolResult } from "./tool-res
|
|
|
5
5
|
import { isServerDisabled } from "./types.js";
|
|
6
6
|
import { normalizeDirectToolInputSchema } from "./utils.js";
|
|
7
7
|
import { isToolCallApprovalRequired } from "./tool-approval.js";
|
|
8
|
+
const featureRefs = { gateway: { name: "mcp" }, script: { name: "mcp_script" } };
|
|
8
9
|
const ENTRY = "mcp-tool-selection";
|
|
9
10
|
const key = (value) => JSON.stringify([value.server, value.tool]);
|
|
10
11
|
const refKey = (value) => JSON.stringify([value.namespace ?? null, value.name]);
|
|
@@ -23,6 +24,10 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
23
24
|
let available = new Set();
|
|
24
25
|
let selected = new Map();
|
|
25
26
|
let inactive = new Map();
|
|
27
|
+
let features = {};
|
|
28
|
+
let selectedFeatures = new Set();
|
|
29
|
+
let inactiveFeatures = new Set();
|
|
30
|
+
const registeredFeatures = new Set();
|
|
26
31
|
let restoring = true;
|
|
27
32
|
const active = () => native ? pi.getActiveToolReferences() : pi.getActiveTools().map(name => ({ name }));
|
|
28
33
|
const setActive = (refs) => {
|
|
@@ -34,8 +39,24 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
34
39
|
else
|
|
35
40
|
pi.setActiveTools(unique.map(ref => ref.name));
|
|
36
41
|
};
|
|
42
|
+
function registerFeature(feature, definition) {
|
|
43
|
+
pi.registerTool(definition);
|
|
44
|
+
registeredFeatures.add(feature);
|
|
45
|
+
}
|
|
37
46
|
function capture() {
|
|
38
47
|
const current = new Set(active().map(refKey));
|
|
48
|
+
for (const feature of Object.keys(features)) {
|
|
49
|
+
if (current.has(refKey(featureRefs[feature]))) {
|
|
50
|
+
if (!features[feature]?.eager)
|
|
51
|
+
selectedFeatures.add(feature);
|
|
52
|
+
inactiveFeatures.delete(feature);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
selectedFeatures.delete(feature);
|
|
56
|
+
if (features[feature]?.eager)
|
|
57
|
+
inactiveFeatures.add(feature);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
39
60
|
for (const [id, { selection, ref, pinned }] of registered) {
|
|
40
61
|
if (!available.has(id))
|
|
41
62
|
continue;
|
|
@@ -53,15 +74,19 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
53
74
|
}
|
|
54
75
|
function persist() {
|
|
55
76
|
capture();
|
|
56
|
-
pi.appendEntry(ENTRY, { selected: [...selected.values()], inactive: [...inactive.values()] });
|
|
77
|
+
pi.appendEntry(ENTRY, { selected: [...selected.values()], inactive: [...inactive.values()], features: [...selectedFeatures], inactiveFeatures: [...inactiveFeatures] });
|
|
57
78
|
}
|
|
58
79
|
function restore(ctx) {
|
|
59
80
|
selected = new Map();
|
|
60
81
|
inactive = new Map();
|
|
82
|
+
selectedFeatures = new Set();
|
|
83
|
+
inactiveFeatures = new Set();
|
|
61
84
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
62
85
|
if (entry.type !== "custom" || entry.customType !== ENTRY || !entry.data || typeof entry.data !== "object")
|
|
63
86
|
continue;
|
|
64
87
|
const data = entry.data;
|
|
88
|
+
selectedFeatures = new Set(Array.isArray(data.features) ? data.features.filter(value => value === "gateway" || value === "script") : []);
|
|
89
|
+
inactiveFeatures = new Set(Array.isArray(data.inactiveFeatures) ? data.inactiveFeatures.filter(value => value === "gateway" || value === "script") : []);
|
|
65
90
|
for (const [field, target] of [[data.selected, selected], [data.inactive, inactive]]) {
|
|
66
91
|
target.clear();
|
|
67
92
|
for (const selection of Array.isArray(field) ? field : []) {
|
|
@@ -125,28 +150,54 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
125
150
|
}
|
|
126
151
|
const permitted = new Set(pi.getAllTools().map(refKey));
|
|
127
152
|
const pins = specs.filter(item => item.pinned && available.has(key(item.selection)) && permitted.has(refKey(item.ref))).map(item => item.spec);
|
|
128
|
-
registerGateways?.(pins);
|
|
153
|
+
const policy = registerGateways?.(pins);
|
|
154
|
+
if (policy) {
|
|
155
|
+
const permittedFeatures = new Set(pi.getAllTools().map(refKey));
|
|
156
|
+
const loaderMissing = !permittedFeatures.has(refKey({ name: "mcp_search" }));
|
|
157
|
+
features = {};
|
|
158
|
+
for (const feature of Object.keys(featureRefs)) {
|
|
159
|
+
if (policy[feature] && permittedFeatures.has(refKey(featureRefs[feature]))) {
|
|
160
|
+
features[feature] = { eager: policy[feature].eager || loaderMissing };
|
|
161
|
+
if (selectedFeatures.has(feature) || (features[feature]?.eager && !inactiveFeatures.has(feature)))
|
|
162
|
+
enabled.push(featureRefs[feature]);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
selectedFeatures.delete(feature);
|
|
166
|
+
inactiveFeatures.delete(feature);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
129
170
|
// Official 0.87 reactivates allowlisted definitions on registration. Apply our loadout last.
|
|
130
171
|
const ownedRefs = new Set([...previousRefs, ...[...registered.values()].map(item => refKey(item.ref))]);
|
|
172
|
+
for (const feature of registeredFeatures)
|
|
173
|
+
ownedRefs.add(refKey(featureRefs[feature]));
|
|
131
174
|
setActive([...active().filter(ref => !ownedRefs.has(refKey(ref))), ...enabled]);
|
|
132
175
|
restoring = false;
|
|
133
176
|
return pins;
|
|
134
177
|
}
|
|
135
|
-
function activate(matches) {
|
|
178
|
+
function activate(matches, enable = []) {
|
|
136
179
|
const requested = matches.flatMap(({ server, tool }) => {
|
|
137
180
|
const id = key({ server, tool: tool.originalName });
|
|
138
181
|
const registration = available.has(id) ? registered.get(id) : undefined;
|
|
139
182
|
return registration ? [registration] : [];
|
|
140
183
|
});
|
|
141
184
|
// No await between reading and extending the current loadout: parallel searches union their selections.
|
|
142
|
-
|
|
185
|
+
const permitted = new Set(pi.getAllTools().map(refKey));
|
|
186
|
+
const requestedFeatures = [...new Set(enable)].filter(feature => features[feature] && permitted.has(refKey(featureRefs[feature])));
|
|
187
|
+
setActive([...active(), ...requested.map(item => item.ref), ...requestedFeatures.map(feature => featureRefs[feature])]);
|
|
143
188
|
const current = new Set(active().map(refKey));
|
|
144
189
|
const tools = requested.filter(item => current.has(refKey(item.ref))).map(({ selection, ref }) => {
|
|
145
190
|
selected.set(key(selection), selection);
|
|
146
191
|
return ref;
|
|
147
192
|
});
|
|
193
|
+
for (const feature of requestedFeatures) {
|
|
194
|
+
if (!current.has(refKey(featureRefs[feature])))
|
|
195
|
+
continue;
|
|
196
|
+
selectedFeatures.add(feature);
|
|
197
|
+
tools.push(featureRefs[feature]);
|
|
198
|
+
}
|
|
148
199
|
persist();
|
|
149
200
|
return tools;
|
|
150
201
|
}
|
|
151
|
-
return { native, restore, sync, activate, persist, selectedServers: () => [...new Set([...selected.values()].map(item => item.server))] };
|
|
202
|
+
return { native, registerFeature, restore, sync, activate, persist, selectedServers: () => [...new Set([...selected.values()].map(item => item.server))] };
|
|
152
203
|
}
|
package/dist/ui-server.js
CHANGED
|
@@ -481,7 +481,7 @@ export async function startUiServer(options) {
|
|
|
481
481
|
}
|
|
482
482
|
catch (error) {
|
|
483
483
|
if (error instanceof SessionRecoveryAuthRequiredError) {
|
|
484
|
-
const fallback = `Server "${options.serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${options.serverName}" }) to get a browser URL, or /mcp-auth ${options.serverName} in an interactive local session.`;
|
|
484
|
+
const fallback = `Server "${options.serverName}" requires OAuth authentication. If mcp is inactive, first mcp_search({ enable: ["gateway"] }). Run mcp({ action: "auth-start", server: "${options.serverName}" }) to get a browser URL, or /mcp-auth ${options.serverName} in an interactive local session.`;
|
|
485
485
|
const message = error.authMessage ?? (options.config
|
|
486
486
|
? formatAuthRequiredMessage(options.config, options.serverName, fallback)
|
|
487
487
|
: fallback);
|
package/package.json
CHANGED
|
@@ -7,6 +7,8 @@ description: Write mcp_script JavaScript for discovering, inspecting, and callin
|
|
|
7
7
|
|
|
8
8
|
Use `mcp_script` for MCP calls with loops, filtering, chaining, fan-out, or other logic between calls. Pass ordinary JavaScript as its `code` argument. For a single call, use an already loaded typed tool. Use `mcp_search({ query, server?, limit?, offset? })` to discover and load typed tools for the next request, or explicit `mcp` actions for gateway-only workflows. Discovery never executes a search hit.
|
|
9
9
|
|
|
10
|
+
If `mcp_script` is inactive, first call `mcp_search({ enable: ["script"] })`. For gateway actions, load `mcp` with `mcp_search({ enable: ["gateway"] })`. Enable both in one call when needed. Enabling does not connect to a server or run an integration action; selections stay active for the session/branch. Hosts permitting only one gateway without the loader keep that tool eager.
|
|
11
|
+
|
|
10
12
|
## Discover, inspect, then call
|
|
11
13
|
|
|
12
14
|
```js
|