@fitchmultz/pi-mcp-adapter 5.0.0 → 6.0.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 +25 -1
- package/OAUTH.md +3 -3
- package/README.md +153 -85
- package/app-bridge.bundle.js +75 -33
- package/commands.ts +9 -6
- package/direct-tools.ts +25 -358
- package/dist/app-bridge.bundle.js +75 -33
- package/dist/commands.js +9 -6
- package/dist/direct-tools.d.ts +2 -4
- package/dist/direct-tools.js +21 -319
- package/dist/gateway-arguments.d.ts +33 -0
- package/dist/gateway-arguments.js +63 -0
- package/dist/index.js +181 -276
- package/dist/init.js +6 -20
- package/dist/mcp-code.d.ts +2 -1
- package/dist/mcp-code.js +87 -41
- package/dist/mcp-output-guard.d.ts +57 -2
- package/dist/mcp-output-guard.js +129 -24
- package/dist/mcp-panel.d.ts +1 -1
- package/dist/mcp-panel.js +34 -97
- package/dist/mcp-script-worker.mjs +5 -2
- package/dist/mcp-status.js +2 -1
- package/dist/metadata-cache.d.ts +1 -1
- package/dist/metadata-cache.js +25 -88
- package/dist/prompts.d.ts +1 -1
- package/dist/prompts.js +1 -1
- package/dist/proxy-modes.d.ts +40 -6
- package/dist/proxy-modes.js +270 -485
- package/dist/resource-tools.d.ts +15 -0
- package/dist/resource-tools.js +26 -1
- package/dist/sampling-handler.js +4 -1
- package/dist/search-ranking.d.ts +0 -2
- package/dist/search-ranking.js +66 -137
- package/dist/state.d.ts +1 -1
- package/dist/tool-loader.d.ts +32 -0
- package/dist/tool-loader.js +152 -0
- package/dist/tool-metadata.d.ts +67 -0
- package/dist/tool-metadata.js +23 -17
- package/dist/tool-registrar.d.ts +4 -4
- package/dist/tool-registrar.js +18 -13
- package/dist/tool-result-renderer.d.ts +3 -0
- package/dist/tool-result-renderer.js +8 -3
- package/dist/types.d.ts +11 -36
- package/dist/utils.js +2 -4
- package/gateway-arguments.ts +73 -0
- package/index.ts +166 -299
- package/init.ts +6 -20
- package/mcp-code.ts +71 -45
- package/mcp-output-guard.ts +145 -27
- package/mcp-panel.ts +38 -109
- package/mcp-script-worker.mjs +5 -2
- package/mcp-status.ts +2 -1
- package/metadata-cache.ts +31 -93
- package/package.json +14 -9
- package/prompts.ts +1 -1
- package/proxy-modes.ts +265 -517
- package/resource-tools.ts +36 -1
- package/sampling-handler.ts +4 -1
- package/scripts/build-app-bridge.mjs +15 -0
- package/scripts/build.mjs +3 -2
- package/scripts/prepare.mjs +1 -0
- package/search-ranking.ts +71 -135
- package/skills/mcp-scripting/SKILL.md +67 -20
- package/state.ts +1 -1
- package/tool-loader.ts +153 -0
- package/tool-metadata.ts +22 -17
- package/tool-registrar.ts +18 -13
- package/tool-result-renderer.ts +10 -2
- package/types.ts +15 -40
- package/utils.ts +2 -4
- package/dist/ts-shape.d.ts +0 -2
- package/dist/ts-shape.js +0 -147
- package/ts-shape.ts +0 -145
package/index.ts
CHANGED
|
@@ -5,20 +5,24 @@ import type { McpOAuthRuntime } from "./mcp-auth-flow.ts";
|
|
|
5
5
|
import { Type } from "typebox";
|
|
6
6
|
import { showStatus, showTools, showPrompts, reconnectServer, reconnectServers, authenticateServer, logoutServer, openMcpAuthPanel, openMcpPanel, openMcpSetup } from "./commands.ts";
|
|
7
7
|
import { cloneMcpConfig, isPathInsideProject, loadMcpConfig, writeProjectServerDisabledOverride } from "./config.ts";
|
|
8
|
-
import { buildProxyDescription,
|
|
9
|
-
import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.ts";
|
|
10
|
-
import { getMetadataCachePath, loadMetadataCache, type MetadataCache } from "./metadata-cache.ts";
|
|
8
|
+
import { buildProxyDescription, directToolSelection, getMissingConfiguredDirectToolServers } from "./direct-tools.ts";
|
|
9
|
+
import { flushMetadataCache, initializeMcp, lazyConnect, updateStatusBar } from "./init.ts";
|
|
10
|
+
import { getMetadataCachePath, isServerCacheValid, loadMetadataCache, reconstructToolMetadata, type MetadataCache } from "./metadata-cache.ts";
|
|
11
11
|
import { createPromptCommand, resolveCachedPrompts } from "./prompts.ts";
|
|
12
12
|
import { logger } from "./logger.ts";
|
|
13
13
|
import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, executeDescribe, executeInstructions, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.ts";
|
|
14
|
-
import { formatTerminalError
|
|
14
|
+
import { formatTerminalError } from "./utils.ts";
|
|
15
15
|
import { createOAuthRuntime, shutdownOAuth } from "./mcp-auth-flow.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.ts";
|
|
17
17
|
import { toolErrorOverride } from "./error-signal.ts";
|
|
18
18
|
import { createMcpRuntimeOwner, createOwnedUi, isAbortError, type McpRuntimeOwner } from "./runtime-owner.ts";
|
|
19
19
|
import { publishMcpStatusShutdown } from "./mcp-status.ts";
|
|
20
20
|
import { DEFAULT_MCP_SCRIPT_TIMEOUT_MS, runMcpScript } from "./mcp-code.ts";
|
|
21
|
-
import { MAX_PAGE_SIZE,
|
|
21
|
+
import { MAX_PAGE_SIZE, paginate, rankToolMatches } from "./search-ranking.ts";
|
|
22
|
+
import { createToolLoader, supportsNativeAsync, type SearchAPI } from "./tool-loader.ts";
|
|
23
|
+
import { gatewayParameters, prepareGatewayArguments, type GatewayArguments } from "./gateway-arguments.ts";
|
|
24
|
+
import { executeResourceList, executeResourceRead } from "./resource-tools.ts";
|
|
25
|
+
import { guardMcpOutput, guardedMcpDetails, readMcpResult, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
|
|
22
26
|
import { abortable } from "./abort.ts";
|
|
23
27
|
import { onSessionCheckpoint, prepareMcpCheckpoint } from "./checkpoint.ts";
|
|
24
28
|
|
|
@@ -33,13 +37,7 @@ export {
|
|
|
33
37
|
|
|
34
38
|
const INIT_WAIT_TIMEOUT_MS = 30_000;
|
|
35
39
|
const INIT_WAIT_TIMED_OUT: unique symbol = Symbol("init-wait-timed-out");
|
|
36
|
-
|
|
37
|
-
const inactiveDirectToolsBySession = (() => {
|
|
38
|
-
const shared = globalThis as typeof globalThis & {
|
|
39
|
-
[inactiveDirectToolsKey]?: Map<string, Set<string>>;
|
|
40
|
-
};
|
|
41
|
-
return shared[inactiveDirectToolsKey] ??= new Map<string, Set<string>>();
|
|
42
|
-
})();
|
|
40
|
+
|
|
43
41
|
|
|
44
42
|
async function awaitWithTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T | typeof INIT_WAIT_TIMED_OUT> {
|
|
45
43
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -91,55 +89,14 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
91
89
|
const earlyCache: MetadataCache | null = null;
|
|
92
90
|
const envRaw = process.env.MCP_DIRECT_TOOLS;
|
|
93
91
|
const envDirectToolOverride = envRaw?.split(",").map(s => s.trim()).filter(Boolean);
|
|
94
|
-
const
|
|
92
|
+
const toolLoader = createToolLoader(pi, () => state, () => initPromise, beforeExecute);
|
|
95
93
|
const deactivatedTools = new Set<string>();
|
|
96
|
-
|
|
97
|
-
let
|
|
94
|
+
let runtimeCwd: string | undefined;
|
|
95
|
+
let nativeAsyncSupported = false;
|
|
98
96
|
let proxyToolRegistered = false;
|
|
99
97
|
let proxyToolDescription: string | null = null;
|
|
100
98
|
let directToolsFrozen = false;
|
|
101
99
|
|
|
102
|
-
function directToolFingerprint(spec: DirectToolSpec): string {
|
|
103
|
-
return JSON.stringify({
|
|
104
|
-
serverName: spec.serverName,
|
|
105
|
-
originalName: spec.originalName,
|
|
106
|
-
prefixedName: spec.prefixedName,
|
|
107
|
-
description: spec.description,
|
|
108
|
-
inputSchema: spec.inputSchema,
|
|
109
|
-
annotations: spec.annotations,
|
|
110
|
-
resourceUri: spec.resourceUri,
|
|
111
|
-
uiResourceUri: spec.uiResourceUri,
|
|
112
|
-
uiStreamMode: spec.uiStreamMode,
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function registerDirectTool(spec: DirectToolSpec): void {
|
|
117
|
-
const execute = createDirectToolExecutor(() => state, () => initPromise, spec, beforeExecute);
|
|
118
|
-
pi.registerTool({
|
|
119
|
-
...(beforeExecute ? { executionMode: "sequential" as const } : {}),
|
|
120
|
-
name: spec.prefixedName,
|
|
121
|
-
label: `MCP: ${spec.originalName}`,
|
|
122
|
-
description: spec.description || "(no description)",
|
|
123
|
-
promptSnippet: truncateAtWord(spec.description, 100) || `MCP tool from ${spec.serverName}`,
|
|
124
|
-
parameters: Type.Unsafe<Record<string, unknown>>(normalizeDirectToolInputSchema(spec.inputSchema)),
|
|
125
|
-
execute,
|
|
126
|
-
renderCall: createMcpDirectToolCallRenderer(spec.prefixedName),
|
|
127
|
-
renderResult: renderMcpToolResult,
|
|
128
|
-
});
|
|
129
|
-
if (preservedInactiveTools.has(spec.prefixedName)) {
|
|
130
|
-
const activeTools = pi.getActiveTools();
|
|
131
|
-
if (activeTools.includes(spec.prefixedName)) {
|
|
132
|
-
pi.setActiveTools(activeTools.filter((name) => name !== spec.prefixedName));
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function resolveCurrentDirectTools(config: McpConfig, cache: MetadataCache | null): DirectToolSpec[] {
|
|
138
|
-
if (envRaw === "__none__") return [];
|
|
139
|
-
const prefix = config.settings?.toolPrefix ?? "server";
|
|
140
|
-
return resolveDirectTools(config, cache, prefix, envDirectToolOverride);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
100
|
function deactivateTools(toolNames: string[]): void {
|
|
144
101
|
if (toolNames.length === 0) return;
|
|
145
102
|
const remove = new Set(toolNames);
|
|
@@ -153,44 +110,6 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
153
110
|
}
|
|
154
111
|
}
|
|
155
112
|
|
|
156
|
-
function syncDirectTools(config: McpConfig, cache: MetadataCache | null): {
|
|
157
|
-
specs: DirectToolSpec[];
|
|
158
|
-
added: string[];
|
|
159
|
-
updated: string[];
|
|
160
|
-
deactivated: string[];
|
|
161
|
-
} {
|
|
162
|
-
const specs = resolveCurrentDirectTools(config, cache);
|
|
163
|
-
const nextNames = new Set(specs.map((spec) => spec.prefixedName));
|
|
164
|
-
const added: string[] = [];
|
|
165
|
-
const updated: string[] = [];
|
|
166
|
-
const deactivated: string[] = [];
|
|
167
|
-
|
|
168
|
-
for (const spec of specs) {
|
|
169
|
-
const fingerprint = directToolFingerprint(spec);
|
|
170
|
-
const previous = registeredDirectTools.get(spec.prefixedName);
|
|
171
|
-
if (previous !== fingerprint) {
|
|
172
|
-
registerDirectTool(spec);
|
|
173
|
-
registeredDirectTools.set(spec.prefixedName, fingerprint);
|
|
174
|
-
if (deactivatedTools.delete(spec.prefixedName)) {
|
|
175
|
-
const activeTools = pi.getActiveTools();
|
|
176
|
-
if (!activeTools.includes(spec.prefixedName)) {
|
|
177
|
-
pi.setActiveTools([...activeTools, spec.prefixedName]);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
(previous ? updated : added).push(spec.prefixedName);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
for (const toolName of [...registeredDirectTools.keys()]) {
|
|
185
|
-
if (nextNames.has(toolName)) continue;
|
|
186
|
-
registeredDirectTools.delete(toolName);
|
|
187
|
-
deactivated.push(toolName);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
deactivateTools(deactivated);
|
|
191
|
-
return { specs, added, updated, deactivated };
|
|
192
|
-
}
|
|
193
|
-
|
|
194
113
|
function applyDirectToolConfigChanges(changes: Map<string, true | string[] | false>): void {
|
|
195
114
|
if (!state) return;
|
|
196
115
|
for (const [serverName, value] of changes) {
|
|
@@ -203,16 +122,10 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
203
122
|
function syncToolSurface(ctx?: ExtensionContext): void {
|
|
204
123
|
const config = state?.config ?? earlyConfig;
|
|
205
124
|
const cache = loadMetadataCache(state?.metadataCacheEnabled ?? false);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
if (changed > 0 && ctx?.hasUI) {
|
|
211
|
-
ctx.ui.notify(
|
|
212
|
-
`MCP: direct tools refreshed (+${result.added.length}, ~${result.updated.length}, -${result.deactivated.length})`,
|
|
213
|
-
"info",
|
|
214
|
-
);
|
|
215
|
-
}
|
|
125
|
+
toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => {
|
|
126
|
+
syncProxyTool(config, cache, specs);
|
|
127
|
+
syncScriptTool(config);
|
|
128
|
+
});
|
|
216
129
|
}
|
|
217
130
|
|
|
218
131
|
const registeredPromptCommands = new Set<string>();
|
|
@@ -277,13 +190,17 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
277
190
|
}
|
|
278
191
|
syncToolSurface(ctx);
|
|
279
192
|
};
|
|
193
|
+
for (const server of toolLoader.selectedServers()) {
|
|
194
|
+
if (!nextState.toolMetadata.has(server) && nextState.config.mcpServers[server]) await lazyConnect(nextState, server, owner.signal);
|
|
195
|
+
}
|
|
196
|
+
owner.throwIfInactive();
|
|
280
197
|
syncPromptCommands();
|
|
281
198
|
syncToolSurface(ctx);
|
|
282
199
|
updateStatusBar(nextState);
|
|
283
200
|
initPromise = null;
|
|
284
201
|
if (nextState.config.settings?.freezeDirectTools === true) {
|
|
285
202
|
directToolsFrozen = true;
|
|
286
|
-
logger.info("MCP: direct tools frozen after initial sync — reconnects won't rebuild the system prompt; use mcp({ connect: \"server\" }) to rediscover");
|
|
203
|
+
logger.info("MCP: direct tools frozen after initial sync — reconnects won't rebuild the system prompt; use mcp({ action: \"connect\", server: \"server\" }) to rediscover");
|
|
287
204
|
}
|
|
288
205
|
}).catch(async err => {
|
|
289
206
|
if (!owner.isActive() || generation !== lifecycleGeneration) {
|
|
@@ -307,7 +224,10 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
307
224
|
});
|
|
308
225
|
}
|
|
309
226
|
|
|
310
|
-
|
|
227
|
+
async function startSession(ctx: ExtensionContext) {
|
|
228
|
+
runtimeCwd = ctx.cwd;
|
|
229
|
+
nativeAsyncSupported = supportsNativeAsync(ctx);
|
|
230
|
+
directToolsFrozen = false;
|
|
311
231
|
const generation = ++lifecycleGeneration;
|
|
312
232
|
const previousState = state;
|
|
313
233
|
const previousOwner = currentOwner;
|
|
@@ -353,16 +273,13 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
353
273
|
}
|
|
354
274
|
const metadataCacheEnabled = ctx.isProjectTrusted()
|
|
355
275
|
|| !isPathInsideProject(getMetadataCachePath(), ctx.cwd);
|
|
356
|
-
|
|
357
|
-
preservedInactiveTools.clear();
|
|
358
|
-
for (const toolName of inactiveDirectToolsBySession.get(activationKey) ?? []) {
|
|
359
|
-
preservedInactiveTools.add(toolName);
|
|
360
|
-
}
|
|
276
|
+
toolLoader.restore(ctx);
|
|
361
277
|
registerPromptCommands(resolveCachedPrompts(runtimeConfig, metadataCacheEnabled));
|
|
362
278
|
const runtimeCache = loadMetadataCache(metadataCacheEnabled);
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
279
|
+
toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => {
|
|
280
|
+
syncProxyTool(runtimeConfig, runtimeCache, specs);
|
|
281
|
+
syncScriptTool(runtimeConfig);
|
|
282
|
+
});
|
|
366
283
|
|
|
367
284
|
const initialization = startInitialization(ctx, owner, oauthRuntime, runtimeConfig, generation);
|
|
368
285
|
if (envRaw !== undefined && envRaw !== "__none__") {
|
|
@@ -375,6 +292,19 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
375
292
|
await initialization;
|
|
376
293
|
}
|
|
377
294
|
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
pi.on("session_start", (_event, ctx) => startSession(ctx));
|
|
298
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
299
|
+
toolLoader.restore(ctx);
|
|
300
|
+
syncToolSurface(ctx);
|
|
301
|
+
});
|
|
302
|
+
pi.on("model_select", (_event, ctx) => {
|
|
303
|
+
nativeAsyncSupported = supportsNativeAsync(ctx);
|
|
304
|
+
if (state) syncToolSurface(ctx);
|
|
305
|
+
});
|
|
306
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
307
|
+
if (ctx.cwd !== runtimeCwd) await startSession(ctx);
|
|
378
308
|
});
|
|
379
309
|
|
|
380
310
|
onSessionCheckpoint(pi, async event => {
|
|
@@ -384,14 +314,8 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
384
314
|
});
|
|
385
315
|
|
|
386
316
|
pi.on("session_shutdown", async () => {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
const inactiveTools = new Set(
|
|
390
|
-
[...registeredDirectTools.keys()].filter((name) => !activeTools.has(name)),
|
|
391
|
-
);
|
|
392
|
-
if (inactiveTools.size > 0) inactiveDirectToolsBySession.set(activationKey, inactiveTools);
|
|
393
|
-
else inactiveDirectToolsBySession.delete(activationKey);
|
|
394
|
-
}
|
|
317
|
+
// Cached tool selections exist before background server initialization finishes.
|
|
318
|
+
toolLoader.persist();
|
|
395
319
|
++lifecycleGeneration;
|
|
396
320
|
const currentState = state;
|
|
397
321
|
const owner = currentOwner;
|
|
@@ -615,6 +539,68 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
615
539
|
},
|
|
616
540
|
});
|
|
617
541
|
|
|
542
|
+
async function getToolState(signal?: AbortSignal) {
|
|
543
|
+
const owner = currentOwner;
|
|
544
|
+
if (!state && initPromise) {
|
|
545
|
+
try {
|
|
546
|
+
const initialized = await awaitWithTimeout(abortable(initPromise, signal), INIT_WAIT_TIMEOUT_MS);
|
|
547
|
+
if (initialized === INIT_WAIT_TIMED_OUT) return {
|
|
548
|
+
content: [{ type: "text" as const, text: "MCP initialization is still in progress. Try again shortly." }],
|
|
549
|
+
details: { error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
|
|
550
|
+
};
|
|
551
|
+
owner?.throwIfInactive();
|
|
552
|
+
state = initialized;
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (signal?.aborted || (owner && isAbortError(error, owner.signal))) throw error;
|
|
555
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
556
|
+
return { content: [{ type: "text" as const, text: `MCP initialization failed: ${message}` }], details: { error: "init_failed", message } };
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
owner?.throwIfInactive();
|
|
560
|
+
return state ?? { content: [{ type: "text" as const, text: "MCP not initialized" }], details: { error: "not_initialized" } };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function searchAndLoad(params: { query: string; server?: string; limit?: number; offset?: number }, signal: AbortSignal | undefined, ctx: ExtensionContext) {
|
|
564
|
+
const ready = await getToolState(signal);
|
|
565
|
+
if ("content" in ready) return { ...ready, tools: [] };
|
|
566
|
+
if (params.server && !ready.toolMetadata.has(params.server)) {
|
|
567
|
+
const discovery = await executeConnect(ready, params.server, signal);
|
|
568
|
+
if (discovery.details.error) return { ...discovery, tools: [] };
|
|
569
|
+
}
|
|
570
|
+
syncToolSurface(ctx);
|
|
571
|
+
const matches = params.query.trim() ? rankToolMatches(ready, params.query, params.server)
|
|
572
|
+
: 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)) : [];
|
|
573
|
+
const page = paginate(matches, params.offset ?? 0, params.limit ?? 5);
|
|
574
|
+
const tools = toolLoader.activate(page.items);
|
|
575
|
+
const result = executeSearch(ready, params.query, params.server, tools.length < page.items.length, params.limit ?? 5, params.offset);
|
|
576
|
+
const guarded = await guardMcpOutput([
|
|
577
|
+
...result.content,
|
|
578
|
+
{ type: "text", text: tools.length ? `Loaded for the next request: ${JSON.stringify(tools)}` : "No direct tools loaded. Use mcp action call or tools.call in mcp_script with the returned exact names." },
|
|
579
|
+
], resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
|
|
580
|
+
return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded), loaded: tools }, tools };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const searchDefinition = {
|
|
584
|
+
name: "mcp_search", label: "MCP Search",
|
|
585
|
+
description: "Discover MCP tools by name, description, and parameter guidance, and load matching tools with their complete schemas for the next request. Start with a specific task query. Specify server to discover an uncached server; an empty query with server browses it. Results report incomplete catalog coverage. Resources use mcp action resources/read-resource.",
|
|
586
|
+
parameters: Type.Object({
|
|
587
|
+
query: Type.String(),
|
|
588
|
+
server: Type.Optional(Type.String()),
|
|
589
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Tools to load (default 5)." })),
|
|
590
|
+
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
591
|
+
}),
|
|
592
|
+
renderResult: renderMcpToolResult,
|
|
593
|
+
async execute(toolCallId: string, params: { query: string; server?: string; limit?: number; offset?: number }, signal: AbortSignal | undefined, _onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, ctx: ExtensionContext) {
|
|
594
|
+
if (beforeExecute) {
|
|
595
|
+
if (currentOwner) await currentOwner.runCallback("beforeExecute", () => beforeExecute(toolCallId, ctx));
|
|
596
|
+
else await beforeExecute(toolCallId, ctx);
|
|
597
|
+
}
|
|
598
|
+
return searchAndLoad(params, signal, ctx);
|
|
599
|
+
},
|
|
600
|
+
};
|
|
601
|
+
if (toolLoader.native) (pi as SearchAPI).registerToolSearch!(searchDefinition);
|
|
602
|
+
else pi.registerTool(searchDefinition);
|
|
603
|
+
|
|
618
604
|
let scriptToolRegistered = false;
|
|
619
605
|
|
|
620
606
|
function registerScriptTool(): void {
|
|
@@ -623,7 +609,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
623
609
|
...(beforeExecute ? { executionMode: "sequential" as const } : {}),
|
|
624
610
|
name: "mcp_script",
|
|
625
611
|
label: "MCP Script",
|
|
626
|
-
description: "
|
|
612
|
+
description: "Compose MCP calls with trusted JavaScript: await tools.search({query, server?}), tools.describe({path}), tools.call(path,args). Describe returns complete JSON schemas. Calls return {ok,data} with raw MCP results, or {ok:false,error,data?}. Resources: tools.resources({server}), tools.readResource({server,uri}). Saved results: tools.readResult({ref,path?,fields?,offset?,limit?}); never repeats a remote call. Use emit(value) to return selected data or content blocks. No Node, filesystem, or network globals. Prefer a direct tool for one call; use scripts for loops, filtering and chains.",
|
|
627
613
|
promptSnippet: "Batch multiple MCP tool calls in one JavaScript request (loop, filter, chain)",
|
|
628
614
|
parameters: Type.Object({
|
|
629
615
|
code: Type.String({ description: "Trusted JavaScript MCP script. Use tools.<prefixedToolName>(args) and emit(value)." }),
|
|
@@ -633,35 +619,9 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
633
619
|
}),
|
|
634
620
|
renderResult: renderMcpToolResult,
|
|
635
621
|
async execute(toolCallId: string, params: { code: string; timeoutMs?: number }, signal: AbortSignal | undefined, _onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, ctx: ExtensionContext) {
|
|
636
|
-
const
|
|
637
|
-
if (
|
|
638
|
-
|
|
639
|
-
const initialized = await awaitWithTimeout(abortable(initPromise, signal), INIT_WAIT_TIMEOUT_MS);
|
|
640
|
-
if (initialized === INIT_WAIT_TIMED_OUT) {
|
|
641
|
-
return {
|
|
642
|
-
content: [{ type: "text" as const, text: "MCP initialization is still in progress. Try again shortly." }],
|
|
643
|
-
details: { mode: "script", error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
|
|
644
|
-
};
|
|
645
|
-
}
|
|
646
|
-
executeOwner?.throwIfInactive();
|
|
647
|
-
state = initialized;
|
|
648
|
-
} catch (error) {
|
|
649
|
-
if (signal?.aborted || (executeOwner && isAbortError(error, executeOwner.signal))) throw error;
|
|
650
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
651
|
-
return {
|
|
652
|
-
content: [{ type: "text" as const, text: `MCP initialization failed: ${message}` }],
|
|
653
|
-
details: { mode: "script", error: "init_failed", message },
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
if (!state) {
|
|
658
|
-
return {
|
|
659
|
-
content: [{ type: "text" as const, text: "MCP not initialized" }],
|
|
660
|
-
details: { mode: "script", error: "not_initialized" },
|
|
661
|
-
};
|
|
662
|
-
}
|
|
663
|
-
executeOwner?.throwIfInactive();
|
|
664
|
-
return runMcpScript(state, params.code, params.timeoutMs ?? options.defaultScriptTimeoutMs, getPiTools, signal, toolCallId,
|
|
622
|
+
const ready = await getToolState(signal);
|
|
623
|
+
if ("content" in ready) return ready;
|
|
624
|
+
return runMcpScript(ready, params.code, params.timeoutMs ?? options.defaultScriptTimeoutMs, getPiTools, signal, toolCallId,
|
|
665
625
|
beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined);
|
|
666
626
|
},
|
|
667
627
|
});
|
|
@@ -684,157 +644,55 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
684
644
|
function registerProxyTool(description: string): void {
|
|
685
645
|
pi.registerTool({
|
|
686
646
|
...(beforeExecute ? { executionMode: "sequential" as const } : {}),
|
|
687
|
-
name: "mcp",
|
|
688
|
-
|
|
689
|
-
description,
|
|
690
|
-
promptSnippet: "MCP gateway — status, search, describe, auth, and single MCP tool calls",
|
|
647
|
+
name: "mcp", label: "MCP", description,
|
|
648
|
+
promptSnippet: "MCP server operations, resources, saved result readback, and fallback tool calls",
|
|
691
649
|
renderCall: renderMcpProxyToolCall,
|
|
692
|
-
parameters:
|
|
693
|
-
|
|
694
|
-
args: Type.Optional(Type.Union([
|
|
695
|
-
Type.String({ description: "Arguments as a JSON string (e.g., '{\"key\": \"value\"}')" }),
|
|
696
|
-
Type.Object({}, {
|
|
697
|
-
additionalProperties: true,
|
|
698
|
-
description: 'Arguments as a JSON object (e.g., { "key": "value" })',
|
|
699
|
-
}),
|
|
700
|
-
], { description: "Tool arguments as a JSON object, or as a JSON string encoding one" })),
|
|
701
|
-
connect: Type.Optional(Type.String({ description: "Server name to connect (lazy connect + metadata refresh)" })),
|
|
702
|
-
describe: Type.Optional(Type.String({ maxLength: MAX_TOOL_NAME_LENGTH, description: "Tool name to describe (shows parameters)" })),
|
|
703
|
-
instructions: Type.Optional(Type.String({ description: "Server name to show that server's usage instructions" })),
|
|
704
|
-
search: Type.Optional(Type.String({ description: "Search tools by name/description" })),
|
|
705
|
-
includeSchemas: Type.Optional(Type.Boolean({ description: "Include parameter schemas in search results (default: true)" })),
|
|
706
|
-
limit: Type.Optional(Type.Number({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Maximum search or server-list results to return (default: 12, max: 100)" })),
|
|
707
|
-
offset: Type.Optional(Type.Number({ minimum: 0, description: "Search or server-list result offset (default: 0)" })),
|
|
708
|
-
server: Type.Optional(Type.String({ description: "Filter to specific server (also disambiguates tool calls)" })),
|
|
709
|
-
action: Type.Optional(Type.String({ description: "Action: 'ui-messages', 'auth-start', or 'auth-complete'" })),
|
|
710
|
-
}),
|
|
650
|
+
parameters: gatewayParameters,
|
|
651
|
+
prepareArguments: prepareGatewayArguments,
|
|
711
652
|
renderResult: renderMcpToolResult,
|
|
712
|
-
async execute(toolCallId: string,
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
instructions?: string;
|
|
718
|
-
search?: string;
|
|
719
|
-
includeSchemas?: boolean;
|
|
720
|
-
limit?: number;
|
|
721
|
-
offset?: number;
|
|
722
|
-
server?: string;
|
|
723
|
-
action?: string;
|
|
724
|
-
}, signal: AbortSignal | undefined, _onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, ctx: ExtensionContext) {
|
|
725
|
-
// Non-call modes have no resolved MCP operation; hosts keep their conservative policy.
|
|
726
|
-
if (!params.tool || params.action === "ui-messages" || params.action === "auth-start" || params.action === "auth-complete") {
|
|
727
|
-
if (beforeExecute) {
|
|
728
|
-
if (currentOwner) await currentOwner.runCallback("beforeExecute", () => beforeExecute(toolCallId, ctx));
|
|
729
|
-
else await beforeExecute(toolCallId, ctx);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
const executeOwner = currentOwner;
|
|
733
|
-
let parsedArgs: Record<string, unknown> | undefined;
|
|
734
|
-
if (params.args !== undefined && params.args !== "") {
|
|
735
|
-
let args: unknown;
|
|
736
|
-
if (typeof params.args === "string") {
|
|
737
|
-
try {
|
|
738
|
-
args = JSON.parse(params.args);
|
|
739
|
-
} catch (error) {
|
|
740
|
-
if (error instanceof SyntaxError) {
|
|
741
|
-
throw new Error(`Invalid args JSON: ${error.message}`, { cause: error });
|
|
742
|
-
}
|
|
743
|
-
throw error;
|
|
744
|
-
}
|
|
745
|
-
} else {
|
|
746
|
-
args = params.args;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
750
|
-
const gotType = Array.isArray(args) ? "array" : args === null ? "null" : typeof args;
|
|
751
|
-
throw new Error(`Invalid args: expected a JSON object, got ${gotType}`);
|
|
752
|
-
}
|
|
753
|
-
parsedArgs = args as Record<string, unknown>;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
if (!state && initPromise) {
|
|
757
|
-
try {
|
|
758
|
-
const initialized = await awaitWithTimeout(abortable(initPromise, signal), INIT_WAIT_TIMEOUT_MS);
|
|
759
|
-
if (initialized === INIT_WAIT_TIMED_OUT) {
|
|
760
|
-
return {
|
|
761
|
-
content: [{ type: "text" as const, text: "MCP initialization is still in progress. Try again shortly." }],
|
|
762
|
-
details: { error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
executeOwner?.throwIfInactive();
|
|
766
|
-
state = initialized;
|
|
767
|
-
} catch (error) {
|
|
768
|
-
if (signal?.aborted || (executeOwner && isAbortError(error, executeOwner.signal))) throw error;
|
|
769
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
770
|
-
return {
|
|
771
|
-
content: [{ type: "text" as const, text: `MCP initialization failed: ${message}` }],
|
|
772
|
-
details: { error: "init_failed", message },
|
|
773
|
-
};
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
if (!state) {
|
|
777
|
-
return {
|
|
778
|
-
content: [{ type: "text" as const, text: "MCP not initialized" }],
|
|
779
|
-
details: { error: "not_initialized" },
|
|
780
|
-
};
|
|
781
|
-
}
|
|
782
|
-
executeOwner?.throwIfInactive();
|
|
783
|
-
|
|
784
|
-
if (params.action === "ui-messages") {
|
|
785
|
-
return executeUiMessages(state);
|
|
653
|
+
async execute(toolCallId: string, rawParams: GatewayArguments, signal: AbortSignal | undefined, _onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined, ctx: ExtensionContext) {
|
|
654
|
+
const params = prepareGatewayArguments(rawParams);
|
|
655
|
+
if (beforeExecute && params.action !== "call" && params.action !== "read-resource") {
|
|
656
|
+
if (currentOwner) await currentOwner.runCallback("beforeExecute", () => beforeExecute(toolCallId, ctx));
|
|
657
|
+
else await beforeExecute(toolCallId, ctx);
|
|
786
658
|
}
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
659
|
+
const ready = await getToolState(signal);
|
|
660
|
+
if ("content" in ready) return ready;
|
|
661
|
+
const present = async (result: Awaited<ReturnType<typeof executeCall>>) => {
|
|
662
|
+
const guarded = await guardMcpOutput(result.content, resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
|
|
663
|
+
return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded) } };
|
|
664
|
+
};
|
|
665
|
+
const beforeDispatch = beforeExecute
|
|
666
|
+
? (callSignal: AbortSignal | undefined, operation: import("./types.ts").McpOperationContext) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation)
|
|
667
|
+
: undefined;
|
|
668
|
+
if (params.server && ["list", "search", "describe", "instructions"].includes(params.action) && !ready.toolMetadata.has(params.server)) {
|
|
669
|
+
const discovery = await executeConnect(ready, params.server, signal);
|
|
670
|
+
if (discovery.details.error) return discovery;
|
|
671
|
+
syncToolSurface(ctx);
|
|
797
672
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
673
|
+
switch (params.action) {
|
|
674
|
+
case "ui-messages": return executeUiMessages(ready);
|
|
675
|
+
case "auth-start": return executeAuthStart(ready, params.server!, signal);
|
|
676
|
+
case "auth-complete": {
|
|
677
|
+
const input = params.args?.redirectUrl ?? params.args?.code ?? params.args?.input;
|
|
678
|
+
if (input !== undefined && (typeof input !== "string" || input.trim().length === 0)) throw new Error("auth-complete requires a non-empty redirectUrl, code, or input");
|
|
679
|
+
return executeAuthComplete(ready, params.server!, input, signal);
|
|
804
680
|
}
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
};
|
|
681
|
+
case "call": return executeCall(ready, params.tool!, params.args, params.server, getPiTools, signal, { toolCallId }, beforeDispatch);
|
|
682
|
+
case "connect": {
|
|
683
|
+
const result = await executeConnect(ready, params.server!, signal, params.limit, params.offset);
|
|
684
|
+
syncToolSurface(ctx);
|
|
685
|
+
return present(result);
|
|
811
686
|
}
|
|
812
|
-
return
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
return
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
if (params.connect) {
|
|
821
|
-
const result = await executeConnect(state, params.connect, signal, params.limit, params.offset);
|
|
822
|
-
syncToolSurface(ctx);
|
|
823
|
-
return result;
|
|
687
|
+
case "describe": return present(executeDescribe(ready, params.tool!, params.server));
|
|
688
|
+
case "instructions": return present(executeInstructions(ready, params.server!));
|
|
689
|
+
case "search": return present(executeSearch(ready, params.query!, params.server, params.includeSchemas, params.limit, params.offset));
|
|
690
|
+
case "list": return present(executeList(ready, params.server!, params.limit, params.offset));
|
|
691
|
+
case "resources": return present(await executeResourceList(ready, params.server!, params.limit, params.offset, signal));
|
|
692
|
+
case "read-resource": return executeResourceRead(ready, params.server!, params.uri!, signal, { toolCallId }, beforeDispatch);
|
|
693
|
+
case "read-result": return readMcpResult({ ref: params.ref!, ...(params.path !== undefined ? { path: params.path } : {}), ...(params.fields ? { fields: params.fields } : {}), ...(params.offset !== undefined ? { offset: params.offset } : {}), ...(params.limit !== undefined ? { limit: params.limit } : {}) }, resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
|
|
694
|
+
case "status": return present(executeStatus(ready));
|
|
824
695
|
}
|
|
825
|
-
if (params.describe) {
|
|
826
|
-
return executeDescribe(state, params.describe);
|
|
827
|
-
}
|
|
828
|
-
if (params.instructions) {
|
|
829
|
-
return executeInstructions(state, params.instructions);
|
|
830
|
-
}
|
|
831
|
-
if (params.search !== undefined) {
|
|
832
|
-
return executeSearch(state, params.search, params.server, params.includeSchemas, params.limit, params.offset);
|
|
833
|
-
}
|
|
834
|
-
if (params.server) {
|
|
835
|
-
return executeList(state, params.server, params.limit, params.offset);
|
|
836
|
-
}
|
|
837
|
-
return executeStatus(state);
|
|
838
696
|
},
|
|
839
697
|
});
|
|
840
698
|
proxyToolRegistered = true;
|
|
@@ -847,8 +705,18 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
847
705
|
cache,
|
|
848
706
|
envRaw === undefined || envRaw === "__none__" ? undefined : envDirectToolOverride,
|
|
849
707
|
);
|
|
708
|
+
const hasPinnedResources = envRaw !== "__none__" && Object.entries(config.mcpServers).some(([server, definition]) => {
|
|
709
|
+
if (definition.disabled === true) return false;
|
|
710
|
+
const selection = directToolSelection(config, server, envDirectToolOverride);
|
|
711
|
+
if (!selection) return false;
|
|
712
|
+
const entry = cache?.servers[server];
|
|
713
|
+
const metadata = state?.toolMetadata.get(server) ?? (entry && isServerCacheValid(entry, definition)
|
|
714
|
+
? reconstructToolMetadata(server, entry, config.settings?.toolPrefix ?? "server", definition) : []);
|
|
715
|
+
return metadata.some(tool => tool.resourceUri && (selection === true || selection.includes(tool.originalName)));
|
|
716
|
+
});
|
|
850
717
|
const shouldRegisterProxyTool =
|
|
851
718
|
config.settings?.disableProxyTool !== true
|
|
719
|
+
|| hasPinnedResources
|
|
852
720
|
|| directSpecs.length === 0
|
|
853
721
|
|| missingConfiguredDirectToolServers.length > 0;
|
|
854
722
|
|
|
@@ -871,8 +739,7 @@ function installMcpAdapter(pi: ExtensionAPI, options: McpAdapterOptions) {
|
|
|
871
739
|
|
|
872
740
|
// A transform needs the session context before any configured tool surface is registered.
|
|
873
741
|
if (!options.transformConfig) {
|
|
874
|
-
|
|
875
|
-
syncProxyTool(earlyConfig, earlyCache, initialDirectTools);
|
|
742
|
+
syncProxyTool(earlyConfig, earlyCache, []);
|
|
876
743
|
syncScriptTool(earlyConfig);
|
|
877
744
|
}
|
|
878
745
|
}
|