@fitchmultz/pi-mcp-adapter 8.0.5 → 8.0.7
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 +140 -7
- package/OAUTH.md +13 -16
- package/README.md +8 -8
- package/cli.js +67 -28
- package/dist/abort.js +18 -9
- package/dist/client-operations.js +4 -2
- package/dist/commands.js +77 -43
- package/dist/config.js +194 -105
- package/dist/connection-runtime.d.ts +2 -2
- package/dist/connection-runtime.js +29 -18
- package/dist/consent-manager.d.ts +2 -2
- package/dist/consent-manager.js +5 -2
- package/dist/direct-tools.d.ts +1 -1
- package/dist/direct-tools.js +13 -7
- package/dist/elicitation-handler.d.ts +2 -3
- package/dist/elicitation-handler.js +113 -58
- package/dist/gateway-arguments.d.ts +1 -1
- package/dist/gateway-arguments.js +90 -23
- package/dist/glimpse-ui.js +10 -5
- package/dist/host-html-template.d.ts +3 -1
- package/dist/host-html-template.js +9 -12
- package/dist/index.d.ts +1 -1
- package/dist/index.js +264 -119
- package/dist/init.d.ts +1 -3
- package/dist/init.js +91 -51
- package/dist/legacy-migration.d.ts +1 -1
- package/dist/legacy-migration.js +17 -12
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +33 -17
- package/dist/logger.d.ts +2 -2
- package/dist/logger.js +4 -2
- package/dist/mcp-auth-flow.js +150 -58
- package/dist/mcp-auth.d.ts +7 -7
- package/dist/mcp-auth.js +189 -138
- package/dist/mcp-callback-server.js +19 -8
- package/dist/mcp-code.js +168 -54
- package/dist/mcp-keyring-helper.cjs +57 -23
- package/dist/mcp-oauth-provider.d.ts +6 -7
- package/dist/mcp-oauth-provider.js +146 -72
- package/dist/mcp-oauth-runtime.js +29 -16
- package/dist/mcp-output-guard.js +193 -65
- package/dist/mcp-output-preview.js +41 -20
- package/dist/mcp-panel.d.ts +9 -9
- package/dist/mcp-panel.js +141 -70
- package/dist/mcp-script-worker.mjs +135 -43
- package/dist/mcp-setup-panel.d.ts +8 -8
- package/dist/mcp-setup-panel.js +130 -44
- package/dist/mcp-status.js +14 -7
- package/dist/mcp-trace.d.ts +10 -5
- package/dist/mcp-trace.js +127 -61
- package/dist/metadata-cache.d.ts +3 -4
- package/dist/metadata-cache.js +83 -43
- package/dist/npx-resolver.js +101 -60
- package/dist/onboarding-state.js +4 -2
- package/dist/plain-result-reader.js +78 -31
- package/dist/prompts.js +36 -20
- package/dist/proxy-modes.js +670 -193
- package/dist/resource-tools.js +30 -9
- package/dist/runtime-owner.js +36 -18
- package/dist/sampling-handler.d.ts +1 -2
- package/dist/sampling-handler.js +41 -21
- package/dist/search-ranking.d.ts +1 -1
- package/dist/search-ranking.js +67 -34
- package/dist/server-manager.d.ts +11 -11
- package/dist/server-manager.js +283 -166
- package/dist/session-recovery.js +113 -61
- package/dist/tool-approval.js +9 -3
- package/dist/tool-loader.js +154 -55
- package/dist/tool-metadata.d.ts +1 -1
- package/dist/tool-metadata.js +51 -23
- package/dist/tool-registrar.js +32 -10
- package/dist/tool-result-renderer.d.ts +3 -3
- package/dist/tool-result-renderer.js +65 -44
- package/dist/types.d.ts +2 -2
- package/dist/types.js +39 -18
- package/dist/ui-message-history.js +33 -13
- package/dist/ui-resource-handler.d.ts +3 -3
- package/dist/ui-resource-handler.js +37 -21
- package/dist/ui-server.js +101 -43
- package/dist/ui-session.js +66 -29
- package/dist/ui-stream-types.js +14 -4
- package/dist/ui-tool-visibility.js +12 -6
- package/dist/unix-socket-transport.d.ts +1 -2
- package/dist/unix-socket-transport.js +19 -12
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +60 -35
- package/package.json +29 -5
- package/skills/mcp-scripting/SKILL.md +6 -6
package/dist/proxy-modes.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import { getServerPrefix, isServerDisabled, isNonInteractiveOAuth, parseUiPromptHandoff, resolveToolPrefix } from "./types.js";
|
|
3
|
-
import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar, clearFailure, recordFailure } from "./init.js";
|
|
2
|
+
import { getServerPrefix, isServerDisabled, isNonInteractiveOAuth, parseUiPromptHandoff, resolveToolPrefix, } from "./types.js";
|
|
3
|
+
import { lazyConnect, markKeepAliveAfterConnect, notifyToolMetadataUpdated, updateServerMetadata, 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, catalogCoverage, findToolByName, formatSchema, toToolDescriptor } from "./tool-metadata.js";
|
|
6
|
+
import { buildToolMetadata, catalogCoverage, findToolByName, formatSchema, toToolDescriptor, } from "./tool-metadata.js";
|
|
7
7
|
import { reconstructPromptMetadata } from "./metadata-cache.js";
|
|
8
8
|
import { renderMcpResultContent } from "./tool-registrar.js";
|
|
9
|
-
import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference, saveMcpOutputStream } from "./mcp-output-guard.js";
|
|
10
|
-
import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord } from "./utils.js";
|
|
9
|
+
import { guardMcpOutput, guardedMcpDetails, retainMcpResult, resolveMcpOutputGuardOptions, formatMcpResultReference, saveMcpOutputStream, } from "./mcp-output-guard.js";
|
|
10
|
+
import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord, } from "./utils.js";
|
|
11
11
|
import { supportsOAuth } from "./mcp-oauth-runtime.js";
|
|
12
12
|
import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.js";
|
|
13
13
|
import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.js";
|
|
@@ -22,8 +22,9 @@ function disabledResult(mode, serverName) {
|
|
|
22
22
|
}
|
|
23
23
|
function getAuthRequiredMessage(state, serverName, defaultMessage = `Server "${serverName}" requires OAuth authentication. If mcp is inactive, first mcp_search({ enable: ["gateway"] }). Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`) {
|
|
24
24
|
const oauth = state.config.mcpServers[serverName]?.oauth;
|
|
25
|
-
if (oauth && oauth.crossAppAccess)
|
|
25
|
+
if (oauth && oauth.crossAppAccess) {
|
|
26
26
|
defaultMessage = `Server "${serverName}" requires enterprise authorization. Check oauth.crossAppAccess IdP configuration and ID-token source, then retry.`;
|
|
27
|
+
}
|
|
27
28
|
return formatAuthRequiredMessage(state.config, serverName, defaultMessage);
|
|
28
29
|
}
|
|
29
30
|
function getAuthFailedMessage(state, serverName, message) {
|
|
@@ -37,8 +38,9 @@ function getAuthFailedMessage(state, serverName, message) {
|
|
|
37
38
|
function getRedirectPort(authorizationUrl) {
|
|
38
39
|
try {
|
|
39
40
|
const redirectUri = new URL(authorizationUrl).searchParams.get("redirect_uri");
|
|
40
|
-
if (!redirectUri)
|
|
41
|
+
if (!redirectUri) {
|
|
41
42
|
return undefined;
|
|
43
|
+
}
|
|
42
44
|
const port = Number.parseInt(new URL(redirectUri).port, 10);
|
|
43
45
|
return Number.isInteger(port) ? port : undefined;
|
|
44
46
|
}
|
|
@@ -66,7 +68,9 @@ function formatManualAuthInstructions(serverName, authorizationUrl) {
|
|
|
66
68
|
"",
|
|
67
69
|
'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`.',
|
|
68
70
|
portNote.trimEnd(),
|
|
69
|
-
]
|
|
71
|
+
]
|
|
72
|
+
.filter(Boolean)
|
|
73
|
+
.join("\n");
|
|
70
74
|
}
|
|
71
75
|
async function attemptAutoAuth(state, serverName, signal) {
|
|
72
76
|
if (state.config.settings?.autoAuth !== true) {
|
|
@@ -102,7 +106,10 @@ async function attemptAutoAuth(state, serverName, signal) {
|
|
|
102
106
|
}
|
|
103
107
|
else {
|
|
104
108
|
if (signal) {
|
|
105
|
-
await authenticate(serverName, serverUrl, definition, {
|
|
109
|
+
await authenticate(serverName, serverUrl, definition, {
|
|
110
|
+
signal,
|
|
111
|
+
runtime: state.oauthRuntime,
|
|
112
|
+
});
|
|
106
113
|
}
|
|
107
114
|
else {
|
|
108
115
|
await authenticate(serverName, serverUrl, definition, { runtime: state.oauthRuntime });
|
|
@@ -111,8 +118,9 @@ async function attemptAutoAuth(state, serverName, signal) {
|
|
|
111
118
|
return { status: "success" };
|
|
112
119
|
}
|
|
113
120
|
catch (error) {
|
|
114
|
-
if (isAbortError(error, signal))
|
|
121
|
+
if (isAbortError(error, signal)) {
|
|
115
122
|
throw error;
|
|
123
|
+
}
|
|
116
124
|
const message = error instanceof Error ? error.message : String(error);
|
|
117
125
|
return {
|
|
118
126
|
status: "failed",
|
|
@@ -124,8 +132,9 @@ async function* uiSessionText(session) {
|
|
|
124
132
|
yield `\n## ${session.serverName} / ${session.toolName} (${session.completedAt.toLocaleTimeString()}, ${session.reason})`;
|
|
125
133
|
let heading = false;
|
|
126
134
|
for await (const prompt of uiMessages(session.messages, "prompts")) {
|
|
127
|
-
if (parseUiPromptHandoff(prompt))
|
|
135
|
+
if (parseUiPromptHandoff(prompt)) {
|
|
128
136
|
continue;
|
|
137
|
+
}
|
|
129
138
|
if (!heading) {
|
|
130
139
|
yield "\n\n### Prompts:";
|
|
131
140
|
heading = true;
|
|
@@ -143,8 +152,9 @@ async function* uiSessionText(session) {
|
|
|
143
152
|
}
|
|
144
153
|
for await (const prompt of uiMessages(session.messages, "prompts")) {
|
|
145
154
|
const handoff = parseUiPromptHandoff(prompt);
|
|
146
|
-
if (!handoff)
|
|
155
|
+
if (!handoff) {
|
|
147
156
|
continue;
|
|
157
|
+
}
|
|
148
158
|
if (!heading) {
|
|
149
159
|
yield "\n\n### Intents:";
|
|
150
160
|
heading = true;
|
|
@@ -153,8 +163,9 @@ async function* uiSessionText(session) {
|
|
|
153
163
|
}
|
|
154
164
|
if (session.messages.contexts.length) {
|
|
155
165
|
yield "\n\n### Context updates:";
|
|
156
|
-
for (const context of session.messages.contexts)
|
|
166
|
+
for (const context of session.messages.contexts) {
|
|
157
167
|
yield `\n- ${context.summary}${context.truncated ? " (truncated)" : ""}`;
|
|
168
|
+
}
|
|
158
169
|
}
|
|
159
170
|
heading = false;
|
|
160
171
|
for await (const notification of uiMessages(session.messages, "notifications")) {
|
|
@@ -169,30 +180,69 @@ export async function executeUiMessages(state) {
|
|
|
169
180
|
const pending = [...state.completedUiSessions];
|
|
170
181
|
const options = resolveMcpOutputGuardOptions(state.config?.settings, state.outputDirectory);
|
|
171
182
|
// Bound both inline output and details. Large retrieval streams; it never flattens all events.
|
|
172
|
-
const size = pending.reduce((bytes, session) => bytes +
|
|
183
|
+
const size = pending.reduce((bytes, session) => bytes +
|
|
184
|
+
(session.messages.historyBytes ?? 0) +
|
|
185
|
+
Buffer.byteLength(JSON.stringify(session.messages)), 0);
|
|
173
186
|
const large = size > Math.min(options.maxBytes, options.detailsMaxBytes);
|
|
174
|
-
const warnings = pending.flatMap(session => session.messages.historyError ? [session.messages.historyError] : []);
|
|
187
|
+
const warnings = pending.flatMap((session) => session.messages.historyError ? [session.messages.historyError] : []);
|
|
175
188
|
const paged = async () => {
|
|
176
189
|
async function* output() {
|
|
177
190
|
yield `UI Session Messages (${pending.length} sessions):\n`;
|
|
178
|
-
for (const session of pending)
|
|
191
|
+
for (const session of pending) {
|
|
179
192
|
yield* uiSessionText(session);
|
|
193
|
+
}
|
|
180
194
|
}
|
|
181
195
|
const artifact = await saveMcpOutputStream(output(), state.outputDirectory);
|
|
182
|
-
if (!artifact.path)
|
|
183
|
-
return {
|
|
196
|
+
if (!artifact.path) {
|
|
197
|
+
return {
|
|
198
|
+
content: [
|
|
199
|
+
{
|
|
200
|
+
type: "text",
|
|
201
|
+
text: `UI messages remain available; retrieval could not be saved: ${artifact.error}`,
|
|
202
|
+
},
|
|
203
|
+
],
|
|
204
|
+
isError: true,
|
|
205
|
+
details: { error: "ui_history_delivery_failed", cleared: false },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
184
208
|
// Newly completed sessions during I/O are not part of this drain.
|
|
185
|
-
state.completedUiSessions = state.completedUiSessions.filter(session => !pending.includes(session));
|
|
186
|
-
return {
|
|
209
|
+
state.completedUiSessions = state.completedUiSessions.filter((session) => !pending.includes(session));
|
|
210
|
+
return {
|
|
211
|
+
content: [
|
|
212
|
+
{
|
|
213
|
+
type: "text",
|
|
214
|
+
text: `UI Session Messages (${pending.length} sessions). ${formatMcpResultReference(artifact.path)}${warnings.length ? `\nHistory persistence warning: ${warnings.join("; ")}` : ""}`,
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
details: {
|
|
218
|
+
sessions: pending.length,
|
|
219
|
+
resultRef: artifact.path,
|
|
220
|
+
historyRefs: pending.flatMap((session) => session.messages.historyRef ? [session.messages.historyRef] : []),
|
|
221
|
+
cleared: true,
|
|
222
|
+
},
|
|
223
|
+
};
|
|
187
224
|
};
|
|
188
|
-
if (large)
|
|
225
|
+
if (large) {
|
|
189
226
|
return paged();
|
|
227
|
+
}
|
|
190
228
|
let sessions;
|
|
191
229
|
try {
|
|
192
|
-
sessions = await Promise.all(pending.map(async (session) => ({
|
|
230
|
+
sessions = await Promise.all(pending.map(async (session) => ({
|
|
231
|
+
...session,
|
|
232
|
+
messages: await materializeUiMessages(session.messages),
|
|
233
|
+
})));
|
|
193
234
|
}
|
|
194
235
|
catch (error) {
|
|
195
|
-
return {
|
|
236
|
+
return {
|
|
237
|
+
content: [
|
|
238
|
+
{
|
|
239
|
+
type: "text",
|
|
240
|
+
text: `UI messages remain available; history could not be read: ${String(error)}`,
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
isError: true,
|
|
244
|
+
details: { error: "ui_history_delivery_failed", cleared: false },
|
|
245
|
+
};
|
|
196
246
|
}
|
|
197
247
|
if (sessions.length === 0) {
|
|
198
248
|
return {
|
|
@@ -230,7 +280,7 @@ export async function executeUiMessages(state) {
|
|
|
230
280
|
...session.messages.intents,
|
|
231
281
|
...session.messages.prompts
|
|
232
282
|
.map((prompt) => parseUiPromptHandoff(prompt))
|
|
233
|
-
.filter((handoff) =>
|
|
283
|
+
.filter((handoff) => Boolean(handoff))
|
|
234
284
|
.map((handoff) => ({ intent: handoff.intent, params: handoff.params })),
|
|
235
285
|
];
|
|
236
286
|
if (intentsForSession.length > 0) {
|
|
@@ -256,7 +306,13 @@ export async function executeUiMessages(state) {
|
|
|
256
306
|
}
|
|
257
307
|
const count = sessions.length;
|
|
258
308
|
const inline = {
|
|
259
|
-
content: [
|
|
309
|
+
content: [
|
|
310
|
+
{
|
|
311
|
+
type: "text",
|
|
312
|
+
text: output.join("\n") +
|
|
313
|
+
(warnings.length ? `\nHistory persistence warning: ${warnings.join("; ")}` : ""),
|
|
314
|
+
},
|
|
315
|
+
],
|
|
260
316
|
details: {
|
|
261
317
|
sessions: count,
|
|
262
318
|
prompts: allPrompts,
|
|
@@ -266,10 +322,13 @@ export async function executeUiMessages(state) {
|
|
|
266
322
|
cleared: true,
|
|
267
323
|
},
|
|
268
324
|
};
|
|
269
|
-
const text = inline.content.map(block => block.type === "text" ? block.text : "").join("\n");
|
|
270
|
-
if (Buffer.byteLength(JSON.stringify(inline.details)) > options.detailsMaxBytes ||
|
|
325
|
+
const text = inline.content.map((block) => (block.type === "text" ? block.text : "")).join("\n");
|
|
326
|
+
if (Buffer.byteLength(JSON.stringify(inline.details)) > options.detailsMaxBytes ||
|
|
327
|
+
Buffer.byteLength(text) > options.maxBytes ||
|
|
328
|
+
text.split("\n").length > options.maxLines) {
|
|
271
329
|
return paged();
|
|
272
|
-
|
|
330
|
+
}
|
|
331
|
+
state.completedUiSessions = state.completedUiSessions.filter((session) => !pending.includes(session));
|
|
273
332
|
return inline;
|
|
274
333
|
}
|
|
275
334
|
export function executeStatus(state) {
|
|
@@ -279,7 +338,7 @@ export function executeStatus(state) {
|
|
|
279
338
|
const disabled = isServerDisabled(definition);
|
|
280
339
|
const connection = disabled ? undefined : state.manager.getConnection(name);
|
|
281
340
|
const metadata = disabled ? undefined : state.toolMetadata.get(name);
|
|
282
|
-
const toolCount = metadata?.filter(tool => !tool.resourceUri).length ?? 0;
|
|
341
|
+
const toolCount = metadata?.filter((tool) => !tool.resourceUri).length ?? 0;
|
|
283
342
|
const failedAgo = disabled ? null : getFailureAgeSeconds(state, name);
|
|
284
343
|
let status = disabled ? "disabled" : "not connected";
|
|
285
344
|
if (!disabled && connection?.status === "connected") {
|
|
@@ -296,14 +355,15 @@ export function executeStatus(state) {
|
|
|
296
355
|
}
|
|
297
356
|
servers.push({ name, status, toolCount, failedAgo, ...(disabled ? { disabled: true } : {}) });
|
|
298
357
|
}
|
|
299
|
-
const disabledCount = servers.filter(s => s.disabled).length;
|
|
300
|
-
const enabledServers = servers.filter(s => !s.disabled);
|
|
358
|
+
const disabledCount = servers.filter((s) => s.disabled).length;
|
|
359
|
+
const enabledServers = servers.filter((s) => !s.disabled);
|
|
301
360
|
const totalTools = enabledServers.reduce((sum, s) => sum + s.toolCount, 0);
|
|
302
|
-
const connectedCount = enabledServers.filter(s => s.status === "connected").length;
|
|
361
|
+
const connectedCount = enabledServers.filter((s) => s.status === "connected").length;
|
|
303
362
|
const coverage = catalogCoverage(state);
|
|
304
363
|
let text = `MCP: ${connectedCount}/${enabledServers.length} connected, ${totalTools} tools available (calls connect lazily)`;
|
|
305
|
-
if (disabledCount > 0)
|
|
364
|
+
if (disabledCount > 0) {
|
|
306
365
|
text += ` (${disabledCount} disabled)`;
|
|
366
|
+
}
|
|
307
367
|
text += "\n\n";
|
|
308
368
|
for (const server of servers) {
|
|
309
369
|
if (server.disabled) {
|
|
@@ -330,8 +390,9 @@ export function executeStatus(state) {
|
|
|
330
390
|
}
|
|
331
391
|
if (servers.length > 0) {
|
|
332
392
|
text += `\nmcp_search({ query: "...", server: "name" }) to discover tools; mcp({ action: "resources", server: "name" }) for resources.`;
|
|
333
|
-
if (!coverage.complete)
|
|
393
|
+
if (!coverage.complete) {
|
|
334
394
|
text += `\nPartial catalog: ${coverage.unknownServers.length} undiscovered server(s).`;
|
|
395
|
+
}
|
|
335
396
|
}
|
|
336
397
|
return {
|
|
337
398
|
content: [{ type: "text", text: text.trim() }],
|
|
@@ -345,42 +406,69 @@ export async function executeAuthStart(state, serverName, signal) {
|
|
|
345
406
|
const definition = state.config.mcpServers[serverName];
|
|
346
407
|
if (!definition) {
|
|
347
408
|
return {
|
|
348
|
-
content: [
|
|
409
|
+
content: [
|
|
410
|
+
{
|
|
411
|
+
type: "text",
|
|
412
|
+
text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).`,
|
|
413
|
+
},
|
|
414
|
+
],
|
|
349
415
|
details: { mode: "auth-start", error: "not_found", server: serverName },
|
|
350
416
|
};
|
|
351
417
|
}
|
|
352
|
-
if (isServerDisabled(definition))
|
|
418
|
+
if (isServerDisabled(definition)) {
|
|
353
419
|
return disabledResult("auth-start", serverName);
|
|
420
|
+
}
|
|
354
421
|
try {
|
|
355
422
|
const serverUrl = resolveServerUrl(definition);
|
|
356
423
|
if (!serverUrl || !supportsOAuth(definition)) {
|
|
357
424
|
return {
|
|
358
|
-
content: [
|
|
425
|
+
content: [
|
|
426
|
+
{
|
|
427
|
+
type: "text",
|
|
428
|
+
text: `Server "${serverName}" is not configured for OAuth over HTTP.`,
|
|
429
|
+
},
|
|
430
|
+
],
|
|
359
431
|
details: { mode: "auth-start", error: "oauth_not_supported", server: serverName },
|
|
360
432
|
};
|
|
361
433
|
}
|
|
362
434
|
const { authorizationUrl } = state.authStorageOptions
|
|
363
435
|
? ownedSignal
|
|
364
|
-
? await startAuth(serverName, serverUrl, definition, {
|
|
365
|
-
|
|
436
|
+
? await startAuth(serverName, serverUrl, definition, {
|
|
437
|
+
authStorageOptions: state.authStorageOptions,
|
|
438
|
+
signal: ownedSignal,
|
|
439
|
+
runtime: state.oauthRuntime,
|
|
440
|
+
})
|
|
441
|
+
: await startAuth(serverName, serverUrl, definition, {
|
|
442
|
+
authStorageOptions: state.authStorageOptions,
|
|
443
|
+
runtime: state.oauthRuntime,
|
|
444
|
+
})
|
|
366
445
|
: ownedSignal
|
|
367
|
-
? await startAuth(serverName, serverUrl, definition, {
|
|
446
|
+
? await startAuth(serverName, serverUrl, definition, {
|
|
447
|
+
signal: ownedSignal,
|
|
448
|
+
runtime: state.oauthRuntime,
|
|
449
|
+
})
|
|
368
450
|
: await startAuth(serverName, serverUrl, definition, { runtime: state.oauthRuntime });
|
|
369
451
|
if (!authorizationUrl) {
|
|
370
452
|
return {
|
|
371
|
-
content: [
|
|
453
|
+
content: [
|
|
454
|
+
{ type: "text", text: `OAuth authentication successful for "${serverName}".` },
|
|
455
|
+
],
|
|
372
456
|
details: { mode: "auth-start", server: serverName, authenticated: true },
|
|
373
457
|
};
|
|
374
458
|
}
|
|
375
459
|
return {
|
|
376
|
-
content: [
|
|
460
|
+
content: [
|
|
461
|
+
{ type: "text", text: formatManualAuthInstructions(serverName, authorizationUrl) },
|
|
462
|
+
],
|
|
377
463
|
details: { mode: "auth-start", server: serverName, authorizationUrl },
|
|
378
464
|
};
|
|
379
465
|
}
|
|
380
466
|
catch (error) {
|
|
381
467
|
const message = error instanceof Error ? error.message : String(error);
|
|
382
468
|
return {
|
|
383
|
-
content: [
|
|
469
|
+
content: [
|
|
470
|
+
{ type: "text", text: `Failed to start OAuth for "${serverName}": ${message}` },
|
|
471
|
+
],
|
|
384
472
|
details: { mode: "auth-start", error: "auth_start_failed", server: serverName, message },
|
|
385
473
|
};
|
|
386
474
|
}
|
|
@@ -392,62 +480,111 @@ export async function executeAuthComplete(state, serverName, input, signal) {
|
|
|
392
480
|
const definition = state.config.mcpServers[serverName];
|
|
393
481
|
if (!definition) {
|
|
394
482
|
return {
|
|
395
|
-
content: [
|
|
483
|
+
content: [
|
|
484
|
+
{
|
|
485
|
+
type: "text",
|
|
486
|
+
text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).`,
|
|
487
|
+
},
|
|
488
|
+
],
|
|
396
489
|
details: { mode: "auth-complete", error: "not_found", server: serverName },
|
|
397
490
|
};
|
|
398
491
|
}
|
|
399
|
-
if (isServerDisabled(definition))
|
|
492
|
+
if (isServerDisabled(definition)) {
|
|
400
493
|
return disabledResult("auth-complete", serverName);
|
|
494
|
+
}
|
|
401
495
|
try {
|
|
402
496
|
const previousConnection = state.manager.getConnection(serverName);
|
|
403
497
|
const status = state.authStorageOptions
|
|
404
498
|
? ownedSignal
|
|
405
|
-
? await completeAuthFromInput(serverName, input, {
|
|
406
|
-
|
|
499
|
+
? await completeAuthFromInput(serverName, input, {
|
|
500
|
+
authStorageOptions: state.authStorageOptions,
|
|
501
|
+
signal: ownedSignal,
|
|
502
|
+
runtime: state.oauthRuntime,
|
|
503
|
+
})
|
|
504
|
+
: await completeAuthFromInput(serverName, input, {
|
|
505
|
+
authStorageOptions: state.authStorageOptions,
|
|
506
|
+
runtime: state.oauthRuntime,
|
|
507
|
+
})
|
|
407
508
|
: ownedSignal
|
|
408
|
-
? await completeAuthFromInput(serverName, input, {
|
|
509
|
+
? await completeAuthFromInput(serverName, input, {
|
|
510
|
+
signal: ownedSignal,
|
|
511
|
+
runtime: state.oauthRuntime,
|
|
512
|
+
})
|
|
409
513
|
: await completeAuthFromInput(serverName, input, { runtime: state.oauthRuntime });
|
|
410
514
|
if (status !== "authenticated") {
|
|
411
515
|
return {
|
|
412
|
-
content: [
|
|
516
|
+
content: [
|
|
517
|
+
{
|
|
518
|
+
type: "text",
|
|
519
|
+
text: `OAuth authentication did not complete for "${serverName}".`,
|
|
520
|
+
},
|
|
521
|
+
],
|
|
413
522
|
details: { mode: "auth-complete", error: "not_authenticated", server: serverName, status },
|
|
414
523
|
};
|
|
415
524
|
}
|
|
416
|
-
if (previousConnection)
|
|
525
|
+
if (previousConnection) {
|
|
417
526
|
state.manager.retire(serverName, previousConnection);
|
|
527
|
+
}
|
|
418
528
|
clearFailure(state, serverName);
|
|
419
529
|
updateStatusBar(state);
|
|
420
530
|
return {
|
|
421
|
-
content: [
|
|
531
|
+
content: [
|
|
532
|
+
{
|
|
533
|
+
type: "text",
|
|
534
|
+
text: `OAuth authentication successful for "${serverName}". Run mcp({ action: "connect", server: "${serverName}" }) to connect with the new token.`,
|
|
535
|
+
},
|
|
536
|
+
],
|
|
422
537
|
details: { mode: "auth-complete", server: serverName, authenticated: true },
|
|
423
538
|
};
|
|
424
539
|
}
|
|
425
540
|
catch (error) {
|
|
426
541
|
const message = error instanceof Error ? error.message : String(error);
|
|
427
542
|
return {
|
|
428
|
-
content: [
|
|
429
|
-
|
|
543
|
+
content: [
|
|
544
|
+
{ type: "text", text: `Failed to complete OAuth for "${serverName}": ${message}` },
|
|
545
|
+
],
|
|
546
|
+
details: {
|
|
547
|
+
mode: "auth-complete",
|
|
548
|
+
error: "auth_complete_failed",
|
|
549
|
+
server: serverName,
|
|
550
|
+
message,
|
|
551
|
+
},
|
|
430
552
|
};
|
|
431
553
|
}
|
|
432
554
|
}
|
|
433
555
|
export function executeDescribe(state, toolName, serverFilter) {
|
|
434
|
-
if (serverFilter && isServerDisabled(state.config.mcpServers[serverFilter]))
|
|
556
|
+
if (serverFilter && isServerDisabled(state.config.mcpServers[serverFilter])) {
|
|
435
557
|
return disabledResult("describe", serverFilter);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
558
|
+
}
|
|
559
|
+
const catalog = [...state.toolMetadata].flatMap(([server, tools]) => (!serverFilter || serverFilter === server) &&
|
|
560
|
+
state.config.mcpServers[server] &&
|
|
561
|
+
!isServerDisabled(state.config.mcpServers[server])
|
|
562
|
+
? tools.map((tool) => ({ server, tool }))
|
|
563
|
+
: []);
|
|
564
|
+
const originals = serverFilter
|
|
565
|
+
? catalog.filter(({ tool }) => tool.originalName === toolName)
|
|
566
|
+
: [];
|
|
439
567
|
const exact = originals.length ? originals : catalog.filter(({ tool }) => tool.name === toolName);
|
|
440
|
-
const matches = exact.length
|
|
441
|
-
|
|
568
|
+
const matches = exact.length
|
|
569
|
+
? exact
|
|
570
|
+
: catalog.filter(({ tool }) => tool.name.replace(/-/g, "_") === toolName.replace(/-/g, "_"));
|
|
571
|
+
if (matches.length > 1) {
|
|
442
572
|
return {
|
|
443
|
-
content: [
|
|
573
|
+
content: [
|
|
574
|
+
{
|
|
575
|
+
type: "text",
|
|
576
|
+
text: `Tool ${JSON.stringify(toolName)} is ambiguous. Specify server and original tool name: ${JSON.stringify(matches.map(({ server, tool }) => ({ server, tool: tool.originalName })))}`,
|
|
577
|
+
},
|
|
578
|
+
],
|
|
444
579
|
details: { mode: "describe", error: "ambiguous_tool", requestedTool: toolName },
|
|
445
580
|
};
|
|
581
|
+
}
|
|
446
582
|
const match = matches[0];
|
|
447
583
|
if (!match) {
|
|
448
584
|
const disabled = [...state.toolMetadata].find(([server, tools]) => isServerDisabled(state.config.mcpServers[server]) && findToolByName(tools, toolName));
|
|
449
|
-
if (disabled)
|
|
585
|
+
if (disabled) {
|
|
450
586
|
return disabledResult("describe", disabled[0]);
|
|
587
|
+
}
|
|
451
588
|
const suggestions = rankSuggestions(state, toolName, 5, serverFilter);
|
|
452
589
|
const hint = suggestions.length > 0
|
|
453
590
|
? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: "${suggestions[0]}" }). Load if inactive: mcp_search({ enable: ["gateway"] }).`
|
|
@@ -461,15 +598,34 @@ export function executeDescribe(state, toolName, serverFilter) {
|
|
|
461
598
|
const descriptor = toToolDescriptor(serverName, toolMeta);
|
|
462
599
|
const approvalRequired = isToolCallApprovalRequired(state.config, serverName, toolMeta);
|
|
463
600
|
return {
|
|
464
|
-
content: [
|
|
601
|
+
content: [
|
|
602
|
+
{
|
|
603
|
+
type: "text",
|
|
604
|
+
text: JSON.stringify({
|
|
605
|
+
...descriptor,
|
|
606
|
+
...(toolMeta.resourceUri
|
|
607
|
+
? {
|
|
608
|
+
resourceUri: toolMeta.resourceUri,
|
|
609
|
+
readWith: {
|
|
610
|
+
action: "read-resource",
|
|
611
|
+
server: serverName,
|
|
612
|
+
uri: toolMeta.resourceUri,
|
|
613
|
+
},
|
|
614
|
+
}
|
|
615
|
+
: {}),
|
|
616
|
+
...(approvalRequired ? { approvalRequired: true } : {}),
|
|
617
|
+
}, null, 2),
|
|
618
|
+
},
|
|
619
|
+
],
|
|
465
620
|
details: { mode: "describe", tool: toolMeta, server: serverName },
|
|
466
621
|
};
|
|
467
622
|
}
|
|
468
623
|
export function executeSearch(state, query, server, includeSchemas, limit = 12, offset = 0, caller = "mcp") {
|
|
469
624
|
const showSchemas = includeSchemas !== false;
|
|
470
625
|
const searchCall = (nextOffset) => `${caller}({ ${caller === "mcp" ? 'action: "search", ' : ""}query: ${JSON.stringify(query)}${server ? `, server: ${JSON.stringify(server)}` : ""}${caller === "mcp" && !showSchemas ? ", includeSchemas: false" : ""}, limit: ${limit}, offset: ${nextOffset} })`;
|
|
471
|
-
if (server && isServerDisabled(state.config.mcpServers[server]))
|
|
626
|
+
if (server && isServerDisabled(state.config.mcpServers[server])) {
|
|
472
627
|
return disabledResult("search", server);
|
|
628
|
+
}
|
|
473
629
|
let matches;
|
|
474
630
|
if (query.trim().length === 0) {
|
|
475
631
|
if (!server) {
|
|
@@ -478,8 +634,9 @@ export function executeSearch(state, query, server, includeSchemas, limit = 12,
|
|
|
478
634
|
details: { mode: "search", error: "empty_query" },
|
|
479
635
|
};
|
|
480
636
|
}
|
|
481
|
-
matches = (state.toolMetadata.get(server) ?? [])
|
|
482
|
-
.
|
|
637
|
+
matches = (state.toolMetadata.get(server) ?? [])
|
|
638
|
+
.filter((tool) => !tool.resourceUri)
|
|
639
|
+
.map((tool) => ({ server, tool, score: 0 }))
|
|
483
640
|
.sort((a, b) => a.tool.name.localeCompare(b.tool.name));
|
|
484
641
|
}
|
|
485
642
|
else {
|
|
@@ -487,19 +644,44 @@ export function executeSearch(state, query, server, includeSchemas, limit = 12,
|
|
|
487
644
|
}
|
|
488
645
|
const page = paginate(matches, offset, limit);
|
|
489
646
|
const coverage = catalogCoverage(state, server);
|
|
490
|
-
const coverageNotice = coverage.complete
|
|
647
|
+
const coverageNotice = coverage.complete
|
|
648
|
+
? ""
|
|
649
|
+
: `\nCatalog is incomplete. Undiscovered servers: ${coverage.unknownServers.join(", ")}. Search with server to discover one.`;
|
|
491
650
|
if (page.total === 0) {
|
|
492
|
-
const msg = server
|
|
651
|
+
const msg = server
|
|
652
|
+
? `No tools matching "${query}" in "${server}"`
|
|
653
|
+
: `No tools matching "${query}"`;
|
|
493
654
|
return {
|
|
494
655
|
content: [{ type: "text", text: msg + coverageNotice }],
|
|
495
|
-
details: {
|
|
656
|
+
details: {
|
|
657
|
+
mode: "search",
|
|
658
|
+
matches: [],
|
|
659
|
+
count: 0,
|
|
660
|
+
hasMore: false,
|
|
661
|
+
nextOffset: null,
|
|
662
|
+
query,
|
|
663
|
+
coverage,
|
|
664
|
+
},
|
|
496
665
|
};
|
|
497
666
|
}
|
|
498
667
|
if (page.items.length === 0) {
|
|
499
668
|
const retry = searchCall(0);
|
|
500
669
|
return {
|
|
501
|
-
content: [
|
|
502
|
-
|
|
670
|
+
content: [
|
|
671
|
+
{
|
|
672
|
+
type: "text",
|
|
673
|
+
text: `No search results at offset ${offset}; ${page.total} tools match. Retry with ${retry}.`,
|
|
674
|
+
},
|
|
675
|
+
],
|
|
676
|
+
details: {
|
|
677
|
+
mode: "search",
|
|
678
|
+
error: "offset_out_of_range",
|
|
679
|
+
matches: [],
|
|
680
|
+
count: page.total,
|
|
681
|
+
hasMore: false,
|
|
682
|
+
nextOffset: null,
|
|
683
|
+
query,
|
|
684
|
+
},
|
|
503
685
|
};
|
|
504
686
|
}
|
|
505
687
|
let text = `Found ${page.total} tool${page.total === 1 ? "" : "s"} matching "${query}":\n\n`;
|
|
@@ -512,8 +694,9 @@ export function executeSearch(state, query, server, includeSchemas, limit = 12,
|
|
|
512
694
|
}
|
|
513
695
|
else {
|
|
514
696
|
text += `- ${match.tool.name}${approvalMarker}`;
|
|
515
|
-
if (match.tool.description)
|
|
697
|
+
if (match.tool.description) {
|
|
516
698
|
text += ` - ${truncateAtWord(match.tool.description, 50)}`;
|
|
699
|
+
}
|
|
517
700
|
text += "\n";
|
|
518
701
|
}
|
|
519
702
|
}
|
|
@@ -527,7 +710,11 @@ export function executeSearch(state, query, server, includeSchemas, limit = 12,
|
|
|
527
710
|
details: {
|
|
528
711
|
mode: "search",
|
|
529
712
|
coverage,
|
|
530
|
-
matches: page.items.map(match => ({
|
|
713
|
+
matches: page.items.map((match) => ({
|
|
714
|
+
server: match.server,
|
|
715
|
+
tool: match.tool.name,
|
|
716
|
+
score: match.score,
|
|
717
|
+
})),
|
|
531
718
|
count: page.total,
|
|
532
719
|
hasMore: page.hasMore,
|
|
533
720
|
nextOffset: page.nextOffset,
|
|
@@ -539,13 +726,19 @@ export function executeList(state, server, limit = 12, offset = 0) {
|
|
|
539
726
|
const definition = state.config.mcpServers[server];
|
|
540
727
|
if (!definition) {
|
|
541
728
|
return {
|
|
542
|
-
content: [
|
|
729
|
+
content: [
|
|
730
|
+
{
|
|
731
|
+
type: "text",
|
|
732
|
+
text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).`,
|
|
733
|
+
},
|
|
734
|
+
],
|
|
543
735
|
details: { mode: "list", server, tools: [], count: 0, error: "not_found" },
|
|
544
736
|
};
|
|
545
737
|
}
|
|
546
|
-
if (isServerDisabled(definition))
|
|
738
|
+
if (isServerDisabled(definition)) {
|
|
547
739
|
return disabledResult("list", server);
|
|
548
|
-
|
|
740
|
+
}
|
|
741
|
+
const metadata = state.toolMetadata.get(server)?.filter((tool) => !tool.resourceUri);
|
|
549
742
|
const connection = state.manager.getConnection(server);
|
|
550
743
|
const instructions = state.serverInstructions.get(server);
|
|
551
744
|
let instructionsText = "";
|
|
@@ -559,26 +752,72 @@ export function executeList(state, server, limit = 12, offset = 0) {
|
|
|
559
752
|
if (!metadata?.length) {
|
|
560
753
|
if (connection?.status === "connected") {
|
|
561
754
|
return {
|
|
562
|
-
content: [
|
|
563
|
-
|
|
755
|
+
content: [
|
|
756
|
+
{ type: "text", text: `Server "${server}" has no tools.${instructionsText}` },
|
|
757
|
+
],
|
|
758
|
+
details: {
|
|
759
|
+
mode: "list",
|
|
760
|
+
server,
|
|
761
|
+
tools: [],
|
|
762
|
+
count: 0,
|
|
763
|
+
hasInstructions: Boolean(instructions),
|
|
764
|
+
},
|
|
564
765
|
};
|
|
565
766
|
}
|
|
566
767
|
if (metadata !== undefined) {
|
|
567
768
|
return {
|
|
568
|
-
content: [
|
|
569
|
-
|
|
769
|
+
content: [
|
|
770
|
+
{
|
|
771
|
+
type: "text",
|
|
772
|
+
text: `Server "${server}" has no cached tools (not connected).${instructionsText}`,
|
|
773
|
+
},
|
|
774
|
+
],
|
|
775
|
+
details: {
|
|
776
|
+
mode: "list",
|
|
777
|
+
server,
|
|
778
|
+
tools: [],
|
|
779
|
+
count: 0,
|
|
780
|
+
cached: true,
|
|
781
|
+
hasInstructions: Boolean(instructions),
|
|
782
|
+
},
|
|
570
783
|
};
|
|
571
784
|
}
|
|
572
785
|
return {
|
|
573
|
-
content: [
|
|
574
|
-
|
|
786
|
+
content: [
|
|
787
|
+
{
|
|
788
|
+
type: "text",
|
|
789
|
+
text: `Server "${server}" is configured but not connected. Use mcp({ action: "connect", server: "${server}" }) or /mcp reconnect ${server} to retry.${instructionsText}`,
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
details: {
|
|
793
|
+
mode: "list",
|
|
794
|
+
server,
|
|
795
|
+
tools: [],
|
|
796
|
+
count: 0,
|
|
797
|
+
error: "not_connected",
|
|
798
|
+
hasInstructions: Boolean(instructions),
|
|
799
|
+
},
|
|
575
800
|
};
|
|
576
801
|
}
|
|
577
802
|
const page = paginate(metadata, offset, limit);
|
|
578
803
|
if (page.items.length === 0) {
|
|
579
804
|
return {
|
|
580
|
-
content: [
|
|
581
|
-
|
|
805
|
+
content: [
|
|
806
|
+
{
|
|
807
|
+
type: "text",
|
|
808
|
+
text: `No tools at offset ${offset}; "${server}" has ${page.total} tools. Retry with mcp({ action: "list", server: ${JSON.stringify(server)}, limit: ${limit}, offset: 0 }).`,
|
|
809
|
+
},
|
|
810
|
+
],
|
|
811
|
+
details: {
|
|
812
|
+
mode: "list",
|
|
813
|
+
error: "offset_out_of_range",
|
|
814
|
+
server,
|
|
815
|
+
tools: [],
|
|
816
|
+
count: page.total,
|
|
817
|
+
hasMore: false,
|
|
818
|
+
nextOffset: null,
|
|
819
|
+
hasInstructions: Boolean(instructions),
|
|
820
|
+
},
|
|
582
821
|
};
|
|
583
822
|
}
|
|
584
823
|
const cachedNote = connection?.status === "connected" ? "" : ", not connected, cached";
|
|
@@ -586,8 +825,9 @@ export function executeList(state, server, limit = 12, offset = 0) {
|
|
|
586
825
|
for (const tool of page.items) {
|
|
587
826
|
const description = truncateAtWord(tool.description, 50);
|
|
588
827
|
text += `- ${tool.name}`;
|
|
589
|
-
if (description)
|
|
828
|
+
if (description) {
|
|
590
829
|
text += ` - ${description}`;
|
|
830
|
+
}
|
|
591
831
|
text += "\n";
|
|
592
832
|
}
|
|
593
833
|
const first = (Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0) + 1;
|
|
@@ -600,7 +840,7 @@ export function executeList(state, server, limit = 12, offset = 0) {
|
|
|
600
840
|
details: {
|
|
601
841
|
mode: "list",
|
|
602
842
|
server,
|
|
603
|
-
tools: page.items.map(tool => tool.name),
|
|
843
|
+
tools: page.items.map((tool) => tool.name),
|
|
604
844
|
count: page.total,
|
|
605
845
|
hasMore: page.hasMore,
|
|
606
846
|
nextOffset: page.nextOffset,
|
|
@@ -612,12 +852,18 @@ export function executeInstructions(state, server) {
|
|
|
612
852
|
const definition = state.config.mcpServers[server];
|
|
613
853
|
if (!definition) {
|
|
614
854
|
return {
|
|
615
|
-
content: [
|
|
855
|
+
content: [
|
|
856
|
+
{
|
|
857
|
+
type: "text",
|
|
858
|
+
text: `Server "${server}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).`,
|
|
859
|
+
},
|
|
860
|
+
],
|
|
616
861
|
details: { mode: "instructions", server, error: "not_found" },
|
|
617
862
|
};
|
|
618
863
|
}
|
|
619
|
-
if (isServerDisabled(definition))
|
|
864
|
+
if (isServerDisabled(definition)) {
|
|
620
865
|
return disabledResult("instructions", server);
|
|
866
|
+
}
|
|
621
867
|
const instructions = state.serverInstructions.get(server);
|
|
622
868
|
if (instructions) {
|
|
623
869
|
return {
|
|
@@ -628,12 +874,19 @@ export function executeInstructions(state, server) {
|
|
|
628
874
|
const connection = state.manager.getConnection(server);
|
|
629
875
|
if (connection?.status === "connected") {
|
|
630
876
|
return {
|
|
631
|
-
content: [
|
|
877
|
+
content: [
|
|
878
|
+
{ type: "text", text: `Server "${server}" does not provide instructions.` },
|
|
879
|
+
],
|
|
632
880
|
details: { mode: "instructions", server, error: "no_instructions" },
|
|
633
881
|
};
|
|
634
882
|
}
|
|
635
883
|
return {
|
|
636
|
-
content: [
|
|
884
|
+
content: [
|
|
885
|
+
{
|
|
886
|
+
type: "text",
|
|
887
|
+
text: `No instructions cached for "${server}". Use mcp({ action: "connect", server: "${server}" }) to connect and refresh.`,
|
|
888
|
+
},
|
|
889
|
+
],
|
|
637
890
|
details: { mode: "instructions", server, error: "not_connected" },
|
|
638
891
|
};
|
|
639
892
|
}
|
|
@@ -643,12 +896,18 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
|
643
896
|
const definition = state.config.mcpServers[serverName];
|
|
644
897
|
if (!definition) {
|
|
645
898
|
return {
|
|
646
|
-
content: [
|
|
899
|
+
content: [
|
|
900
|
+
{
|
|
901
|
+
type: "text",
|
|
902
|
+
text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).`,
|
|
903
|
+
},
|
|
904
|
+
],
|
|
647
905
|
details: { mode: "connect", error: "not_found", server: serverName },
|
|
648
906
|
};
|
|
649
907
|
}
|
|
650
|
-
if (isServerDisabled(definition))
|
|
908
|
+
if (isServerDisabled(definition)) {
|
|
651
909
|
return disabledResult("connect", serverName);
|
|
910
|
+
}
|
|
652
911
|
try {
|
|
653
912
|
if (state.ui) {
|
|
654
913
|
state.ui.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${serverName}...`));
|
|
@@ -662,7 +921,12 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
|
662
921
|
if (autoAuth.status === "failed") {
|
|
663
922
|
return {
|
|
664
923
|
content: [{ type: "text", text: autoAuth.message }],
|
|
665
|
-
details: {
|
|
924
|
+
details: {
|
|
925
|
+
mode: "connect",
|
|
926
|
+
error: "auth_required",
|
|
927
|
+
server: serverName,
|
|
928
|
+
message: autoAuth.message,
|
|
929
|
+
},
|
|
666
930
|
};
|
|
667
931
|
}
|
|
668
932
|
if (autoAuth.status === "success") {
|
|
@@ -700,12 +964,20 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
|
700
964
|
}
|
|
701
965
|
catch (error) {
|
|
702
966
|
const message = error instanceof Error ? error.message : String(error);
|
|
703
|
-
if (!isAbortError(error, ownedSignal))
|
|
967
|
+
if (!isAbortError(error, ownedSignal)) {
|
|
704
968
|
recordFailure(state, serverName, message);
|
|
969
|
+
}
|
|
705
970
|
updateStatusBar(state);
|
|
706
971
|
return {
|
|
707
|
-
content: [
|
|
708
|
-
|
|
972
|
+
content: [
|
|
973
|
+
{ type: "text", text: `Failed to connect to "${serverName}": ${message}` },
|
|
974
|
+
],
|
|
975
|
+
details: {
|
|
976
|
+
mode: "connect",
|
|
977
|
+
error: isAbortError(error, ownedSignal) ? "aborted" : "connect_failed",
|
|
978
|
+
server: serverName,
|
|
979
|
+
message,
|
|
980
|
+
},
|
|
709
981
|
};
|
|
710
982
|
}
|
|
711
983
|
}
|
|
@@ -716,9 +988,11 @@ export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
|
716
988
|
* Raw scripts skip content rendering but retain protocol results and artifacts.
|
|
717
989
|
*/
|
|
718
990
|
export async function runToolCall(state, serverName, target, args, options) {
|
|
719
|
-
const { DEFAULT_REQUEST_TIMEOUT_MSEC, UrlElicitationRequiredError } = await
|
|
720
|
-
|
|
721
|
-
|
|
991
|
+
const [{ DEFAULT_REQUEST_TIMEOUT_MSEC, UrlElicitationRequiredError }, { isInterruptedToolCall, isToolTransportFailure, trackToolCallOutcome, SessionRecoveryAuthRequiredError, withSessionRecovery, }, { maybeStartUiSession, summarizeUiSessionResult },] = await Promise.all([
|
|
992
|
+
import("@modelcontextprotocol/client"),
|
|
993
|
+
import("./session-recovery.js"),
|
|
994
|
+
import("./ui-session.js"),
|
|
995
|
+
]);
|
|
722
996
|
const { detailsBase, signal, recoverAuthConnection, authRequiredMessage, autoAuthAttempted } = options;
|
|
723
997
|
const configuredOptions = state.manager.getRequestOptions?.(serverName, options.ownedSignal);
|
|
724
998
|
const callerSignal = combineAbortSignals(configuredOptions?.signal, options.ownedSignal);
|
|
@@ -728,15 +1002,17 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
728
1002
|
const schemaSuffix = () => target.inputSchema ? `\n\nExpected parameters:\n${formatSchema(target.inputSchema)}` : "";
|
|
729
1003
|
let uiSession = null;
|
|
730
1004
|
const identity = {
|
|
731
|
-
server: serverName,
|
|
1005
|
+
server: serverName,
|
|
1006
|
+
tool: target.originalName,
|
|
732
1007
|
...(options.toolCallId !== undefined ? { toolCallId: options.toolCallId } : {}),
|
|
733
1008
|
...(options.innerCallId !== undefined ? { innerCallId: options.innerCallId } : {}),
|
|
734
1009
|
};
|
|
735
1010
|
let beforeDispatchPending = false;
|
|
736
1011
|
let captureFailure;
|
|
737
1012
|
const capture = async (event) => {
|
|
738
|
-
if (!state.onToolCall)
|
|
1013
|
+
if (!state.onToolCall) {
|
|
739
1014
|
return;
|
|
1015
|
+
}
|
|
740
1016
|
try {
|
|
741
1017
|
const run = () => state.onToolCall(event);
|
|
742
1018
|
await abortable(state.owner ? state.owner.runCallback("onToolCall", run) : run(), event.signal);
|
|
@@ -752,31 +1028,34 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
752
1028
|
state.manager.incrementInFlight(serverName);
|
|
753
1029
|
// Loading the connection-only modules can yield to cancellation or shutdown.
|
|
754
1030
|
throwIfAborted(callerSignal);
|
|
755
|
-
uiSession =
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1031
|
+
uiSession =
|
|
1032
|
+
!target.resourceUri && target.uiResourceUri
|
|
1033
|
+
? await abortable(maybeStartUiSession(state, {
|
|
1034
|
+
serverName,
|
|
1035
|
+
toolName: target.originalName,
|
|
1036
|
+
toolArgs: args ?? {},
|
|
1037
|
+
uiResourceUri: target.uiResourceUri,
|
|
1038
|
+
...(target.uiStreamMode !== undefined ? { streamMode: target.uiStreamMode } : {}),
|
|
1039
|
+
...(signal ? { signal } : {}),
|
|
1040
|
+
onNeedsAuth: recoverAuthConnection,
|
|
1041
|
+
}), callerSignal)
|
|
1042
|
+
: null;
|
|
766
1043
|
// UI preparation can replace the connection; use its current annotations.
|
|
767
1044
|
const liveTools = state.manager.getConnection(serverName)?.tools;
|
|
768
1045
|
const annotations = liveTools
|
|
769
|
-
? liveTools.find(tool => tool.name === target.originalName)?.annotations
|
|
1046
|
+
? liveTools.find((tool) => tool.name === target.originalName)?.annotations
|
|
770
1047
|
: target.annotations;
|
|
771
1048
|
const operation = {
|
|
772
|
-
...identity,
|
|
1049
|
+
...identity,
|
|
1050
|
+
args: args ?? {},
|
|
773
1051
|
...(annotations !== undefined ? { annotations } : {}),
|
|
774
1052
|
...(target.resourceUri ? { resourceUri: target.resourceUri } : {}),
|
|
775
1053
|
};
|
|
776
1054
|
if (options.beforeDispatch) {
|
|
777
1055
|
beforeDispatchPending = true;
|
|
778
1056
|
const run = () => options.beforeDispatch(callerSignal, {
|
|
779
|
-
...operation,
|
|
1057
|
+
...operation,
|
|
1058
|
+
annotationsTrusted: state.config.mcpServers[serverName]?.retryOnTransportFailure === true,
|
|
780
1059
|
});
|
|
781
1060
|
await abortable(state.owner ? state.owner.runCallback("beforeExecute", run) : run(), callerSignal);
|
|
782
1061
|
beforeDispatchPending = false;
|
|
@@ -785,7 +1064,12 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
785
1064
|
// Approval and UI preparation do not consume the service deadline.
|
|
786
1065
|
const ownedSignal = combineAbortSignals(callerSignal, AbortSignal.timeout(Math.ceil(configuredOptions?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC)));
|
|
787
1066
|
const requestOptions = { ...configuredOptions, signal: ownedSignal };
|
|
788
|
-
const recovery = {
|
|
1067
|
+
const recovery = {
|
|
1068
|
+
manager: state.manager,
|
|
1069
|
+
config: state.config,
|
|
1070
|
+
signal: ownedSignal,
|
|
1071
|
+
onNeedsAuth: recoverAuthConnection,
|
|
1072
|
+
};
|
|
789
1073
|
const call = { ...operation, signal: ownedSignal };
|
|
790
1074
|
await capture({ ...call, phase: "before" });
|
|
791
1075
|
throwIfAborted(ownedSignal);
|
|
@@ -802,36 +1086,71 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
802
1086
|
return result;
|
|
803
1087
|
};
|
|
804
1088
|
const result = target.resourceUri
|
|
805
|
-
? await dispatch(() => withSessionRecovery(recovery, serverName, conn => abortable(conn.client.readResource({ uri: target.resourceUri }, requestOptions), ownedSignal)))
|
|
806
|
-
: await dispatch(() => trackToolCallOutcome(() => withSessionRecovery({
|
|
807
|
-
|
|
1089
|
+
? await dispatch(() => withSessionRecovery(recovery, serverName, (conn) => abortable(conn.client.readResource({ uri: target.resourceUri }, requestOptions), ownedSignal)))
|
|
1090
|
+
: await dispatch(() => trackToolCallOutcome(() => withSessionRecovery({
|
|
1091
|
+
...recovery,
|
|
1092
|
+
retryOnTransportFailure: annotations?.readOnlyHint === true || annotations?.idempotentHint === true,
|
|
1093
|
+
}, serverName, (conn) => abortable(conn.client.callTool({
|
|
1094
|
+
name: target.originalName,
|
|
1095
|
+
arguments: args ?? {},
|
|
1096
|
+
_meta: uiSession?.requestMeta,
|
|
1097
|
+
}, requestOptions), ownedSignal)), ownedSignal));
|
|
1098
|
+
if (!target.resourceUri) {
|
|
808
1099
|
uiSession?.sendToolResult(result);
|
|
1100
|
+
}
|
|
809
1101
|
const record = result;
|
|
810
1102
|
const isError = record.isError === true;
|
|
811
1103
|
const uiSummary = target.uiResourceUri ? summarizeUiSessionResult(uiSession) : undefined;
|
|
812
1104
|
const details = {
|
|
813
1105
|
...detailsBase,
|
|
814
1106
|
...(isError ? { error: "tool_error" } : {}),
|
|
815
|
-
...(uiSummary
|
|
1107
|
+
...(uiSummary
|
|
1108
|
+
? { uiOpen: uiSummary.uiOpen, uiViewer: uiSummary.uiViewer, uiUrl: uiSummary.uiUrl }
|
|
1109
|
+
: {}),
|
|
816
1110
|
};
|
|
817
1111
|
const retained = await retainMcpResult(result, outputGuardOptions, options.raw);
|
|
818
1112
|
if (options.raw) {
|
|
819
1113
|
const message = isError
|
|
820
|
-
? (Array.isArray(record.content)
|
|
821
|
-
|
|
1114
|
+
? (Array.isArray(record.content)
|
|
1115
|
+
? record.content
|
|
1116
|
+
.filter((block) => block.type === "text")
|
|
1117
|
+
.map((block) => block.text)
|
|
1118
|
+
.join("\n")
|
|
1119
|
+
: "") || "Tool execution failed"
|
|
1120
|
+
: undefined;
|
|
1121
|
+
return {
|
|
1122
|
+
content: [],
|
|
1123
|
+
...(isError ? { isError: true } : {}),
|
|
1124
|
+
details: { ...details, ...retained, ...(message ? { message } : {}) },
|
|
1125
|
+
};
|
|
822
1126
|
}
|
|
823
1127
|
const content = renderMcpResultContent(record, retained.payloadFiles);
|
|
824
|
-
const guarded = await guardMcpOutput(content.length
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
1128
|
+
const guarded = await guardMcpOutput(content.length
|
|
1129
|
+
? content
|
|
1130
|
+
: [{ type: "text", text: target.resourceUri ? "(empty resource)" : "(empty result)" }], {
|
|
1131
|
+
...outputGuardOptions,
|
|
1132
|
+
retainedMcpResult: retained,
|
|
1133
|
+
...(isError
|
|
1134
|
+
? {
|
|
1135
|
+
prefix: "Error: ",
|
|
1136
|
+
suffix: schemaSuffix(),
|
|
1137
|
+
emptyTextFallback: "Tool execution failed",
|
|
1138
|
+
}
|
|
1139
|
+
: uiSummary
|
|
1140
|
+
? { suffix: `\n\n${uiSummary.message}` }
|
|
1141
|
+
: {}),
|
|
828
1142
|
});
|
|
829
|
-
return {
|
|
1143
|
+
return {
|
|
1144
|
+
content: guarded.content,
|
|
1145
|
+
...(isError ? { isError: true } : {}),
|
|
1146
|
+
details: { ...details, ...guardedMcpDetails(guarded) },
|
|
1147
|
+
};
|
|
830
1148
|
}
|
|
831
1149
|
catch (error) {
|
|
832
1150
|
// Direct/proxy host errors throw; scripts retain their failed-call envelope.
|
|
833
|
-
if (beforeDispatchPending && options.innerCallId === undefined)
|
|
1151
|
+
if (beforeDispatchPending && options.innerCallId === undefined) {
|
|
834
1152
|
throw error;
|
|
1153
|
+
}
|
|
835
1154
|
if (captureFailure) {
|
|
836
1155
|
const { signal: _signal, ...context } = captureFailure;
|
|
837
1156
|
const message = captureFailure.phase === "before"
|
|
@@ -839,11 +1158,27 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
839
1158
|
: "MCP outcome capture failed. Use the retained outcome to continue the original work; do not repeat a completed call or rerun the script.";
|
|
840
1159
|
uiSession?.sendToolCancelled(message);
|
|
841
1160
|
const retained = "result" in captureFailure && captureFailure.result !== undefined
|
|
842
|
-
? await retainMcpResult(captureFailure.result, {
|
|
1161
|
+
? await retainMcpResult(captureFailure.result, {
|
|
1162
|
+
...outputGuardOptions,
|
|
1163
|
+
enabled: true,
|
|
1164
|
+
detailsMaxBytes: 1,
|
|
1165
|
+
})
|
|
1166
|
+
: {};
|
|
843
1167
|
return {
|
|
844
|
-
content: [
|
|
1168
|
+
content: [
|
|
1169
|
+
{ type: "text", text: message },
|
|
1170
|
+
...(retained.resultRef
|
|
1171
|
+
? [{ type: "text", text: formatMcpResultReference(retained.resultRef) }]
|
|
1172
|
+
: []),
|
|
1173
|
+
],
|
|
845
1174
|
isError: true,
|
|
846
|
-
details: {
|
|
1175
|
+
details: {
|
|
1176
|
+
...detailsBase,
|
|
1177
|
+
...retained,
|
|
1178
|
+
error: isAbortError(error, callerSignal) ? "aborted" : "call_capture_failed",
|
|
1179
|
+
message,
|
|
1180
|
+
recovery: context,
|
|
1181
|
+
},
|
|
847
1182
|
};
|
|
848
1183
|
}
|
|
849
1184
|
if (isToolTransportFailure(error) || isInterruptedToolCall(error)) {
|
|
@@ -852,7 +1187,12 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
852
1187
|
return {
|
|
853
1188
|
content: [{ type: "text", text: message }],
|
|
854
1189
|
isError: true,
|
|
855
|
-
details: {
|
|
1190
|
+
details: {
|
|
1191
|
+
...detailsBase,
|
|
1192
|
+
error: "ambiguous_outcome",
|
|
1193
|
+
message,
|
|
1194
|
+
recovery: { ...identity, action: "readback" },
|
|
1195
|
+
},
|
|
856
1196
|
};
|
|
857
1197
|
}
|
|
858
1198
|
if (error instanceof SessionRecoveryAuthRequiredError) {
|
|
@@ -860,7 +1200,12 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
860
1200
|
uiSession?.sendToolCancelled(message);
|
|
861
1201
|
return {
|
|
862
1202
|
content: [{ type: "text", text: message }],
|
|
863
|
-
details: {
|
|
1203
|
+
details: {
|
|
1204
|
+
...detailsBase,
|
|
1205
|
+
error: "auth_required",
|
|
1206
|
+
message,
|
|
1207
|
+
autoAuthAttempted: autoAuthAttempted(),
|
|
1208
|
+
},
|
|
864
1209
|
};
|
|
865
1210
|
}
|
|
866
1211
|
if (error instanceof UrlElicitationRequiredError) {
|
|
@@ -876,7 +1221,11 @@ export async function runToolCall(state, serverName, target, args, options) {
|
|
|
876
1221
|
}
|
|
877
1222
|
const message = error instanceof Error ? error.message : String(error);
|
|
878
1223
|
uiSession?.sendToolCancelled(message);
|
|
879
|
-
const guarded = await guardMcpOutput([{ type: "text", text: message }], {
|
|
1224
|
+
const guarded = await guardMcpOutput([{ type: "text", text: message }], {
|
|
1225
|
+
...outputGuardOptions,
|
|
1226
|
+
prefix: "Failed to call tool: ",
|
|
1227
|
+
suffix: schemaSuffix(),
|
|
1228
|
+
});
|
|
880
1229
|
return {
|
|
881
1230
|
content: guarded.content,
|
|
882
1231
|
isError: true,
|
|
@@ -905,19 +1254,22 @@ export async function prepareMcpConnection(state, serverName, signal) {
|
|
|
905
1254
|
const recoverAuthConnection = async (_name, recoverySignal = ownedSignal, challenge) => {
|
|
906
1255
|
throwIfAborted(recoverySignal);
|
|
907
1256
|
const current = state.manager.getConnection(serverName);
|
|
908
|
-
if (current?.status === "connected" && current !== challenge?.connection)
|
|
1257
|
+
if (current?.status === "connected" && current !== challenge?.connection) {
|
|
909
1258
|
return current;
|
|
1259
|
+
}
|
|
910
1260
|
if (!autoAuthAttempted) {
|
|
911
1261
|
autoAuthAttempted = true;
|
|
912
1262
|
const auth = await attemptAutoAuth(state, serverName, recoverySignal);
|
|
913
1263
|
throwIfAborted(recoverySignal);
|
|
914
|
-
if (auth.status === "failed")
|
|
1264
|
+
if (auth.status === "failed") {
|
|
915
1265
|
throw new SessionRecoveryAuthRequiredError(serverName, auth.message);
|
|
1266
|
+
}
|
|
916
1267
|
if (auth.status === "success" && current) {
|
|
917
1268
|
clearFailure(state, serverName);
|
|
918
1269
|
const definition = state.config.mcpServers[serverName];
|
|
919
|
-
if (!definition)
|
|
1270
|
+
if (!definition) {
|
|
920
1271
|
return undefined;
|
|
1272
|
+
}
|
|
921
1273
|
state.ui?.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${serverName}...`));
|
|
922
1274
|
await state.manager.reconnect(serverName, definition, current, recoverySignal);
|
|
923
1275
|
if (await lazyConnect(state, serverName, recoverySignal)) {
|
|
@@ -933,10 +1285,12 @@ export async function prepareMcpConnection(state, serverName, signal) {
|
|
|
933
1285
|
details: { mode: "call", error, server: serverName, message },
|
|
934
1286
|
});
|
|
935
1287
|
const definition = state.config.mcpServers[serverName];
|
|
936
|
-
if (!definition)
|
|
1288
|
+
if (!definition) {
|
|
937
1289
|
return { error: failure("server_not_found", `Server "${serverName}" not found.`) };
|
|
938
|
-
|
|
1290
|
+
}
|
|
1291
|
+
if (isServerDisabled(definition)) {
|
|
939
1292
|
return { error: disabledResult("call", serverName) };
|
|
1293
|
+
}
|
|
940
1294
|
try {
|
|
941
1295
|
// Connected metadata is read below, so it does not require another connection attempt.
|
|
942
1296
|
let connection = state.manager.getConnection(serverName);
|
|
@@ -947,162 +1301,285 @@ export async function prepareMcpConnection(state, serverName, signal) {
|
|
|
947
1301
|
else {
|
|
948
1302
|
markKeepAliveAfterConnect(state, serverName);
|
|
949
1303
|
}
|
|
950
|
-
if (connection?.status === "needs-auth")
|
|
1304
|
+
if (connection?.status === "needs-auth") {
|
|
951
1305
|
connection = await recoverAuthConnection(serverName, ownedSignal);
|
|
952
|
-
|
|
1306
|
+
}
|
|
1307
|
+
if (connection?.status === "needs-auth") {
|
|
953
1308
|
throw new SessionRecoveryAuthRequiredError(serverName);
|
|
1309
|
+
}
|
|
954
1310
|
if (connection?.status !== "connected") {
|
|
955
1311
|
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
956
|
-
return {
|
|
1312
|
+
return {
|
|
1313
|
+
error: failure(failedAgo === null ? "server_unavailable" : "server_backoff", `Server "${serverName}" not available${failedAgo === null ? "" : ` (last failed ${failedAgo}s ago)`}`),
|
|
1314
|
+
};
|
|
957
1315
|
}
|
|
958
1316
|
// Use the live descriptor for visibility, filtering, approval, schema and UI hints.
|
|
959
1317
|
if (connection.tools) {
|
|
960
1318
|
const { metadata } = buildToolMetadata(connection.tools, connection.resources ?? [], definition, serverName, state.config.settings?.toolPrefix ?? "server");
|
|
961
1319
|
// Search indexes by metadata-array identity, so retain it for unchanged projections.
|
|
962
|
-
if (!isDeepStrictEqual(state.toolMetadata.get(serverName), metadata))
|
|
1320
|
+
if (!isDeepStrictEqual(state.toolMetadata.get(serverName), metadata)) {
|
|
963
1321
|
state.toolMetadata.set(serverName, metadata);
|
|
1322
|
+
}
|
|
964
1323
|
}
|
|
965
1324
|
return { connection, recoverAuthConnection, autoAuthAttempted: () => autoAuthAttempted };
|
|
966
1325
|
}
|
|
967
1326
|
catch (error) {
|
|
968
|
-
if (isAbortError(error, ownedSignal))
|
|
1327
|
+
if (isAbortError(error, ownedSignal)) {
|
|
969
1328
|
throw error;
|
|
1329
|
+
}
|
|
970
1330
|
const message = error instanceof SessionRecoveryAuthRequiredError
|
|
971
|
-
? error.authMessage ?? getAuthRequiredMessage(state, serverName)
|
|
972
|
-
: error instanceof Error
|
|
1331
|
+
? (error.authMessage ?? getAuthRequiredMessage(state, serverName))
|
|
1332
|
+
: error instanceof Error
|
|
1333
|
+
? error.message
|
|
1334
|
+
: String(error);
|
|
973
1335
|
if (!(error instanceof SessionRecoveryAuthRequiredError)) {
|
|
974
1336
|
recordFailure(state, serverName, message);
|
|
975
1337
|
updateStatusBar(state);
|
|
976
1338
|
}
|
|
977
|
-
return {
|
|
1339
|
+
return {
|
|
1340
|
+
error: failure(error instanceof SessionRecoveryAuthRequiredError ? "auth_required" : "connect_failed", message),
|
|
1341
|
+
};
|
|
978
1342
|
}
|
|
979
1343
|
}
|
|
980
1344
|
export async function executeCall(state, toolName, args, serverOverride, getPiTools, signal, identity = {}, beforeDispatch, options = {}) {
|
|
981
1345
|
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
982
1346
|
throwIfAborted(ownedSignal);
|
|
983
|
-
if (typeof toolName !== "string" ||
|
|
1347
|
+
if (typeof toolName !== "string" ||
|
|
1348
|
+
!toolName.trim() ||
|
|
1349
|
+
(serverOverride !== undefined && (typeof serverOverride !== "string" || !serverOverride))) {
|
|
984
1350
|
return {
|
|
985
|
-
content: [
|
|
1351
|
+
content: [
|
|
1352
|
+
{ type: "text", text: "Specify a non-empty tool name and, when supplied, server." },
|
|
1353
|
+
],
|
|
986
1354
|
details: { mode: "call", error: "invalid_arguments" },
|
|
987
1355
|
};
|
|
988
|
-
|
|
1356
|
+
}
|
|
1357
|
+
if (args !== undefined && (args === null || typeof args !== "object" || Array.isArray(args))) {
|
|
989
1358
|
return {
|
|
990
1359
|
content: [{ type: "text", text: "Tool arguments must be an object." }],
|
|
991
1360
|
details: { mode: "call", error: "invalid_arguments", requestedTool: toolName },
|
|
992
1361
|
};
|
|
1362
|
+
}
|
|
993
1363
|
let serverName = serverOverride;
|
|
994
1364
|
const matches = (metadata = []) => {
|
|
995
|
-
if (options.resourceUri !== undefined)
|
|
996
|
-
return metadata.filter(tool => tool.resourceUri === options.resourceUri);
|
|
997
|
-
|
|
998
|
-
|
|
1365
|
+
if (options.resourceUri !== undefined) {
|
|
1366
|
+
return metadata.filter((tool) => tool.resourceUri === options.resourceUri);
|
|
1367
|
+
}
|
|
1368
|
+
if (options.exactOriginalName) {
|
|
1369
|
+
return metadata.filter((tool) => tool.originalName === toolName && !tool.resourceUri);
|
|
1370
|
+
}
|
|
999
1371
|
if (serverOverride) {
|
|
1000
|
-
const originals = metadata.filter(tool => tool.originalName === toolName);
|
|
1001
|
-
if (originals.length)
|
|
1372
|
+
const originals = metadata.filter((tool) => tool.originalName === toolName);
|
|
1373
|
+
if (originals.length) {
|
|
1002
1374
|
return originals;
|
|
1375
|
+
}
|
|
1003
1376
|
}
|
|
1004
|
-
const exact = metadata.filter(tool => tool.name === toolName);
|
|
1005
|
-
if (exact.length)
|
|
1377
|
+
const exact = metadata.filter((tool) => tool.name === toolName);
|
|
1378
|
+
if (exact.length) {
|
|
1006
1379
|
return exact;
|
|
1007
|
-
|
|
1380
|
+
}
|
|
1381
|
+
return metadata.filter((tool) => tool.name.replace(/-/g, "_") === toolName.replace(/-/g, "_"));
|
|
1008
1382
|
};
|
|
1009
1383
|
const ambiguous = () => ({
|
|
1010
|
-
content: [
|
|
1011
|
-
|
|
1384
|
+
content: [
|
|
1385
|
+
{
|
|
1386
|
+
type: "text",
|
|
1387
|
+
text: `Tool "${toolName}" is ambiguous. Specify its exact server and tool name; no call was dispatched.`,
|
|
1388
|
+
},
|
|
1389
|
+
],
|
|
1390
|
+
details: {
|
|
1391
|
+
mode: "call",
|
|
1392
|
+
error: "ambiguous_tool",
|
|
1393
|
+
requestedTool: toolName,
|
|
1394
|
+
...(serverName ? { server: serverName } : {}),
|
|
1395
|
+
},
|
|
1012
1396
|
});
|
|
1013
1397
|
if (!serverName) {
|
|
1014
|
-
const candidates = [...state.toolMetadata].flatMap(([server, metadata]) => isServerDisabled(state.config.mcpServers[server])
|
|
1015
|
-
|
|
1398
|
+
const candidates = [...state.toolMetadata].flatMap(([server, metadata]) => isServerDisabled(state.config.mcpServers[server])
|
|
1399
|
+
? []
|
|
1400
|
+
: matches(metadata).map((tool) => ({ server, tool })));
|
|
1401
|
+
if (candidates.length > 1) {
|
|
1016
1402
|
return ambiguous();
|
|
1403
|
+
}
|
|
1017
1404
|
serverName = candidates[0]?.server;
|
|
1018
1405
|
if (!serverName) {
|
|
1019
1406
|
const disabled = [...state.toolMetadata].find(([server, metadata]) => isServerDisabled(state.config.mcpServers[server]) && matches(metadata).length > 0);
|
|
1020
|
-
if (disabled)
|
|
1407
|
+
if (disabled) {
|
|
1021
1408
|
return disabledResult("call", disabled[0]);
|
|
1409
|
+
}
|
|
1022
1410
|
const prefixes = Object.entries(state.config.mcpServers)
|
|
1023
1411
|
.filter(([, definition]) => !isServerDisabled(definition))
|
|
1024
|
-
.map(([server, definition]) => ({
|
|
1412
|
+
.map(([server, definition]) => ({
|
|
1413
|
+
server,
|
|
1414
|
+
prefix: getServerPrefix(server, resolveToolPrefix(definition, state.config.settings?.toolPrefix ?? "server")),
|
|
1415
|
+
}))
|
|
1025
1416
|
.filter(({ prefix }) => prefix && toolName.startsWith(`${prefix}_`))
|
|
1026
1417
|
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
1027
|
-
if (prefixes.length > 1 && prefixes[0]?.prefix === prefixes[1]?.prefix)
|
|
1418
|
+
if (prefixes.length > 1 && prefixes[0]?.prefix === prefixes[1]?.prefix) {
|
|
1028
1419
|
return ambiguous();
|
|
1420
|
+
}
|
|
1029
1421
|
serverName = prefixes[0]?.server;
|
|
1030
1422
|
}
|
|
1031
1423
|
}
|
|
1032
1424
|
const missing = () => {
|
|
1033
|
-
const nativeTool = !serverOverride &&
|
|
1034
|
-
|
|
1425
|
+
const nativeTool = !serverOverride &&
|
|
1426
|
+
getPiTools?.().find((tool) => tool.name === toolName && tool.name !== "mcp");
|
|
1427
|
+
if (nativeTool) {
|
|
1035
1428
|
return {
|
|
1036
|
-
content: [
|
|
1429
|
+
content: [
|
|
1430
|
+
{
|
|
1431
|
+
type: "text",
|
|
1432
|
+
text: `"${toolName}" is a native Pi tool. Call ${toolName} directly instead of using mcp({ action: "call", tool: "${toolName}" }).`,
|
|
1433
|
+
},
|
|
1434
|
+
],
|
|
1037
1435
|
details: { mode: "call", error: "native_tool", requestedTool: toolName },
|
|
1038
1436
|
};
|
|
1437
|
+
}
|
|
1039
1438
|
const normalizedName = toolName.replace(/-/g, "_");
|
|
1040
|
-
const localOriginal = serverOverride &&
|
|
1439
|
+
const localOriginal = serverOverride &&
|
|
1440
|
+
state.toolMetadata
|
|
1441
|
+
.get(serverOverride)
|
|
1442
|
+
?.some((tool) => tool.originalName.replace(/-/g, "_") === normalizedName);
|
|
1041
1443
|
const owners = serverOverride && !localOriginal
|
|
1042
|
-
? [...state.toolMetadata].filter(([server, metadata]) => server !== serverOverride &&
|
|
1444
|
+
? [...state.toolMetadata].filter(([server, metadata]) => server !== serverOverride &&
|
|
1445
|
+
!isServerDisabled(state.config.mcpServers[server]) &&
|
|
1446
|
+
matches(metadata).length === 1)
|
|
1043
1447
|
: [];
|
|
1044
1448
|
const suggestedOwner = owners.length === 1 ? owners[0] : undefined;
|
|
1045
|
-
if (suggestedOwner)
|
|
1449
|
+
if (suggestedOwner) {
|
|
1046
1450
|
return {
|
|
1047
|
-
content: [
|
|
1048
|
-
|
|
1451
|
+
content: [
|
|
1452
|
+
{
|
|
1453
|
+
type: "text",
|
|
1454
|
+
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.`,
|
|
1455
|
+
},
|
|
1456
|
+
],
|
|
1457
|
+
details: {
|
|
1458
|
+
mode: "call",
|
|
1459
|
+
error: "tool_not_found",
|
|
1460
|
+
requestedTool: toolName,
|
|
1461
|
+
hintServer: serverName,
|
|
1462
|
+
suggestedServer: suggestedOwner[0],
|
|
1463
|
+
suggestions: matches(suggestedOwner[1]).map((tool) => tool.name),
|
|
1464
|
+
},
|
|
1049
1465
|
};
|
|
1466
|
+
}
|
|
1050
1467
|
const suggestions = rankSuggestions(state, toolName, 5, serverName);
|
|
1051
1468
|
let hint = suggestions.length
|
|
1052
1469
|
? ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ action: "describe", tool: ${JSON.stringify(suggestions[0])} }).`
|
|
1053
|
-
: serverName
|
|
1470
|
+
: serverName
|
|
1471
|
+
? ` Search with mcp({ action: "search", query: ${JSON.stringify(toolName)}, server: ${JSON.stringify(serverName)} }).`
|
|
1472
|
+
: ' Use mcp({ action: "search", query: "..." }) to search.';
|
|
1054
1473
|
hint += ' If mcp is inactive, load it with mcp_search({ enable: ["gateway"] }).';
|
|
1055
|
-
if (serverName &&
|
|
1474
|
+
if (serverName &&
|
|
1475
|
+
state.toolMetadata.has(serverName) &&
|
|
1476
|
+
state.manager.getConnection(serverName)?.status !== "connected") {
|
|
1056
1477
|
hint += ` Refresh a stale catalog with mcp({ action: "connect", server: ${JSON.stringify(serverName)} }).`;
|
|
1478
|
+
}
|
|
1057
1479
|
return {
|
|
1058
1480
|
content: [{ type: "text", text: `Tool "${toolName}" not found.${hint}` }],
|
|
1059
|
-
details: {
|
|
1481
|
+
details: {
|
|
1482
|
+
mode: "call",
|
|
1483
|
+
error: "tool_not_found",
|
|
1484
|
+
requestedTool: toolName,
|
|
1485
|
+
hintServer: serverName,
|
|
1486
|
+
suggestions,
|
|
1487
|
+
},
|
|
1060
1488
|
};
|
|
1061
1489
|
};
|
|
1062
|
-
if (!serverName)
|
|
1490
|
+
if (!serverName) {
|
|
1063
1491
|
return missing();
|
|
1492
|
+
}
|
|
1064
1493
|
const definition = state.config.mcpServers[serverName];
|
|
1065
|
-
if (!definition)
|
|
1066
|
-
return {
|
|
1067
|
-
|
|
1494
|
+
if (!definition) {
|
|
1495
|
+
return {
|
|
1496
|
+
content: [
|
|
1497
|
+
{
|
|
1498
|
+
type: "text",
|
|
1499
|
+
text: `Server "${serverName}" not found. Use mcp({ action: "status" }) to see available servers (load it first with mcp_search({ enable: ["gateway"] }) if inactive).`,
|
|
1500
|
+
},
|
|
1501
|
+
],
|
|
1502
|
+
details: {
|
|
1503
|
+
mode: "call",
|
|
1504
|
+
error: "server_not_found",
|
|
1505
|
+
server: serverName,
|
|
1506
|
+
requestedTool: toolName,
|
|
1507
|
+
},
|
|
1508
|
+
};
|
|
1509
|
+
}
|
|
1510
|
+
if (isServerDisabled(definition)) {
|
|
1068
1511
|
return disabledResult("call", serverName);
|
|
1512
|
+
}
|
|
1069
1513
|
const cached = state.toolMetadata.get(serverName);
|
|
1070
|
-
if (matches(cached).length > 1)
|
|
1514
|
+
if (matches(cached).length > 1) {
|
|
1071
1515
|
return ambiguous();
|
|
1516
|
+
}
|
|
1072
1517
|
// Misspellings in a known catalog do not cause connection churn or guessed execution.
|
|
1073
|
-
if (cached &&
|
|
1518
|
+
if (cached &&
|
|
1519
|
+
!matches(cached).length &&
|
|
1520
|
+
!options.exactOriginalName &&
|
|
1521
|
+
options.resourceUri === undefined) {
|
|
1074
1522
|
return missing();
|
|
1523
|
+
}
|
|
1075
1524
|
const prepared = await prepareMcpConnection(state, serverName, ownedSignal);
|
|
1076
1525
|
if (prepared.error) {
|
|
1077
1526
|
const cachedTarget = matches(cached)[0];
|
|
1078
|
-
return {
|
|
1079
|
-
|
|
1080
|
-
|
|
1527
|
+
return {
|
|
1528
|
+
...prepared.error,
|
|
1529
|
+
details: {
|
|
1530
|
+
...prepared.error.details,
|
|
1531
|
+
...(cachedTarget
|
|
1532
|
+
? cachedTarget.resourceUri
|
|
1533
|
+
? { resourceUri: cachedTarget.resourceUri }
|
|
1534
|
+
: { tool: cachedTarget.originalName }
|
|
1535
|
+
: { requestedTool: toolName }),
|
|
1536
|
+
},
|
|
1537
|
+
};
|
|
1081
1538
|
}
|
|
1082
1539
|
const targets = matches(state.toolMetadata.get(serverName));
|
|
1083
|
-
if (targets.length > 1)
|
|
1540
|
+
if (targets.length > 1) {
|
|
1084
1541
|
return ambiguous();
|
|
1542
|
+
}
|
|
1085
1543
|
const toolMeta = targets[0];
|
|
1086
|
-
if (!toolMeta)
|
|
1544
|
+
if (!toolMeta) {
|
|
1087
1545
|
return missing();
|
|
1088
|
-
|
|
1546
|
+
}
|
|
1547
|
+
if (isServerDisabled(state.config.mcpServers[serverName])) {
|
|
1089
1548
|
return disabledResult("call", serverName);
|
|
1549
|
+
}
|
|
1090
1550
|
const approval = await ensureToolCallApproved(state, serverName, toolMeta, args, ownedSignal);
|
|
1091
1551
|
if (!approval.ok) {
|
|
1092
1552
|
const denied = approval.reason === "denied";
|
|
1093
1553
|
return {
|
|
1094
|
-
content: [
|
|
1554
|
+
content: [
|
|
1555
|
+
{
|
|
1556
|
+
type: "text",
|
|
1557
|
+
text: denied
|
|
1095
1558
|
? `The user declined approval to run MCP tool "${toolMeta.originalName}" on server "${serverName}".`
|
|
1096
|
-
: `MCP tool "${toolMeta.originalName}" on server "${serverName}" is approval-gated and requires an interactive session
|
|
1097
|
-
|
|
1559
|
+
: `MCP tool "${toolMeta.originalName}" on server "${serverName}" is approval-gated and requires an interactive session.`,
|
|
1560
|
+
},
|
|
1561
|
+
],
|
|
1562
|
+
details: {
|
|
1563
|
+
mode: "call",
|
|
1564
|
+
error: denied ? "approval_denied" : "approval_required",
|
|
1565
|
+
server: serverName,
|
|
1566
|
+
tool: toolMeta.originalName,
|
|
1567
|
+
},
|
|
1098
1568
|
};
|
|
1099
1569
|
}
|
|
1100
1570
|
return runToolCall(state, serverName, toolMeta, args, {
|
|
1101
1571
|
...identity,
|
|
1102
1572
|
...(beforeDispatch ? { beforeDispatch } : {}),
|
|
1103
1573
|
...(options.raw !== undefined ? { raw: options.raw } : {}),
|
|
1104
|
-
detailsBase: {
|
|
1105
|
-
|
|
1574
|
+
detailsBase: {
|
|
1575
|
+
mode: "call",
|
|
1576
|
+
server: serverName,
|
|
1577
|
+
...(toolMeta.resourceUri
|
|
1578
|
+
? { resourceUri: toolMeta.resourceUri }
|
|
1579
|
+
: { tool: toolMeta.originalName }),
|
|
1580
|
+
},
|
|
1581
|
+
ownedSignal,
|
|
1582
|
+
signal,
|
|
1106
1583
|
recoverAuthConnection: prepared.recoverAuthConnection,
|
|
1107
1584
|
authRequiredMessage: () => getAuthRequiredMessage(state, serverName),
|
|
1108
1585
|
autoAuthAttempted: prepared.autoAuthAttempted,
|