@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/proxy-modes.ts
CHANGED
|
@@ -3,16 +3,15 @@ import type { Client } from "@modelcontextprotocol/client";
|
|
|
3
3
|
import { DEFAULT_REQUEST_TIMEOUT_MSEC, UrlElicitationRequiredError } from "@modelcontextprotocol/client";
|
|
4
4
|
import type { McpExtensionState } from "./state.ts";
|
|
5
5
|
import type { ServerConnection } from "./server-manager.ts";
|
|
6
|
-
import type { ToolMetadata,
|
|
6
|
+
import type { ToolMetadata, McpOperationContext, McpToolCallEvent, McpToolCallIdentity } from "./types.ts";
|
|
7
7
|
import { getServerPrefix, isServerDisabled, isNonInteractiveOAuth, parseUiPromptHandoff, resolveToolPrefix } from "./types.ts";
|
|
8
8
|
import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar, clearFailure, recordFailure } from "./init.ts";
|
|
9
9
|
import { abortable, throwIfAborted } from "./abort.ts";
|
|
10
10
|
import { combineAbortSignals, isAbortError } from "./runtime-owner.ts";
|
|
11
|
-
import { buildToolMetadata, findToolByName, formatSchema } from "./tool-metadata.ts";
|
|
12
|
-
import { renderTsShape } from "./ts-shape.ts";
|
|
11
|
+
import { buildToolMetadata, catalogCoverage, findToolByName, formatSchema, toToolDescriptor } from "./tool-metadata.ts";
|
|
13
12
|
import { reconstructPromptMetadata } from "./metadata-cache.ts";
|
|
14
|
-
import {
|
|
15
|
-
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.ts";
|
|
13
|
+
import { renderMcpResultContent } from "./tool-registrar.ts";
|
|
14
|
+
import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference } from "./mcp-output-guard.ts";
|
|
16
15
|
import { maybeStartUiSession, summarizeUiSessionResult, type UiSessionRuntime } from "./ui-session.ts";
|
|
17
16
|
import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord } from "./utils.ts";
|
|
18
17
|
import { authenticate, completeAuthFromInput, startAuth, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
@@ -87,7 +86,7 @@ function formatManualAuthInstructions(serverName: string, authorizationUrl: stri
|
|
|
87
86
|
"If the browser cannot reach the callback, copy the full redirected localhost URL from your address bar and send it back with:",
|
|
88
87
|
`mcp({ action: "auth-complete", server: "${serverName}", args: { redirectUrl: "PASTE_REDIRECT_URL_HERE" } })`,
|
|
89
88
|
"",
|
|
90
|
-
'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`.
|
|
89
|
+
'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`.',
|
|
91
90
|
portNote.trimEnd(),
|
|
92
91
|
].filter(Boolean).join("\n");
|
|
93
92
|
}
|
|
@@ -252,7 +251,7 @@ export function executeStatus(state: McpExtensionState): ProxyToolResult {
|
|
|
252
251
|
const disabled = isServerDisabled(definition);
|
|
253
252
|
const connection = disabled ? undefined : state.manager.getConnection(name);
|
|
254
253
|
const metadata = disabled ? undefined : state.toolMetadata.get(name);
|
|
255
|
-
const toolCount = metadata?.length ?? 0;
|
|
254
|
+
const toolCount = metadata?.filter(tool => !tool.resourceUri).length ?? 0;
|
|
256
255
|
const failedAgo = disabled ? null : getFailureAgeSeconds(state, name);
|
|
257
256
|
let status = disabled ? "disabled" : "not connected";
|
|
258
257
|
if (!disabled && connection?.status === "connected") {
|
|
@@ -272,6 +271,7 @@ export function executeStatus(state: McpExtensionState): ProxyToolResult {
|
|
|
272
271
|
const enabledServers = servers.filter(s => !s.disabled);
|
|
273
272
|
const totalTools = enabledServers.reduce((sum, s) => sum + s.toolCount, 0);
|
|
274
273
|
const connectedCount = enabledServers.filter(s => s.status === "connected").length;
|
|
274
|
+
const coverage = catalogCoverage(state);
|
|
275
275
|
|
|
276
276
|
let text = `MCP: ${connectedCount}/${enabledServers.length} connected, ${totalTools} tools available (calls connect lazily)`;
|
|
277
277
|
if (disabledCount > 0) text += ` (${disabledCount} disabled)`;
|
|
@@ -297,16 +297,17 @@ export function executeStatus(state: McpExtensionState): ProxyToolResult {
|
|
|
297
297
|
text += `✗ ${server.name} (failed ${server.failedAgo ?? 0}s ago)\n`;
|
|
298
298
|
continue;
|
|
299
299
|
}
|
|
300
|
-
text += `○ ${server.name} (not
|
|
300
|
+
text += `○ ${server.name} (not discovered)\n`;
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
if (servers.length > 0) {
|
|
304
|
-
text += `\
|
|
304
|
+
text += `\nmcp_search({ query: "...", server: "name" }) to discover tools; mcp({ action: "resources", server: "name" }) for resources.`;
|
|
305
|
+
if (!coverage.complete) text += `\nPartial catalog: ${coverage.unknownServers.length} undiscovered server(s).`;
|
|
305
306
|
}
|
|
306
307
|
|
|
307
308
|
return {
|
|
308
309
|
content: [{ type: "text" as const, text: text.trim() }],
|
|
309
|
-
details: { mode: "status", servers, totalTools, connectedCount, disabledCount },
|
|
310
|
+
details: { mode: "status", servers, totalTools, connectedCount, disabledCount, coverage },
|
|
310
311
|
};
|
|
311
312
|
}
|
|
312
313
|
|
|
@@ -316,7 +317,7 @@ export async function executeAuthStart(state: McpExtensionState, serverName: str
|
|
|
316
317
|
const definition = state.config.mcpServers[serverName];
|
|
317
318
|
if (!definition) {
|
|
318
319
|
return {
|
|
319
|
-
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
320
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
320
321
|
details: { mode: "auth-start", error: "not_found", server: serverName },
|
|
321
322
|
};
|
|
322
323
|
}
|
|
@@ -364,7 +365,7 @@ export async function executeAuthComplete(state: McpExtensionState, serverName:
|
|
|
364
365
|
const definition = state.config.mcpServers[serverName];
|
|
365
366
|
if (!definition) {
|
|
366
367
|
return {
|
|
367
|
-
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
368
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
368
369
|
details: { mode: "auth-complete", error: "not_found", server: serverName },
|
|
369
370
|
};
|
|
370
371
|
}
|
|
@@ -390,7 +391,7 @@ export async function executeAuthComplete(state: McpExtensionState, serverName:
|
|
|
390
391
|
clearFailure(state, serverName);
|
|
391
392
|
updateStatusBar(state);
|
|
392
393
|
return {
|
|
393
|
-
content: [{ type: "text" as const, text: `OAuth authentication successful for "${serverName}". Run mcp({ connect: "${serverName}" }) to connect with the new token.` }],
|
|
394
|
+
content: [{ type: "text" as const, text: `OAuth authentication successful for "${serverName}". Run mcp({ action: "connect", server: "${serverName}" }) to connect with the new token.` }],
|
|
394
395
|
details: { mode: "auth-complete", server: serverName, authenticated: true },
|
|
395
396
|
};
|
|
396
397
|
} catch (error) {
|
|
@@ -402,57 +403,36 @@ export async function executeAuthComplete(state: McpExtensionState, serverName:
|
|
|
402
403
|
}
|
|
403
404
|
}
|
|
404
405
|
|
|
405
|
-
export function executeDescribe(state: McpExtensionState, toolName: string): ProxyToolResult {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (!serverName || !toolMeta) {
|
|
423
|
-
if (disabledMatch) return disabledResult("describe", disabledMatch);
|
|
424
|
-
const suggestions = rankSuggestions(state, toolName, 5);
|
|
406
|
+
export function executeDescribe(state: McpExtensionState, toolName: string, serverFilter?: string): ProxyToolResult {
|
|
407
|
+
if (serverFilter && isServerDisabled(state.config.mcpServers[serverFilter])) return disabledResult("describe", serverFilter);
|
|
408
|
+
const catalog = [...state.toolMetadata].flatMap(([server, tools]) =>
|
|
409
|
+
(!serverFilter || serverFilter === server) && state.config.mcpServers[server] && !isServerDisabled(state.config.mcpServers[server])
|
|
410
|
+
? tools.map(tool => ({ server, tool })) : []);
|
|
411
|
+
const originals = serverFilter ? catalog.filter(({ tool }) => tool.originalName === toolName) : [];
|
|
412
|
+
const exact = originals.length ? originals : catalog.filter(({ tool }) => tool.name === toolName);
|
|
413
|
+
const matches = exact.length ? exact : catalog.filter(({ tool }) => tool.name.replace(/-/g, "_") === toolName.replace(/-/g, "_"));
|
|
414
|
+
if (matches.length > 1) return {
|
|
415
|
+
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 })))}` }],
|
|
416
|
+
details: { mode: "describe", error: "ambiguous_tool", requestedTool: toolName },
|
|
417
|
+
};
|
|
418
|
+
const match = matches[0];
|
|
419
|
+
if (!match) {
|
|
420
|
+
const disabled = [...state.toolMetadata].find(([server, tools]) => isServerDisabled(state.config.mcpServers[server]) && findToolByName(tools, toolName));
|
|
421
|
+
if (disabled) return disabledResult("describe", disabled[0]);
|
|
422
|
+
const suggestions = rankSuggestions(state, toolName, 5, serverFilter);
|
|
425
423
|
const hint = suggestions.length > 0
|
|
426
|
-
? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ describe: "${suggestions[0]}" }).`
|
|
427
|
-
: `Use
|
|
424
|
+
? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: "${suggestions[0]}" }).`
|
|
425
|
+
: `Use mcp_search({ query: "..." }) to search.`;
|
|
428
426
|
return {
|
|
429
|
-
content: [{ type: "text"
|
|
427
|
+
content: [{ type: "text", text: `Tool "${toolName}" not found. ${hint}` }],
|
|
430
428
|
details: { mode: "describe", error: "tool_not_found", requestedTool: toolName, suggestions },
|
|
431
429
|
};
|
|
432
430
|
}
|
|
433
|
-
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
: "";
|
|
437
|
-
let text = `${toolMeta.name}${approvalMarker}\n`;
|
|
438
|
-
text += `Server: ${serverName}\n`;
|
|
439
|
-
if (toolMeta.resourceUri) {
|
|
440
|
-
text += `Type: Resource (reads from ${toolMeta.resourceUri})\n`;
|
|
441
|
-
}
|
|
442
|
-
text += `\n${toolMeta.description || "(no description)"}\n`;
|
|
443
|
-
if (toolMeta.annotations) text += `\nAnnotations: ${JSON.stringify(toolMeta.annotations)}\n`;
|
|
444
|
-
|
|
445
|
-
if (toolMeta.inputSchema && !toolMeta.resourceUri) {
|
|
446
|
-
const shape = renderTsShape(toolMeta.inputSchema);
|
|
447
|
-
text += shape === null ? `\nParameters:\n${formatSchema(toolMeta.inputSchema)}` : `\nShape:\n${shape}`;
|
|
448
|
-
} else if (toolMeta.resourceUri) {
|
|
449
|
-
text += `\nNo parameters required (resource tool).`;
|
|
450
|
-
} else {
|
|
451
|
-
text += `\nNo parameters defined.`;
|
|
452
|
-
}
|
|
453
|
-
|
|
431
|
+
const { server: serverName, tool: toolMeta } = match;
|
|
432
|
+
const descriptor = toToolDescriptor(serverName, toolMeta);
|
|
433
|
+
const approvalRequired = isToolCallApprovalRequired(state.config, serverName, toolMeta);
|
|
454
434
|
return {
|
|
455
|
-
content: [{ type: "text"
|
|
435
|
+
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) }],
|
|
456
436
|
details: { mode: "describe", tool: toolMeta, server: serverName },
|
|
457
437
|
};
|
|
458
438
|
}
|
|
@@ -467,7 +447,7 @@ export function executeSearch(
|
|
|
467
447
|
): ProxyToolResult {
|
|
468
448
|
const showSchemas = includeSchemas !== false;
|
|
469
449
|
const searchCall = (nextOffset: number | null) =>
|
|
470
|
-
`mcp({ search: ${JSON.stringify(query)}${server ? `, server: ${JSON.stringify(server)}` : ""}${showSchemas ? "" : ", includeSchemas: false"}, limit: ${limit}, offset: ${nextOffset} })`;
|
|
450
|
+
`mcp({ action: "search", query: ${JSON.stringify(query)}${server ? `, server: ${JSON.stringify(server)}` : ""}${showSchemas ? "" : ", includeSchemas: false"}, limit: ${limit}, offset: ${nextOffset} })`;
|
|
471
451
|
if (server && isServerDisabled(state.config.mcpServers[server])) return disabledResult("search", server);
|
|
472
452
|
|
|
473
453
|
let matches: Array<{ server: string; tool: ToolMetadata; score: number }>;
|
|
@@ -478,7 +458,7 @@ export function executeSearch(
|
|
|
478
458
|
details: { mode: "search", error: "empty_query" },
|
|
479
459
|
};
|
|
480
460
|
}
|
|
481
|
-
matches = (state.toolMetadata.get(server) ?? [])
|
|
461
|
+
matches = (state.toolMetadata.get(server) ?? []).filter(tool => !tool.resourceUri)
|
|
482
462
|
.map(tool => ({ server, tool, score: 0 }))
|
|
483
463
|
.sort((a, b) => a.tool.name.localeCompare(b.tool.name));
|
|
484
464
|
} else {
|
|
@@ -486,11 +466,13 @@ export function executeSearch(
|
|
|
486
466
|
}
|
|
487
467
|
|
|
488
468
|
const page = paginate(matches, offset, limit);
|
|
469
|
+
const coverage = catalogCoverage(state, server);
|
|
470
|
+
const coverageNotice = coverage.complete ? "" : `\nCatalog is incomplete. Undiscovered servers: ${coverage.unknownServers.join(", ")}. Search with server to discover one.`;
|
|
489
471
|
if (page.total === 0) {
|
|
490
472
|
const msg = server ? `No tools matching "${query}" in "${server}"` : `No tools matching "${query}"`;
|
|
491
473
|
return {
|
|
492
|
-
content: [{ type: "text" as const, text: msg }],
|
|
493
|
-
details: { mode: "search", matches: [], count: 0, hasMore: false, nextOffset: null, query },
|
|
474
|
+
content: [{ type: "text" as const, text: msg + coverageNotice }],
|
|
475
|
+
details: { mode: "search", matches: [], count: 0, hasMore: false, nextOffset: null, query, coverage },
|
|
494
476
|
};
|
|
495
477
|
}
|
|
496
478
|
if (page.items.length === 0) {
|
|
@@ -507,17 +489,7 @@ export function executeSearch(
|
|
|
507
489
|
? " (requires approval)"
|
|
508
490
|
: "";
|
|
509
491
|
if (showSchemas) {
|
|
510
|
-
text += `${match.tool
|
|
511
|
-
text += ` ${match.tool.description || "(no description)"}\n`;
|
|
512
|
-
if (match.tool.inputSchema && !match.tool.resourceUri) {
|
|
513
|
-
const shape = renderTsShape(match.tool.inputSchema);
|
|
514
|
-
text += shape === null
|
|
515
|
-
? `\n Parameters:\n${formatSchema(match.tool.inputSchema, " ")}\n`
|
|
516
|
-
: `\n Shape:\n${shape.split("\n").map(line => ` ${line}`).join("\n")}\n`;
|
|
517
|
-
} else if (match.tool.resourceUri) {
|
|
518
|
-
text += " No parameters (resource tool).\n";
|
|
519
|
-
}
|
|
520
|
-
text += "\n";
|
|
492
|
+
text += `${JSON.stringify({ ...toToolDescriptor(match.server, match.tool), ...(approvalMarker ? { approvalRequired: true } : {}) }, null, 2)}\n\n`;
|
|
521
493
|
} else {
|
|
522
494
|
text += `- ${match.tool.name}${approvalMarker}`;
|
|
523
495
|
if (match.tool.description) text += ` - ${truncateAtWord(match.tool.description, 50)}`;
|
|
@@ -531,9 +503,10 @@ export function executeSearch(
|
|
|
531
503
|
: `\n${first}-${first + page.items.length - 1} of ${page.total} — end\n`;
|
|
532
504
|
|
|
533
505
|
return {
|
|
534
|
-
content: [{ type: "text" as const, text: text.trim() }],
|
|
506
|
+
content: [{ type: "text" as const, text: text.trim() + coverageNotice }],
|
|
535
507
|
details: {
|
|
536
508
|
mode: "search",
|
|
509
|
+
coverage,
|
|
537
510
|
matches: page.items.map(match => ({ server: match.server, tool: match.tool.name, score: match.score })),
|
|
538
511
|
count: page.total,
|
|
539
512
|
hasMore: page.hasMore,
|
|
@@ -547,13 +520,13 @@ export function executeList(state: McpExtensionState, server: string, limit = 12
|
|
|
547
520
|
const definition = state.config.mcpServers[server];
|
|
548
521
|
if (!definition) {
|
|
549
522
|
return {
|
|
550
|
-
content: [{ type: "text" as const, text: `Server "${server}" not found. Use mcp({}) to see available servers.` }],
|
|
523
|
+
content: [{ type: "text" as const, text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
551
524
|
details: { mode: "list", server, tools: [], count: 0, error: "not_found" },
|
|
552
525
|
};
|
|
553
526
|
}
|
|
554
527
|
if (isServerDisabled(definition)) return disabledResult("list", server);
|
|
555
528
|
|
|
556
|
-
const metadata = state.toolMetadata.get(server);
|
|
529
|
+
const metadata = state.toolMetadata.get(server)?.filter(tool => !tool.resourceUri);
|
|
557
530
|
const connection = state.manager.getConnection(server);
|
|
558
531
|
const instructions = state.serverInstructions.get(server);
|
|
559
532
|
let instructionsText = "";
|
|
@@ -561,7 +534,7 @@ export function executeList(state: McpExtensionState, server: string, limit = 12
|
|
|
561
534
|
const preview = truncateAtWord(instructions, INSTRUCTIONS_PREVIEW_LENGTH);
|
|
562
535
|
instructionsText = `\n\nServer instructions:\n${preview}`;
|
|
563
536
|
if (preview !== instructions) {
|
|
564
|
-
instructionsText += `\nUse mcp({ instructions: "${server}" }) for the full text.`;
|
|
537
|
+
instructionsText += `\nUse mcp({ action: "instructions", server: "${server}" }) for the full text.`;
|
|
565
538
|
}
|
|
566
539
|
}
|
|
567
540
|
|
|
@@ -579,7 +552,7 @@ export function executeList(state: McpExtensionState, server: string, limit = 12
|
|
|
579
552
|
};
|
|
580
553
|
}
|
|
581
554
|
return {
|
|
582
|
-
content: [{ type: "text" as const, text: `Server "${server}" is configured but not connected. Use mcp({ connect: "${server}" }) or /mcp reconnect ${server} to retry.${instructionsText}` }],
|
|
555
|
+
content: [{ type: "text" as const, text: `Server "${server}" is configured but not connected. Use mcp({ action: "connect", server: "${server}" }) or /mcp reconnect ${server} to retry.${instructionsText}` }],
|
|
583
556
|
details: { mode: "list", server, tools: [], count: 0, error: "not_connected", hasInstructions: Boolean(instructions) },
|
|
584
557
|
};
|
|
585
558
|
}
|
|
@@ -587,7 +560,7 @@ export function executeList(state: McpExtensionState, server: string, limit = 12
|
|
|
587
560
|
const page = paginate(metadata, offset, limit);
|
|
588
561
|
if (page.items.length === 0) {
|
|
589
562
|
return {
|
|
590
|
-
content: [{ type: "text" as const, text: `No tools at offset ${offset}; "${server}" has ${page.total} tools. Retry with mcp({ server: ${JSON.stringify(server)}, limit: ${limit}, offset: 0 }).` }],
|
|
563
|
+
content: [{ type: "text" as const, text: `No tools at offset ${offset}; "${server}" has ${page.total} tools. Retry with mcp({ action: "list", server: ${JSON.stringify(server)}, limit: ${limit}, offset: 0 }).` }],
|
|
591
564
|
details: { mode: "list", error: "offset_out_of_range", server, tools: [], count: page.total, hasMore: false, nextOffset: null, hasInstructions: Boolean(instructions) },
|
|
592
565
|
};
|
|
593
566
|
}
|
|
@@ -603,7 +576,7 @@ export function executeList(state: McpExtensionState, server: string, limit = 12
|
|
|
603
576
|
}
|
|
604
577
|
const first = (Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0) + 1;
|
|
605
578
|
text += page.hasMore
|
|
606
|
-
? `\n${first}-${first + page.items.length - 1} of ${page.total} — mcp({ server: ${JSON.stringify(server)}, limit: ${limit}, offset: ${page.nextOffset} }) for more`
|
|
579
|
+
? `\n${first}-${first + page.items.length - 1} of ${page.total} — mcp({ action: "list", server: ${JSON.stringify(server)}, limit: ${limit}, offset: ${page.nextOffset} }) for more`
|
|
607
580
|
: `\n${first}-${first + page.items.length - 1} of ${page.total} — end`;
|
|
608
581
|
text += instructionsText;
|
|
609
582
|
|
|
@@ -625,7 +598,7 @@ export function executeInstructions(state: McpExtensionState, server: string): P
|
|
|
625
598
|
const definition = state.config.mcpServers[server];
|
|
626
599
|
if (!definition) {
|
|
627
600
|
return {
|
|
628
|
-
content: [{ type: "text" as const, text: `Server "${server}" not found. Use mcp({}) to see available servers.` }],
|
|
601
|
+
content: [{ type: "text" as const, text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
629
602
|
details: { mode: "instructions", server, error: "not_found" },
|
|
630
603
|
};
|
|
631
604
|
}
|
|
@@ -648,7 +621,7 @@ export function executeInstructions(state: McpExtensionState, server: string): P
|
|
|
648
621
|
}
|
|
649
622
|
|
|
650
623
|
return {
|
|
651
|
-
content: [{ type: "text" as const, text: `No instructions cached for "${server}". Use mcp({ connect: "${server}" }) to connect and refresh.` }],
|
|
624
|
+
content: [{ type: "text" as const, text: `No instructions cached for "${server}". Use mcp({ action: "connect", server: "${server}" }) to connect and refresh.` }],
|
|
652
625
|
details: { mode: "instructions", server, error: "not_connected" },
|
|
653
626
|
};
|
|
654
627
|
}
|
|
@@ -665,7 +638,7 @@ export async function executeConnect(
|
|
|
665
638
|
const definition = state.config.mcpServers[serverName];
|
|
666
639
|
if (!definition) {
|
|
667
640
|
return {
|
|
668
|
-
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
641
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers.` }],
|
|
669
642
|
details: { mode: "connect", error: "not_found", server: serverName },
|
|
670
643
|
};
|
|
671
644
|
}
|
|
@@ -702,6 +675,7 @@ export async function executeConnect(
|
|
|
702
675
|
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
703
676
|
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
|
|
704
677
|
state.toolMetadata.set(serverName, metadata);
|
|
678
|
+
state.resourceCounts?.set(serverName, connection.resources.length);
|
|
705
679
|
if (!connection.promptDiscoveryFailed) {
|
|
706
680
|
state.promptMetadata?.set(serverName, reconstructPromptMetadata(serverName, connection.prompts ?? [], prefix, definition));
|
|
707
681
|
state.promptMetadataLive?.add(serverName);
|
|
@@ -741,15 +715,14 @@ interface ToolCallOptions extends McpToolCallIdentity {
|
|
|
741
715
|
recoverAuthConnection: NonNullable<SessionRecoveryDeps["onNeedsAuth"]>;
|
|
742
716
|
authRequiredMessage: () => string;
|
|
743
717
|
autoAuthAttempted: () => boolean;
|
|
744
|
-
|
|
718
|
+
raw?: boolean;
|
|
745
719
|
}
|
|
746
720
|
|
|
747
721
|
/**
|
|
748
722
|
* Executes an already-approved tool call against a connected server.
|
|
749
723
|
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
* auth strategy, approval) differs between them and stays with the caller.
|
|
724
|
+
* Direct, gateway, resource and script calls share executeCall preparation.
|
|
725
|
+
* Raw scripts skip content rendering but retain protocol results and artifacts.
|
|
753
726
|
*/
|
|
754
727
|
export async function runToolCall(
|
|
755
728
|
state: McpExtensionState,
|
|
@@ -758,7 +731,7 @@ export async function runToolCall(
|
|
|
758
731
|
args: Record<string, unknown> | undefined,
|
|
759
732
|
options: ToolCallOptions,
|
|
760
733
|
): Promise<ProxyToolResult> {
|
|
761
|
-
const { detailsBase, signal, recoverAuthConnection, authRequiredMessage, autoAuthAttempted
|
|
734
|
+
const { detailsBase, signal, recoverAuthConnection, authRequiredMessage, autoAuthAttempted } = options;
|
|
762
735
|
const configuredOptions = state.manager.getRequestOptions?.(serverName, options.ownedSignal);
|
|
763
736
|
const callerSignal = combineAbortSignals(configuredOptions?.signal, options.ownedSignal);
|
|
764
737
|
const outputGuardOptions = resolveMcpOutputGuardOptions(state.config.settings, state.outputDirectory);
|
|
@@ -801,8 +774,7 @@ export async function runToolCall(
|
|
|
801
774
|
})
|
|
802
775
|
: null;
|
|
803
776
|
|
|
804
|
-
//
|
|
805
|
-
// stale cached hints in preference to the connected server's annotations.
|
|
777
|
+
// UI preparation can replace the connection; use its current annotations.
|
|
806
778
|
const liveTools = state.manager.getConnection(serverName)?.tools;
|
|
807
779
|
const annotations = liveTools
|
|
808
780
|
? liveTools.find(tool => tool.name === target.originalName)?.annotations
|
|
@@ -841,62 +813,38 @@ export async function runToolCall(
|
|
|
841
813
|
return result;
|
|
842
814
|
};
|
|
843
815
|
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
)
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
uiSession?.sendToolResult(result);
|
|
869
|
-
|
|
870
|
-
if (result.isError) {
|
|
871
|
-
const content = transformMcpContent((result.content ?? []) as McpContent[]);
|
|
872
|
-
const guarded = await guardMcpOutput(
|
|
873
|
-
content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }],
|
|
874
|
-
{ ...outputGuardOptions, prefix: "Error: ", suffix: schemaSuffix(), emptyTextFallback: "Tool execution failed", rawMcpResult: result },
|
|
875
|
-
);
|
|
876
|
-
return { content: guarded.content, details: { ...detailsBase, error: "tool_error", ...guardedMcpDetails(guarded) } };
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
onRawResult?.(result);
|
|
880
|
-
const content = resolveMcpResultContent(result as Record<string, unknown>);
|
|
881
|
-
const outputContent = content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }];
|
|
882
|
-
|
|
883
|
-
if (target.uiResourceUri) {
|
|
884
|
-
const uiSummary = summarizeUiSessionResult(uiSession);
|
|
885
|
-
const guarded = await guardMcpOutput(outputContent, { ...outputGuardOptions, suffix: `\n\n${uiSummary.message}`, rawMcpResult: result });
|
|
886
|
-
return {
|
|
887
|
-
content: guarded.content,
|
|
888
|
-
details: {
|
|
889
|
-
...detailsBase,
|
|
890
|
-
...guardedMcpDetails(guarded),
|
|
891
|
-
uiOpen: uiSummary.uiOpen,
|
|
892
|
-
uiViewer: uiSummary.uiViewer,
|
|
893
|
-
uiUrl: uiSummary.uiUrl,
|
|
894
|
-
},
|
|
895
|
-
};
|
|
816
|
+
const result = target.resourceUri
|
|
817
|
+
? await dispatch(() => withSessionRecovery(
|
|
818
|
+
recovery, serverName,
|
|
819
|
+
conn => abortable(conn.client.readResource({ uri: target.resourceUri! }, requestOptions), ownedSignal),
|
|
820
|
+
))
|
|
821
|
+
: await dispatch(() => withSessionRecovery<ClientCallToolResult>(
|
|
822
|
+
{ ...recovery, retryOnTransportFailure: annotations?.readOnlyHint === true || annotations?.idempotentHint === true },
|
|
823
|
+
serverName,
|
|
824
|
+
conn => abortable(conn.client.callTool({ name: target.originalName, arguments: args ?? {}, _meta: uiSession?.requestMeta }, requestOptions), ownedSignal),
|
|
825
|
+
));
|
|
826
|
+
if (!target.resourceUri) uiSession?.sendToolResult(result as ClientCallToolResult);
|
|
827
|
+
const record = result as Record<string, unknown>;
|
|
828
|
+
const isError = record.isError === true;
|
|
829
|
+
const uiSummary = target.uiResourceUri ? summarizeUiSessionResult(uiSession) : undefined;
|
|
830
|
+
const details = {
|
|
831
|
+
...detailsBase,
|
|
832
|
+
...(isError ? { error: "tool_error" } : {}),
|
|
833
|
+
...(uiSummary ? { uiOpen: uiSummary.uiOpen, uiViewer: uiSummary.uiViewer, uiUrl: uiSummary.uiUrl } : {}),
|
|
834
|
+
};
|
|
835
|
+
const retained = await retainMcpResult(result, outputGuardOptions, options.raw);
|
|
836
|
+
if (options.raw) {
|
|
837
|
+
const message = isError
|
|
838
|
+
? (Array.isArray(record.content) ? record.content.filter(block => block.type === "text").map(block => block.text).join("\n") : "") || "Tool execution failed" : undefined;
|
|
839
|
+
return { content: [], details: { ...details, ...retained, ...(message ? { message } : {}) } };
|
|
896
840
|
}
|
|
897
|
-
|
|
898
|
-
const guarded = await guardMcpOutput(
|
|
899
|
-
|
|
841
|
+
const content = renderMcpResultContent(record, retained.payloadFiles);
|
|
842
|
+
const guarded = await guardMcpOutput(content.length ? content : [{ type: "text", text: target.resourceUri ? "(empty resource)" : "(empty result)" }], {
|
|
843
|
+
...outputGuardOptions, retainedMcpResult: retained,
|
|
844
|
+
...(isError ? { prefix: "Error: ", suffix: schemaSuffix(), emptyTextFallback: "Tool execution failed" }
|
|
845
|
+
: uiSummary ? { suffix: `\n\n${uiSummary.message}` } : {}),
|
|
846
|
+
});
|
|
847
|
+
return { content: guarded.content, details: { ...details, ...guardedMcpDetails(guarded) } };
|
|
900
848
|
} catch (error) {
|
|
901
849
|
// Direct/proxy host errors throw; scripts retain their failed-call envelope.
|
|
902
850
|
if (beforeDispatchPending && options.innerCallId === undefined) throw error;
|
|
@@ -906,9 +854,11 @@ export async function runToolCall(
|
|
|
906
854
|
? "MCP call capture failed before dispatch; the tool did not run. Restore capture before continuing."
|
|
907
855
|
: "MCP outcome capture failed. Use the retained outcome to continue the original work; do not repeat a completed call or rerun the script.";
|
|
908
856
|
uiSession?.sendToolCancelled(message);
|
|
857
|
+
const retained = "result" in captureFailure && captureFailure.result !== undefined
|
|
858
|
+
? await retainMcpResult(captureFailure.result, { ...outputGuardOptions, enabled: true, detailsMaxBytes: 1 }) : {};
|
|
909
859
|
return {
|
|
910
|
-
content: [{ type: "text", text: message }],
|
|
911
|
-
details: { ...detailsBase, error: isAbortError(error, callerSignal) ? "aborted" : "call_capture_failed", message, recovery: context },
|
|
860
|
+
content: [{ type: "text", text: message }, ...(retained.resultRef ? [{ type: "text" as const, text: formatMcpResultReference(retained.resultRef) }] : [])],
|
|
861
|
+
details: { ...detailsBase, ...retained, error: isAbortError(error, callerSignal) ? "aborted" : "call_capture_failed", message, recovery: context },
|
|
912
862
|
};
|
|
913
863
|
}
|
|
914
864
|
if (isToolTransportFailure(error)) {
|
|
@@ -959,6 +909,82 @@ export async function runToolCall(
|
|
|
959
909
|
}
|
|
960
910
|
}
|
|
961
911
|
|
|
912
|
+
export interface ExecuteCallOptions {
|
|
913
|
+
exactOriginalName?: boolean;
|
|
914
|
+
resourceUri?: string;
|
|
915
|
+
/** Scripts receive the raw protocol result without model-facing content transformation. */
|
|
916
|
+
raw?: boolean;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Connect/auth once for calls and resource discovery. Recovery stays bounded by withSessionRecovery. */
|
|
920
|
+
export async function prepareMcpConnection(state: McpExtensionState, serverName: string, signal?: AbortSignal) {
|
|
921
|
+
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
922
|
+
throwIfAborted(ownedSignal);
|
|
923
|
+
let autoAuthAttempted = false;
|
|
924
|
+
const recoverAuthConnection: NonNullable<SessionRecoveryDeps["onNeedsAuth"]> = async (_name, recoverySignal = ownedSignal, challenge) => {
|
|
925
|
+
throwIfAborted(recoverySignal);
|
|
926
|
+
const current = state.manager.getConnection(serverName);
|
|
927
|
+
if (current?.status === "connected" && current !== challenge?.connection) return current;
|
|
928
|
+
if (!autoAuthAttempted) {
|
|
929
|
+
autoAuthAttempted = true;
|
|
930
|
+
const auth = await attemptAutoAuth(state, serverName, recoverySignal);
|
|
931
|
+
throwIfAborted(recoverySignal);
|
|
932
|
+
if (auth.status === "failed") throw new SessionRecoveryAuthRequiredError(serverName, auth.message);
|
|
933
|
+
if (auth.status === "success" && current) {
|
|
934
|
+
clearFailure(state, serverName);
|
|
935
|
+
const definition = state.config.mcpServers[serverName];
|
|
936
|
+
if (!definition) return undefined;
|
|
937
|
+
state.ui?.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${serverName}...`));
|
|
938
|
+
await state.manager.reconnect(serverName, definition, current, recoverySignal);
|
|
939
|
+
if (await lazyConnect(state, serverName, recoverySignal)) {
|
|
940
|
+
updateStatusBar(state);
|
|
941
|
+
return state.manager.getConnection(serverName);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
return challenge ? undefined : state.manager.getConnection(serverName);
|
|
946
|
+
};
|
|
947
|
+
const failure = (error: string, message: string) => ({
|
|
948
|
+
content: [{ type: "text" as const, text: message }],
|
|
949
|
+
details: { mode: "call", error, server: serverName, message },
|
|
950
|
+
});
|
|
951
|
+
const definition = state.config.mcpServers[serverName];
|
|
952
|
+
if (!definition) return { error: failure("server_not_found", `Server "${serverName}" not found.`) };
|
|
953
|
+
if (isServerDisabled(definition)) return { error: disabledResult("call", serverName) };
|
|
954
|
+
try {
|
|
955
|
+
// Connected metadata is read below, so it does not require another connection attempt.
|
|
956
|
+
let connection = state.manager.getConnection(serverName);
|
|
957
|
+
if (connection?.status !== "connected") {
|
|
958
|
+
await lazyConnect(state, serverName, ownedSignal);
|
|
959
|
+
connection = state.manager.getConnection(serverName);
|
|
960
|
+
} else {
|
|
961
|
+
markKeepAliveAfterConnect(state, serverName);
|
|
962
|
+
}
|
|
963
|
+
if (connection?.status === "needs-auth") connection = await recoverAuthConnection(serverName, ownedSignal);
|
|
964
|
+
if (connection?.status === "needs-auth") throw new SessionRecoveryAuthRequiredError(serverName);
|
|
965
|
+
if (connection?.status !== "connected") {
|
|
966
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
967
|
+
return { error: failure(failedAgo === null ? "server_unavailable" : "server_backoff", `Server "${serverName}" not available${failedAgo === null ? "" : ` (last failed ${failedAgo}s ago)`}`) };
|
|
968
|
+
}
|
|
969
|
+
// Use the live descriptor for visibility, filtering, approval, schema and UI hints.
|
|
970
|
+
if (connection.tools) {
|
|
971
|
+
const { metadata } = buildToolMetadata(connection.tools, connection.resources ?? [], definition, serverName, state.config.settings?.toolPrefix ?? "server");
|
|
972
|
+
state.toolMetadata.set(serverName, metadata);
|
|
973
|
+
}
|
|
974
|
+
return { connection, recoverAuthConnection, autoAuthAttempted: () => autoAuthAttempted };
|
|
975
|
+
} catch (error) {
|
|
976
|
+
if (isAbortError(error, ownedSignal)) throw error;
|
|
977
|
+
const message = error instanceof SessionRecoveryAuthRequiredError
|
|
978
|
+
? error.authMessage ?? getAuthRequiredMessage(state, serverName)
|
|
979
|
+
: error instanceof Error ? error.message : String(error);
|
|
980
|
+
if (!(error instanceof SessionRecoveryAuthRequiredError)) {
|
|
981
|
+
recordFailure(state, serverName, message);
|
|
982
|
+
updateStatusBar(state);
|
|
983
|
+
}
|
|
984
|
+
return { error: failure(error instanceof SessionRecoveryAuthRequiredError ? "auth_required" : "connect_failed", message) };
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
962
988
|
export async function executeCall(
|
|
963
989
|
state: McpExtensionState,
|
|
964
990
|
toolName: string,
|
|
@@ -966,396 +992,118 @@ export async function executeCall(
|
|
|
966
992
|
serverOverride?: string,
|
|
967
993
|
getPiTools?: () => ToolInfo[],
|
|
968
994
|
signal?: AbortSignal,
|
|
969
|
-
onRawResult?: (result: unknown) => void,
|
|
970
995
|
identity: McpToolCallIdentity = {},
|
|
971
996
|
beforeDispatch?: (signal: AbortSignal | undefined, operation: McpOperationContext) => Promise<void>,
|
|
997
|
+
options: ExecuteCallOptions = {},
|
|
972
998
|
): Promise<ProxyToolResult> {
|
|
973
999
|
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
974
1000
|
throwIfAborted(ownedSignal);
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const prefixMode = state.config.settings?.toolPrefix ?? "server";
|
|
979
|
-
const disabledCallResult = (disabledServer: string, metadata?: ToolMetadata): ProxyToolResult => {
|
|
980
|
-
if (!metadata) {
|
|
981
|
-
const message = `Server "${disabledServer}" is disabled. Run /mcp enable ${disabledServer} and /reload to enable it.`;
|
|
982
|
-
return {
|
|
983
|
-
content: [{ type: "text" as const, text: message }],
|
|
984
|
-
details: { mode: "call", error: "server_disabled", server: disabledServer, requestedTool: toolName, message },
|
|
985
|
-
};
|
|
986
|
-
}
|
|
987
|
-
const message = `Server "${disabledServer}" is disabled. Run /mcp enable ${disabledServer} and /reload to enable it.`;
|
|
988
|
-
const identity = metadata.resourceUri
|
|
989
|
-
? { server: disabledServer, resourceUri: metadata.resourceUri }
|
|
990
|
-
: { server: disabledServer, tool: metadata.originalName };
|
|
991
|
-
return {
|
|
992
|
-
content: [{ type: "text" as const, text: message }],
|
|
993
|
-
details: { mode: "call", error: "server_disabled", ...identity, message },
|
|
994
|
-
};
|
|
1001
|
+
if (typeof toolName !== "string" || !toolName.trim() || (serverOverride !== undefined && (typeof serverOverride !== "string" || !serverOverride))) return {
|
|
1002
|
+
content: [{ type: "text", text: "Specify a non-empty tool name and, when supplied, server." }],
|
|
1003
|
+
details: { mode: "call", error: "invalid_arguments" },
|
|
995
1004
|
};
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
}
|
|
1008
|
-
} else {
|
|
1009
|
-
let disabledMatch: { serverName: string; toolMeta: ToolMetadata } | undefined;
|
|
1010
|
-
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
1011
|
-
const found = findToolByName(metadata, toolName);
|
|
1012
|
-
if (!found) continue;
|
|
1013
|
-
if (isServerDisabled(state.config.mcpServers[server])) {
|
|
1014
|
-
disabledMatch ??= { serverName: server, toolMeta: found };
|
|
1015
|
-
continue;
|
|
1016
|
-
}
|
|
1017
|
-
serverName = server;
|
|
1018
|
-
toolMeta = found;
|
|
1019
|
-
break;
|
|
1020
|
-
}
|
|
1021
|
-
if (!toolMeta && disabledMatch) return disabledCallResult(disabledMatch.serverName, disabledMatch.toolMeta);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
if (serverName && !toolMeta && !state.toolMetadata.has(serverName)) {
|
|
1025
|
-
const connected = await lazyConnect(state, serverName, ownedSignal);
|
|
1026
|
-
if (connected) {
|
|
1027
|
-
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
1028
|
-
} else {
|
|
1029
|
-
const needsAuthConnection = state.manager.getConnection(serverName);
|
|
1030
|
-
if (needsAuthConnection?.status === "needs-auth") {
|
|
1031
|
-
if (!autoAuthAttempted) {
|
|
1032
|
-
autoAuthAttempted = true;
|
|
1033
|
-
const autoAuth = await attemptAutoAuth(state, serverName, ownedSignal);
|
|
1034
|
-
if (autoAuth.status === "failed") {
|
|
1035
|
-
return {
|
|
1036
|
-
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
1037
|
-
details: { mode: "call", error: "auth_required", server: serverName, requestedTool: toolName, message: autoAuth.message },
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
if (autoAuth.status === "success") {
|
|
1041
|
-
clearFailure(state, serverName);
|
|
1042
|
-
let connectedAfterAuth = false;
|
|
1043
|
-
try {
|
|
1044
|
-
const definition = state.config.mcpServers[serverName];
|
|
1045
|
-
if (definition) {
|
|
1046
|
-
await state.manager.reconnect(serverName, definition, needsAuthConnection, ownedSignal);
|
|
1047
|
-
connectedAfterAuth = true;
|
|
1048
|
-
}
|
|
1049
|
-
} catch (error) {
|
|
1050
|
-
if (isAbortError(error, ownedSignal)) throwIfAborted(ownedSignal);
|
|
1051
|
-
recordFailure(state, serverName, error instanceof Error ? error.message : String(error));
|
|
1052
|
-
updateStatusBar(state);
|
|
1053
|
-
}
|
|
1054
|
-
if (connectedAfterAuth) connectedAfterAuth = await lazyConnect(state, serverName, ownedSignal);
|
|
1055
|
-
if (connectedAfterAuth) {
|
|
1056
|
-
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
1057
|
-
if (!toolMeta) {
|
|
1058
|
-
const suggestions = rankSuggestions(state, toolName, 5, serverName);
|
|
1059
|
-
const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}` : "";
|
|
1060
|
-
return {
|
|
1061
|
-
content: [{ type: "text" as const, text: `Tool "${toolName}" not found on "${serverName}" after reconnect.${suggestionText}` }],
|
|
1062
|
-
details: { mode: "call", error: "tool_not_found_after_reconnect", server: serverName, requestedTool: toolName, suggestions },
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
if (!toolMeta && state.manager.getConnection(serverName)?.status === "needs-auth") {
|
|
1070
|
-
const message = getAuthRequiredMessage(state, serverName);
|
|
1071
|
-
return {
|
|
1072
|
-
content: [{ type: "text" as const, text: message }],
|
|
1073
|
-
details: { mode: "call", error: "auth_required", server: serverName, requestedTool: toolName, message },
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
if (!toolMeta) {
|
|
1079
|
-
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
1080
|
-
if (failedAgo !== null) {
|
|
1081
|
-
return {
|
|
1082
|
-
content: [{ type: "text" as const, text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
1083
|
-
details: { mode: "call", error: "server_backoff", server: serverName, requestedTool: toolName },
|
|
1084
|
-
};
|
|
1085
|
-
}
|
|
1086
|
-
}
|
|
1005
|
+
if (args !== undefined && (args === null || typeof args !== "object" || Array.isArray(args))) return {
|
|
1006
|
+
content: [{ type: "text", text: "Tool arguments must be an object." }],
|
|
1007
|
+
details: { mode: "call", error: "invalid_arguments", requestedTool: toolName },
|
|
1008
|
+
};
|
|
1009
|
+
let serverName = serverOverride;
|
|
1010
|
+
const matches = (metadata: ToolMetadata[] = []) => {
|
|
1011
|
+
if (options.resourceUri !== undefined) return metadata.filter(tool => tool.resourceUri === options.resourceUri);
|
|
1012
|
+
if (options.exactOriginalName) return metadata.filter(tool => tool.originalName === toolName && !tool.resourceUri);
|
|
1013
|
+
if (serverOverride) {
|
|
1014
|
+
const originals = metadata.filter(tool => tool.originalName === toolName);
|
|
1015
|
+
if (originals.length) return originals;
|
|
1087
1016
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
}))
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
let connected = await lazyConnect(state, configuredServer, ownedSignal);
|
|
1113
|
-
const needsAuthConnection = state.manager.getConnection(configuredServer);
|
|
1114
|
-
if (!connected && needsAuthConnection?.status === "needs-auth" && !autoAuthAttempted) {
|
|
1115
|
-
autoAuthAttempted = true;
|
|
1116
|
-
const autoAuth = await attemptAutoAuth(state, configuredServer, ownedSignal);
|
|
1117
|
-
if (autoAuth.status === "failed") {
|
|
1118
|
-
return {
|
|
1119
|
-
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
1120
|
-
details: { mode: "call", error: "auth_required", server: configuredServer, requestedTool: toolName, message: autoAuth.message },
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
if (autoAuth.status === "success") {
|
|
1124
|
-
clearFailure(state, configuredServer);
|
|
1125
|
-
try {
|
|
1126
|
-
const definition = state.config.mcpServers[configuredServer];
|
|
1127
|
-
if (definition) {
|
|
1128
|
-
await state.manager.reconnect(configuredServer, definition, needsAuthConnection, ownedSignal);
|
|
1129
|
-
connected = true;
|
|
1130
|
-
}
|
|
1131
|
-
} catch (error) {
|
|
1132
|
-
if (isAbortError(error, ownedSignal)) throwIfAborted(ownedSignal);
|
|
1133
|
-
recordFailure(state, configuredServer, error instanceof Error ? error.message : String(error));
|
|
1134
|
-
updateStatusBar(state);
|
|
1135
|
-
}
|
|
1136
|
-
if (connected) connected = await lazyConnect(state, configuredServer, ownedSignal);
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
if (!connected) continue;
|
|
1141
|
-
if (!prefixMatchedServer) prefixMatchedServer = configuredServer;
|
|
1142
|
-
toolMeta = findToolByName(state.toolMetadata.get(configuredServer), toolName);
|
|
1143
|
-
if (toolMeta) {
|
|
1144
|
-
serverName = configuredServer;
|
|
1145
|
-
break;
|
|
1146
|
-
}
|
|
1017
|
+
const exact = metadata.filter(tool => tool.name === toolName);
|
|
1018
|
+
if (exact.length) return exact;
|
|
1019
|
+
return metadata.filter(tool => tool.name.replace(/-/g, "_") === toolName.replace(/-/g, "_"));
|
|
1020
|
+
};
|
|
1021
|
+
const ambiguous = (): ProxyToolResult => ({
|
|
1022
|
+
content: [{ type: "text", text: `Tool "${toolName}" is ambiguous. Specify its exact server and tool name; no call was dispatched.` }],
|
|
1023
|
+
details: { mode: "call", error: "ambiguous_tool", requestedTool: toolName, ...(serverName ? { server: serverName } : {}) },
|
|
1024
|
+
});
|
|
1025
|
+
if (!serverName) {
|
|
1026
|
+
const candidates = [...state.toolMetadata].flatMap(([server, metadata]) =>
|
|
1027
|
+
isServerDisabled(state.config.mcpServers[server]) ? [] : matches(metadata).map(tool => ({ server, tool })));
|
|
1028
|
+
if (candidates.length > 1) return ambiguous();
|
|
1029
|
+
serverName = candidates[0]?.server;
|
|
1030
|
+
if (!serverName) {
|
|
1031
|
+
const disabled = [...state.toolMetadata].find(([server, metadata]) => isServerDisabled(state.config.mcpServers[server]) && matches(metadata).length > 0);
|
|
1032
|
+
if (disabled) return disabledResult("call", disabled[0]);
|
|
1033
|
+
const prefixes = Object.entries(state.config.mcpServers)
|
|
1034
|
+
.filter(([, definition]) => !isServerDisabled(definition))
|
|
1035
|
+
.map(([server, definition]) => ({ server, prefix: getServerPrefix(server, resolveToolPrefix(definition, state.config.settings?.toolPrefix ?? "server")) }))
|
|
1036
|
+
.filter(({ prefix }) => prefix && toolName.startsWith(`${prefix}_`))
|
|
1037
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
1038
|
+
if (prefixes.length > 1 && prefixes[0]?.prefix === prefixes[1]?.prefix) return ambiguous();
|
|
1039
|
+
serverName = prefixes[0]?.server;
|
|
1147
1040
|
}
|
|
1148
1041
|
}
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
:
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
: undefined;
|
|
1171
|
-
const suggestedTool = suggestedOwner ? findToolByName(suggestedOwner[1], toolName) : undefined;
|
|
1172
|
-
if (suggestedOwner && suggestedTool) {
|
|
1173
|
-
const suggestedServer = suggestedOwner[0];
|
|
1174
|
-
return {
|
|
1175
|
-
content: [{ type: "text" as const, text: `Tool ${JSON.stringify(toolName)} is on server ${JSON.stringify(suggestedServer)}, not ${JSON.stringify(serverOverride)}. Retry the same call with server: ${JSON.stringify(suggestedServer)} or omit server.` }],
|
|
1176
|
-
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer, suggestedServer, suggestions: [suggestedTool.name] },
|
|
1177
|
-
};
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
const suggestions = rankSuggestions(state, toolName, 5, hintServer);
|
|
1181
|
-
let hint: string;
|
|
1182
|
-
if (suggestions.length > 0) {
|
|
1183
|
-
hint = ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ describe: "${suggestions[0]}" }).`;
|
|
1184
|
-
} else if (hintServer) {
|
|
1185
|
-
hint = ` Search with mcp({ search: ${JSON.stringify(toolName)}, server: ${JSON.stringify(hintServer)} }).`;
|
|
1186
|
-
} else {
|
|
1187
|
-
hint = ` Use mcp({ search: "..." }) to search.`;
|
|
1188
|
-
}
|
|
1189
|
-
const suggestionOnHintServer = hintServer && suggestions.some(suggestion =>
|
|
1190
|
-
state.toolMetadata.get(hintServer)?.some(tool => tool.name === suggestion));
|
|
1191
|
-
if (hintServer && (suggestions.length === 0 || suggestionOnHintServer) && state.toolMetadata.has(hintServer) && state.manager.getConnection(hintServer)?.status !== "connected") {
|
|
1192
|
-
hint += ` Refresh a stale catalog with mcp({ connect: ${JSON.stringify(hintServer)} }).`;
|
|
1193
|
-
}
|
|
1042
|
+
const missing = (): ProxyToolResult => {
|
|
1043
|
+
const nativeTool = !serverOverride && getPiTools?.().find(tool => tool.name === toolName && tool.name !== "mcp");
|
|
1044
|
+
if (nativeTool) return {
|
|
1045
|
+
content: [{ type: "text", text: `"${toolName}" is a native Pi tool. Call ${toolName} directly instead of using mcp({ action: "call", tool: "${toolName}" }).` }],
|
|
1046
|
+
details: { mode: "call", error: "native_tool", requestedTool: toolName },
|
|
1047
|
+
};
|
|
1048
|
+
const normalizedName = toolName.replace(/-/g, "_");
|
|
1049
|
+
const localOriginal = serverOverride && state.toolMetadata.get(serverOverride)?.some(tool => tool.originalName.replace(/-/g, "_") === normalizedName);
|
|
1050
|
+
const owners = serverOverride && !localOriginal
|
|
1051
|
+
? [...state.toolMetadata].filter(([server, metadata]) => server !== serverOverride && !isServerDisabled(state.config.mcpServers[server]) && matches(metadata).length === 1)
|
|
1052
|
+
: [];
|
|
1053
|
+
const suggestedOwner = owners.length === 1 ? owners[0] : undefined;
|
|
1054
|
+
if (suggestedOwner) return {
|
|
1055
|
+
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.` }],
|
|
1056
|
+
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer: serverName, suggestedServer: suggestedOwner[0], suggestions: matches(suggestedOwner[1]).map(tool => tool.name) },
|
|
1057
|
+
};
|
|
1058
|
+
const suggestions = rankSuggestions(state, toolName, 5, serverName);
|
|
1059
|
+
let hint = suggestions.length
|
|
1060
|
+
? ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: ${JSON.stringify(suggestions[0])} }).`
|
|
1061
|
+
: serverName ? ` Search with mcp({ action: "search", query: ${JSON.stringify(toolName)}, server: ${JSON.stringify(serverName)} }).` : ' Use mcp({ action: "search", query: "..." }) to search.';
|
|
1062
|
+
if (serverName && state.toolMetadata.has(serverName) && state.manager.getConnection(serverName)?.status !== "connected") hint += ` Refresh a stale catalog with mcp({ action: "connect", server: ${JSON.stringify(serverName)} }).`;
|
|
1194
1063
|
return {
|
|
1195
|
-
content: [{ type: "text"
|
|
1196
|
-
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer, suggestions },
|
|
1064
|
+
content: [{ type: "text", text: `Tool "${toolName}" not found.${hint}` }],
|
|
1065
|
+
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer: serverName, suggestions },
|
|
1197
1066
|
};
|
|
1067
|
+
};
|
|
1068
|
+
if (!serverName) return missing();
|
|
1069
|
+
const definition = state.config.mcpServers[serverName];
|
|
1070
|
+
if (!definition) 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 } };
|
|
1071
|
+
if (isServerDisabled(definition)) return disabledResult("call", serverName);
|
|
1072
|
+
const cached = state.toolMetadata.get(serverName);
|
|
1073
|
+
if (matches(cached).length > 1) return ambiguous();
|
|
1074
|
+
// Misspellings in a known catalog do not cause connection churn or guessed execution.
|
|
1075
|
+
if (cached && !matches(cached).length && !options.exactOriginalName && options.resourceUri === undefined) return missing();
|
|
1076
|
+
|
|
1077
|
+
const prepared = await prepareMcpConnection(state, serverName, ownedSignal);
|
|
1078
|
+
if (prepared.error) {
|
|
1079
|
+
const cachedTarget = matches(cached)[0];
|
|
1080
|
+
return { ...prepared.error, details: { ...prepared.error.details, ...(cachedTarget
|
|
1081
|
+
? cachedTarget.resourceUri ? { resourceUri: cachedTarget.resourceUri } : { tool: cachedTarget.originalName }
|
|
1082
|
+
: { requestedTool: toolName }) } };
|
|
1198
1083
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
let connection = state.manager.getConnection(serverName);
|
|
1205
|
-
let refreshAfterAuth = false;
|
|
1206
|
-
if (connection?.status === "needs-auth") {
|
|
1207
|
-
if (!autoAuthAttempted) {
|
|
1208
|
-
autoAuthAttempted = true;
|
|
1209
|
-
const autoAuth = await attemptAutoAuth(state, serverName, ownedSignal);
|
|
1210
|
-
if (autoAuth.status === "failed") {
|
|
1211
|
-
return {
|
|
1212
|
-
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
1213
|
-
details: { mode: "call", error: "auth_required", ...callIdentity, message: autoAuth.message },
|
|
1214
|
-
};
|
|
1215
|
-
}
|
|
1216
|
-
if (autoAuth.status === "success") {
|
|
1217
|
-
clearFailure(state, serverName);
|
|
1218
|
-
refreshAfterAuth = true;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
if (connection?.status === "needs-auth" && !refreshAfterAuth) {
|
|
1223
|
-
const message = getAuthRequiredMessage(state, serverName);
|
|
1224
|
-
return {
|
|
1225
|
-
content: [{ type: "text" as const, text: message }],
|
|
1226
|
-
details: { mode: "call", error: "auth_required", ...callIdentity, message },
|
|
1227
|
-
};
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
if (!connection || connection.status !== "connected") {
|
|
1231
|
-
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
1232
|
-
if (failedAgo !== null) {
|
|
1233
|
-
return {
|
|
1234
|
-
content: [{ type: "text" as const, text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
1235
|
-
details: { mode: "call", error: "server_backoff", ...callIdentity },
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
const definition = state.config.mcpServers[serverName];
|
|
1240
|
-
if (!definition) {
|
|
1241
|
-
return {
|
|
1242
|
-
content: [{ type: "text" as const, text: `Server "${serverName}" not connected` }],
|
|
1243
|
-
details: { mode: "call", error: "server_not_connected", ...callIdentity },
|
|
1244
|
-
};
|
|
1245
|
-
}
|
|
1246
|
-
|
|
1247
|
-
try {
|
|
1248
|
-
if (state.ui) {
|
|
1249
|
-
state.ui.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${serverName}...`));
|
|
1250
|
-
}
|
|
1251
|
-
connection = refreshAfterAuth && connection
|
|
1252
|
-
? await state.manager.reconnect(serverName, definition, connection, ownedSignal)
|
|
1253
|
-
: await state.manager.connect(serverName, definition, ownedSignal);
|
|
1254
|
-
if (connection.status === "needs-auth") {
|
|
1255
|
-
if (!autoAuthAttempted) {
|
|
1256
|
-
autoAuthAttempted = true;
|
|
1257
|
-
const autoAuth = await attemptAutoAuth(state, serverName, ownedSignal);
|
|
1258
|
-
if (autoAuth.status === "failed") {
|
|
1259
|
-
return {
|
|
1260
|
-
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
1261
|
-
details: { mode: "call", error: "auth_required", ...callIdentity, message: autoAuth.message },
|
|
1262
|
-
};
|
|
1263
|
-
}
|
|
1264
|
-
if (autoAuth.status === "success") {
|
|
1265
|
-
connection = await state.manager.reconnect(serverName, definition, connection, ownedSignal);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
if (connection.status === "needs-auth") {
|
|
1270
|
-
const message = getAuthRequiredMessage(state, serverName);
|
|
1271
|
-
return {
|
|
1272
|
-
content: [{ type: "text" as const, text: message }],
|
|
1273
|
-
details: { mode: "call", error: "auth_required", ...callIdentity, message },
|
|
1274
|
-
};
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
clearFailure(state, serverName);
|
|
1278
|
-
updateServerMetadata(state, serverName);
|
|
1279
|
-
updateMetadataCache(state, serverName);
|
|
1280
|
-
notifyToolMetadataUpdated(state, serverName, "proxy-call-reconnect");
|
|
1281
|
-
markKeepAliveAfterConnect(state, serverName);
|
|
1282
|
-
updateStatusBar(state);
|
|
1283
|
-
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
1284
|
-
if (!toolMeta) {
|
|
1285
|
-
const suggestions = rankSuggestions(state, toolName, 5, serverName);
|
|
1286
|
-
const hint = suggestions.length > 0
|
|
1287
|
-
? ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ describe: "${suggestions[0]}" }).`
|
|
1288
|
-
: ` Search with mcp({ search: ${JSON.stringify(toolName)}, server: ${JSON.stringify(serverName)} }).`;
|
|
1289
|
-
return {
|
|
1290
|
-
content: [{ type: "text" as const, text: `Tool "${toolName}" not found on "${serverName}" after reconnect.${hint}` }],
|
|
1291
|
-
details: { mode: "call", error: "tool_not_found_after_reconnect", server: serverName, requestedTool: toolName, suggestions },
|
|
1292
|
-
};
|
|
1293
|
-
}
|
|
1294
|
-
} catch (error) {
|
|
1295
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1296
|
-
if (!isAbortError(error, ownedSignal)) recordFailure(state, serverName, message);
|
|
1297
|
-
updateStatusBar(state);
|
|
1298
|
-
return {
|
|
1299
|
-
content: [{ type: "text" as const, text: `Failed to connect to "${serverName}": ${message}` }],
|
|
1300
|
-
details: { mode: "call", error: isAbortError(error, ownedSignal) ? "aborted" : "connect_failed", ...callIdentity, message },
|
|
1301
|
-
};
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
if (isServerDisabled(state.config.mcpServers[serverName])) {
|
|
1306
|
-
return disabledCallResult(serverName, toolMeta);
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1084
|
+
const targets = matches(state.toolMetadata.get(serverName));
|
|
1085
|
+
if (targets.length > 1) return ambiguous();
|
|
1086
|
+
const toolMeta = targets[0];
|
|
1087
|
+
if (!toolMeta) return missing();
|
|
1088
|
+
if (isServerDisabled(state.config.mcpServers[serverName])) return disabledResult("call", serverName);
|
|
1309
1089
|
const approval = await ensureToolCallApproved(state, serverName, toolMeta, args, ownedSignal);
|
|
1310
|
-
if (approval.ok
|
|
1090
|
+
if (!approval.ok) {
|
|
1311
1091
|
const denied = approval.reason === "denied";
|
|
1312
|
-
const message = denied
|
|
1313
|
-
? `The user declined approval to run MCP tool "${toolMeta.originalName}" on server "${serverName}".`
|
|
1314
|
-
: `MCP tool "${toolMeta.originalName}" on server "${serverName}" is approval-gated and requires an interactive session.`;
|
|
1315
1092
|
return {
|
|
1316
|
-
content: [{ type: "text"
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
server: serverName,
|
|
1321
|
-
tool: toolMeta.originalName,
|
|
1322
|
-
},
|
|
1093
|
+
content: [{ type: "text", text: denied
|
|
1094
|
+
? `The user declined approval to run MCP tool "${toolMeta.originalName}" on server "${serverName}".`
|
|
1095
|
+
: `MCP tool "${toolMeta.originalName}" on server "${serverName}" is approval-gated and requires an interactive session.` }],
|
|
1096
|
+
details: { mode: "call", error: denied ? "approval_denied" : "approval_required", server: serverName, tool: toolMeta.originalName },
|
|
1323
1097
|
};
|
|
1324
1098
|
}
|
|
1325
|
-
|
|
1326
|
-
const recoverAuthConnection: NonNullable<SessionRecoveryDeps["onNeedsAuth"]> = async (_serverName, recoverySignal = ownedSignal, challenge) => {
|
|
1327
|
-
throwIfAborted(recoverySignal);
|
|
1328
|
-
const current = state.manager.getConnection(serverName);
|
|
1329
|
-
if (current?.status === "connected" && current !== challenge?.connection) return current;
|
|
1330
|
-
|
|
1331
|
-
if (!autoAuthAttempted) {
|
|
1332
|
-
autoAuthAttempted = true;
|
|
1333
|
-
const autoAuth = await attemptAutoAuth(state, serverName, recoverySignal);
|
|
1334
|
-
throwIfAborted(recoverySignal);
|
|
1335
|
-
if (autoAuth.status === "failed") {
|
|
1336
|
-
throw new SessionRecoveryAuthRequiredError(serverName, autoAuth.message);
|
|
1337
|
-
}
|
|
1338
|
-
if (autoAuth.status === "success") {
|
|
1339
|
-
const definition = state.config.mcpServers[serverName];
|
|
1340
|
-
if (!definition) return undefined;
|
|
1341
|
-
if (!current) return undefined;
|
|
1342
|
-
clearFailure(state, serverName);
|
|
1343
|
-
connection = await state.manager.reconnect(serverName, definition, current, recoverySignal);
|
|
1344
|
-
return connection;
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
return challenge ? undefined : state.manager.getConnection(serverName);
|
|
1348
|
-
};
|
|
1349
|
-
|
|
1350
1099
|
return runToolCall(state, serverName, toolMeta, args, {
|
|
1351
1100
|
...identity,
|
|
1352
1101
|
...(beforeDispatch ? { beforeDispatch } : {}),
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
signal,
|
|
1356
|
-
recoverAuthConnection
|
|
1102
|
+
...(options.raw !== undefined ? { raw: options.raw } : {}),
|
|
1103
|
+
detailsBase: { mode: "call", server: serverName, ...(toolMeta.resourceUri ? { resourceUri: toolMeta.resourceUri } : { tool: toolMeta.originalName }) },
|
|
1104
|
+
ownedSignal, signal,
|
|
1105
|
+
recoverAuthConnection: prepared.recoverAuthConnection!,
|
|
1357
1106
|
authRequiredMessage: () => getAuthRequiredMessage(state, serverName),
|
|
1358
|
-
autoAuthAttempted:
|
|
1359
|
-
...(onRawResult ? { onRawResult } : {}),
|
|
1107
|
+
autoAuthAttempted: prepared.autoAuthAttempted!,
|
|
1360
1108
|
});
|
|
1361
1109
|
}
|