@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/proxy-modes.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { DEFAULT_REQUEST_TIMEOUT_MSEC, UrlElicitationRequiredError } from "@modelcontextprotocol/client";
|
|
2
2
|
import { getServerPrefix, isServerDisabled, isNonInteractiveOAuth, parseUiPromptHandoff, resolveToolPrefix } from "./types.js";
|
|
3
|
-
import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated,
|
|
3
|
+
import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateMetadataCache, getFailureAgeSeconds, updateStatusBar, clearFailure, recordFailure } from "./init.js";
|
|
4
4
|
import { abortable, throwIfAborted } from "./abort.js";
|
|
5
5
|
import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
|
|
6
|
-
import { buildToolMetadata, findToolByName, formatSchema } from "./tool-metadata.js";
|
|
7
|
-
import { renderTsShape } from "./ts-shape.js";
|
|
6
|
+
import { buildToolMetadata, catalogCoverage, findToolByName, formatSchema, toToolDescriptor } from "./tool-metadata.js";
|
|
8
7
|
import { reconstructPromptMetadata } from "./metadata-cache.js";
|
|
9
|
-
import {
|
|
10
|
-
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
|
|
8
|
+
import { renderMcpResultContent } from "./tool-registrar.js";
|
|
9
|
+
import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference } from "./mcp-output-guard.js";
|
|
11
10
|
import { maybeStartUiSession, summarizeUiSessionResult } from "./ui-session.js";
|
|
12
11
|
import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord } from "./utils.js";
|
|
13
12
|
import { authenticate, completeAuthFromInput, startAuth, supportsOAuth } from "./mcp-auth-flow.js";
|
|
@@ -66,7 +65,7 @@ function formatManualAuthInstructions(serverName, authorizationUrl) {
|
|
|
66
65
|
"If the browser cannot reach the callback, copy the full redirected localhost URL from your address bar and send it back with:",
|
|
67
66
|
`mcp({ action: "auth-complete", server: "${serverName}", args: { redirectUrl: "PASTE_REDIRECT_URL_HERE" } })`,
|
|
68
67
|
"",
|
|
69
|
-
'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`.
|
|
68
|
+
'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`.',
|
|
70
69
|
portNote.trimEnd(),
|
|
71
70
|
].filter(Boolean).join("\n");
|
|
72
71
|
}
|
|
@@ -204,7 +203,7 @@ export function executeStatus(state) {
|
|
|
204
203
|
const disabled = isServerDisabled(definition);
|
|
205
204
|
const connection = disabled ? undefined : state.manager.getConnection(name);
|
|
206
205
|
const metadata = disabled ? undefined : state.toolMetadata.get(name);
|
|
207
|
-
const toolCount = metadata?.length ?? 0;
|
|
206
|
+
const toolCount = metadata?.filter(tool => !tool.resourceUri).length ?? 0;
|
|
208
207
|
const failedAgo = disabled ? null : getFailureAgeSeconds(state, name);
|
|
209
208
|
let status = disabled ? "disabled" : "not connected";
|
|
210
209
|
if (!disabled && connection?.status === "connected") {
|
|
@@ -225,6 +224,7 @@ export function executeStatus(state) {
|
|
|
225
224
|
const enabledServers = servers.filter(s => !s.disabled);
|
|
226
225
|
const totalTools = enabledServers.reduce((sum, s) => sum + s.toolCount, 0);
|
|
227
226
|
const connectedCount = enabledServers.filter(s => s.status === "connected").length;
|
|
227
|
+
const coverage = catalogCoverage(state);
|
|
228
228
|
let text = `MCP: ${connectedCount}/${enabledServers.length} connected, ${totalTools} tools available (calls connect lazily)`;
|
|
229
229
|
if (disabledCount > 0)
|
|
230
230
|
text += ` (${disabledCount} disabled)`;
|
|
@@ -250,14 +250,16 @@ export function executeStatus(state) {
|
|
|
250
250
|
text += `✗ ${server.name} (failed ${server.failedAgo ?? 0}s ago)\n`;
|
|
251
251
|
continue;
|
|
252
252
|
}
|
|
253
|
-
text += `○ ${server.name} (not
|
|
253
|
+
text += `○ ${server.name} (not discovered)\n`;
|
|
254
254
|
}
|
|
255
255
|
if (servers.length > 0) {
|
|
256
|
-
text += `\
|
|
256
|
+
text += `\nmcp_search({ query: "...", server: "name" }) to discover tools; mcp({ action: "resources", server: "name" }) for resources.`;
|
|
257
|
+
if (!coverage.complete)
|
|
258
|
+
text += `\nPartial catalog: ${coverage.unknownServers.length} undiscovered server(s).`;
|
|
257
259
|
}
|
|
258
260
|
return {
|
|
259
261
|
content: [{ type: "text", text: text.trim() }],
|
|
260
|
-
details: { mode: "status", servers, totalTools, connectedCount, disabledCount },
|
|
262
|
+
details: { mode: "status", servers, totalTools, connectedCount, disabledCount, coverage },
|
|
261
263
|
};
|
|
262
264
|
}
|
|
263
265
|
export async function executeAuthStart(state, serverName, signal) {
|
|
@@ -266,7 +268,7 @@ export async function executeAuthStart(state, serverName, signal) {
|
|
|
266
268
|
const definition = state.config.mcpServers[serverName];
|
|
267
269
|
if (!definition) {
|
|
268
270
|
return {
|
|
269
|
-
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
271
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
270
272
|
details: { mode: "auth-start", error: "not_found", server: serverName },
|
|
271
273
|
};
|
|
272
274
|
}
|
|
@@ -312,7 +314,7 @@ export async function executeAuthComplete(state, serverName, input, signal) {
|
|
|
312
314
|
const definition = state.config.mcpServers[serverName];
|
|
313
315
|
if (!definition) {
|
|
314
316
|
return {
|
|
315
|
-
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
317
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
316
318
|
details: { mode: "auth-complete", error: "not_found", server: serverName },
|
|
317
319
|
};
|
|
318
320
|
}
|
|
@@ -338,7 +340,7 @@ export async function executeAuthComplete(state, serverName, input, signal) {
|
|
|
338
340
|
clearFailure(state, serverName);
|
|
339
341
|
updateStatusBar(state);
|
|
340
342
|
return {
|
|
341
|
-
content: [{ type: "text", text: `OAuth authentication successful for "${serverName}". Run mcp({ connect: "${serverName}" }) to connect with the new token.` }],
|
|
343
|
+
content: [{ type: "text", text: `OAuth authentication successful for "${serverName}". Run mcp({ action: "connect", server: "${serverName}" }) to connect with the new token.` }],
|
|
342
344
|
details: { mode: "auth-complete", server: serverName, authenticated: true },
|
|
343
345
|
};
|
|
344
346
|
}
|
|
@@ -350,63 +352,44 @@ export async function executeAuthComplete(state, serverName, input, signal) {
|
|
|
350
352
|
};
|
|
351
353
|
}
|
|
352
354
|
}
|
|
353
|
-
export function executeDescribe(state, toolName) {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const suggestions = rankSuggestions(state, toolName, 5);
|
|
355
|
+
export function executeDescribe(state, toolName, serverFilter) {
|
|
356
|
+
if (serverFilter && isServerDisabled(state.config.mcpServers[serverFilter]))
|
|
357
|
+
return disabledResult("describe", serverFilter);
|
|
358
|
+
const catalog = [...state.toolMetadata].flatMap(([server, tools]) => (!serverFilter || serverFilter === server) && state.config.mcpServers[server] && !isServerDisabled(state.config.mcpServers[server])
|
|
359
|
+
? tools.map(tool => ({ server, tool })) : []);
|
|
360
|
+
const originals = serverFilter ? catalog.filter(({ tool }) => tool.originalName === toolName) : [];
|
|
361
|
+
const exact = originals.length ? originals : catalog.filter(({ tool }) => tool.name === toolName);
|
|
362
|
+
const matches = exact.length ? exact : catalog.filter(({ tool }) => tool.name.replace(/-/g, "_") === toolName.replace(/-/g, "_"));
|
|
363
|
+
if (matches.length > 1)
|
|
364
|
+
return {
|
|
365
|
+
content: [{ type: "text", text: `Tool ${JSON.stringify(toolName)} is ambiguous. Specify server and original tool name: ${JSON.stringify(matches.map(({ server, tool }) => ({ server, tool: tool.originalName })))}` }],
|
|
366
|
+
details: { mode: "describe", error: "ambiguous_tool", requestedTool: toolName },
|
|
367
|
+
};
|
|
368
|
+
const match = matches[0];
|
|
369
|
+
if (!match) {
|
|
370
|
+
const disabled = [...state.toolMetadata].find(([server, tools]) => isServerDisabled(state.config.mcpServers[server]) && findToolByName(tools, toolName));
|
|
371
|
+
if (disabled)
|
|
372
|
+
return disabledResult("describe", disabled[0]);
|
|
373
|
+
const suggestions = rankSuggestions(state, toolName, 5, serverFilter);
|
|
373
374
|
const hint = suggestions.length > 0
|
|
374
|
-
? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ describe: "${suggestions[0]}" }).`
|
|
375
|
-
: `Use
|
|
375
|
+
? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: "${suggestions[0]}" }).`
|
|
376
|
+
: `Use mcp_search({ query: "..." }) to search.`;
|
|
376
377
|
return {
|
|
377
378
|
content: [{ type: "text", text: `Tool "${toolName}" not found. ${hint}` }],
|
|
378
379
|
details: { mode: "describe", error: "tool_not_found", requestedTool: toolName, suggestions },
|
|
379
380
|
};
|
|
380
381
|
}
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
let text = `${toolMeta.name}${approvalMarker}\n`;
|
|
385
|
-
text += `Server: ${serverName}\n`;
|
|
386
|
-
if (toolMeta.resourceUri) {
|
|
387
|
-
text += `Type: Resource (reads from ${toolMeta.resourceUri})\n`;
|
|
388
|
-
}
|
|
389
|
-
text += `\n${toolMeta.description || "(no description)"}\n`;
|
|
390
|
-
if (toolMeta.annotations)
|
|
391
|
-
text += `\nAnnotations: ${JSON.stringify(toolMeta.annotations)}\n`;
|
|
392
|
-
if (toolMeta.inputSchema && !toolMeta.resourceUri) {
|
|
393
|
-
const shape = renderTsShape(toolMeta.inputSchema);
|
|
394
|
-
text += shape === null ? `\nParameters:\n${formatSchema(toolMeta.inputSchema)}` : `\nShape:\n${shape}`;
|
|
395
|
-
}
|
|
396
|
-
else if (toolMeta.resourceUri) {
|
|
397
|
-
text += `\nNo parameters required (resource tool).`;
|
|
398
|
-
}
|
|
399
|
-
else {
|
|
400
|
-
text += `\nNo parameters defined.`;
|
|
401
|
-
}
|
|
382
|
+
const { server: serverName, tool: toolMeta } = match;
|
|
383
|
+
const descriptor = toToolDescriptor(serverName, toolMeta);
|
|
384
|
+
const approvalRequired = isToolCallApprovalRequired(state.config, serverName, toolMeta);
|
|
402
385
|
return {
|
|
403
|
-
content: [{ type: "text", text:
|
|
386
|
+
content: [{ type: "text", text: JSON.stringify({ ...descriptor, ...(toolMeta.resourceUri ? { resourceUri: toolMeta.resourceUri, readWith: { action: "read-resource", server: serverName, uri: toolMeta.resourceUri } } : {}), ...(approvalRequired ? { approvalRequired: true } : {}) }, null, 2) }],
|
|
404
387
|
details: { mode: "describe", tool: toolMeta, server: serverName },
|
|
405
388
|
};
|
|
406
389
|
}
|
|
407
390
|
export function executeSearch(state, query, server, includeSchemas, limit = 12, offset = 0) {
|
|
408
391
|
const showSchemas = includeSchemas !== false;
|
|
409
|
-
const searchCall = (nextOffset) => `mcp({ search: ${JSON.stringify(query)}${server ? `, server: ${JSON.stringify(server)}` : ""}${showSchemas ? "" : ", includeSchemas: false"}, limit: ${limit}, offset: ${nextOffset} })`;
|
|
392
|
+
const searchCall = (nextOffset) => `mcp({ action: "search", query: ${JSON.stringify(query)}${server ? `, server: ${JSON.stringify(server)}` : ""}${showSchemas ? "" : ", includeSchemas: false"}, limit: ${limit}, offset: ${nextOffset} })`;
|
|
410
393
|
if (server && isServerDisabled(state.config.mcpServers[server]))
|
|
411
394
|
return disabledResult("search", server);
|
|
412
395
|
let matches;
|
|
@@ -417,7 +400,7 @@ export function executeSearch(state, query, server, includeSchemas, limit = 12,
|
|
|
417
400
|
details: { mode: "search", error: "empty_query" },
|
|
418
401
|
};
|
|
419
402
|
}
|
|
420
|
-
matches = (state.toolMetadata.get(server) ?? [])
|
|
403
|
+
matches = (state.toolMetadata.get(server) ?? []).filter(tool => !tool.resourceUri)
|
|
421
404
|
.map(tool => ({ server, tool, score: 0 }))
|
|
422
405
|
.sort((a, b) => a.tool.name.localeCompare(b.tool.name));
|
|
423
406
|
}
|
|
@@ -425,11 +408,13 @@ export function executeSearch(state, query, server, includeSchemas, limit = 12,
|
|
|
425
408
|
matches = rankToolMatches(state, query, server);
|
|
426
409
|
}
|
|
427
410
|
const page = paginate(matches, offset, limit);
|
|
411
|
+
const coverage = catalogCoverage(state, server);
|
|
412
|
+
const coverageNotice = coverage.complete ? "" : `\nCatalog is incomplete. Undiscovered servers: ${coverage.unknownServers.join(", ")}. Search with server to discover one.`;
|
|
428
413
|
if (page.total === 0) {
|
|
429
414
|
const msg = server ? `No tools matching "${query}" in "${server}"` : `No tools matching "${query}"`;
|
|
430
415
|
return {
|
|
431
|
-
content: [{ type: "text", text: msg }],
|
|
432
|
-
details: { mode: "search", matches: [], count: 0, hasMore: false, nextOffset: null, query },
|
|
416
|
+
content: [{ type: "text", text: msg + coverageNotice }],
|
|
417
|
+
details: { mode: "search", matches: [], count: 0, hasMore: false, nextOffset: null, query, coverage },
|
|
433
418
|
};
|
|
434
419
|
}
|
|
435
420
|
if (page.items.length === 0) {
|
|
@@ -445,18 +430,7 @@ export function executeSearch(state, query, server, includeSchemas, limit = 12,
|
|
|
445
430
|
? " (requires approval)"
|
|
446
431
|
: "";
|
|
447
432
|
if (showSchemas) {
|
|
448
|
-
text += `${match.tool
|
|
449
|
-
text += ` ${match.tool.description || "(no description)"}\n`;
|
|
450
|
-
if (match.tool.inputSchema && !match.tool.resourceUri) {
|
|
451
|
-
const shape = renderTsShape(match.tool.inputSchema);
|
|
452
|
-
text += shape === null
|
|
453
|
-
? `\n Parameters:\n${formatSchema(match.tool.inputSchema, " ")}\n`
|
|
454
|
-
: `\n Shape:\n${shape.split("\n").map(line => ` ${line}`).join("\n")}\n`;
|
|
455
|
-
}
|
|
456
|
-
else if (match.tool.resourceUri) {
|
|
457
|
-
text += " No parameters (resource tool).\n";
|
|
458
|
-
}
|
|
459
|
-
text += "\n";
|
|
433
|
+
text += `${JSON.stringify({ ...toToolDescriptor(match.server, match.tool), ...(approvalMarker ? { approvalRequired: true } : {}) }, null, 2)}\n\n`;
|
|
460
434
|
}
|
|
461
435
|
else {
|
|
462
436
|
text += `- ${match.tool.name}${approvalMarker}`;
|
|
@@ -471,9 +445,10 @@ export function executeSearch(state, query, server, includeSchemas, limit = 12,
|
|
|
471
445
|
? `\n${first}-${first + page.items.length - 1} of ${page.total} — ${nextSearch} for more\n`
|
|
472
446
|
: `\n${first}-${first + page.items.length - 1} of ${page.total} — end\n`;
|
|
473
447
|
return {
|
|
474
|
-
content: [{ type: "text", text: text.trim() }],
|
|
448
|
+
content: [{ type: "text", text: text.trim() + coverageNotice }],
|
|
475
449
|
details: {
|
|
476
450
|
mode: "search",
|
|
451
|
+
coverage,
|
|
477
452
|
matches: page.items.map(match => ({ server: match.server, tool: match.tool.name, score: match.score })),
|
|
478
453
|
count: page.total,
|
|
479
454
|
hasMore: page.hasMore,
|
|
@@ -486,13 +461,13 @@ export function executeList(state, server, limit = 12, offset = 0) {
|
|
|
486
461
|
const definition = state.config.mcpServers[server];
|
|
487
462
|
if (!definition) {
|
|
488
463
|
return {
|
|
489
|
-
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({}) to see available servers.` }],
|
|
464
|
+
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
490
465
|
details: { mode: "list", server, tools: [], count: 0, error: "not_found" },
|
|
491
466
|
};
|
|
492
467
|
}
|
|
493
468
|
if (isServerDisabled(definition))
|
|
494
469
|
return disabledResult("list", server);
|
|
495
|
-
const metadata = state.toolMetadata.get(server);
|
|
470
|
+
const metadata = state.toolMetadata.get(server)?.filter(tool => !tool.resourceUri);
|
|
496
471
|
const connection = state.manager.getConnection(server);
|
|
497
472
|
const instructions = state.serverInstructions.get(server);
|
|
498
473
|
let instructionsText = "";
|
|
@@ -500,7 +475,7 @@ export function executeList(state, server, limit = 12, offset = 0) {
|
|
|
500
475
|
const preview = truncateAtWord(instructions, INSTRUCTIONS_PREVIEW_LENGTH);
|
|
501
476
|
instructionsText = `\n\nServer instructions:\n${preview}`;
|
|
502
477
|
if (preview !== instructions) {
|
|
503
|
-
instructionsText += `\nUse mcp({ instructions: "${server}" }) for the full text.`;
|
|
478
|
+
instructionsText += `\nUse mcp({ action: "instructions", server: "${server}" }) for the full text.`;
|
|
504
479
|
}
|
|
505
480
|
}
|
|
506
481
|
if (!metadata?.length) {
|
|
@@ -517,14 +492,14 @@ export function executeList(state, server, limit = 12, offset = 0) {
|
|
|
517
492
|
};
|
|
518
493
|
}
|
|
519
494
|
return {
|
|
520
|
-
content: [{ type: "text", text: `Server "${server}" is configured but not connected. Use mcp({ connect: "${server}" }) or /mcp reconnect ${server} to retry.${instructionsText}` }],
|
|
495
|
+
content: [{ type: "text", text: `Server "${server}" is configured but not connected. Use mcp({ action: "connect", server: "${server}" }) or /mcp reconnect ${server} to retry.${instructionsText}` }],
|
|
521
496
|
details: { mode: "list", server, tools: [], count: 0, error: "not_connected", hasInstructions: Boolean(instructions) },
|
|
522
497
|
};
|
|
523
498
|
}
|
|
524
499
|
const page = paginate(metadata, offset, limit);
|
|
525
500
|
if (page.items.length === 0) {
|
|
526
501
|
return {
|
|
527
|
-
content: [{ type: "text", text: `No tools at offset ${offset}; "${server}" has ${page.total} tools. Retry with mcp({ server: ${JSON.stringify(server)}, limit: ${limit}, offset: 0 }).` }],
|
|
502
|
+
content: [{ type: "text", text: `No tools at offset ${offset}; "${server}" has ${page.total} tools. Retry with mcp({ action: "list", server: ${JSON.stringify(server)}, limit: ${limit}, offset: 0 }).` }],
|
|
528
503
|
details: { mode: "list", error: "offset_out_of_range", server, tools: [], count: page.total, hasMore: false, nextOffset: null, hasInstructions: Boolean(instructions) },
|
|
529
504
|
};
|
|
530
505
|
}
|
|
@@ -539,7 +514,7 @@ export function executeList(state, server, limit = 12, offset = 0) {
|
|
|
539
514
|
}
|
|
540
515
|
const first = (Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0) + 1;
|
|
541
516
|
text += page.hasMore
|
|
542
|
-
? `\n${first}-${first + page.items.length - 1} of ${page.total} — mcp({ server: ${JSON.stringify(server)}, limit: ${limit}, offset: ${page.nextOffset} }) for more`
|
|
517
|
+
? `\n${first}-${first + page.items.length - 1} of ${page.total} — mcp({ action: "list", server: ${JSON.stringify(server)}, limit: ${limit}, offset: ${page.nextOffset} }) for more`
|
|
543
518
|
: `\n${first}-${first + page.items.length - 1} of ${page.total} — end`;
|
|
544
519
|
text += instructionsText;
|
|
545
520
|
return {
|
|
@@ -559,7 +534,7 @@ export function executeInstructions(state, server) {
|
|
|
559
534
|
const definition = state.config.mcpServers[server];
|
|
560
535
|
if (!definition) {
|
|
561
536
|
return {
|
|
562
|
-
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({}) to see available servers.` }],
|
|
537
|
+
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
563
538
|
details: { mode: "instructions", server, error: "not_found" },
|
|
564
539
|
};
|
|
565
540
|
}
|
|
@@ -580,7 +555,7 @@ export function executeInstructions(state, server) {
|
|
|
580
555
|
};
|
|
581
556
|
}
|
|
582
557
|
return {
|
|
583
|
-
content: [{ type: "text", text: `No instructions cached for "${server}". Use mcp({ connect: "${server}" }) to connect and refresh.` }],
|
|
558
|
+
content: [{ type: "text", text: `No instructions cached for "${server}". Use mcp({ action: "connect", server: "${server}" }) to connect and refresh.` }],
|
|
584
559
|
details: { mode: "instructions", server, error: "not_connected" },
|
|
585
560
|
};
|
|
586
561
|
}
|
|
@@ -590,7 +565,7 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
|
590
565
|
const definition = state.config.mcpServers[serverName];
|
|
591
566
|
if (!definition) {
|
|
592
567
|
return {
|
|
593
|
-
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
568
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
594
569
|
details: { mode: "connect", error: "not_found", server: serverName },
|
|
595
570
|
};
|
|
596
571
|
}
|
|
@@ -627,6 +602,7 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
|
627
602
|
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
628
603
|
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
|
|
629
604
|
state.toolMetadata.set(serverName, metadata);
|
|
605
|
+
state.resourceCounts?.set(serverName, connection.resources.length);
|
|
630
606
|
if (!connection.promptDiscoveryFailed) {
|
|
631
607
|
state.promptMetadata?.set(serverName, reconstructPromptMetadata(serverName, connection.prompts ?? [], prefix, definition));
|
|
632
608
|
state.promptMetadataLive?.add(serverName);
|
|
@@ -658,12 +634,11 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
|
658
634
|
/**
|
|
659
635
|
* Executes an already-approved tool call against a connected server.
|
|
660
636
|
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
* auth strategy, approval) differs between them and stays with the caller.
|
|
637
|
+
* Direct, gateway, resource and script calls share executeCall preparation.
|
|
638
|
+
* Raw scripts skip content rendering but retain protocol results and artifacts.
|
|
664
639
|
*/
|
|
665
640
|
export async function runToolCall(state, serverName, target, args, options) {
|
|
666
|
-
const { detailsBase, signal, recoverAuthConnection, authRequiredMessage, autoAuthAttempted
|
|
641
|
+
const { detailsBase, signal, recoverAuthConnection, authRequiredMessage, autoAuthAttempted } = options;
|
|
667
642
|
const configuredOptions = state.manager.getRequestOptions?.(serverName, options.ownedSignal);
|
|
668
643
|
const callerSignal = combineAbortSignals(configuredOptions?.signal, options.ownedSignal);
|
|
669
644
|
const outputGuardOptions = resolveMcpOutputGuardOptions(state.config.settings, state.outputDirectory);
|
|
@@ -705,8 +680,7 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
705
680
|
onNeedsAuth: recoverAuthConnection,
|
|
706
681
|
})
|
|
707
682
|
: null;
|
|
708
|
-
//
|
|
709
|
-
// stale cached hints in preference to the connected server's annotations.
|
|
683
|
+
// UI preparation can replace the connection; use its current annotations.
|
|
710
684
|
const liveTools = state.manager.getConnection(serverName)?.tools;
|
|
711
685
|
const annotations = liveTools
|
|
712
686
|
? liveTools.find(tool => tool.name === target.originalName)?.annotations
|
|
@@ -744,46 +718,32 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
744
718
|
await capture({ ...call, phase: "after", result });
|
|
745
719
|
return result;
|
|
746
720
|
};
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
const guarded = await guardMcpOutput(outputContent, { ...outputGuardOptions, suffix: `\n\n${uiSummary.message}`, rawMcpResult: result });
|
|
774
|
-
return {
|
|
775
|
-
content: guarded.content,
|
|
776
|
-
details: {
|
|
777
|
-
...detailsBase,
|
|
778
|
-
...guardedMcpDetails(guarded),
|
|
779
|
-
uiOpen: uiSummary.uiOpen,
|
|
780
|
-
uiViewer: uiSummary.uiViewer,
|
|
781
|
-
uiUrl: uiSummary.uiUrl,
|
|
782
|
-
},
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
const guarded = await guardMcpOutput(outputContent, { ...outputGuardOptions, rawMcpResult: result });
|
|
786
|
-
return { content: guarded.content, details: { ...detailsBase, ...guardedMcpDetails(guarded) } };
|
|
721
|
+
const result = target.resourceUri
|
|
722
|
+
? await dispatch(() => withSessionRecovery(recovery, serverName, conn => abortable(conn.client.readResource({ uri: target.resourceUri }, requestOptions), ownedSignal)))
|
|
723
|
+
: await dispatch(() => withSessionRecovery({ ...recovery, retryOnTransportFailure: annotations?.readOnlyHint === true || annotations?.idempotentHint === true }, serverName, conn => abortable(conn.client.callTool({ name: target.originalName, arguments: args ?? {}, _meta: uiSession?.requestMeta }, requestOptions), ownedSignal)));
|
|
724
|
+
if (!target.resourceUri)
|
|
725
|
+
uiSession?.sendToolResult(result);
|
|
726
|
+
const record = result;
|
|
727
|
+
const isError = record.isError === true;
|
|
728
|
+
const uiSummary = target.uiResourceUri ? summarizeUiSessionResult(uiSession) : undefined;
|
|
729
|
+
const details = {
|
|
730
|
+
...detailsBase,
|
|
731
|
+
...(isError ? { error: "tool_error" } : {}),
|
|
732
|
+
...(uiSummary ? { uiOpen: uiSummary.uiOpen, uiViewer: uiSummary.uiViewer, uiUrl: uiSummary.uiUrl } : {}),
|
|
733
|
+
};
|
|
734
|
+
const retained = await retainMcpResult(result, outputGuardOptions, options.raw);
|
|
735
|
+
if (options.raw) {
|
|
736
|
+
const message = isError
|
|
737
|
+
? (Array.isArray(record.content) ? record.content.filter(block => block.type === "text").map(block => block.text).join("\n") : "") || "Tool execution failed" : undefined;
|
|
738
|
+
return { content: [], details: { ...details, ...retained, ...(message ? { message } : {}) } };
|
|
739
|
+
}
|
|
740
|
+
const content = renderMcpResultContent(record, retained.payloadFiles);
|
|
741
|
+
const guarded = await guardMcpOutput(content.length ? content : [{ type: "text", text: target.resourceUri ? "(empty resource)" : "(empty result)" }], {
|
|
742
|
+
...outputGuardOptions, retainedMcpResult: retained,
|
|
743
|
+
...(isError ? { prefix: "Error: ", suffix: schemaSuffix(), emptyTextFallback: "Tool execution failed" }
|
|
744
|
+
: uiSummary ? { suffix: `\n\n${uiSummary.message}` } : {}),
|
|
745
|
+
});
|
|
746
|
+
return { content: guarded.content, details: { ...details, ...guardedMcpDetails(guarded) } };
|
|
787
747
|
}
|
|
788
748
|
catch (error) {
|
|
789
749
|
// Direct/proxy host errors throw; scripts retain their failed-call envelope.
|
|
@@ -795,9 +755,11 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
795
755
|
? "MCP call capture failed before dispatch; the tool did not run. Restore capture before continuing."
|
|
796
756
|
: "MCP outcome capture failed. Use the retained outcome to continue the original work; do not repeat a completed call or rerun the script.";
|
|
797
757
|
uiSession?.sendToolCancelled(message);
|
|
758
|
+
const retained = "result" in captureFailure && captureFailure.result !== undefined
|
|
759
|
+
? await retainMcpResult(captureFailure.result, { ...outputGuardOptions, enabled: true, detailsMaxBytes: 1 }) : {};
|
|
798
760
|
return {
|
|
799
|
-
content: [{ type: "text", text: message }],
|
|
800
|
-
details: { ...detailsBase, error: isAbortError(error, callerSignal) ? "aborted" : "call_capture_failed", message, recovery: context },
|
|
761
|
+
content: [{ type: "text", text: message }, ...(retained.resultRef ? [{ type: "text", text: formatMcpResultReference(retained.resultRef) }] : [])],
|
|
762
|
+
details: { ...detailsBase, ...retained, error: isAbortError(error, callerSignal) ? "aborted" : "call_capture_failed", message, recovery: context },
|
|
801
763
|
};
|
|
802
764
|
}
|
|
803
765
|
if (isToolTransportFailure(error)) {
|
|
@@ -848,388 +810,211 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
848
810
|
state.manager.touch(serverName);
|
|
849
811
|
}
|
|
850
812
|
}
|
|
851
|
-
|
|
813
|
+
/** Connect/auth once for calls and resource discovery. Recovery stays bounded by withSessionRecovery. */
|
|
814
|
+
export async function prepareMcpConnection(state, serverName, signal) {
|
|
852
815
|
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
853
816
|
throwIfAborted(ownedSignal);
|
|
854
|
-
let serverName = serverOverride;
|
|
855
|
-
let toolMeta;
|
|
856
817
|
let autoAuthAttempted = false;
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
return
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
818
|
+
const recoverAuthConnection = async (_name, recoverySignal = ownedSignal, challenge) => {
|
|
819
|
+
throwIfAborted(recoverySignal);
|
|
820
|
+
const current = state.manager.getConnection(serverName);
|
|
821
|
+
if (current?.status === "connected" && current !== challenge?.connection)
|
|
822
|
+
return current;
|
|
823
|
+
if (!autoAuthAttempted) {
|
|
824
|
+
autoAuthAttempted = true;
|
|
825
|
+
const auth = await attemptAutoAuth(state, serverName, recoverySignal);
|
|
826
|
+
throwIfAborted(recoverySignal);
|
|
827
|
+
if (auth.status === "failed")
|
|
828
|
+
throw new SessionRecoveryAuthRequiredError(serverName, auth.message);
|
|
829
|
+
if (auth.status === "success" && current) {
|
|
830
|
+
clearFailure(state, serverName);
|
|
831
|
+
const definition = state.config.mcpServers[serverName];
|
|
832
|
+
if (!definition)
|
|
833
|
+
return undefined;
|
|
834
|
+
state.ui?.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${serverName}...`));
|
|
835
|
+
await state.manager.reconnect(serverName, definition, current, recoverySignal);
|
|
836
|
+
if (await lazyConnect(state, serverName, recoverySignal)) {
|
|
837
|
+
updateStatusBar(state);
|
|
838
|
+
return state.manager.getConnection(serverName);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
865
841
|
}
|
|
866
|
-
|
|
867
|
-
const identity = metadata.resourceUri
|
|
868
|
-
? { server: disabledServer, resourceUri: metadata.resourceUri }
|
|
869
|
-
: { server: disabledServer, tool: metadata.originalName };
|
|
870
|
-
return {
|
|
871
|
-
content: [{ type: "text", text: message }],
|
|
872
|
-
details: { mode: "call", error: "server_disabled", ...identity, message },
|
|
873
|
-
};
|
|
842
|
+
return challenge ? undefined : state.manager.getConnection(serverName);
|
|
874
843
|
};
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
844
|
+
const failure = (error, message) => ({
|
|
845
|
+
content: [{ type: "text", text: message }],
|
|
846
|
+
details: { mode: "call", error, server: serverName, message },
|
|
847
|
+
});
|
|
848
|
+
const definition = state.config.mcpServers[serverName];
|
|
849
|
+
if (!definition)
|
|
850
|
+
return { error: failure("server_not_found", `Server "${serverName}" not found.`) };
|
|
851
|
+
if (isServerDisabled(definition))
|
|
852
|
+
return { error: disabledResult("call", serverName) };
|
|
853
|
+
try {
|
|
854
|
+
// Connected metadata is read below, so it does not require another connection attempt.
|
|
855
|
+
let connection = state.manager.getConnection(serverName);
|
|
856
|
+
if (connection?.status !== "connected") {
|
|
857
|
+
await lazyConnect(state, serverName, ownedSignal);
|
|
858
|
+
connection = state.manager.getConnection(serverName);
|
|
885
859
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
let disabledMatch;
|
|
889
|
-
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
890
|
-
const found = findToolByName(metadata, toolName);
|
|
891
|
-
if (!found)
|
|
892
|
-
continue;
|
|
893
|
-
if (isServerDisabled(state.config.mcpServers[server])) {
|
|
894
|
-
disabledMatch ??= { serverName: server, toolMeta: found };
|
|
895
|
-
continue;
|
|
896
|
-
}
|
|
897
|
-
serverName = server;
|
|
898
|
-
toolMeta = found;
|
|
899
|
-
break;
|
|
860
|
+
else {
|
|
861
|
+
markKeepAliveAfterConnect(state, serverName);
|
|
900
862
|
}
|
|
901
|
-
if (
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
863
|
+
if (connection?.status === "needs-auth")
|
|
864
|
+
connection = await recoverAuthConnection(serverName, ownedSignal);
|
|
865
|
+
if (connection?.status === "needs-auth")
|
|
866
|
+
throw new SessionRecoveryAuthRequiredError(serverName);
|
|
867
|
+
if (connection?.status !== "connected") {
|
|
868
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
869
|
+
return { error: failure(failedAgo === null ? "server_unavailable" : "server_backoff", `Server "${serverName}" not available${failedAgo === null ? "" : ` (last failed ${failedAgo}s ago)`}`) };
|
|
908
870
|
}
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
autoAuthAttempted = true;
|
|
914
|
-
const autoAuth = await attemptAutoAuth(state, serverName, ownedSignal);
|
|
915
|
-
if (autoAuth.status === "failed") {
|
|
916
|
-
return {
|
|
917
|
-
content: [{ type: "text", text: autoAuth.message }],
|
|
918
|
-
details: { mode: "call", error: "auth_required", server: serverName, requestedTool: toolName, message: autoAuth.message },
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
if (autoAuth.status === "success") {
|
|
922
|
-
clearFailure(state, serverName);
|
|
923
|
-
let connectedAfterAuth = false;
|
|
924
|
-
try {
|
|
925
|
-
const definition = state.config.mcpServers[serverName];
|
|
926
|
-
if (definition) {
|
|
927
|
-
await state.manager.reconnect(serverName, definition, needsAuthConnection, ownedSignal);
|
|
928
|
-
connectedAfterAuth = true;
|
|
929
|
-
}
|
|
930
|
-
}
|
|
931
|
-
catch (error) {
|
|
932
|
-
if (isAbortError(error, ownedSignal))
|
|
933
|
-
throwIfAborted(ownedSignal);
|
|
934
|
-
recordFailure(state, serverName, error instanceof Error ? error.message : String(error));
|
|
935
|
-
updateStatusBar(state);
|
|
936
|
-
}
|
|
937
|
-
if (connectedAfterAuth)
|
|
938
|
-
connectedAfterAuth = await lazyConnect(state, serverName, ownedSignal);
|
|
939
|
-
if (connectedAfterAuth) {
|
|
940
|
-
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
941
|
-
if (!toolMeta) {
|
|
942
|
-
const suggestions = rankSuggestions(state, toolName, 5, serverName);
|
|
943
|
-
const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}` : "";
|
|
944
|
-
return {
|
|
945
|
-
content: [{ type: "text", text: `Tool "${toolName}" not found on "${serverName}" after reconnect.${suggestionText}` }],
|
|
946
|
-
details: { mode: "call", error: "tool_not_found_after_reconnect", server: serverName, requestedTool: toolName, suggestions },
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
if (!toolMeta && state.manager.getConnection(serverName)?.status === "needs-auth") {
|
|
953
|
-
const message = getAuthRequiredMessage(state, serverName);
|
|
954
|
-
return {
|
|
955
|
-
content: [{ type: "text", text: message }],
|
|
956
|
-
details: { mode: "call", error: "auth_required", server: serverName, requestedTool: toolName, message },
|
|
957
|
-
};
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
if (!toolMeta) {
|
|
961
|
-
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
962
|
-
if (failedAgo !== null) {
|
|
963
|
-
return {
|
|
964
|
-
content: [{ type: "text", text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
965
|
-
details: { mode: "call", error: "server_backoff", server: serverName, requestedTool: toolName },
|
|
966
|
-
};
|
|
967
|
-
}
|
|
968
|
-
}
|
|
871
|
+
// Use the live descriptor for visibility, filtering, approval, schema and UI hints.
|
|
872
|
+
if (connection.tools) {
|
|
873
|
+
const { metadata } = buildToolMetadata(connection.tools, connection.resources ?? [], definition, serverName, state.config.settings?.toolPrefix ?? "server");
|
|
874
|
+
state.toolMetadata.set(serverName, metadata);
|
|
969
875
|
}
|
|
876
|
+
return { connection, recoverAuthConnection, autoAuthAttempted: () => autoAuthAttempted };
|
|
970
877
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
.
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
.sort((a, b) => b.prefix.length - a.prefix.length)
|
|
981
|
-
.slice(0, 1);
|
|
982
|
-
for (const { name: configuredServer } of candidates) {
|
|
983
|
-
const existingConnection = state.manager.getConnection(configuredServer);
|
|
984
|
-
const failedAgo = getFailureAgeSeconds(state, configuredServer);
|
|
985
|
-
if (failedAgo !== null && existingConnection?.status !== "needs-auth")
|
|
986
|
-
continue;
|
|
987
|
-
if (state.toolMetadata.has(configuredServer)) {
|
|
988
|
-
prefixMatchedServer ??= configuredServer;
|
|
989
|
-
continue;
|
|
990
|
-
}
|
|
991
|
-
let connected = await lazyConnect(state, configuredServer, ownedSignal);
|
|
992
|
-
const needsAuthConnection = state.manager.getConnection(configuredServer);
|
|
993
|
-
if (!connected && needsAuthConnection?.status === "needs-auth" && !autoAuthAttempted) {
|
|
994
|
-
autoAuthAttempted = true;
|
|
995
|
-
const autoAuth = await attemptAutoAuth(state, configuredServer, ownedSignal);
|
|
996
|
-
if (autoAuth.status === "failed") {
|
|
997
|
-
return {
|
|
998
|
-
content: [{ type: "text", text: autoAuth.message }],
|
|
999
|
-
details: { mode: "call", error: "auth_required", server: configuredServer, requestedTool: toolName, message: autoAuth.message },
|
|
1000
|
-
};
|
|
1001
|
-
}
|
|
1002
|
-
if (autoAuth.status === "success") {
|
|
1003
|
-
clearFailure(state, configuredServer);
|
|
1004
|
-
try {
|
|
1005
|
-
const definition = state.config.mcpServers[configuredServer];
|
|
1006
|
-
if (definition) {
|
|
1007
|
-
await state.manager.reconnect(configuredServer, definition, needsAuthConnection, ownedSignal);
|
|
1008
|
-
connected = true;
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
catch (error) {
|
|
1012
|
-
if (isAbortError(error, ownedSignal))
|
|
1013
|
-
throwIfAborted(ownedSignal);
|
|
1014
|
-
recordFailure(state, configuredServer, error instanceof Error ? error.message : String(error));
|
|
1015
|
-
updateStatusBar(state);
|
|
1016
|
-
}
|
|
1017
|
-
if (connected)
|
|
1018
|
-
connected = await lazyConnect(state, configuredServer, ownedSignal);
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
if (!connected)
|
|
1022
|
-
continue;
|
|
1023
|
-
if (!prefixMatchedServer)
|
|
1024
|
-
prefixMatchedServer = configuredServer;
|
|
1025
|
-
toolMeta = findToolByName(state.toolMetadata.get(configuredServer), toolName);
|
|
1026
|
-
if (toolMeta) {
|
|
1027
|
-
serverName = configuredServer;
|
|
1028
|
-
break;
|
|
1029
|
-
}
|
|
878
|
+
catch (error) {
|
|
879
|
+
if (isAbortError(error, ownedSignal))
|
|
880
|
+
throw error;
|
|
881
|
+
const message = error instanceof SessionRecoveryAuthRequiredError
|
|
882
|
+
? error.authMessage ?? getAuthRequiredMessage(state, serverName)
|
|
883
|
+
: error instanceof Error ? error.message : String(error);
|
|
884
|
+
if (!(error instanceof SessionRecoveryAuthRequiredError)) {
|
|
885
|
+
recordFailure(state, serverName, message);
|
|
886
|
+
updateStatusBar(state);
|
|
1030
887
|
}
|
|
888
|
+
return { error: failure(error instanceof SessionRecoveryAuthRequiredError ? "auth_required" : "connect_failed", message) };
|
|
1031
889
|
}
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
890
|
+
}
|
|
891
|
+
export async function executeCall(state, toolName, args, serverOverride, getPiTools, signal, identity = {}, beforeDispatch, options = {}) {
|
|
892
|
+
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
893
|
+
throwIfAborted(ownedSignal);
|
|
894
|
+
if (typeof toolName !== "string" || !toolName.trim() || (serverOverride !== undefined && (typeof serverOverride !== "string" || !serverOverride)))
|
|
895
|
+
return {
|
|
896
|
+
content: [{ type: "text", text: "Specify a non-empty tool name and, when supplied, server." }],
|
|
897
|
+
details: { mode: "call", error: "invalid_arguments" },
|
|
898
|
+
};
|
|
899
|
+
if (args !== undefined && (args === null || typeof args !== "object" || Array.isArray(args)))
|
|
900
|
+
return {
|
|
901
|
+
content: [{ type: "text", text: "Tool arguments must be an object." }],
|
|
902
|
+
details: { mode: "call", error: "invalid_arguments", requestedTool: toolName },
|
|
903
|
+
};
|
|
904
|
+
let serverName = serverOverride;
|
|
905
|
+
const matches = (metadata = []) => {
|
|
906
|
+
if (options.resourceUri !== undefined)
|
|
907
|
+
return metadata.filter(tool => tool.resourceUri === options.resourceUri);
|
|
908
|
+
if (options.exactOriginalName)
|
|
909
|
+
return metadata.filter(tool => tool.originalName === toolName && !tool.resourceUri);
|
|
910
|
+
if (serverOverride) {
|
|
911
|
+
const originals = metadata.filter(tool => tool.originalName === toolName);
|
|
912
|
+
if (originals.length)
|
|
913
|
+
return originals;
|
|
914
|
+
}
|
|
915
|
+
const exact = metadata.filter(tool => tool.name === toolName);
|
|
916
|
+
if (exact.length)
|
|
917
|
+
return exact;
|
|
918
|
+
return metadata.filter(tool => tool.name.replace(/-/g, "_") === toolName.replace(/-/g, "_"));
|
|
919
|
+
};
|
|
920
|
+
const ambiguous = () => ({
|
|
921
|
+
content: [{ type: "text", text: `Tool "${toolName}" is ambiguous. Specify its exact server and tool name; no call was dispatched.` }],
|
|
922
|
+
details: { mode: "call", error: "ambiguous_tool", requestedTool: toolName, ...(serverName ? { server: serverName } : {}) },
|
|
923
|
+
});
|
|
924
|
+
if (!serverName) {
|
|
925
|
+
const candidates = [...state.toolMetadata].flatMap(([server, metadata]) => isServerDisabled(state.config.mcpServers[server]) ? [] : matches(metadata).map(tool => ({ server, tool })));
|
|
926
|
+
if (candidates.length > 1)
|
|
927
|
+
return ambiguous();
|
|
928
|
+
serverName = candidates[0]?.server;
|
|
929
|
+
if (!serverName) {
|
|
930
|
+
const disabled = [...state.toolMetadata].find(([server, metadata]) => isServerDisabled(state.config.mcpServers[server]) && matches(metadata).length > 0);
|
|
931
|
+
if (disabled)
|
|
932
|
+
return disabledResult("call", disabled[0]);
|
|
933
|
+
const prefixes = Object.entries(state.config.mcpServers)
|
|
934
|
+
.filter(([, definition]) => !isServerDisabled(definition))
|
|
935
|
+
.map(([server, definition]) => ({ server, prefix: getServerPrefix(server, resolveToolPrefix(definition, state.config.settings?.toolPrefix ?? "server")) }))
|
|
936
|
+
.filter(({ prefix }) => prefix && toolName.startsWith(`${prefix}_`))
|
|
937
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
938
|
+
if (prefixes.length > 1 && prefixes[0]?.prefix === prefixes[1]?.prefix)
|
|
939
|
+
return ambiguous();
|
|
940
|
+
serverName = prefixes[0]?.server;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
const missing = () => {
|
|
944
|
+
const nativeTool = !serverOverride && getPiTools?.().find(tool => tool.name === toolName && tool.name !== "mcp");
|
|
945
|
+
if (nativeTool)
|
|
1037
946
|
return {
|
|
1038
|
-
content: [{ type: "text", text: `"${toolName}" is a native Pi tool. Call ${toolName} directly instead of using mcp({ tool: "${toolName}" }).` }],
|
|
947
|
+
content: [{ type: "text", text: `"${toolName}" is a native Pi tool. Call ${toolName} directly instead of using mcp({ action: "call", tool: "${toolName}" }).` }],
|
|
1039
948
|
details: { mode: "call", error: "native_tool", requestedTool: toolName },
|
|
1040
949
|
};
|
|
1041
|
-
|
|
1042
|
-
const
|
|
1043
|
-
const
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const suggestedOwner =
|
|
1047
|
-
|
|
1048
|
-
&& !isServerDisabled(state.config.mcpServers[candidateServer])
|
|
1049
|
-
&& findToolByName(metadata, toolName))
|
|
1050
|
-
: undefined;
|
|
1051
|
-
const suggestedTool = suggestedOwner ? findToolByName(suggestedOwner[1], toolName) : undefined;
|
|
1052
|
-
if (suggestedOwner && suggestedTool) {
|
|
1053
|
-
const suggestedServer = suggestedOwner[0];
|
|
950
|
+
const normalizedName = toolName.replace(/-/g, "_");
|
|
951
|
+
const localOriginal = serverOverride && state.toolMetadata.get(serverOverride)?.some(tool => tool.originalName.replace(/-/g, "_") === normalizedName);
|
|
952
|
+
const owners = serverOverride && !localOriginal
|
|
953
|
+
? [...state.toolMetadata].filter(([server, metadata]) => server !== serverOverride && !isServerDisabled(state.config.mcpServers[server]) && matches(metadata).length === 1)
|
|
954
|
+
: [];
|
|
955
|
+
const suggestedOwner = owners.length === 1 ? owners[0] : undefined;
|
|
956
|
+
if (suggestedOwner)
|
|
1054
957
|
return {
|
|
1055
|
-
content: [{ type: "text", text: `Tool ${JSON.stringify(toolName)} is on server ${JSON.stringify(
|
|
1056
|
-
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer, suggestedServer, suggestions: [
|
|
958
|
+
content: [{ type: "text", text: `Tool ${JSON.stringify(toolName)} is on server ${JSON.stringify(suggestedOwner[0])}, not ${JSON.stringify(serverOverride)}. Retry the same call with server: ${JSON.stringify(suggestedOwner[0])} or omit server.` }],
|
|
959
|
+
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer: serverName, suggestedServer: suggestedOwner[0], suggestions: matches(suggestedOwner[1]).map(tool => tool.name) },
|
|
1057
960
|
};
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
else if (hintServer) {
|
|
1065
|
-
hint = ` Search with mcp({ search: ${JSON.stringify(toolName)}, server: ${JSON.stringify(hintServer)} }).`;
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
hint = ` Use mcp({ search: "..." }) to search.`;
|
|
1069
|
-
}
|
|
1070
|
-
const suggestionOnHintServer = hintServer && suggestions.some(suggestion => state.toolMetadata.get(hintServer)?.some(tool => tool.name === suggestion));
|
|
1071
|
-
if (hintServer && (suggestions.length === 0 || suggestionOnHintServer) && state.toolMetadata.has(hintServer) && state.manager.getConnection(hintServer)?.status !== "connected") {
|
|
1072
|
-
hint += ` Refresh a stale catalog with mcp({ connect: ${JSON.stringify(hintServer)} }).`;
|
|
1073
|
-
}
|
|
961
|
+
const suggestions = rankSuggestions(state, toolName, 5, serverName);
|
|
962
|
+
let hint = suggestions.length
|
|
963
|
+
? ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: ${JSON.stringify(suggestions[0])} }).`
|
|
964
|
+
: serverName ? ` Search with mcp({ action: "search", query: ${JSON.stringify(toolName)}, server: ${JSON.stringify(serverName)} }).` : ' Use mcp({ action: "search", query: "..." }) to search.';
|
|
965
|
+
if (serverName && state.toolMetadata.has(serverName) && state.manager.getConnection(serverName)?.status !== "connected")
|
|
966
|
+
hint += ` Refresh a stale catalog with mcp({ action: "connect", server: ${JSON.stringify(serverName)} }).`;
|
|
1074
967
|
return {
|
|
1075
968
|
content: [{ type: "text", text: `Tool "${toolName}" not found.${hint}` }],
|
|
1076
|
-
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer, suggestions },
|
|
969
|
+
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer: serverName, suggestions },
|
|
1077
970
|
};
|
|
1078
|
-
}
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
if (
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
if (!connection || connection.status !== "connected") {
|
|
1108
|
-
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
1109
|
-
if (failedAgo !== null) {
|
|
1110
|
-
return {
|
|
1111
|
-
content: [{ type: "text", text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
1112
|
-
details: { mode: "call", error: "server_backoff", ...callIdentity },
|
|
1113
|
-
};
|
|
1114
|
-
}
|
|
1115
|
-
const definition = state.config.mcpServers[serverName];
|
|
1116
|
-
if (!definition) {
|
|
1117
|
-
return {
|
|
1118
|
-
content: [{ type: "text", text: `Server "${serverName}" not connected` }],
|
|
1119
|
-
details: { mode: "call", error: "server_not_connected", ...callIdentity },
|
|
1120
|
-
};
|
|
1121
|
-
}
|
|
1122
|
-
try {
|
|
1123
|
-
if (state.ui) {
|
|
1124
|
-
state.ui.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${serverName}...`));
|
|
1125
|
-
}
|
|
1126
|
-
connection = refreshAfterAuth && connection
|
|
1127
|
-
? await state.manager.reconnect(serverName, definition, connection, ownedSignal)
|
|
1128
|
-
: await state.manager.connect(serverName, definition, ownedSignal);
|
|
1129
|
-
if (connection.status === "needs-auth") {
|
|
1130
|
-
if (!autoAuthAttempted) {
|
|
1131
|
-
autoAuthAttempted = true;
|
|
1132
|
-
const autoAuth = await attemptAutoAuth(state, serverName, ownedSignal);
|
|
1133
|
-
if (autoAuth.status === "failed") {
|
|
1134
|
-
return {
|
|
1135
|
-
content: [{ type: "text", text: autoAuth.message }],
|
|
1136
|
-
details: { mode: "call", error: "auth_required", ...callIdentity, message: autoAuth.message },
|
|
1137
|
-
};
|
|
1138
|
-
}
|
|
1139
|
-
if (autoAuth.status === "success") {
|
|
1140
|
-
connection = await state.manager.reconnect(serverName, definition, connection, ownedSignal);
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
if (connection.status === "needs-auth") {
|
|
1144
|
-
const message = getAuthRequiredMessage(state, serverName);
|
|
1145
|
-
return {
|
|
1146
|
-
content: [{ type: "text", text: message }],
|
|
1147
|
-
details: { mode: "call", error: "auth_required", ...callIdentity, message },
|
|
1148
|
-
};
|
|
1149
|
-
}
|
|
1150
|
-
}
|
|
1151
|
-
clearFailure(state, serverName);
|
|
1152
|
-
updateServerMetadata(state, serverName);
|
|
1153
|
-
updateMetadataCache(state, serverName);
|
|
1154
|
-
notifyToolMetadataUpdated(state, serverName, "proxy-call-reconnect");
|
|
1155
|
-
markKeepAliveAfterConnect(state, serverName);
|
|
1156
|
-
updateStatusBar(state);
|
|
1157
|
-
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
1158
|
-
if (!toolMeta) {
|
|
1159
|
-
const suggestions = rankSuggestions(state, toolName, 5, serverName);
|
|
1160
|
-
const hint = suggestions.length > 0
|
|
1161
|
-
? ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ describe: "${suggestions[0]}" }).`
|
|
1162
|
-
: ` Search with mcp({ search: ${JSON.stringify(toolName)}, server: ${JSON.stringify(serverName)} }).`;
|
|
1163
|
-
return {
|
|
1164
|
-
content: [{ type: "text", text: `Tool "${toolName}" not found on "${serverName}" after reconnect.${hint}` }],
|
|
1165
|
-
details: { mode: "call", error: "tool_not_found_after_reconnect", server: serverName, requestedTool: toolName, suggestions },
|
|
1166
|
-
};
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
catch (error) {
|
|
1170
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1171
|
-
if (!isAbortError(error, ownedSignal))
|
|
1172
|
-
recordFailure(state, serverName, message);
|
|
1173
|
-
updateStatusBar(state);
|
|
1174
|
-
return {
|
|
1175
|
-
content: [{ type: "text", text: `Failed to connect to "${serverName}": ${message}` }],
|
|
1176
|
-
details: { mode: "call", error: isAbortError(error, ownedSignal) ? "aborted" : "connect_failed", ...callIdentity, message },
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
if (isServerDisabled(state.config.mcpServers[serverName])) {
|
|
1181
|
-
return disabledCallResult(serverName, toolMeta);
|
|
1182
|
-
}
|
|
971
|
+
};
|
|
972
|
+
if (!serverName)
|
|
973
|
+
return missing();
|
|
974
|
+
const definition = state.config.mcpServers[serverName];
|
|
975
|
+
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
|
+
if (isServerDisabled(definition))
|
|
978
|
+
return disabledResult("call", serverName);
|
|
979
|
+
const cached = state.toolMetadata.get(serverName);
|
|
980
|
+
if (matches(cached).length > 1)
|
|
981
|
+
return ambiguous();
|
|
982
|
+
// Misspellings in a known catalog do not cause connection churn or guessed execution.
|
|
983
|
+
if (cached && !matches(cached).length && !options.exactOriginalName && options.resourceUri === undefined)
|
|
984
|
+
return missing();
|
|
985
|
+
const prepared = await prepareMcpConnection(state, serverName, ownedSignal);
|
|
986
|
+
if (prepared.error) {
|
|
987
|
+
const cachedTarget = matches(cached)[0];
|
|
988
|
+
return { ...prepared.error, details: { ...prepared.error.details, ...(cachedTarget
|
|
989
|
+
? cachedTarget.resourceUri ? { resourceUri: cachedTarget.resourceUri } : { tool: cachedTarget.originalName }
|
|
990
|
+
: { requestedTool: toolName }) } };
|
|
991
|
+
}
|
|
992
|
+
const targets = matches(state.toolMetadata.get(serverName));
|
|
993
|
+
if (targets.length > 1)
|
|
994
|
+
return ambiguous();
|
|
995
|
+
const toolMeta = targets[0];
|
|
996
|
+
if (!toolMeta)
|
|
997
|
+
return missing();
|
|
998
|
+
if (isServerDisabled(state.config.mcpServers[serverName]))
|
|
999
|
+
return disabledResult("call", serverName);
|
|
1183
1000
|
const approval = await ensureToolCallApproved(state, serverName, toolMeta, args, ownedSignal);
|
|
1184
|
-
if (approval.ok
|
|
1001
|
+
if (!approval.ok) {
|
|
1185
1002
|
const denied = approval.reason === "denied";
|
|
1186
|
-
const message = denied
|
|
1187
|
-
? `The user declined approval to run MCP tool "${toolMeta.originalName}" on server "${serverName}".`
|
|
1188
|
-
: `MCP tool "${toolMeta.originalName}" on server "${serverName}" is approval-gated and requires an interactive session.`;
|
|
1189
1003
|
return {
|
|
1190
|
-
content: [{ type: "text", text:
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
server: serverName,
|
|
1195
|
-
tool: toolMeta.originalName,
|
|
1196
|
-
},
|
|
1004
|
+
content: [{ type: "text", text: denied
|
|
1005
|
+
? `The user declined approval to run MCP tool "${toolMeta.originalName}" on server "${serverName}".`
|
|
1006
|
+
: `MCP tool "${toolMeta.originalName}" on server "${serverName}" is approval-gated and requires an interactive session.` }],
|
|
1007
|
+
details: { mode: "call", error: denied ? "approval_denied" : "approval_required", server: serverName, tool: toolMeta.originalName },
|
|
1197
1008
|
};
|
|
1198
1009
|
}
|
|
1199
|
-
const recoverAuthConnection = async (_serverName, recoverySignal = ownedSignal, challenge) => {
|
|
1200
|
-
throwIfAborted(recoverySignal);
|
|
1201
|
-
const current = state.manager.getConnection(serverName);
|
|
1202
|
-
if (current?.status === "connected" && current !== challenge?.connection)
|
|
1203
|
-
return current;
|
|
1204
|
-
if (!autoAuthAttempted) {
|
|
1205
|
-
autoAuthAttempted = true;
|
|
1206
|
-
const autoAuth = await attemptAutoAuth(state, serverName, recoverySignal);
|
|
1207
|
-
throwIfAborted(recoverySignal);
|
|
1208
|
-
if (autoAuth.status === "failed") {
|
|
1209
|
-
throw new SessionRecoveryAuthRequiredError(serverName, autoAuth.message);
|
|
1210
|
-
}
|
|
1211
|
-
if (autoAuth.status === "success") {
|
|
1212
|
-
const definition = state.config.mcpServers[serverName];
|
|
1213
|
-
if (!definition)
|
|
1214
|
-
return undefined;
|
|
1215
|
-
if (!current)
|
|
1216
|
-
return undefined;
|
|
1217
|
-
clearFailure(state, serverName);
|
|
1218
|
-
connection = await state.manager.reconnect(serverName, definition, current, recoverySignal);
|
|
1219
|
-
return connection;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
1222
|
-
return challenge ? undefined : state.manager.getConnection(serverName);
|
|
1223
|
-
};
|
|
1224
1010
|
return runToolCall(state, serverName, toolMeta, args, {
|
|
1225
1011
|
...identity,
|
|
1226
1012
|
...(beforeDispatch ? { beforeDispatch } : {}),
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
signal,
|
|
1230
|
-
recoverAuthConnection,
|
|
1013
|
+
...(options.raw !== undefined ? { raw: options.raw } : {}),
|
|
1014
|
+
detailsBase: { mode: "call", server: serverName, ...(toolMeta.resourceUri ? { resourceUri: toolMeta.resourceUri } : { tool: toolMeta.originalName }) },
|
|
1015
|
+
ownedSignal, signal,
|
|
1016
|
+
recoverAuthConnection: prepared.recoverAuthConnection,
|
|
1231
1017
|
authRequiredMessage: () => getAuthRequiredMessage(state, serverName),
|
|
1232
|
-
autoAuthAttempted:
|
|
1233
|
-
...(onRawResult ? { onRawResult } : {}),
|
|
1018
|
+
autoAuthAttempted: prepared.autoAuthAttempted,
|
|
1234
1019
|
});
|
|
1235
1020
|
}
|