@fitchmultz/pi-mcp-adapter 7.0.0 → 7.1.1
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 +22 -0
- package/OAUTH.md +3 -3
- package/README.md +31 -15
- package/dist/app-bridge.bundle.js +13 -13
- package/dist/direct-tools.js +1 -1
- package/dist/index.js +40 -67
- package/dist/mcp-output-guard.js +1 -1
- package/dist/proxy-modes.js +11 -10
- package/dist/server-manager.js +2 -1
- package/dist/tool-loader.d.ts +11 -21
- package/dist/tool-loader.js +85 -48
- package/dist/ui-server.js +1 -1
- package/package.json +12 -12
- package/skills/mcp-scripting/SKILL.md +2 -0
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
|
@@ -15,11 +15,11 @@ import { createMcpRuntimeOwner, createOwnedUi, isAbortError } from "./runtime-ow
|
|
|
15
15
|
import { publishMcpStatusShutdown } from "./mcp-status.js";
|
|
16
16
|
import { DEFAULT_MCP_SCRIPT_TIMEOUT_MS, runMcpScript } from "./mcp-code.js";
|
|
17
17
|
import { MAX_PAGE_SIZE, paginate, rankToolMatches } from "./search-ranking.js";
|
|
18
|
-
import { createToolLoader
|
|
18
|
+
import { createToolLoader } 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,14 @@ 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
|
-
let nativeAsyncSupported = false;
|
|
99
98
|
let proxyToolRegistered = false;
|
|
100
99
|
let proxyToolDescription = null;
|
|
101
100
|
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
101
|
function applyDirectToolConfigChanges(changes) {
|
|
117
102
|
if (!state)
|
|
118
103
|
return;
|
|
@@ -124,12 +109,9 @@ function installMcpAdapter(pi, options) {
|
|
|
124
109
|
}
|
|
125
110
|
}
|
|
126
111
|
function syncToolSurface(ctx) {
|
|
127
|
-
const config = state?.config ??
|
|
112
|
+
const config = state?.config ?? currentConfig;
|
|
128
113
|
const cache = loadMetadataCache(state?.metadataCacheEnabled ?? false);
|
|
129
|
-
toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride,
|
|
130
|
-
syncProxyTool(config, cache, specs);
|
|
131
|
-
syncScriptTool(config);
|
|
132
|
-
});
|
|
114
|
+
toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, specs => syncGateways(config, cache, specs));
|
|
133
115
|
}
|
|
134
116
|
const registeredPromptCommands = new Set();
|
|
135
117
|
function registerPromptCommands(specs) {
|
|
@@ -219,7 +201,6 @@ function installMcpAdapter(pi, options) {
|
|
|
219
201
|
}
|
|
220
202
|
async function startSession(ctx) {
|
|
221
203
|
runtimeCwd = ctx.cwd;
|
|
222
|
-
nativeAsyncSupported = supportsNativeAsync(ctx);
|
|
223
204
|
directToolsFrozen = false;
|
|
224
205
|
const generation = ++lifecycleGeneration;
|
|
225
206
|
const previousState = state;
|
|
@@ -260,15 +241,13 @@ function installMcpAdapter(pi, options) {
|
|
|
260
241
|
if (options.transformConfig) {
|
|
261
242
|
runtimeConfig = cloneMcpConfig(options.transformConfig(cloneMcpConfig(runtimeConfig), ctx));
|
|
262
243
|
}
|
|
244
|
+
currentConfig = runtimeConfig;
|
|
263
245
|
const metadataCacheEnabled = ctx.isProjectTrusted()
|
|
264
246
|
|| !isPathInsideProject(getMetadataCachePath(), ctx.cwd);
|
|
265
247
|
toolLoader.restore(ctx);
|
|
266
248
|
registerPromptCommands(resolveCachedPrompts(runtimeConfig, metadataCacheEnabled));
|
|
267
249
|
const runtimeCache = loadMetadataCache(metadataCacheEnabled);
|
|
268
|
-
toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride,
|
|
269
|
-
syncProxyTool(runtimeConfig, runtimeCache, specs);
|
|
270
|
-
syncScriptTool(runtimeConfig);
|
|
271
|
-
});
|
|
250
|
+
toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, specs => syncGateways(runtimeConfig, runtimeCache, specs));
|
|
272
251
|
const initialization = startInitialization(ctx, owner, oauthRuntime, runtimeConfig, generation);
|
|
273
252
|
if (envRaw !== undefined && envRaw !== "__none__") {
|
|
274
253
|
const missingEnvDirectTools = getMissingConfiguredDirectToolServers(runtimeConfig, runtimeCache, envDirectToolOverride);
|
|
@@ -283,7 +262,6 @@ function installMcpAdapter(pi, options) {
|
|
|
283
262
|
syncToolSurface(ctx);
|
|
284
263
|
});
|
|
285
264
|
pi.on("model_select", (_event, ctx) => {
|
|
286
|
-
nativeAsyncSupported = supportsNativeAsync(ctx);
|
|
287
265
|
if (state)
|
|
288
266
|
syncToolSurface(ctx);
|
|
289
267
|
});
|
|
@@ -538,7 +516,17 @@ function installMcpAdapter(pi, options) {
|
|
|
538
516
|
owner?.throwIfInactive();
|
|
539
517
|
return state ?? { content: [{ type: "text", text: "MCP not initialized" }], details: { error: "not_initialized" } };
|
|
540
518
|
}
|
|
519
|
+
const unavailableFeatures = (enable = [], tools) => enable.filter(feature => !tools.some(tool => tool.name === (feature === "gateway" ? "mcp" : "mcp_script")));
|
|
541
520
|
async function searchAndLoad(params, signal, ctx) {
|
|
521
|
+
throwIfAborted(signal);
|
|
522
|
+
if (params.query === undefined && !params.enable?.length)
|
|
523
|
+
throw new Error("Provide query or enable.");
|
|
524
|
+
// Loading front doors does not require initialization or connect to a server.
|
|
525
|
+
if (params.query === undefined) {
|
|
526
|
+
const tools = toolLoader.activate([], params.enable);
|
|
527
|
+
const unavailable = unavailableFeatures(params.enable, tools);
|
|
528
|
+
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 };
|
|
529
|
+
}
|
|
542
530
|
const ready = await getToolState(signal);
|
|
543
531
|
if ("content" in ready)
|
|
544
532
|
return { ...ready, tools: [] };
|
|
@@ -551,23 +539,27 @@ function installMcpAdapter(pi, options) {
|
|
|
551
539
|
const matches = params.query.trim() ? rankToolMatches(ready, params.query, params.server)
|
|
552
540
|
: 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
541
|
const page = paginate(matches, params.offset ?? 0, params.limit ?? 5);
|
|
554
|
-
const tools = toolLoader.activate(page.items);
|
|
555
|
-
const
|
|
542
|
+
const tools = toolLoader.activate(page.items, params.enable);
|
|
543
|
+
const unavailable = unavailableFeatures(params.enable, tools);
|
|
544
|
+
const typedCount = tools.filter(tool => tool.name !== "mcp" && tool.name !== "mcp_script").length;
|
|
545
|
+
const result = executeSearch(ready, params.query, params.server, typedCount < page.items.length, params.limit ?? 5, params.offset, "mcp_search");
|
|
556
546
|
const guarded = await guardMcpOutput([
|
|
557
547
|
...result.content,
|
|
558
|
-
{ type: "text", text: tools.length ? `Loaded for the next request: ${JSON.stringify(tools)}` :
|
|
548
|
+
{ 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.' },
|
|
549
|
+
...(unavailable.length ? [{ type: "text", text: `Unavailable under configuration or host policy: ${unavailable.join(", ")}` }] : []),
|
|
559
550
|
], resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
|
|
560
|
-
return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded), loaded: tools }, tools };
|
|
551
|
+
return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded), loaded: tools, ...(unavailable.length ? { unavailable } : {}) }, tools };
|
|
561
552
|
}
|
|
562
553
|
const searchDefinition = {
|
|
563
554
|
name: "mcp_search", label: "MCP Search",
|
|
564
|
-
description: "Discover
|
|
555
|
+
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
556
|
parameters: Type.Object({
|
|
566
|
-
query: Type.String(),
|
|
557
|
+
query: Type.Optional(Type.String()),
|
|
558
|
+
enable: Type.Optional(Type.Array(Type.Union([Type.Literal("gateway"), Type.Literal("script")]), { minItems: 1, uniqueItems: true })),
|
|
567
559
|
server: Type.Optional(Type.String()),
|
|
568
560
|
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Tools to load (default 5)." })),
|
|
569
561
|
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
570
|
-
}),
|
|
562
|
+
}, { anyOf: [{ required: ["query"] }, { required: ["enable"] }] }),
|
|
571
563
|
renderResult: renderMcpToolResult,
|
|
572
564
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
573
565
|
if (beforeExecute) {
|
|
@@ -579,15 +571,12 @@ function installMcpAdapter(pi, options) {
|
|
|
579
571
|
return searchAndLoad(params, signal, ctx);
|
|
580
572
|
},
|
|
581
573
|
};
|
|
582
|
-
|
|
583
|
-
pi.registerToolSearch(searchDefinition);
|
|
584
|
-
else
|
|
585
|
-
pi.registerTool(searchDefinition);
|
|
574
|
+
pi.registerTool(searchDefinition);
|
|
586
575
|
let scriptToolRegistered = false;
|
|
587
576
|
function registerScriptTool() {
|
|
588
577
|
if (scriptToolRegistered)
|
|
589
578
|
return;
|
|
590
|
-
|
|
579
|
+
toolLoader.registerFeature("script", {
|
|
591
580
|
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
592
581
|
name: "mcp_script",
|
|
593
582
|
label: "MCP Script",
|
|
@@ -609,21 +598,8 @@ function installMcpAdapter(pi, options) {
|
|
|
609
598
|
});
|
|
610
599
|
scriptToolRegistered = true;
|
|
611
600
|
}
|
|
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
601
|
function registerProxyTool(description) {
|
|
626
|
-
|
|
602
|
+
toolLoader.registerFeature("gateway", {
|
|
627
603
|
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
628
604
|
name: "mcp", label: "MCP", description,
|
|
629
605
|
promptSnippet: "MCP server operations, resources, saved result readback, and fallback tool calls",
|
|
@@ -684,7 +660,10 @@ function installMcpAdapter(pi, options) {
|
|
|
684
660
|
proxyToolRegistered = true;
|
|
685
661
|
proxyToolDescription = description;
|
|
686
662
|
}
|
|
687
|
-
function
|
|
663
|
+
function syncGateways(config, cache, directSpecs) {
|
|
664
|
+
const policy = {};
|
|
665
|
+
if (config.settings?.scriptMode !== false)
|
|
666
|
+
policy.script = {};
|
|
688
667
|
const missingConfiguredDirectToolServers = getMissingConfiguredDirectToolServers(config, cache, envRaw === undefined || envRaw === "__none__" ? undefined : envDirectToolOverride);
|
|
689
668
|
const hasPinnedResources = envRaw !== "__none__" && Object.entries(config.mcpServers).some(([server, definition]) => {
|
|
690
669
|
if (definition.disabled === true)
|
|
@@ -706,21 +685,15 @@ function installMcpAdapter(pi, options) {
|
|
|
706
685
|
if (!proxyToolRegistered || proxyToolDescription !== description) {
|
|
707
686
|
registerProxyTool(description);
|
|
708
687
|
}
|
|
709
|
-
|
|
710
|
-
const activeTools = pi.getActiveTools();
|
|
711
|
-
if (!activeTools.includes("mcp")) {
|
|
712
|
-
pi.setActiveTools([...activeTools, "mcp"]);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
return;
|
|
688
|
+
policy.gateway = { eager: hasPinnedResources };
|
|
716
689
|
}
|
|
717
|
-
if (
|
|
718
|
-
|
|
690
|
+
if (policy.script)
|
|
691
|
+
registerScriptTool();
|
|
692
|
+
return policy;
|
|
719
693
|
}
|
|
720
694
|
// A transform needs the session context before any configured tool surface is registered.
|
|
721
695
|
if (!options.transformConfig) {
|
|
722
|
-
|
|
723
|
-
syncScriptTool(earlyConfig);
|
|
696
|
+
syncGateways(earlyConfig, earlyCache, []);
|
|
724
697
|
}
|
|
725
698
|
}
|
|
726
699
|
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/server-manager.js
CHANGED
|
@@ -691,7 +691,8 @@ export class McpServerManager {
|
|
|
691
691
|
let client;
|
|
692
692
|
client = new ManagedClient({ name: `pi-mcp-${serverName}`, version: ADAPTER_VERSION }, {
|
|
693
693
|
versionNegotiation: { mode: definition.protocolVersion ?? (definition.url ? "auto" : "legacy") },
|
|
694
|
-
|
|
694
|
+
// ponytail: reject catalogs over 1024 pages; raise this native cap if a real catalog needs more.
|
|
695
|
+
listMaxPages: 1024,
|
|
695
696
|
...(Object.keys(capabilities).length > 0 ? { capabilities } : {}),
|
|
696
697
|
listChanged: {
|
|
697
698
|
tools: {
|
package/dist/tool-loader.d.ts
CHANGED
|
@@ -1,32 +1,22 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type TSchema } from "typebox";
|
|
3
3
|
import { type MetadataCache } from "./metadata-cache.ts";
|
|
4
4
|
import type { McpExtensionState } from "./state.ts";
|
|
5
5
|
import { type DirectToolSpec, type McpAdapterOptions, type McpConfig, type ToolMetadata } from "./types.ts";
|
|
6
|
-
type
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
tools: Reference[];
|
|
13
|
-
}>;
|
|
14
|
-
};
|
|
15
|
-
export type SearchAPI = ExtensionAPI & {
|
|
16
|
-
registerToolSearch?: <TParams extends TSchema, TDetails>(definition: SearchDefinition<TParams, TDetails>) => void;
|
|
17
|
-
getActiveToolReferences?: () => Reference[];
|
|
18
|
-
setActiveToolReferences?: (tools: Reference[]) => void;
|
|
19
|
-
};
|
|
20
|
-
export declare function supportsNativeAsync(ctx: ExtensionContext): boolean;
|
|
21
|
-
export declare function createToolLoader(pi: SearchAPI, getState: () => McpExtensionState | null, getInitPromise: () => Promise<McpExtensionState> | null, beforeExecute?: McpAdapterOptions["beforeExecute"]): {
|
|
22
|
-
native: boolean;
|
|
6
|
+
export type Feature = "gateway" | "script";
|
|
7
|
+
export type FeaturePolicy = Partial<Record<Feature, {
|
|
8
|
+
eager?: boolean;
|
|
9
|
+
}>>;
|
|
10
|
+
export declare function createToolLoader(pi: ExtensionAPI, getState: () => McpExtensionState | null, getInitPromise: () => Promise<McpExtensionState> | null, beforeExecute?: McpAdapterOptions["beforeExecute"]): {
|
|
11
|
+
registerFeature: <TParams extends TSchema, TDetails>(feature: Feature, definition: ToolDefinition<TParams, TDetails>) => void;
|
|
23
12
|
restore: (ctx: ExtensionContext) => void;
|
|
24
|
-
sync: (config: McpConfig, cache: MetadataCache | null, env?: string[] | null,
|
|
13
|
+
sync: (config: McpConfig, cache: MetadataCache | null, env?: string[] | null, registerGateways?: (pins: DirectToolSpec[]) => FeaturePolicy) => DirectToolSpec[];
|
|
25
14
|
activate: (matches: Array<{
|
|
26
15
|
server: string;
|
|
27
16
|
tool: ToolMetadata;
|
|
28
|
-
}
|
|
17
|
+
}>, enable?: Feature[]) => {
|
|
18
|
+
name: string;
|
|
19
|
+
}[];
|
|
29
20
|
persist: () => void;
|
|
30
21
|
selectedServers: () => string[];
|
|
31
22
|
};
|
|
32
|
-
export {};
|
package/dist/tool-loader.js
CHANGED
|
@@ -4,42 +4,50 @@ import { isServerCacheValid, reconstructToolMetadata } from "./metadata-cache.js
|
|
|
4
4
|
import { createMcpDirectToolCallRenderer, renderMcpToolResult } from "./tool-result-renderer.js";
|
|
5
5
|
import { isServerDisabled } from "./types.js";
|
|
6
6
|
import { normalizeDirectToolInputSchema } from "./utils.js";
|
|
7
|
-
|
|
7
|
+
const featureNames = { gateway: "mcp", script: "mcp_script" };
|
|
8
8
|
const ENTRY = "mcp-tool-selection";
|
|
9
9
|
const key = (value) => JSON.stringify([value.server, value.tool]);
|
|
10
|
-
const refKey = (value) => JSON.stringify([value.namespace ?? null, value.name]);
|
|
11
10
|
const reserved = new Set(["read", "bash", "powershell", "edit", "write", "grep", "find", "ls", "mcp", "mcp_search", "mcp_script"]);
|
|
12
|
-
export function supportsNativeAsync(ctx) {
|
|
13
|
-
const compat = ctx.model?.compat;
|
|
14
|
-
return "getPendingToolCalls" in ctx && typeof ctx.getPendingToolCalls === "function"
|
|
15
|
-
&& !!compat && "supportsAsyncTools" in compat && compat.supportsAsyncTools === true;
|
|
16
|
-
}
|
|
17
11
|
export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
18
|
-
const native = typeof pi.registerToolSearch === "function"
|
|
19
|
-
&& typeof pi.getActiveToolReferences === "function"
|
|
20
|
-
&& typeof pi.setActiveToolReferences === "function";
|
|
21
12
|
pi.registerEntryRenderer(ENTRY, () => undefined);
|
|
22
13
|
const registered = new Map();
|
|
23
14
|
let available = new Set();
|
|
24
15
|
let selected = new Map();
|
|
25
16
|
let inactive = new Map();
|
|
17
|
+
let features = {};
|
|
18
|
+
let selectedFeatures = new Set();
|
|
19
|
+
let inactiveFeatures = new Set();
|
|
20
|
+
const registeredFeatures = new Set();
|
|
26
21
|
let restoring = true;
|
|
27
|
-
const active = () =>
|
|
28
|
-
const setActive = (
|
|
29
|
-
const unique = [...new
|
|
22
|
+
const active = () => pi.getActiveTools();
|
|
23
|
+
const setActive = (names) => {
|
|
24
|
+
const unique = [...new Set(names)];
|
|
30
25
|
if (JSON.stringify(active()) === JSON.stringify(unique))
|
|
31
26
|
return;
|
|
32
|
-
|
|
33
|
-
pi.setActiveToolReferences(unique);
|
|
34
|
-
else
|
|
35
|
-
pi.setActiveTools(unique.map(ref => ref.name));
|
|
27
|
+
pi.setActiveTools(unique);
|
|
36
28
|
};
|
|
29
|
+
function registerFeature(feature, definition) {
|
|
30
|
+
pi.registerTool(definition);
|
|
31
|
+
registeredFeatures.add(feature);
|
|
32
|
+
}
|
|
37
33
|
function capture() {
|
|
38
|
-
const current = new Set(active()
|
|
39
|
-
for (const
|
|
34
|
+
const current = new Set(active());
|
|
35
|
+
for (const feature of Object.keys(features)) {
|
|
36
|
+
if (current.has(featureNames[feature])) {
|
|
37
|
+
if (!features[feature]?.eager)
|
|
38
|
+
selectedFeatures.add(feature);
|
|
39
|
+
inactiveFeatures.delete(feature);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
selectedFeatures.delete(feature);
|
|
43
|
+
if (features[feature]?.eager)
|
|
44
|
+
inactiveFeatures.add(feature);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const [id, { selection, name, pinned }] of registered) {
|
|
40
48
|
if (!available.has(id))
|
|
41
49
|
continue;
|
|
42
|
-
if (current.has(
|
|
50
|
+
if (current.has(name)) {
|
|
43
51
|
if (!pinned)
|
|
44
52
|
selected.set(id, selection);
|
|
45
53
|
inactive.delete(id);
|
|
@@ -53,15 +61,19 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
53
61
|
}
|
|
54
62
|
function persist() {
|
|
55
63
|
capture();
|
|
56
|
-
pi.appendEntry(ENTRY, { selected: [...selected.values()], inactive: [...inactive.values()] });
|
|
64
|
+
pi.appendEntry(ENTRY, { selected: [...selected.values()], inactive: [...inactive.values()], features: [...selectedFeatures], inactiveFeatures: [...inactiveFeatures] });
|
|
57
65
|
}
|
|
58
66
|
function restore(ctx) {
|
|
59
67
|
selected = new Map();
|
|
60
68
|
inactive = new Map();
|
|
69
|
+
selectedFeatures = new Set();
|
|
70
|
+
inactiveFeatures = new Set();
|
|
61
71
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
62
72
|
if (entry.type !== "custom" || entry.customType !== ENTRY || !entry.data || typeof entry.data !== "object")
|
|
63
73
|
continue;
|
|
64
74
|
const data = entry.data;
|
|
75
|
+
selectedFeatures = new Set(Array.isArray(data.features) ? data.features.filter(value => value === "gateway" || value === "script") : []);
|
|
76
|
+
inactiveFeatures = new Set(Array.isArray(data.inactiveFeatures) ? data.inactiveFeatures.filter(value => value === "gateway" || value === "script") : []);
|
|
65
77
|
for (const [field, target] of [[data.selected, selected], [data.inactive, inactive]]) {
|
|
66
78
|
target.clear();
|
|
67
79
|
for (const selection of Array.isArray(field) ? field : []) {
|
|
@@ -72,11 +84,11 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
72
84
|
}
|
|
73
85
|
restoring = true;
|
|
74
86
|
}
|
|
75
|
-
function sync(config, cache, env,
|
|
87
|
+
function sync(config, cache, env, registerGateways) {
|
|
76
88
|
if (!restoring)
|
|
77
89
|
capture();
|
|
78
|
-
const
|
|
79
|
-
const existing = new Set(pi.getAllTools().map(tool =>
|
|
90
|
+
const previousNames = new Set([...registered.values()].map(item => item.name));
|
|
91
|
+
const existing = new Set(pi.getAllTools().map(tool => tool.name));
|
|
80
92
|
const specs = [];
|
|
81
93
|
for (const [server, definition] of Object.entries(env === null ? {} : config.mcpServers)) {
|
|
82
94
|
if (isServerDisabled(definition))
|
|
@@ -89,27 +101,26 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
89
101
|
if (tool.resourceUri)
|
|
90
102
|
continue;
|
|
91
103
|
const pinned = selection === true || (Array.isArray(selection) && selection.includes(tool.originalName));
|
|
92
|
-
|
|
93
|
-
specs.push({ spec: { ...tool, prefixedName, serverName: server }, selection: { server, tool: tool.originalName }, ref, pinned });
|
|
104
|
+
specs.push({ spec: { ...tool, prefixedName, serverName: server }, selection: { server, tool: tool.originalName }, name: prefixedName, pinned });
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
107
|
const counts = new Map();
|
|
97
|
-
for (const {
|
|
98
|
-
counts.set(
|
|
108
|
+
for (const { name } of specs)
|
|
109
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
99
110
|
available = new Set();
|
|
100
111
|
const enabled = [];
|
|
101
|
-
for (const { spec, selection,
|
|
112
|
+
for (const { spec, selection, name, pinned } of specs) {
|
|
102
113
|
const id = key(selection);
|
|
103
|
-
|
|
104
|
-
if ((!ref.namespace && reserved.has(ref.name)) || counts.get(refId) > 1 || (existing.has(refId) && !previousRefs.has(refId)))
|
|
114
|
+
if (reserved.has(name) || counts.get(name) > 1 || (existing.has(name) && !previousNames.has(name)))
|
|
105
115
|
continue;
|
|
106
116
|
available.add(id);
|
|
107
|
-
const
|
|
108
|
-
const fingerprint = JSON.stringify({ spec, ref, asyncTool });
|
|
117
|
+
const fingerprint = JSON.stringify({ spec, name });
|
|
109
118
|
if (registered.get(id)?.fingerprint !== fingerprint) {
|
|
110
119
|
pi.registerTool({
|
|
111
|
-
|
|
112
|
-
|
|
120
|
+
name,
|
|
121
|
+
namespace: { name: `mcp_${spec.serverName}` },
|
|
122
|
+
// Inactive direct tools are not callable through codemode or nested execution.
|
|
123
|
+
defaultActive: false,
|
|
113
124
|
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
114
125
|
label: spec.title ?? `MCP: ${spec.originalName}`,
|
|
115
126
|
description: spec.description || "(no description)",
|
|
@@ -119,34 +130,60 @@ export function createToolLoader(pi, getState, getInitPromise, beforeExecute) {
|
|
|
119
130
|
renderResult: renderMcpToolResult,
|
|
120
131
|
});
|
|
121
132
|
}
|
|
122
|
-
registered.set(id, { selection,
|
|
133
|
+
registered.set(id, { selection, name, fingerprint, pinned });
|
|
123
134
|
if (selected.has(id) || (pinned && !inactive.has(id)))
|
|
124
|
-
enabled.push(
|
|
135
|
+
enabled.push(name);
|
|
136
|
+
}
|
|
137
|
+
const permitted = new Set(pi.getAllTools().map(tool => tool.name));
|
|
138
|
+
const pins = specs.filter(item => item.pinned && available.has(key(item.selection)) && permitted.has(item.name)).map(item => item.spec);
|
|
139
|
+
const policy = registerGateways?.(pins);
|
|
140
|
+
if (policy) {
|
|
141
|
+
const permittedFeatures = new Set(pi.getAllTools().map(tool => tool.name));
|
|
142
|
+
const loaderMissing = !permittedFeatures.has("mcp_search");
|
|
143
|
+
features = {};
|
|
144
|
+
for (const feature of Object.keys(featureNames)) {
|
|
145
|
+
if (policy[feature] && permittedFeatures.has(featureNames[feature])) {
|
|
146
|
+
features[feature] = { eager: policy[feature].eager || loaderMissing };
|
|
147
|
+
if (selectedFeatures.has(feature) || (features[feature]?.eager && !inactiveFeatures.has(feature)))
|
|
148
|
+
enabled.push(featureNames[feature]);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
selectedFeatures.delete(feature);
|
|
152
|
+
inactiveFeatures.delete(feature);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
125
155
|
}
|
|
126
|
-
const permitted = new Set(pi.getAllTools().map(refKey));
|
|
127
|
-
const pins = specs.filter(item => item.pinned && available.has(key(item.selection)) && permitted.has(refKey(item.ref))).map(item => item.spec);
|
|
128
|
-
registerGateways?.(pins);
|
|
129
156
|
// Official 0.87 reactivates allowlisted definitions on registration. Apply our loadout last.
|
|
130
|
-
const
|
|
131
|
-
|
|
157
|
+
const ownedNames = new Set([...previousNames, ...[...registered.values()].map(item => item.name)]);
|
|
158
|
+
for (const feature of registeredFeatures)
|
|
159
|
+
ownedNames.add(featureNames[feature]);
|
|
160
|
+
setActive([...active().filter(name => !ownedNames.has(name)), ...enabled]);
|
|
132
161
|
restoring = false;
|
|
133
162
|
return pins;
|
|
134
163
|
}
|
|
135
|
-
function activate(matches) {
|
|
164
|
+
function activate(matches, enable = []) {
|
|
136
165
|
const requested = matches.flatMap(({ server, tool }) => {
|
|
137
166
|
const id = key({ server, tool: tool.originalName });
|
|
138
167
|
const registration = available.has(id) ? registered.get(id) : undefined;
|
|
139
168
|
return registration ? [registration] : [];
|
|
140
169
|
});
|
|
141
170
|
// No await between reading and extending the current loadout: parallel searches union their selections.
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
171
|
+
const permitted = new Set(pi.getAllTools().map(tool => tool.name));
|
|
172
|
+
const requestedFeatures = [...new Set(enable)].filter(feature => features[feature] && permitted.has(featureNames[feature]));
|
|
173
|
+
setActive([...active(), ...requested.map(item => item.name), ...requestedFeatures.map(feature => featureNames[feature])]);
|
|
174
|
+
const current = new Set(active());
|
|
175
|
+
const tools = requested.filter(item => current.has(item.name)).map(({ selection, name }) => {
|
|
145
176
|
selected.set(key(selection), selection);
|
|
146
|
-
return
|
|
177
|
+
return { name };
|
|
147
178
|
});
|
|
179
|
+
for (const feature of requestedFeatures) {
|
|
180
|
+
if (!current.has(featureNames[feature]))
|
|
181
|
+
continue;
|
|
182
|
+
selectedFeatures.add(feature);
|
|
183
|
+
tools.push({ name: featureNames[feature] });
|
|
184
|
+
}
|
|
148
185
|
persist();
|
|
149
186
|
return tools;
|
|
150
187
|
}
|
|
151
|
-
return {
|
|
188
|
+
return { registerFeature, restore, sync, activate, persist, selectedServers: () => [...new Set([...selected.values()].map(item => item.server))] };
|
|
152
189
|
}
|
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);
|