@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/dist/index.js
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { showStatus, showTools, showPrompts, reconnectServer, reconnectServers, authenticateServer, logoutServer, openMcpAuthPanel, openMcpPanel, openMcpSetup } from "./commands.js";
|
|
3
3
|
import { cloneMcpConfig, isPathInsideProject, loadMcpConfig, writeProjectServerDisabledOverride } from "./config.js";
|
|
4
|
-
import { buildProxyDescription,
|
|
5
|
-
import { flushMetadataCache, initializeMcp, updateStatusBar } from "./init.js";
|
|
6
|
-
import { getMetadataCachePath, loadMetadataCache } from "./metadata-cache.js";
|
|
4
|
+
import { buildProxyDescription, directToolSelection, getMissingConfiguredDirectToolServers } from "./direct-tools.js";
|
|
5
|
+
import { flushMetadataCache, initializeMcp, lazyConnect, updateStatusBar } from "./init.js";
|
|
6
|
+
import { getMetadataCachePath, isServerCacheValid, loadMetadataCache, reconstructToolMetadata } from "./metadata-cache.js";
|
|
7
7
|
import { createPromptCommand, resolveCachedPrompts } from "./prompts.js";
|
|
8
8
|
import { logger } from "./logger.js";
|
|
9
9
|
import { executeAuthComplete, executeAuthStart, executeCall, executeConnect, executeDescribe, executeInstructions, executeList, executeSearch, executeStatus, executeUiMessages } from "./proxy-modes.js";
|
|
10
|
-
import { formatTerminalError
|
|
10
|
+
import { formatTerminalError } from "./utils.js";
|
|
11
11
|
import { createOAuthRuntime, shutdownOAuth } from "./mcp-auth-flow.js";
|
|
12
|
-
import {
|
|
12
|
+
import { renderMcpProxyToolCall, renderMcpToolResult } from "./tool-result-renderer.js";
|
|
13
13
|
import { toolErrorOverride } from "./error-signal.js";
|
|
14
14
|
import { createMcpRuntimeOwner, createOwnedUi, isAbortError } from "./runtime-owner.js";
|
|
15
15
|
import { publishMcpStatusShutdown } from "./mcp-status.js";
|
|
16
16
|
import { DEFAULT_MCP_SCRIPT_TIMEOUT_MS, runMcpScript } from "./mcp-code.js";
|
|
17
|
-
import { MAX_PAGE_SIZE,
|
|
17
|
+
import { MAX_PAGE_SIZE, paginate, rankToolMatches } from "./search-ranking.js";
|
|
18
|
+
import { createToolLoader, supportsNativeAsync } from "./tool-loader.js";
|
|
19
|
+
import { gatewayParameters, prepareGatewayArguments } from "./gateway-arguments.js";
|
|
20
|
+
import { executeResourceList, executeResourceRead } from "./resource-tools.js";
|
|
21
|
+
import { guardMcpOutput, guardedMcpDetails, readMcpResult, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
|
|
18
22
|
import { abortable } from "./abort.js";
|
|
19
23
|
import { onSessionCheckpoint, prepareMcpCheckpoint } from "./checkpoint.js";
|
|
20
24
|
export { MCP_STATUS_EVENT, MCP_STATUS_SNAPSHOT_VERSION, } from "./types.js";
|
|
21
25
|
const INIT_WAIT_TIMEOUT_MS = 30_000;
|
|
22
26
|
const INIT_WAIT_TIMED_OUT = Symbol("init-wait-timed-out");
|
|
23
|
-
const inactiveDirectToolsKey = Symbol.for("fitch-mcp-adapter.inactive-direct-tools");
|
|
24
|
-
const inactiveDirectToolsBySession = (() => {
|
|
25
|
-
const shared = globalThis;
|
|
26
|
-
return shared[inactiveDirectToolsKey] ??= new Map();
|
|
27
|
-
})();
|
|
28
27
|
async function awaitWithTimeout(promise, timeoutMs) {
|
|
29
28
|
let timer;
|
|
30
29
|
try {
|
|
@@ -93,52 +92,13 @@ function installMcpAdapter(pi, options) {
|
|
|
93
92
|
const earlyCache = null;
|
|
94
93
|
const envRaw = process.env.MCP_DIRECT_TOOLS;
|
|
95
94
|
const envDirectToolOverride = envRaw?.split(",").map(s => s.trim()).filter(Boolean);
|
|
96
|
-
const
|
|
95
|
+
const toolLoader = createToolLoader(pi, () => state, () => initPromise, beforeExecute);
|
|
97
96
|
const deactivatedTools = new Set();
|
|
98
|
-
|
|
99
|
-
let
|
|
97
|
+
let runtimeCwd;
|
|
98
|
+
let nativeAsyncSupported = false;
|
|
100
99
|
let proxyToolRegistered = false;
|
|
101
100
|
let proxyToolDescription = null;
|
|
102
101
|
let directToolsFrozen = false;
|
|
103
|
-
function directToolFingerprint(spec) {
|
|
104
|
-
return JSON.stringify({
|
|
105
|
-
serverName: spec.serverName,
|
|
106
|
-
originalName: spec.originalName,
|
|
107
|
-
prefixedName: spec.prefixedName,
|
|
108
|
-
description: spec.description,
|
|
109
|
-
inputSchema: spec.inputSchema,
|
|
110
|
-
annotations: spec.annotations,
|
|
111
|
-
resourceUri: spec.resourceUri,
|
|
112
|
-
uiResourceUri: spec.uiResourceUri,
|
|
113
|
-
uiStreamMode: spec.uiStreamMode,
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
function registerDirectTool(spec) {
|
|
117
|
-
const execute = createDirectToolExecutor(() => state, () => initPromise, spec, beforeExecute);
|
|
118
|
-
pi.registerTool({
|
|
119
|
-
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
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(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
|
-
function resolveCurrentDirectTools(config, cache) {
|
|
137
|
-
if (envRaw === "__none__")
|
|
138
|
-
return [];
|
|
139
|
-
const prefix = config.settings?.toolPrefix ?? "server";
|
|
140
|
-
return resolveDirectTools(config, cache, prefix, envDirectToolOverride);
|
|
141
|
-
}
|
|
142
102
|
function deactivateTools(toolNames) {
|
|
143
103
|
if (toolNames.length === 0)
|
|
144
104
|
return;
|
|
@@ -153,36 +113,6 @@ function installMcpAdapter(pi, options) {
|
|
|
153
113
|
pi.setActiveTools(nextActiveTools);
|
|
154
114
|
}
|
|
155
115
|
}
|
|
156
|
-
function syncDirectTools(config, cache) {
|
|
157
|
-
const specs = resolveCurrentDirectTools(config, cache);
|
|
158
|
-
const nextNames = new Set(specs.map((spec) => spec.prefixedName));
|
|
159
|
-
const added = [];
|
|
160
|
-
const updated = [];
|
|
161
|
-
const deactivated = [];
|
|
162
|
-
for (const spec of specs) {
|
|
163
|
-
const fingerprint = directToolFingerprint(spec);
|
|
164
|
-
const previous = registeredDirectTools.get(spec.prefixedName);
|
|
165
|
-
if (previous !== fingerprint) {
|
|
166
|
-
registerDirectTool(spec);
|
|
167
|
-
registeredDirectTools.set(spec.prefixedName, fingerprint);
|
|
168
|
-
if (deactivatedTools.delete(spec.prefixedName)) {
|
|
169
|
-
const activeTools = pi.getActiveTools();
|
|
170
|
-
if (!activeTools.includes(spec.prefixedName)) {
|
|
171
|
-
pi.setActiveTools([...activeTools, spec.prefixedName]);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
(previous ? updated : added).push(spec.prefixedName);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
for (const toolName of [...registeredDirectTools.keys()]) {
|
|
178
|
-
if (nextNames.has(toolName))
|
|
179
|
-
continue;
|
|
180
|
-
registeredDirectTools.delete(toolName);
|
|
181
|
-
deactivated.push(toolName);
|
|
182
|
-
}
|
|
183
|
-
deactivateTools(deactivated);
|
|
184
|
-
return { specs, added, updated, deactivated };
|
|
185
|
-
}
|
|
186
116
|
function applyDirectToolConfigChanges(changes) {
|
|
187
117
|
if (!state)
|
|
188
118
|
return;
|
|
@@ -196,13 +126,10 @@ function installMcpAdapter(pi, options) {
|
|
|
196
126
|
function syncToolSurface(ctx) {
|
|
197
127
|
const config = state?.config ?? earlyConfig;
|
|
198
128
|
const cache = loadMetadataCache(state?.metadataCacheEnabled ?? false);
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
if (changed > 0 && ctx?.hasUI) {
|
|
204
|
-
ctx.ui.notify(`MCP: direct tools refreshed (+${result.added.length}, ~${result.updated.length}, -${result.deactivated.length})`, "info");
|
|
205
|
-
}
|
|
129
|
+
toolLoader.sync(config, cache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => {
|
|
130
|
+
syncProxyTool(config, cache, specs);
|
|
131
|
+
syncScriptTool(config);
|
|
132
|
+
});
|
|
206
133
|
}
|
|
207
134
|
const registeredPromptCommands = new Set();
|
|
208
135
|
function registerPromptCommands(specs) {
|
|
@@ -255,13 +182,18 @@ function installMcpAdapter(pi, options) {
|
|
|
255
182
|
}
|
|
256
183
|
syncToolSurface(ctx);
|
|
257
184
|
};
|
|
185
|
+
for (const server of toolLoader.selectedServers()) {
|
|
186
|
+
if (!nextState.toolMetadata.has(server) && nextState.config.mcpServers[server])
|
|
187
|
+
await lazyConnect(nextState, server, owner.signal);
|
|
188
|
+
}
|
|
189
|
+
owner.throwIfInactive();
|
|
258
190
|
syncPromptCommands();
|
|
259
191
|
syncToolSurface(ctx);
|
|
260
192
|
updateStatusBar(nextState);
|
|
261
193
|
initPromise = null;
|
|
262
194
|
if (nextState.config.settings?.freezeDirectTools === true) {
|
|
263
195
|
directToolsFrozen = true;
|
|
264
|
-
logger.info("MCP: direct tools frozen after initial sync — reconnects won't rebuild the system prompt; use mcp({ connect: \"server\" }) to rediscover");
|
|
196
|
+
logger.info("MCP: direct tools frozen after initial sync — reconnects won't rebuild the system prompt; use mcp({ action: \"connect\", server: \"server\" }) to rediscover");
|
|
265
197
|
}
|
|
266
198
|
}).catch(async (err) => {
|
|
267
199
|
if (!owner.isActive() || generation !== lifecycleGeneration) {
|
|
@@ -285,7 +217,10 @@ function installMcpAdapter(pi, options) {
|
|
|
285
217
|
}
|
|
286
218
|
});
|
|
287
219
|
}
|
|
288
|
-
|
|
220
|
+
async function startSession(ctx) {
|
|
221
|
+
runtimeCwd = ctx.cwd;
|
|
222
|
+
nativeAsyncSupported = supportsNativeAsync(ctx);
|
|
223
|
+
directToolsFrozen = false;
|
|
289
224
|
const generation = ++lifecycleGeneration;
|
|
290
225
|
const previousState = state;
|
|
291
226
|
const previousOwner = currentOwner;
|
|
@@ -327,16 +262,13 @@ function installMcpAdapter(pi, options) {
|
|
|
327
262
|
}
|
|
328
263
|
const metadataCacheEnabled = ctx.isProjectTrusted()
|
|
329
264
|
|| !isPathInsideProject(getMetadataCachePath(), ctx.cwd);
|
|
330
|
-
|
|
331
|
-
preservedInactiveTools.clear();
|
|
332
|
-
for (const toolName of inactiveDirectToolsBySession.get(activationKey) ?? []) {
|
|
333
|
-
preservedInactiveTools.add(toolName);
|
|
334
|
-
}
|
|
265
|
+
toolLoader.restore(ctx);
|
|
335
266
|
registerPromptCommands(resolveCachedPrompts(runtimeConfig, metadataCacheEnabled));
|
|
336
267
|
const runtimeCache = loadMetadataCache(metadataCacheEnabled);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
268
|
+
toolLoader.sync(runtimeConfig, runtimeCache, envRaw === "__none__" ? null : envDirectToolOverride, nativeAsyncSupported, specs => {
|
|
269
|
+
syncProxyTool(runtimeConfig, runtimeCache, specs);
|
|
270
|
+
syncScriptTool(runtimeConfig);
|
|
271
|
+
});
|
|
340
272
|
const initialization = startInitialization(ctx, owner, oauthRuntime, runtimeConfig, generation);
|
|
341
273
|
if (envRaw !== undefined && envRaw !== "__none__") {
|
|
342
274
|
const missingEnvDirectTools = getMissingConfiguredDirectToolServers(runtimeConfig, runtimeCache, envDirectToolOverride);
|
|
@@ -344,6 +276,20 @@ function installMcpAdapter(pi, options) {
|
|
|
344
276
|
await initialization;
|
|
345
277
|
}
|
|
346
278
|
}
|
|
279
|
+
}
|
|
280
|
+
pi.on("session_start", (_event, ctx) => startSession(ctx));
|
|
281
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
282
|
+
toolLoader.restore(ctx);
|
|
283
|
+
syncToolSurface(ctx);
|
|
284
|
+
});
|
|
285
|
+
pi.on("model_select", (_event, ctx) => {
|
|
286
|
+
nativeAsyncSupported = supportsNativeAsync(ctx);
|
|
287
|
+
if (state)
|
|
288
|
+
syncToolSurface(ctx);
|
|
289
|
+
});
|
|
290
|
+
pi.on("before_agent_start", async (_event, ctx) => {
|
|
291
|
+
if (ctx.cwd !== runtimeCwd)
|
|
292
|
+
await startSession(ctx);
|
|
347
293
|
});
|
|
348
294
|
onSessionCheckpoint(pi, async (event) => {
|
|
349
295
|
if (initPromise)
|
|
@@ -353,14 +299,8 @@ function installMcpAdapter(pi, options) {
|
|
|
353
299
|
return prepareMcpCheckpoint(state, event);
|
|
354
300
|
});
|
|
355
301
|
pi.on("session_shutdown", async () => {
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
const inactiveTools = new Set([...registeredDirectTools.keys()].filter((name) => !activeTools.has(name)));
|
|
359
|
-
if (inactiveTools.size > 0)
|
|
360
|
-
inactiveDirectToolsBySession.set(activationKey, inactiveTools);
|
|
361
|
-
else
|
|
362
|
-
inactiveDirectToolsBySession.delete(activationKey);
|
|
363
|
-
}
|
|
302
|
+
// Cached tool selections exist before background server initialization finishes.
|
|
303
|
+
toolLoader.persist();
|
|
364
304
|
++lifecycleGeneration;
|
|
365
305
|
const currentState = state;
|
|
366
306
|
const owner = currentOwner;
|
|
@@ -575,6 +515,74 @@ function installMcpAdapter(pi, options) {
|
|
|
575
515
|
}
|
|
576
516
|
},
|
|
577
517
|
});
|
|
518
|
+
async function getToolState(signal) {
|
|
519
|
+
const owner = currentOwner;
|
|
520
|
+
if (!state && initPromise) {
|
|
521
|
+
try {
|
|
522
|
+
const initialized = await awaitWithTimeout(abortable(initPromise, signal), INIT_WAIT_TIMEOUT_MS);
|
|
523
|
+
if (initialized === INIT_WAIT_TIMED_OUT)
|
|
524
|
+
return {
|
|
525
|
+
content: [{ type: "text", text: "MCP initialization is still in progress. Try again shortly." }],
|
|
526
|
+
details: { error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
|
|
527
|
+
};
|
|
528
|
+
owner?.throwIfInactive();
|
|
529
|
+
state = initialized;
|
|
530
|
+
}
|
|
531
|
+
catch (error) {
|
|
532
|
+
if (signal?.aborted || (owner && isAbortError(error, owner.signal)))
|
|
533
|
+
throw error;
|
|
534
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
535
|
+
return { content: [{ type: "text", text: `MCP initialization failed: ${message}` }], details: { error: "init_failed", message } };
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
owner?.throwIfInactive();
|
|
539
|
+
return state ?? { content: [{ type: "text", text: "MCP not initialized" }], details: { error: "not_initialized" } };
|
|
540
|
+
}
|
|
541
|
+
async function searchAndLoad(params, signal, ctx) {
|
|
542
|
+
const ready = await getToolState(signal);
|
|
543
|
+
if ("content" in ready)
|
|
544
|
+
return { ...ready, tools: [] };
|
|
545
|
+
if (params.server && !ready.toolMetadata.has(params.server)) {
|
|
546
|
+
const discovery = await executeConnect(ready, params.server, signal);
|
|
547
|
+
if (discovery.details.error)
|
|
548
|
+
return { ...discovery, tools: [] };
|
|
549
|
+
}
|
|
550
|
+
syncToolSurface(ctx);
|
|
551
|
+
const matches = params.query.trim() ? rankToolMatches(ready, params.query, params.server)
|
|
552
|
+
: 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
|
+
const page = paginate(matches, params.offset ?? 0, params.limit ?? 5);
|
|
554
|
+
const tools = toolLoader.activate(page.items);
|
|
555
|
+
const result = executeSearch(ready, params.query, params.server, tools.length < page.items.length, params.limit ?? 5, params.offset);
|
|
556
|
+
const guarded = await guardMcpOutput([
|
|
557
|
+
...result.content,
|
|
558
|
+
{ 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." },
|
|
559
|
+
], resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
|
|
560
|
+
return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded), loaded: tools }, tools };
|
|
561
|
+
}
|
|
562
|
+
const searchDefinition = {
|
|
563
|
+
name: "mcp_search", label: "MCP Search",
|
|
564
|
+
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.",
|
|
565
|
+
parameters: Type.Object({
|
|
566
|
+
query: Type.String(),
|
|
567
|
+
server: Type.Optional(Type.String()),
|
|
568
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Tools to load (default 5)." })),
|
|
569
|
+
offset: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
570
|
+
}),
|
|
571
|
+
renderResult: renderMcpToolResult,
|
|
572
|
+
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
573
|
+
if (beforeExecute) {
|
|
574
|
+
if (currentOwner)
|
|
575
|
+
await currentOwner.runCallback("beforeExecute", () => beforeExecute(toolCallId, ctx));
|
|
576
|
+
else
|
|
577
|
+
await beforeExecute(toolCallId, ctx);
|
|
578
|
+
}
|
|
579
|
+
return searchAndLoad(params, signal, ctx);
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
if (toolLoader.native)
|
|
583
|
+
pi.registerToolSearch(searchDefinition);
|
|
584
|
+
else
|
|
585
|
+
pi.registerTool(searchDefinition);
|
|
578
586
|
let scriptToolRegistered = false;
|
|
579
587
|
function registerScriptTool() {
|
|
580
588
|
if (scriptToolRegistered)
|
|
@@ -583,7 +591,7 @@ function installMcpAdapter(pi, options) {
|
|
|
583
591
|
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
584
592
|
name: "mcp_script",
|
|
585
593
|
label: "MCP Script",
|
|
586
|
-
description: "
|
|
594
|
+
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.",
|
|
587
595
|
promptSnippet: "Batch multiple MCP tool calls in one JavaScript request (loop, filter, chain)",
|
|
588
596
|
parameters: Type.Object({
|
|
589
597
|
code: Type.String({ description: "Trusted JavaScript MCP script. Use tools.<prefixedToolName>(args) and emit(value)." }),
|
|
@@ -593,37 +601,10 @@ function installMcpAdapter(pi, options) {
|
|
|
593
601
|
}),
|
|
594
602
|
renderResult: renderMcpToolResult,
|
|
595
603
|
async execute(toolCallId, params, signal, _onUpdate, ctx) {
|
|
596
|
-
const
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (initialized === INIT_WAIT_TIMED_OUT) {
|
|
601
|
-
return {
|
|
602
|
-
content: [{ type: "text", text: "MCP initialization is still in progress. Try again shortly." }],
|
|
603
|
-
details: { mode: "script", error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
|
|
604
|
-
};
|
|
605
|
-
}
|
|
606
|
-
executeOwner?.throwIfInactive();
|
|
607
|
-
state = initialized;
|
|
608
|
-
}
|
|
609
|
-
catch (error) {
|
|
610
|
-
if (signal?.aborted || (executeOwner && isAbortError(error, executeOwner.signal)))
|
|
611
|
-
throw error;
|
|
612
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
613
|
-
return {
|
|
614
|
-
content: [{ type: "text", text: `MCP initialization failed: ${message}` }],
|
|
615
|
-
details: { mode: "script", error: "init_failed", message },
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
if (!state) {
|
|
620
|
-
return {
|
|
621
|
-
content: [{ type: "text", text: "MCP not initialized" }],
|
|
622
|
-
details: { mode: "script", error: "not_initialized" },
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
executeOwner?.throwIfInactive();
|
|
626
|
-
return runMcpScript(state, params.code, params.timeoutMs ?? options.defaultScriptTimeoutMs, getPiTools, signal, toolCallId, beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined);
|
|
604
|
+
const ready = await getToolState(signal);
|
|
605
|
+
if ("content" in ready)
|
|
606
|
+
return ready;
|
|
607
|
+
return runMcpScript(ready, params.code, params.timeoutMs ?? options.defaultScriptTimeoutMs, getPiTools, signal, toolCallId, beforeExecute ? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation) : undefined);
|
|
627
608
|
},
|
|
628
609
|
});
|
|
629
610
|
scriptToolRegistered = true;
|
|
@@ -644,147 +625,60 @@ function installMcpAdapter(pi, options) {
|
|
|
644
625
|
function registerProxyTool(description) {
|
|
645
626
|
pi.registerTool({
|
|
646
627
|
...(beforeExecute ? { executionMode: "sequential" } : {}),
|
|
647
|
-
name: "mcp",
|
|
648
|
-
|
|
649
|
-
description,
|
|
650
|
-
promptSnippet: "MCP gateway — status, search, describe, auth, and single MCP tool calls",
|
|
628
|
+
name: "mcp", label: "MCP", description,
|
|
629
|
+
promptSnippet: "MCP server operations, resources, saved result readback, and fallback tool calls",
|
|
651
630
|
renderCall: renderMcpProxyToolCall,
|
|
652
|
-
parameters:
|
|
653
|
-
|
|
654
|
-
args: Type.Optional(Type.Union([
|
|
655
|
-
Type.String({ description: "Arguments as a JSON string (e.g., '{\"key\": \"value\"}')" }),
|
|
656
|
-
Type.Object({}, {
|
|
657
|
-
additionalProperties: true,
|
|
658
|
-
description: 'Arguments as a JSON object (e.g., { "key": "value" })',
|
|
659
|
-
}),
|
|
660
|
-
], { description: "Tool arguments as a JSON object, or as a JSON string encoding one" })),
|
|
661
|
-
connect: Type.Optional(Type.String({ description: "Server name to connect (lazy connect + metadata refresh)" })),
|
|
662
|
-
describe: Type.Optional(Type.String({ maxLength: MAX_TOOL_NAME_LENGTH, description: "Tool name to describe (shows parameters)" })),
|
|
663
|
-
instructions: Type.Optional(Type.String({ description: "Server name to show that server's usage instructions" })),
|
|
664
|
-
search: Type.Optional(Type.String({ description: "Search tools by name/description" })),
|
|
665
|
-
includeSchemas: Type.Optional(Type.Boolean({ description: "Include parameter schemas in search results (default: true)" })),
|
|
666
|
-
limit: Type.Optional(Type.Number({ minimum: 1, maximum: MAX_PAGE_SIZE, description: "Maximum search or server-list results to return (default: 12, max: 100)" })),
|
|
667
|
-
offset: Type.Optional(Type.Number({ minimum: 0, description: "Search or server-list result offset (default: 0)" })),
|
|
668
|
-
server: Type.Optional(Type.String({ description: "Filter to specific server (also disambiguates tool calls)" })),
|
|
669
|
-
action: Type.Optional(Type.String({ description: "Action: 'ui-messages', 'auth-start', or 'auth-complete'" })),
|
|
670
|
-
}),
|
|
631
|
+
parameters: gatewayParameters,
|
|
632
|
+
prepareArguments: prepareGatewayArguments,
|
|
671
633
|
renderResult: renderMcpToolResult,
|
|
672
|
-
async execute(toolCallId,
|
|
673
|
-
|
|
674
|
-
if (
|
|
675
|
-
if (
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
await beforeExecute(toolCallId, ctx);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
const executeOwner = currentOwner;
|
|
683
|
-
let parsedArgs;
|
|
684
|
-
if (params.args !== undefined && params.args !== "") {
|
|
685
|
-
let args;
|
|
686
|
-
if (typeof params.args === "string") {
|
|
687
|
-
try {
|
|
688
|
-
args = JSON.parse(params.args);
|
|
689
|
-
}
|
|
690
|
-
catch (error) {
|
|
691
|
-
if (error instanceof SyntaxError) {
|
|
692
|
-
throw new Error(`Invalid args JSON: ${error.message}`, { cause: error });
|
|
693
|
-
}
|
|
694
|
-
throw error;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
else {
|
|
698
|
-
args = params.args;
|
|
699
|
-
}
|
|
700
|
-
if (typeof args !== "object" || args === null || Array.isArray(args)) {
|
|
701
|
-
const gotType = Array.isArray(args) ? "array" : args === null ? "null" : typeof args;
|
|
702
|
-
throw new Error(`Invalid args: expected a JSON object, got ${gotType}`);
|
|
703
|
-
}
|
|
704
|
-
parsedArgs = args;
|
|
705
|
-
}
|
|
706
|
-
if (!state && initPromise) {
|
|
707
|
-
try {
|
|
708
|
-
const initialized = await awaitWithTimeout(abortable(initPromise, signal), INIT_WAIT_TIMEOUT_MS);
|
|
709
|
-
if (initialized === INIT_WAIT_TIMED_OUT) {
|
|
710
|
-
return {
|
|
711
|
-
content: [{ type: "text", text: "MCP initialization is still in progress. Try again shortly." }],
|
|
712
|
-
details: { error: "init_timeout", timeoutMs: INIT_WAIT_TIMEOUT_MS },
|
|
713
|
-
};
|
|
714
|
-
}
|
|
715
|
-
executeOwner?.throwIfInactive();
|
|
716
|
-
state = initialized;
|
|
717
|
-
}
|
|
718
|
-
catch (error) {
|
|
719
|
-
if (signal?.aborted || (executeOwner && isAbortError(error, executeOwner.signal)))
|
|
720
|
-
throw error;
|
|
721
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
722
|
-
return {
|
|
723
|
-
content: [{ type: "text", text: `MCP initialization failed: ${message}` }],
|
|
724
|
-
details: { error: "init_failed", message },
|
|
725
|
-
};
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
if (!state) {
|
|
729
|
-
return {
|
|
730
|
-
content: [{ type: "text", text: "MCP not initialized" }],
|
|
731
|
-
details: { error: "not_initialized" },
|
|
732
|
-
};
|
|
733
|
-
}
|
|
734
|
-
executeOwner?.throwIfInactive();
|
|
735
|
-
if (params.action === "ui-messages") {
|
|
736
|
-
return executeUiMessages(state);
|
|
634
|
+
async execute(toolCallId, rawParams, signal, _onUpdate, ctx) {
|
|
635
|
+
const params = prepareGatewayArguments(rawParams);
|
|
636
|
+
if (beforeExecute && params.action !== "call" && params.action !== "read-resource") {
|
|
637
|
+
if (currentOwner)
|
|
638
|
+
await currentOwner.runCallback("beforeExecute", () => beforeExecute(toolCallId, ctx));
|
|
639
|
+
else
|
|
640
|
+
await beforeExecute(toolCallId, ctx);
|
|
737
641
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
642
|
+
const ready = await getToolState(signal);
|
|
643
|
+
if ("content" in ready)
|
|
644
|
+
return ready;
|
|
645
|
+
const present = async (result) => {
|
|
646
|
+
const guarded = await guardMcpOutput(result.content, resolveMcpOutputGuardOptions(ready.config.settings, ready.outputDirectory));
|
|
647
|
+
return { content: guarded.content, details: { ...result.details, ...guardedMcpDetails(guarded) } };
|
|
648
|
+
};
|
|
649
|
+
const beforeDispatch = beforeExecute
|
|
650
|
+
? (callSignal, operation) => beforeExecute(toolCallId, { ...ctx, signal: callSignal }, operation)
|
|
651
|
+
: undefined;
|
|
652
|
+
if (params.server && ["list", "search", "describe", "instructions"].includes(params.action) && !ready.toolMetadata.has(params.server)) {
|
|
653
|
+
const discovery = await executeConnect(ready, params.server, signal);
|
|
654
|
+
if (discovery.details.error)
|
|
655
|
+
return discovery;
|
|
656
|
+
syncToolSurface(ctx);
|
|
748
657
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
658
|
+
switch (params.action) {
|
|
659
|
+
case "ui-messages": return executeUiMessages(ready);
|
|
660
|
+
case "auth-start": return executeAuthStart(ready, params.server, signal);
|
|
661
|
+
case "auth-complete": {
|
|
662
|
+
const input = params.args?.redirectUrl ?? params.args?.code ?? params.args?.input;
|
|
663
|
+
if (input !== undefined && (typeof input !== "string" || input.trim().length === 0))
|
|
664
|
+
throw new Error("auth-complete requires a non-empty redirectUrl, code, or input");
|
|
665
|
+
return executeAuthComplete(ready, params.server, input, signal);
|
|
755
666
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
};
|
|
667
|
+
case "call": return executeCall(ready, params.tool, params.args, params.server, getPiTools, signal, { toolCallId }, beforeDispatch);
|
|
668
|
+
case "connect": {
|
|
669
|
+
const result = await executeConnect(ready, params.server, signal, params.limit, params.offset);
|
|
670
|
+
syncToolSurface(ctx);
|
|
671
|
+
return present(result);
|
|
762
672
|
}
|
|
763
|
-
return
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
return
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
const result = await executeConnect(state, params.connect, signal, params.limit, params.offset);
|
|
772
|
-
syncToolSurface(ctx);
|
|
773
|
-
return result;
|
|
673
|
+
case "describe": return present(executeDescribe(ready, params.tool, params.server));
|
|
674
|
+
case "instructions": return present(executeInstructions(ready, params.server));
|
|
675
|
+
case "search": return present(executeSearch(ready, params.query, params.server, params.includeSchemas, params.limit, params.offset));
|
|
676
|
+
case "list": return present(executeList(ready, params.server, params.limit, params.offset));
|
|
677
|
+
case "resources": return present(await executeResourceList(ready, params.server, params.limit, params.offset, signal));
|
|
678
|
+
case "read-resource": return executeResourceRead(ready, params.server, params.uri, signal, { toolCallId }, beforeDispatch);
|
|
679
|
+
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));
|
|
680
|
+
case "status": return present(executeStatus(ready));
|
|
774
681
|
}
|
|
775
|
-
if (params.describe) {
|
|
776
|
-
return executeDescribe(state, params.describe);
|
|
777
|
-
}
|
|
778
|
-
if (params.instructions) {
|
|
779
|
-
return executeInstructions(state, params.instructions);
|
|
780
|
-
}
|
|
781
|
-
if (params.search !== undefined) {
|
|
782
|
-
return executeSearch(state, params.search, params.server, params.includeSchemas, params.limit, params.offset);
|
|
783
|
-
}
|
|
784
|
-
if (params.server) {
|
|
785
|
-
return executeList(state, params.server, params.limit, params.offset);
|
|
786
|
-
}
|
|
787
|
-
return executeStatus(state);
|
|
788
682
|
},
|
|
789
683
|
});
|
|
790
684
|
proxyToolRegistered = true;
|
|
@@ -792,7 +686,19 @@ function installMcpAdapter(pi, options) {
|
|
|
792
686
|
}
|
|
793
687
|
function syncProxyTool(config, cache, directSpecs) {
|
|
794
688
|
const missingConfiguredDirectToolServers = getMissingConfiguredDirectToolServers(config, cache, envRaw === undefined || envRaw === "__none__" ? undefined : envDirectToolOverride);
|
|
689
|
+
const hasPinnedResources = envRaw !== "__none__" && Object.entries(config.mcpServers).some(([server, definition]) => {
|
|
690
|
+
if (definition.disabled === true)
|
|
691
|
+
return false;
|
|
692
|
+
const selection = directToolSelection(config, server, envDirectToolOverride);
|
|
693
|
+
if (!selection)
|
|
694
|
+
return false;
|
|
695
|
+
const entry = cache?.servers[server];
|
|
696
|
+
const metadata = state?.toolMetadata.get(server) ?? (entry && isServerCacheValid(entry, definition)
|
|
697
|
+
? reconstructToolMetadata(server, entry, config.settings?.toolPrefix ?? "server", definition) : []);
|
|
698
|
+
return metadata.some(tool => tool.resourceUri && (selection === true || selection.includes(tool.originalName)));
|
|
699
|
+
});
|
|
795
700
|
const shouldRegisterProxyTool = config.settings?.disableProxyTool !== true
|
|
701
|
+
|| hasPinnedResources
|
|
796
702
|
|| directSpecs.length === 0
|
|
797
703
|
|| missingConfiguredDirectToolServers.length > 0;
|
|
798
704
|
if (shouldRegisterProxyTool) {
|
|
@@ -813,8 +719,7 @@ function installMcpAdapter(pi, options) {
|
|
|
813
719
|
}
|
|
814
720
|
// A transform needs the session context before any configured tool surface is registered.
|
|
815
721
|
if (!options.transformConfig) {
|
|
816
|
-
|
|
817
|
-
syncProxyTool(earlyConfig, earlyCache, initialDirectTools);
|
|
722
|
+
syncProxyTool(earlyConfig, earlyCache, []);
|
|
818
723
|
syncScriptTool(earlyConfig);
|
|
819
724
|
}
|
|
820
725
|
}
|
package/dist/init.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { isServerDisabled } from "./types.js";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
2
|
import { cloneMcpConfig, isPathInsideProject, loadMcpConfig } from "./config.js";
|
|
4
3
|
import { ConsentManager } from "./consent-manager.js";
|
|
5
4
|
import { McpLifecycleManager } from "./lifecycle.js";
|
|
@@ -177,18 +176,7 @@ export async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), op
|
|
|
177
176
|
}
|
|
178
177
|
const idleSetting = typeof config.settings?.idleTimeout === "number" ? config.settings.idleTimeout : 10;
|
|
179
178
|
lifecycle.setGlobalIdleTimeout(idleSetting);
|
|
180
|
-
const
|
|
181
|
-
const cacheFileExists = metadataCacheEnabled && existsSync(cachePath);
|
|
182
|
-
let cache = loadMetadataCache(metadataCacheEnabled);
|
|
183
|
-
let bootstrapAll = false;
|
|
184
|
-
if (!cacheFileExists) {
|
|
185
|
-
bootstrapAll = true;
|
|
186
|
-
saveMetadataCache({ version: 1, servers: {} }, metadataCacheEnabled);
|
|
187
|
-
}
|
|
188
|
-
else if (!cache) {
|
|
189
|
-
cache = { version: 1, servers: {} };
|
|
190
|
-
saveMetadataCache(cache, metadataCacheEnabled);
|
|
191
|
-
}
|
|
179
|
+
const cache = loadMetadataCache(metadataCacheEnabled);
|
|
192
180
|
const prefix = config.settings?.toolPrefix ?? "server";
|
|
193
181
|
for (const [name, definition] of serverEntries) {
|
|
194
182
|
const lifecycleMode = definition.lifecycle ?? "lazy";
|
|
@@ -213,12 +201,10 @@ export async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), op
|
|
|
213
201
|
}
|
|
214
202
|
}
|
|
215
203
|
}
|
|
216
|
-
const startupServers =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return mode === "keep-alive" || mode === "eager";
|
|
221
|
-
});
|
|
204
|
+
const startupServers = serverEntries.filter(([, definition]) => {
|
|
205
|
+
const mode = definition.lifecycle ?? "lazy";
|
|
206
|
+
return mode === "keep-alive" || mode === "eager";
|
|
207
|
+
});
|
|
222
208
|
if (ui && startupServers.length > 0) {
|
|
223
209
|
const status = formatMcpStatus(state.config, `connecting to ${startupServers.length} servers...`);
|
|
224
210
|
ui.setStatus("mcp", status);
|
|
@@ -324,7 +310,7 @@ export async function initializeMcp(pi, ctx, owner = createMcpRuntimeOwner(), op
|
|
|
324
310
|
const bootstrapped = bootstrapResults.filter(r => r.ok).map(r => r.name);
|
|
325
311
|
owner.throwIfInactive();
|
|
326
312
|
if (bootstrapped.length > 0 && ui) {
|
|
327
|
-
ui.notify(`MCP:
|
|
313
|
+
ui.notify(`MCP: tools discovered for ${bootstrapped.join(", ")}`, "info");
|
|
328
314
|
}
|
|
329
315
|
}
|
|
330
316
|
}
|