@fitchmultz/pi-mcp-adapter 5.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 +750 -0
- package/LICENSE +21 -0
- package/OAUTH.md +462 -0
- package/README.md +820 -0
- package/abort.ts +34 -0
- package/agent-dir.ts +24 -0
- package/app-bridge.bundle.js +67 -0
- package/banner.png +0 -0
- package/checkpoint.ts +55 -0
- package/cli.js +222 -0
- package/commands.ts +619 -0
- package/config.ts +1204 -0
- package/consent-manager.ts +25 -0
- package/direct-tools.ts +408 -0
- package/dist/abort.d.ts +2 -0
- package/dist/abort.js +35 -0
- package/dist/agent-dir.d.ts +3 -0
- package/dist/agent-dir.js +21 -0
- package/dist/app-bridge.bundle.js +67 -0
- package/dist/checkpoint.d.ts +18 -0
- package/dist/checkpoint.js +53 -0
- package/dist/commands.d.ts +20 -0
- package/dist/commands.js +521 -0
- package/dist/config.d.ts +109 -0
- package/dist/config.js +1013 -0
- package/dist/consent-manager.d.ts +8 -0
- package/dist/consent-manager.js +21 -0
- package/dist/direct-tools.d.ts +10 -0
- package/dist/direct-tools.js +350 -0
- package/dist/docs/client-metadata.json +14 -0
- package/dist/elicitation-handler.d.ts +17 -0
- package/dist/elicitation-handler.js +308 -0
- package/dist/error-signal.d.ts +15 -0
- package/dist/error-signal.js +21 -0
- package/dist/glimpse-ui.d.ts +9 -0
- package/dist/glimpse-ui.js +75 -0
- package/dist/host-html-template.d.ts +15 -0
- package/dist/host-html-template.js +429 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +840 -0
- package/dist/init.d.ts +25 -0
- package/dist/init.js +562 -0
- package/dist/legacy-migration.d.ts +18 -0
- package/dist/legacy-migration.js +45 -0
- package/dist/lifecycle.d.ts +40 -0
- package/dist/lifecycle.js +167 -0
- package/dist/logger.d.ts +38 -0
- package/dist/logger.js +102 -0
- package/dist/mcp-auth-flow.d.ts +113 -0
- package/dist/mcp-auth-flow.js +834 -0
- package/dist/mcp-auth.d.ts +127 -0
- package/dist/mcp-auth.js +658 -0
- package/dist/mcp-callback-server.d.ts +56 -0
- package/dist/mcp-callback-server.js +371 -0
- package/dist/mcp-code.d.ts +51 -0
- package/dist/mcp-code.js +366 -0
- package/dist/mcp-keyring-helper.cjs +89 -0
- package/dist/mcp-oauth-provider.d.ts +154 -0
- package/dist/mcp-oauth-provider.js +672 -0
- package/dist/mcp-output-guard.d.ts +59 -0
- package/dist/mcp-output-guard.js +332 -0
- package/dist/mcp-panel.d.ts +66 -0
- package/dist/mcp-panel.js +889 -0
- package/dist/mcp-script-worker.mjs +128 -0
- package/dist/mcp-setup-panel.d.ts +84 -0
- package/dist/mcp-setup-panel.js +561 -0
- package/dist/mcp-status.d.ts +11 -0
- package/dist/mcp-status.js +96 -0
- package/dist/mcp-trace.d.ts +95 -0
- package/dist/mcp-trace.js +233 -0
- package/dist/metadata-cache.d.ts +18 -0
- package/dist/metadata-cache.js +277 -0
- package/dist/npx-resolver.d.ts +6 -0
- package/dist/npx-resolver.js +448 -0
- package/dist/onboarding-state.d.ts +11 -0
- package/dist/onboarding-state.js +47 -0
- package/dist/panel-keys.d.ts +16 -0
- package/dist/panel-keys.js +15 -0
- package/dist/prompts.d.ts +56 -0
- package/dist/prompts.js +284 -0
- package/dist/proxy-modes.d.ts +38 -0
- package/dist/proxy-modes.js +1235 -0
- package/dist/resource-tools.d.ts +1 -0
- package/dist/resource-tools.js +14 -0
- package/dist/runtime-owner.d.ts +18 -0
- package/dist/runtime-owner.js +117 -0
- package/dist/sampling-handler.d.ts +15 -0
- package/dist/sampling-handler.js +211 -0
- package/dist/search-ranking.d.ts +20 -0
- package/dist/search-ranking.js +180 -0
- package/dist/server-manager.d.ts +114 -0
- package/dist/server-manager.js +1175 -0
- package/dist/session-recovery.d.ts +31 -0
- package/dist/session-recovery.js +310 -0
- package/dist/state.d.ts +63 -0
- package/dist/state.js +1 -0
- package/dist/tool-approval.d.ts +10 -0
- package/dist/tool-approval.js +42 -0
- package/dist/tool-metadata.d.ts +9 -0
- package/dist/tool-metadata.js +193 -0
- package/dist/tool-registrar.d.ts +10 -0
- package/dist/tool-registrar.js +64 -0
- package/dist/tool-result-renderer.d.ts +45 -0
- package/dist/tool-result-renderer.js +224 -0
- package/dist/ts-shape.d.ts +2 -0
- package/dist/ts-shape.js +147 -0
- package/dist/types.d.ts +516 -0
- package/dist/types.js +168 -0
- package/dist/ui-resource-handler.d.ts +17 -0
- package/dist/ui-resource-handler.js +209 -0
- package/dist/ui-server.d.ts +51 -0
- package/dist/ui-server.js +669 -0
- package/dist/ui-session.d.ts +39 -0
- package/dist/ui-session.js +463 -0
- package/dist/ui-stream-types.d.ts +104 -0
- package/dist/ui-stream-types.js +58 -0
- package/dist/ui-tool-visibility.d.ts +4 -0
- package/dist/ui-tool-visibility.js +26 -0
- package/dist/unix-socket-transport.d.ts +15 -0
- package/dist/unix-socket-transport.js +84 -0
- package/dist/utils.d.ts +30 -0
- package/dist/utils.js +276 -0
- package/docs/client-metadata.json +14 -0
- package/elicitation-handler.ts +349 -0
- package/error-signal.ts +21 -0
- package/glimpse-ui.ts +80 -0
- package/host-html-template.ts +456 -0
- package/index.ts +900 -0
- package/init.ts +627 -0
- package/legacy-migration.ts +47 -0
- package/lifecycle.ts +165 -0
- package/logger.ts +127 -0
- package/mcp-auth-flow.ts +975 -0
- package/mcp-auth.ts +793 -0
- package/mcp-callback-server.ts +450 -0
- package/mcp-code.ts +386 -0
- package/mcp-keyring-helper.cjs +89 -0
- package/mcp-oauth-provider.ts +774 -0
- package/mcp-output-guard.ts +412 -0
- package/mcp-panel.ts +978 -0
- package/mcp-script-worker.mjs +128 -0
- package/mcp-setup-panel.ts +666 -0
- package/mcp-status.ts +109 -0
- package/mcp-trace.ts +295 -0
- package/metadata-cache.ts +333 -0
- package/npx-resolver.ts +488 -0
- package/onboarding-state.ts +58 -0
- package/package.json +182 -0
- package/panel-keys.ts +37 -0
- package/prompts.ts +337 -0
- package/proxy-modes.ts +1361 -0
- package/resource-tools.ts +17 -0
- package/runtime-owner.ts +123 -0
- package/sampling-handler.ts +273 -0
- package/scripts/build.mjs +131 -0
- package/scripts/prepare.mjs +102 -0
- package/search-ranking.ts +196 -0
- package/server-manager.ts +1320 -0
- package/session-recovery.ts +316 -0
- package/skills/mcp-scripting/SKILL.md +38 -0
- package/state.ts +64 -0
- package/tool-approval.ts +78 -0
- package/tool-metadata.ts +235 -0
- package/tool-registrar.ts +69 -0
- package/tool-result-renderer.ts +286 -0
- package/ts-shape.ts +145 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.json +16 -0
- package/types.ts +810 -0
- package/ui-resource-handler.ts +249 -0
- package/ui-server.ts +808 -0
- package/ui-session.ts +568 -0
- package/ui-stream-types.ts +89 -0
- package/ui-tool-visibility.ts +25 -0
- package/unix-socket-transport.ts +85 -0
- package/utils.ts +309 -0
|
@@ -0,0 +1,1235 @@
|
|
|
1
|
+
import { DEFAULT_REQUEST_TIMEOUT_MSEC, UrlElicitationRequiredError } from "@modelcontextprotocol/client";
|
|
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
|
+
import { abortable, throwIfAborted } from "./abort.js";
|
|
5
|
+
import { combineAbortSignals, isAbortError } from "./runtime-owner.js";
|
|
6
|
+
import { buildToolMetadata, findToolByName, formatSchema } from "./tool-metadata.js";
|
|
7
|
+
import { renderTsShape } from "./ts-shape.js";
|
|
8
|
+
import { reconstructPromptMetadata } from "./metadata-cache.js";
|
|
9
|
+
import { resolveMcpResultContent, transformMcpContent } from "./tool-registrar.js";
|
|
10
|
+
import { guardMcpOutput, guardedMcpDetails, resolveMcpOutputGuardOptions } from "./mcp-output-guard.js";
|
|
11
|
+
import { maybeStartUiSession, summarizeUiSessionResult } from "./ui-session.js";
|
|
12
|
+
import { formatAuthRequiredMessage, formatMcpStatus, resolveServerUrl, truncateAtWord } from "./utils.js";
|
|
13
|
+
import { authenticate, completeAuthFromInput, startAuth, supportsOAuth } from "./mcp-auth-flow.js";
|
|
14
|
+
import { isToolTransportFailure, SessionRecoveryAuthRequiredError, withSessionRecovery } from "./session-recovery.js";
|
|
15
|
+
import { paginate, rankSuggestions, rankToolMatches } from "./search-ranking.js";
|
|
16
|
+
import { ensureToolCallApproved, isToolCallApprovalRequired } from "./tool-approval.js";
|
|
17
|
+
const INSTRUCTIONS_PREVIEW_LENGTH = 300;
|
|
18
|
+
function disabledResult(mode, serverName) {
|
|
19
|
+
const message = `Server "${serverName}" is disabled. Run /mcp enable ${serverName} and /reload to enable it.`;
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: message }],
|
|
22
|
+
details: { mode, error: "server_disabled", server: serverName, message },
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function getAuthRequiredMessage(state, serverName, defaultMessage = `Server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`) {
|
|
26
|
+
const oauth = state.config.mcpServers[serverName]?.oauth;
|
|
27
|
+
if (oauth && oauth.crossAppAccess)
|
|
28
|
+
defaultMessage = `Server "${serverName}" requires enterprise authorization. Check oauth.crossAppAccess IdP configuration and ID-token source, then retry.`;
|
|
29
|
+
return formatAuthRequiredMessage(state.config, serverName, defaultMessage);
|
|
30
|
+
}
|
|
31
|
+
function getAuthFailedMessage(state, serverName, message) {
|
|
32
|
+
const customGuidance = state.config.settings?.authRequiredMessage;
|
|
33
|
+
const oauth = state.config.mcpServers[serverName]?.oauth;
|
|
34
|
+
if (customGuidance || (oauth && oauth.crossAppAccess)) {
|
|
35
|
+
return `OAuth authentication failed for "${serverName}": ${message}. ${getAuthRequiredMessage(state, serverName)}`;
|
|
36
|
+
}
|
|
37
|
+
return `OAuth authentication failed for "${serverName}": ${message}. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`;
|
|
38
|
+
}
|
|
39
|
+
function getRedirectPort(authorizationUrl) {
|
|
40
|
+
try {
|
|
41
|
+
const redirectUri = new URL(authorizationUrl).searchParams.get("redirect_uri");
|
|
42
|
+
if (!redirectUri)
|
|
43
|
+
return undefined;
|
|
44
|
+
const port = Number.parseInt(new URL(redirectUri).port, 10);
|
|
45
|
+
return Number.isInteger(port) ? port : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function formatManualAuthInstructions(serverName, authorizationUrl) {
|
|
52
|
+
const port = getRedirectPort(authorizationUrl);
|
|
53
|
+
const portNote = port
|
|
54
|
+
? `\nThe redirect URL will use local port ${port}. On a remote server it is expected for that localhost page to fail locally; copy the address bar URL anyway.`
|
|
55
|
+
: "";
|
|
56
|
+
return [
|
|
57
|
+
`MCP OAuth required for "${serverName}".`,
|
|
58
|
+
"",
|
|
59
|
+
"Open this URL in your local browser:",
|
|
60
|
+
"",
|
|
61
|
+
authorizationUrl,
|
|
62
|
+
"",
|
|
63
|
+
"After approval reaches this Pi session's callback, complete without copying the callback URL or code:",
|
|
64
|
+
`mcp({ action: "auth-complete", server: "${serverName}" })`,
|
|
65
|
+
"",
|
|
66
|
+
"If the browser cannot reach the callback, copy the full redirected localhost URL from your address bar and send it back with:",
|
|
67
|
+
`mcp({ action: "auth-complete", server: "${serverName}", args: { redirectUrl: "PASTE_REDIRECT_URL_HERE" } })`,
|
|
68
|
+
"",
|
|
69
|
+
'You can also pass just the `code` query parameter as `args: { code: "PASTE_CODE_HERE" }`. JSON-string args remain supported.',
|
|
70
|
+
portNote.trimEnd(),
|
|
71
|
+
].filter(Boolean).join("\n");
|
|
72
|
+
}
|
|
73
|
+
async function attemptAutoAuth(state, serverName, signal) {
|
|
74
|
+
if (state.config.settings?.autoAuth !== true) {
|
|
75
|
+
return { status: "skipped" };
|
|
76
|
+
}
|
|
77
|
+
const definition = state.config.mcpServers[serverName];
|
|
78
|
+
if (!definition || isServerDisabled(definition) || !supportsOAuth(definition)) {
|
|
79
|
+
return { status: "skipped" };
|
|
80
|
+
}
|
|
81
|
+
let serverUrl;
|
|
82
|
+
try {
|
|
83
|
+
serverUrl = resolveServerUrl(definition);
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
87
|
+
return { status: "failed", message: getAuthFailedMessage(state, serverName, message) };
|
|
88
|
+
}
|
|
89
|
+
if (!serverUrl) {
|
|
90
|
+
return { status: "skipped" };
|
|
91
|
+
}
|
|
92
|
+
if (!state.ui && !isNonInteractiveOAuth(definition.oauth)) {
|
|
93
|
+
return {
|
|
94
|
+
status: "failed",
|
|
95
|
+
message: getAuthRequiredMessage(state, serverName, `Server "${serverName}" requires OAuth authentication. Run mcp({ action: "auth-start", server: "${serverName}" }) to get a browser URL, or /mcp-auth ${serverName} in an interactive local session.`),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
if (state.authStorageOptions) {
|
|
100
|
+
await authenticate(serverName, serverUrl, definition, signal
|
|
101
|
+
? { authStorageOptions: state.authStorageOptions, signal, runtime: state.oauthRuntime }
|
|
102
|
+
: { authStorageOptions: state.authStorageOptions, runtime: state.oauthRuntime });
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
if (signal) {
|
|
106
|
+
await authenticate(serverName, serverUrl, definition, { signal, runtime: state.oauthRuntime });
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
await authenticate(serverName, serverUrl, definition, { runtime: state.oauthRuntime });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { status: "success" };
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
if (isAbortError(error, signal))
|
|
116
|
+
throw error;
|
|
117
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
118
|
+
return {
|
|
119
|
+
status: "failed",
|
|
120
|
+
message: getAuthFailedMessage(state, serverName, message),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export function executeUiMessages(state) {
|
|
125
|
+
const sessions = state.completedUiSessions;
|
|
126
|
+
if (sessions.length === 0) {
|
|
127
|
+
return {
|
|
128
|
+
content: [{ type: "text", text: "No UI session messages available." }],
|
|
129
|
+
details: { sessions: 0 },
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
const output = [];
|
|
133
|
+
output.push(`UI Session Messages (${sessions.length} session${sessions.length > 1 ? "s" : ""}):\n`);
|
|
134
|
+
const allPrompts = [];
|
|
135
|
+
const allIntents = sessions.flatMap((session) => session.messages.intents);
|
|
136
|
+
const allContexts = sessions.flatMap((session) => session.messages.contexts);
|
|
137
|
+
const parsedHandoffs = [];
|
|
138
|
+
for (const session of sessions) {
|
|
139
|
+
const timestamp = session.completedAt.toLocaleTimeString();
|
|
140
|
+
output.push(`\n## ${session.serverName} / ${session.toolName} (${timestamp}, ${session.reason})`);
|
|
141
|
+
const plainPrompts = [];
|
|
142
|
+
for (const prompt of session.messages.prompts) {
|
|
143
|
+
allPrompts.push(prompt);
|
|
144
|
+
const handoff = parseUiPromptHandoff(prompt);
|
|
145
|
+
if (handoff) {
|
|
146
|
+
parsedHandoffs.push(handoff);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
plainPrompts.push(prompt);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (plainPrompts.length > 0) {
|
|
153
|
+
output.push("\n### Prompts:");
|
|
154
|
+
for (const prompt of plainPrompts) {
|
|
155
|
+
output.push(`- ${prompt}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const intentsForSession = [
|
|
159
|
+
...session.messages.intents,
|
|
160
|
+
...session.messages.prompts
|
|
161
|
+
.map((prompt) => parseUiPromptHandoff(prompt))
|
|
162
|
+
.filter((handoff) => !!handoff)
|
|
163
|
+
.map((handoff) => ({ intent: handoff.intent, params: handoff.params })),
|
|
164
|
+
];
|
|
165
|
+
if (intentsForSession.length > 0) {
|
|
166
|
+
output.push("\n### Intents:");
|
|
167
|
+
for (const intent of intentsForSession) {
|
|
168
|
+
const params = intent.params ? ` (${JSON.stringify(intent.params)})` : "";
|
|
169
|
+
output.push(`- ${intent.intent}${params}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const contexts = session.messages.contexts;
|
|
173
|
+
if (contexts.length > 0) {
|
|
174
|
+
output.push("\n### Context updates:");
|
|
175
|
+
for (const context of contexts) {
|
|
176
|
+
output.push(`- ${context.summary}${context.truncated ? " (truncated)" : ""}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (session.messages.notifications.length > 0) {
|
|
180
|
+
output.push("\n### Notifications:");
|
|
181
|
+
for (const notification of session.messages.notifications) {
|
|
182
|
+
output.push(`- ${notification}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const count = sessions.length;
|
|
187
|
+
state.completedUiSessions = [];
|
|
188
|
+
return {
|
|
189
|
+
content: [{ type: "text", text: output.join("\n") }],
|
|
190
|
+
details: {
|
|
191
|
+
sessions: count,
|
|
192
|
+
prompts: allPrompts,
|
|
193
|
+
intents: [...allIntents, ...parsedHandoffs.map(({ intent, params }) => ({ intent, params }))],
|
|
194
|
+
contexts: allContexts,
|
|
195
|
+
handoffs: parsedHandoffs,
|
|
196
|
+
cleared: true,
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export function executeStatus(state) {
|
|
201
|
+
const servers = [];
|
|
202
|
+
for (const name of Object.keys(state.config.mcpServers)) {
|
|
203
|
+
const definition = state.config.mcpServers[name];
|
|
204
|
+
const disabled = isServerDisabled(definition);
|
|
205
|
+
const connection = disabled ? undefined : state.manager.getConnection(name);
|
|
206
|
+
const metadata = disabled ? undefined : state.toolMetadata.get(name);
|
|
207
|
+
const toolCount = metadata?.length ?? 0;
|
|
208
|
+
const failedAgo = disabled ? null : getFailureAgeSeconds(state, name);
|
|
209
|
+
let status = disabled ? "disabled" : "not connected";
|
|
210
|
+
if (!disabled && connection?.status === "connected") {
|
|
211
|
+
status = "connected";
|
|
212
|
+
}
|
|
213
|
+
else if (!disabled && connection?.status === "needs-auth") {
|
|
214
|
+
status = "needs-auth";
|
|
215
|
+
}
|
|
216
|
+
else if (!disabled && failedAgo !== null) {
|
|
217
|
+
status = "failed";
|
|
218
|
+
}
|
|
219
|
+
else if (!disabled && metadata !== undefined) {
|
|
220
|
+
status = "cached";
|
|
221
|
+
}
|
|
222
|
+
servers.push({ name, status, toolCount, failedAgo, ...(disabled ? { disabled: true } : {}) });
|
|
223
|
+
}
|
|
224
|
+
const disabledCount = servers.filter(s => s.disabled).length;
|
|
225
|
+
const enabledServers = servers.filter(s => !s.disabled);
|
|
226
|
+
const totalTools = enabledServers.reduce((sum, s) => sum + s.toolCount, 0);
|
|
227
|
+
const connectedCount = enabledServers.filter(s => s.status === "connected").length;
|
|
228
|
+
let text = `MCP: ${connectedCount}/${enabledServers.length} connected, ${totalTools} tools available (calls connect lazily)`;
|
|
229
|
+
if (disabledCount > 0)
|
|
230
|
+
text += ` (${disabledCount} disabled)`;
|
|
231
|
+
text += "\n\n";
|
|
232
|
+
for (const server of servers) {
|
|
233
|
+
if (server.disabled) {
|
|
234
|
+
text += `⊘ ${server.name} (disabled)\n`;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
if (server.status === "connected") {
|
|
238
|
+
text += `✓ ${server.name} (${server.toolCount} tools)\n`;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
if (server.status === "needs-auth") {
|
|
242
|
+
text += `⚠ ${server.name} (needs auth)\n`;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (server.status === "cached") {
|
|
246
|
+
text += `○ ${server.name} (${server.toolCount} tools, cached)\n`;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (server.status === "failed") {
|
|
250
|
+
text += `✗ ${server.name} (failed ${server.failedAgo ?? 0}s ago)\n`;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
text += `○ ${server.name} (not connected)\n`;
|
|
254
|
+
}
|
|
255
|
+
if (servers.length > 0) {
|
|
256
|
+
text += `\nmcp({ server: "name" }) to browse tools, mcp({ search: "..." }) to search`;
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
content: [{ type: "text", text: text.trim() }],
|
|
260
|
+
details: { mode: "status", servers, totalTools, connectedCount, disabledCount },
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export async function executeAuthStart(state, serverName, signal) {
|
|
264
|
+
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
265
|
+
throwIfAborted(ownedSignal);
|
|
266
|
+
const definition = state.config.mcpServers[serverName];
|
|
267
|
+
if (!definition) {
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
270
|
+
details: { mode: "auth-start", error: "not_found", server: serverName },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (isServerDisabled(definition))
|
|
274
|
+
return disabledResult("auth-start", serverName);
|
|
275
|
+
try {
|
|
276
|
+
const serverUrl = resolveServerUrl(definition);
|
|
277
|
+
if (!serverUrl || !supportsOAuth(definition)) {
|
|
278
|
+
return {
|
|
279
|
+
content: [{ type: "text", text: `Server "${serverName}" is not configured for OAuth over HTTP.` }],
|
|
280
|
+
details: { mode: "auth-start", error: "oauth_not_supported", server: serverName },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const { authorizationUrl } = state.authStorageOptions
|
|
284
|
+
? ownedSignal
|
|
285
|
+
? await startAuth(serverName, serverUrl, definition, { authStorageOptions: state.authStorageOptions, signal: ownedSignal, runtime: state.oauthRuntime })
|
|
286
|
+
: await startAuth(serverName, serverUrl, definition, { authStorageOptions: state.authStorageOptions, runtime: state.oauthRuntime })
|
|
287
|
+
: ownedSignal
|
|
288
|
+
? await startAuth(serverName, serverUrl, definition, { signal: ownedSignal, runtime: state.oauthRuntime })
|
|
289
|
+
: await startAuth(serverName, serverUrl, definition, { runtime: state.oauthRuntime });
|
|
290
|
+
if (!authorizationUrl) {
|
|
291
|
+
return {
|
|
292
|
+
content: [{ type: "text", text: `OAuth authentication successful for "${serverName}".` }],
|
|
293
|
+
details: { mode: "auth-start", server: serverName, authenticated: true },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text", text: formatManualAuthInstructions(serverName, authorizationUrl) }],
|
|
298
|
+
details: { mode: "auth-start", server: serverName, authorizationUrl },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
303
|
+
return {
|
|
304
|
+
content: [{ type: "text", text: `Failed to start OAuth for "${serverName}": ${message}` }],
|
|
305
|
+
details: { mode: "auth-start", error: "auth_start_failed", server: serverName, message },
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
export async function executeAuthComplete(state, serverName, input, signal) {
|
|
310
|
+
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
311
|
+
throwIfAborted(ownedSignal);
|
|
312
|
+
const definition = state.config.mcpServers[serverName];
|
|
313
|
+
if (!definition) {
|
|
314
|
+
return {
|
|
315
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
316
|
+
details: { mode: "auth-complete", error: "not_found", server: serverName },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (isServerDisabled(definition))
|
|
320
|
+
return disabledResult("auth-complete", serverName);
|
|
321
|
+
try {
|
|
322
|
+
const previousConnection = state.manager.getConnection(serverName);
|
|
323
|
+
const status = state.authStorageOptions
|
|
324
|
+
? ownedSignal
|
|
325
|
+
? await completeAuthFromInput(serverName, input, { authStorageOptions: state.authStorageOptions, signal: ownedSignal, runtime: state.oauthRuntime })
|
|
326
|
+
: await completeAuthFromInput(serverName, input, { authStorageOptions: state.authStorageOptions, runtime: state.oauthRuntime })
|
|
327
|
+
: ownedSignal
|
|
328
|
+
? await completeAuthFromInput(serverName, input, { signal: ownedSignal, runtime: state.oauthRuntime })
|
|
329
|
+
: await completeAuthFromInput(serverName, input, { runtime: state.oauthRuntime });
|
|
330
|
+
if (status !== "authenticated") {
|
|
331
|
+
return {
|
|
332
|
+
content: [{ type: "text", text: `OAuth authentication did not complete for "${serverName}".` }],
|
|
333
|
+
details: { mode: "auth-complete", error: "not_authenticated", server: serverName, status },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (previousConnection)
|
|
337
|
+
state.manager.retire(serverName, previousConnection);
|
|
338
|
+
clearFailure(state, serverName);
|
|
339
|
+
updateStatusBar(state);
|
|
340
|
+
return {
|
|
341
|
+
content: [{ type: "text", text: `OAuth authentication successful for "${serverName}". Run mcp({ connect: "${serverName}" }) to connect with the new token.` }],
|
|
342
|
+
details: { mode: "auth-complete", server: serverName, authenticated: true },
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
347
|
+
return {
|
|
348
|
+
content: [{ type: "text", text: `Failed to complete OAuth for "${serverName}": ${message}` }],
|
|
349
|
+
details: { mode: "auth-complete", error: "auth_complete_failed", server: serverName, message },
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
export function executeDescribe(state, toolName) {
|
|
354
|
+
let serverName;
|
|
355
|
+
let toolMeta;
|
|
356
|
+
let disabledMatch;
|
|
357
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
358
|
+
const found = findToolByName(metadata, toolName);
|
|
359
|
+
if (!found)
|
|
360
|
+
continue;
|
|
361
|
+
if (isServerDisabled(state.config.mcpServers[server])) {
|
|
362
|
+
disabledMatch ??= server;
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
serverName = server;
|
|
366
|
+
toolMeta = found;
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
if (!serverName || !toolMeta) {
|
|
370
|
+
if (disabledMatch)
|
|
371
|
+
return disabledResult("describe", disabledMatch);
|
|
372
|
+
const suggestions = rankSuggestions(state, toolName, 5);
|
|
373
|
+
const hint = suggestions.length > 0
|
|
374
|
+
? `Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ describe: "${suggestions[0]}" }).`
|
|
375
|
+
: `Use mcp({ search: "..." }) to search.`;
|
|
376
|
+
return {
|
|
377
|
+
content: [{ type: "text", text: `Tool "${toolName}" not found. ${hint}` }],
|
|
378
|
+
details: { mode: "describe", error: "tool_not_found", requestedTool: toolName, suggestions },
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
const approvalMarker = isToolCallApprovalRequired(state.config, serverName, toolMeta)
|
|
382
|
+
? " (requires approval)"
|
|
383
|
+
: "";
|
|
384
|
+
let text = `${toolMeta.name}${approvalMarker}\n`;
|
|
385
|
+
text += `Server: ${serverName}\n`;
|
|
386
|
+
if (toolMeta.resourceUri) {
|
|
387
|
+
text += `Type: Resource (reads from ${toolMeta.resourceUri})\n`;
|
|
388
|
+
}
|
|
389
|
+
text += `\n${toolMeta.description || "(no description)"}\n`;
|
|
390
|
+
if (toolMeta.annotations)
|
|
391
|
+
text += `\nAnnotations: ${JSON.stringify(toolMeta.annotations)}\n`;
|
|
392
|
+
if (toolMeta.inputSchema && !toolMeta.resourceUri) {
|
|
393
|
+
const shape = renderTsShape(toolMeta.inputSchema);
|
|
394
|
+
text += shape === null ? `\nParameters:\n${formatSchema(toolMeta.inputSchema)}` : `\nShape:\n${shape}`;
|
|
395
|
+
}
|
|
396
|
+
else if (toolMeta.resourceUri) {
|
|
397
|
+
text += `\nNo parameters required (resource tool).`;
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
text += `\nNo parameters defined.`;
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
content: [{ type: "text", text: text.trim() }],
|
|
404
|
+
details: { mode: "describe", tool: toolMeta, server: serverName },
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
export function executeSearch(state, query, server, includeSchemas, limit = 12, offset = 0) {
|
|
408
|
+
const showSchemas = includeSchemas !== false;
|
|
409
|
+
const searchCall = (nextOffset) => `mcp({ search: ${JSON.stringify(query)}${server ? `, server: ${JSON.stringify(server)}` : ""}${showSchemas ? "" : ", includeSchemas: false"}, limit: ${limit}, offset: ${nextOffset} })`;
|
|
410
|
+
if (server && isServerDisabled(state.config.mcpServers[server]))
|
|
411
|
+
return disabledResult("search", server);
|
|
412
|
+
let matches;
|
|
413
|
+
if (query.trim().length === 0) {
|
|
414
|
+
if (!server) {
|
|
415
|
+
return {
|
|
416
|
+
content: [{ type: "text", text: "Search query cannot be empty" }],
|
|
417
|
+
details: { mode: "search", error: "empty_query" },
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
matches = (state.toolMetadata.get(server) ?? [])
|
|
421
|
+
.map(tool => ({ server, tool, score: 0 }))
|
|
422
|
+
.sort((a, b) => a.tool.name.localeCompare(b.tool.name));
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
matches = rankToolMatches(state, query, server);
|
|
426
|
+
}
|
|
427
|
+
const page = paginate(matches, offset, limit);
|
|
428
|
+
if (page.total === 0) {
|
|
429
|
+
const msg = server ? `No tools matching "${query}" in "${server}"` : `No tools matching "${query}"`;
|
|
430
|
+
return {
|
|
431
|
+
content: [{ type: "text", text: msg }],
|
|
432
|
+
details: { mode: "search", matches: [], count: 0, hasMore: false, nextOffset: null, query },
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
if (page.items.length === 0) {
|
|
436
|
+
const retry = searchCall(0);
|
|
437
|
+
return {
|
|
438
|
+
content: [{ type: "text", text: `No search results at offset ${offset}; ${page.total} tools match. Retry with ${retry}.` }],
|
|
439
|
+
details: { mode: "search", error: "offset_out_of_range", matches: [], count: page.total, hasMore: false, nextOffset: null, query },
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
let text = `Found ${page.total} tool${page.total === 1 ? "" : "s"} matching "${query}":\n\n`;
|
|
443
|
+
for (const match of page.items) {
|
|
444
|
+
const approvalMarker = isToolCallApprovalRequired(state.config, match.server, match.tool)
|
|
445
|
+
? " (requires approval)"
|
|
446
|
+
: "";
|
|
447
|
+
if (showSchemas) {
|
|
448
|
+
text += `${match.tool.name}${approvalMarker}\n`;
|
|
449
|
+
text += ` ${match.tool.description || "(no description)"}\n`;
|
|
450
|
+
if (match.tool.inputSchema && !match.tool.resourceUri) {
|
|
451
|
+
const shape = renderTsShape(match.tool.inputSchema);
|
|
452
|
+
text += shape === null
|
|
453
|
+
? `\n Parameters:\n${formatSchema(match.tool.inputSchema, " ")}\n`
|
|
454
|
+
: `\n Shape:\n${shape.split("\n").map(line => ` ${line}`).join("\n")}\n`;
|
|
455
|
+
}
|
|
456
|
+
else if (match.tool.resourceUri) {
|
|
457
|
+
text += " No parameters (resource tool).\n";
|
|
458
|
+
}
|
|
459
|
+
text += "\n";
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
text += `- ${match.tool.name}${approvalMarker}`;
|
|
463
|
+
if (match.tool.description)
|
|
464
|
+
text += ` - ${truncateAtWord(match.tool.description, 50)}`;
|
|
465
|
+
text += "\n";
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const first = (Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0) + 1;
|
|
469
|
+
const nextSearch = searchCall(page.nextOffset);
|
|
470
|
+
text += page.hasMore
|
|
471
|
+
? `\n${first}-${first + page.items.length - 1} of ${page.total} — ${nextSearch} for more\n`
|
|
472
|
+
: `\n${first}-${first + page.items.length - 1} of ${page.total} — end\n`;
|
|
473
|
+
return {
|
|
474
|
+
content: [{ type: "text", text: text.trim() }],
|
|
475
|
+
details: {
|
|
476
|
+
mode: "search",
|
|
477
|
+
matches: page.items.map(match => ({ server: match.server, tool: match.tool.name, score: match.score })),
|
|
478
|
+
count: page.total,
|
|
479
|
+
hasMore: page.hasMore,
|
|
480
|
+
nextOffset: page.nextOffset,
|
|
481
|
+
query,
|
|
482
|
+
},
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
export function executeList(state, server, limit = 12, offset = 0) {
|
|
486
|
+
const definition = state.config.mcpServers[server];
|
|
487
|
+
if (!definition) {
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({}) to see available servers.` }],
|
|
490
|
+
details: { mode: "list", server, tools: [], count: 0, error: "not_found" },
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
if (isServerDisabled(definition))
|
|
494
|
+
return disabledResult("list", server);
|
|
495
|
+
const metadata = state.toolMetadata.get(server);
|
|
496
|
+
const connection = state.manager.getConnection(server);
|
|
497
|
+
const instructions = state.serverInstructions.get(server);
|
|
498
|
+
let instructionsText = "";
|
|
499
|
+
if (instructions) {
|
|
500
|
+
const preview = truncateAtWord(instructions, INSTRUCTIONS_PREVIEW_LENGTH);
|
|
501
|
+
instructionsText = `\n\nServer instructions:\n${preview}`;
|
|
502
|
+
if (preview !== instructions) {
|
|
503
|
+
instructionsText += `\nUse mcp({ instructions: "${server}" }) for the full text.`;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!metadata?.length) {
|
|
507
|
+
if (connection?.status === "connected") {
|
|
508
|
+
return {
|
|
509
|
+
content: [{ type: "text", text: `Server "${server}" has no tools.${instructionsText}` }],
|
|
510
|
+
details: { mode: "list", server, tools: [], count: 0, hasInstructions: Boolean(instructions) },
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
if (metadata !== undefined) {
|
|
514
|
+
return {
|
|
515
|
+
content: [{ type: "text", text: `Server "${server}" has no cached tools (not connected).${instructionsText}` }],
|
|
516
|
+
details: { mode: "list", server, tools: [], count: 0, cached: true, hasInstructions: Boolean(instructions) },
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
content: [{ type: "text", text: `Server "${server}" is configured but not connected. Use mcp({ connect: "${server}" }) or /mcp reconnect ${server} to retry.${instructionsText}` }],
|
|
521
|
+
details: { mode: "list", server, tools: [], count: 0, error: "not_connected", hasInstructions: Boolean(instructions) },
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const page = paginate(metadata, offset, limit);
|
|
525
|
+
if (page.items.length === 0) {
|
|
526
|
+
return {
|
|
527
|
+
content: [{ type: "text", text: `No tools at offset ${offset}; "${server}" has ${page.total} tools. Retry with mcp({ server: ${JSON.stringify(server)}, limit: ${limit}, offset: 0 }).` }],
|
|
528
|
+
details: { mode: "list", error: "offset_out_of_range", server, tools: [], count: page.total, hasMore: false, nextOffset: null, hasInstructions: Boolean(instructions) },
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
const cachedNote = connection?.status === "connected" ? "" : ", not connected, cached";
|
|
532
|
+
let text = `${server} (${page.total} tools${cachedNote}):\n\n`;
|
|
533
|
+
for (const tool of page.items) {
|
|
534
|
+
const description = truncateAtWord(tool.description, 50);
|
|
535
|
+
text += `- ${tool.name}`;
|
|
536
|
+
if (description)
|
|
537
|
+
text += ` - ${description}`;
|
|
538
|
+
text += "\n";
|
|
539
|
+
}
|
|
540
|
+
const first = (Number.isFinite(offset) ? Math.max(0, Math.trunc(offset)) : 0) + 1;
|
|
541
|
+
text += page.hasMore
|
|
542
|
+
? `\n${first}-${first + page.items.length - 1} of ${page.total} — mcp({ server: ${JSON.stringify(server)}, limit: ${limit}, offset: ${page.nextOffset} }) for more`
|
|
543
|
+
: `\n${first}-${first + page.items.length - 1} of ${page.total} — end`;
|
|
544
|
+
text += instructionsText;
|
|
545
|
+
return {
|
|
546
|
+
content: [{ type: "text", text: text.trim() }],
|
|
547
|
+
details: {
|
|
548
|
+
mode: "list",
|
|
549
|
+
server,
|
|
550
|
+
tools: page.items.map(tool => tool.name),
|
|
551
|
+
count: page.total,
|
|
552
|
+
hasMore: page.hasMore,
|
|
553
|
+
nextOffset: page.nextOffset,
|
|
554
|
+
hasInstructions: Boolean(instructions),
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
export function executeInstructions(state, server) {
|
|
559
|
+
const definition = state.config.mcpServers[server];
|
|
560
|
+
if (!definition) {
|
|
561
|
+
return {
|
|
562
|
+
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({}) to see available servers.` }],
|
|
563
|
+
details: { mode: "instructions", server, error: "not_found" },
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
if (isServerDisabled(definition))
|
|
567
|
+
return disabledResult("instructions", server);
|
|
568
|
+
const instructions = state.serverInstructions.get(server);
|
|
569
|
+
if (instructions) {
|
|
570
|
+
return {
|
|
571
|
+
content: [{ type: "text", text: `${server} instructions:\n\n${instructions}` }],
|
|
572
|
+
details: { mode: "instructions", server, length: instructions.length },
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
const connection = state.manager.getConnection(server);
|
|
576
|
+
if (connection?.status === "connected") {
|
|
577
|
+
return {
|
|
578
|
+
content: [{ type: "text", text: `Server "${server}" does not provide instructions.` }],
|
|
579
|
+
details: { mode: "instructions", server, error: "no_instructions" },
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
content: [{ type: "text", text: `No instructions cached for "${server}". Use mcp({ connect: "${server}" }) to connect and refresh.` }],
|
|
584
|
+
details: { mode: "instructions", server, error: "not_connected" },
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
export async function executeConnect(state, serverName, signal, limit, offset) {
|
|
588
|
+
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
589
|
+
throwIfAborted(ownedSignal);
|
|
590
|
+
const definition = state.config.mcpServers[serverName];
|
|
591
|
+
if (!definition) {
|
|
592
|
+
return {
|
|
593
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
594
|
+
details: { mode: "connect", error: "not_found", server: serverName },
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (isServerDisabled(definition))
|
|
598
|
+
return disabledResult("connect", serverName);
|
|
599
|
+
try {
|
|
600
|
+
if (state.ui) {
|
|
601
|
+
state.ui.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${serverName}...`));
|
|
602
|
+
}
|
|
603
|
+
const currentConnection = state.manager.getConnection(serverName);
|
|
604
|
+
let connection = currentConnection?.status === "connected"
|
|
605
|
+
? await state.manager.reconnect(serverName, definition, currentConnection, ownedSignal)
|
|
606
|
+
: await state.manager.connect(serverName, definition, ownedSignal);
|
|
607
|
+
if (connection.status === "needs-auth") {
|
|
608
|
+
const autoAuth = await attemptAutoAuth(state, serverName, ownedSignal);
|
|
609
|
+
if (autoAuth.status === "failed") {
|
|
610
|
+
return {
|
|
611
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
612
|
+
details: { mode: "connect", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
if (autoAuth.status === "success") {
|
|
616
|
+
throwIfAborted(ownedSignal);
|
|
617
|
+
connection = await state.manager.reconnect(serverName, definition, connection, ownedSignal);
|
|
618
|
+
}
|
|
619
|
+
if (connection.status === "needs-auth") {
|
|
620
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
621
|
+
return {
|
|
622
|
+
content: [{ type: "text", text: message }],
|
|
623
|
+
details: { mode: "connect", error: "auth_required", server: serverName, message },
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
628
|
+
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
|
|
629
|
+
state.toolMetadata.set(serverName, metadata);
|
|
630
|
+
if (!connection.promptDiscoveryFailed) {
|
|
631
|
+
state.promptMetadata?.set(serverName, reconstructPromptMetadata(serverName, connection.prompts ?? [], prefix, definition));
|
|
632
|
+
state.promptMetadataLive?.add(serverName);
|
|
633
|
+
}
|
|
634
|
+
if (connection.instructions) {
|
|
635
|
+
state.serverInstructions.set(serverName, connection.instructions);
|
|
636
|
+
}
|
|
637
|
+
else {
|
|
638
|
+
state.serverInstructions.delete(serverName);
|
|
639
|
+
}
|
|
640
|
+
updateMetadataCache(state, serverName);
|
|
641
|
+
notifyToolMetadataUpdated(state, serverName, "proxy-connect");
|
|
642
|
+
markKeepAliveAfterConnect(state, serverName);
|
|
643
|
+
clearFailure(state, serverName);
|
|
644
|
+
updateStatusBar(state);
|
|
645
|
+
return executeList(state, serverName, limit, offset);
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
649
|
+
if (!isAbortError(error, ownedSignal))
|
|
650
|
+
recordFailure(state, serverName, message);
|
|
651
|
+
updateStatusBar(state);
|
|
652
|
+
return {
|
|
653
|
+
content: [{ type: "text", text: `Failed to connect to "${serverName}": ${message}` }],
|
|
654
|
+
details: { mode: "connect", error: isAbortError(error, ownedSignal) ? "aborted" : "connect_failed", server: serverName, message },
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Executes an already-approved tool call against a connected server.
|
|
660
|
+
*
|
|
661
|
+
* Both the `mcp` proxy and direct tools reach this with a resolved target and a
|
|
662
|
+
* live connection; everything before this point (name resolution, connect and
|
|
663
|
+
* auth strategy, approval) differs between them and stays with the caller.
|
|
664
|
+
*/
|
|
665
|
+
export async function runToolCall(state, serverName, target, args, options) {
|
|
666
|
+
const { detailsBase, signal, recoverAuthConnection, authRequiredMessage, autoAuthAttempted, onRawResult } = options;
|
|
667
|
+
const configuredOptions = state.manager.getRequestOptions?.(serverName, options.ownedSignal);
|
|
668
|
+
const callerSignal = combineAbortSignals(configuredOptions?.signal, options.ownedSignal);
|
|
669
|
+
const outputGuardOptions = resolveMcpOutputGuardOptions(state.config.settings, state.outputDirectory);
|
|
670
|
+
// Lazy: formatSchema recurses, so only pay for it (and only risk it throwing)
|
|
671
|
+
// on the error paths that actually print it.
|
|
672
|
+
const schemaSuffix = () => target.inputSchema ? `\n\nExpected parameters:\n${formatSchema(target.inputSchema)}` : "";
|
|
673
|
+
let uiSession = null;
|
|
674
|
+
const identity = {
|
|
675
|
+
server: serverName, tool: target.originalName,
|
|
676
|
+
...(options.toolCallId !== undefined ? { toolCallId: options.toolCallId } : {}),
|
|
677
|
+
...(options.innerCallId !== undefined ? { innerCallId: options.innerCallId } : {}),
|
|
678
|
+
};
|
|
679
|
+
let beforeDispatchPending = false;
|
|
680
|
+
let captureFailure;
|
|
681
|
+
const capture = async (event) => {
|
|
682
|
+
if (!state.onToolCall)
|
|
683
|
+
return;
|
|
684
|
+
try {
|
|
685
|
+
const run = () => state.onToolCall(event);
|
|
686
|
+
await abortable(state.owner ? state.owner.runCallback("onToolCall", run) : run(), event.signal);
|
|
687
|
+
throwIfAborted(event.signal);
|
|
688
|
+
}
|
|
689
|
+
catch (error) {
|
|
690
|
+
captureFailure = event;
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
try {
|
|
695
|
+
state.manager.touch(serverName);
|
|
696
|
+
state.manager.incrementInFlight(serverName);
|
|
697
|
+
uiSession = !target.resourceUri && target.uiResourceUri
|
|
698
|
+
? await maybeStartUiSession(state, {
|
|
699
|
+
serverName,
|
|
700
|
+
toolName: target.originalName,
|
|
701
|
+
toolArgs: args ?? {},
|
|
702
|
+
uiResourceUri: target.uiResourceUri,
|
|
703
|
+
...(target.uiStreamMode !== undefined ? { streamMode: target.uiStreamMode } : {}),
|
|
704
|
+
...(signal ? { signal } : {}),
|
|
705
|
+
onNeedsAuth: recoverAuthConnection,
|
|
706
|
+
})
|
|
707
|
+
: null;
|
|
708
|
+
// Frozen direct-tool schemas may be older than the live catalog. Never use
|
|
709
|
+
// stale cached hints in preference to the connected server's annotations.
|
|
710
|
+
const liveTools = state.manager.getConnection(serverName)?.tools;
|
|
711
|
+
const annotations = liveTools
|
|
712
|
+
? liveTools.find(tool => tool.name === target.originalName)?.annotations
|
|
713
|
+
: target.annotations;
|
|
714
|
+
const operation = {
|
|
715
|
+
...identity, args: args ?? {},
|
|
716
|
+
...(annotations !== undefined ? { annotations } : {}),
|
|
717
|
+
...(target.resourceUri ? { resourceUri: target.resourceUri } : {}),
|
|
718
|
+
};
|
|
719
|
+
if (options.beforeDispatch) {
|
|
720
|
+
beforeDispatchPending = true;
|
|
721
|
+
const run = () => options.beforeDispatch(callerSignal, {
|
|
722
|
+
...operation, annotationsTrusted: state.config.mcpServers[serverName]?.retryOnTransportFailure === true,
|
|
723
|
+
});
|
|
724
|
+
await abortable(state.owner ? state.owner.runCallback("beforeExecute", run) : run(), callerSignal);
|
|
725
|
+
beforeDispatchPending = false;
|
|
726
|
+
}
|
|
727
|
+
throwIfAborted(callerSignal);
|
|
728
|
+
// Host checkpoints and UI preparation do not consume the service deadline.
|
|
729
|
+
const ownedSignal = combineAbortSignals(callerSignal, AbortSignal.timeout(Math.ceil(configuredOptions?.timeout ?? DEFAULT_REQUEST_TIMEOUT_MSEC)));
|
|
730
|
+
const requestOptions = { ...configuredOptions, signal: ownedSignal };
|
|
731
|
+
const recovery = { manager: state.manager, config: state.config, signal: ownedSignal, onNeedsAuth: recoverAuthConnection };
|
|
732
|
+
const call = { ...operation, signal: ownedSignal };
|
|
733
|
+
await capture({ ...call, phase: "before" });
|
|
734
|
+
throwIfAborted(ownedSignal);
|
|
735
|
+
const dispatch = async (fn) => {
|
|
736
|
+
let result;
|
|
737
|
+
try {
|
|
738
|
+
result = await fn();
|
|
739
|
+
}
|
|
740
|
+
catch (error) {
|
|
741
|
+
await capture({ ...call, phase: "after", error });
|
|
742
|
+
throw error;
|
|
743
|
+
}
|
|
744
|
+
await capture({ ...call, phase: "after", result });
|
|
745
|
+
return result;
|
|
746
|
+
};
|
|
747
|
+
if (target.resourceUri) {
|
|
748
|
+
const result = await dispatch(() => withSessionRecovery(recovery, serverName, (conn) => conn.client.readResource({ uri: target.resourceUri }, requestOptions)));
|
|
749
|
+
onRawResult?.(result);
|
|
750
|
+
const contents = (result.contents ?? []).map(c => ({
|
|
751
|
+
type: "text",
|
|
752
|
+
text: "text" in c ? c.text : ("blob" in c ? `[Binary data: ${c.mimeType ?? "unknown"}]` : JSON.stringify(c)),
|
|
753
|
+
}));
|
|
754
|
+
const guarded = await guardMcpOutput(contents.length > 0 ? contents : [{ type: "text", text: "(empty resource)" }], outputGuardOptions);
|
|
755
|
+
return { content: guarded.content, details: { ...detailsBase, ...guardedMcpDetails(guarded) } };
|
|
756
|
+
}
|
|
757
|
+
const result = await dispatch(() => withSessionRecovery({ ...recovery, retryOnTransportFailure: annotations?.readOnlyHint === true || annotations?.idempotentHint === true }, serverName, (conn) => abortable(conn.client.callTool({
|
|
758
|
+
name: target.originalName,
|
|
759
|
+
arguments: args ?? {},
|
|
760
|
+
_meta: uiSession?.requestMeta,
|
|
761
|
+
}, requestOptions), ownedSignal)));
|
|
762
|
+
uiSession?.sendToolResult(result);
|
|
763
|
+
if (result.isError) {
|
|
764
|
+
const content = transformMcpContent((result.content ?? []));
|
|
765
|
+
const guarded = await guardMcpOutput(content.length > 0 ? content : [{ type: "text", text: "(empty result)" }], { ...outputGuardOptions, prefix: "Error: ", suffix: schemaSuffix(), emptyTextFallback: "Tool execution failed", rawMcpResult: result });
|
|
766
|
+
return { content: guarded.content, details: { ...detailsBase, error: "tool_error", ...guardedMcpDetails(guarded) } };
|
|
767
|
+
}
|
|
768
|
+
onRawResult?.(result);
|
|
769
|
+
const content = resolveMcpResultContent(result);
|
|
770
|
+
const outputContent = content.length > 0 ? content : [{ type: "text", text: "(empty result)" }];
|
|
771
|
+
if (target.uiResourceUri) {
|
|
772
|
+
const uiSummary = summarizeUiSessionResult(uiSession);
|
|
773
|
+
const guarded = await guardMcpOutput(outputContent, { ...outputGuardOptions, suffix: `\n\n${uiSummary.message}`, rawMcpResult: result });
|
|
774
|
+
return {
|
|
775
|
+
content: guarded.content,
|
|
776
|
+
details: {
|
|
777
|
+
...detailsBase,
|
|
778
|
+
...guardedMcpDetails(guarded),
|
|
779
|
+
uiOpen: uiSummary.uiOpen,
|
|
780
|
+
uiViewer: uiSummary.uiViewer,
|
|
781
|
+
uiUrl: uiSummary.uiUrl,
|
|
782
|
+
},
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
const guarded = await guardMcpOutput(outputContent, { ...outputGuardOptions, rawMcpResult: result });
|
|
786
|
+
return { content: guarded.content, details: { ...detailsBase, ...guardedMcpDetails(guarded) } };
|
|
787
|
+
}
|
|
788
|
+
catch (error) {
|
|
789
|
+
// Direct/proxy host errors throw; scripts retain their failed-call envelope.
|
|
790
|
+
if (beforeDispatchPending && options.innerCallId === undefined)
|
|
791
|
+
throw error;
|
|
792
|
+
if (captureFailure) {
|
|
793
|
+
const { signal: _signal, ...context } = captureFailure;
|
|
794
|
+
const message = captureFailure.phase === "before"
|
|
795
|
+
? "MCP call capture failed before dispatch; the tool did not run. Restore capture before continuing."
|
|
796
|
+
: "MCP outcome capture failed. Use the retained outcome to continue the original work; do not repeat a completed call or rerun the script.";
|
|
797
|
+
uiSession?.sendToolCancelled(message);
|
|
798
|
+
return {
|
|
799
|
+
content: [{ type: "text", text: message }],
|
|
800
|
+
details: { ...detailsBase, error: isAbortError(error, callerSignal) ? "aborted" : "call_capture_failed", message, recovery: context },
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
if (isToolTransportFailure(error)) {
|
|
804
|
+
const message = `The outcome of MCP tool "${target.originalName}" on "${serverName}" is unknown after transport loss. Read back the original operation using its saved arguments and provider identity before continuing. Do not blindly repeat the call or rerun its script.`;
|
|
805
|
+
uiSession?.sendToolCancelled(message);
|
|
806
|
+
return {
|
|
807
|
+
content: [{ type: "text", text: message }],
|
|
808
|
+
details: { ...detailsBase, error: "ambiguous_outcome", message, recovery: { ...identity, action: "readback" } },
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
if (error instanceof SessionRecoveryAuthRequiredError) {
|
|
812
|
+
const message = error.authMessage ?? authRequiredMessage();
|
|
813
|
+
uiSession?.sendToolCancelled(message);
|
|
814
|
+
return {
|
|
815
|
+
content: [{ type: "text", text: message }],
|
|
816
|
+
details: { ...detailsBase, error: "auth_required", message, autoAuthAttempted: autoAuthAttempted() },
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
if (error instanceof UrlElicitationRequiredError) {
|
|
820
|
+
const action = await state.manager.handleUrlElicitationRequired(serverName, error);
|
|
821
|
+
const message = action === "accept"
|
|
822
|
+
? "The original MCP tool did not run. Complete the opened browser interaction, then retry the tool."
|
|
823
|
+
: `The URL interaction was ${action === "decline" ? "declined" : "cancelled"}.`;
|
|
824
|
+
uiSession?.sendToolCancelled(message);
|
|
825
|
+
return {
|
|
826
|
+
content: [{ type: "text", text: message }],
|
|
827
|
+
details: { ...detailsBase, error: "url_elicitation_required", action },
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
831
|
+
uiSession?.sendToolCancelled(message);
|
|
832
|
+
const guarded = await guardMcpOutput([{ type: "text", text: message }], { ...outputGuardOptions, prefix: "Failed to call tool: ", suffix: schemaSuffix() });
|
|
833
|
+
return {
|
|
834
|
+
content: guarded.content,
|
|
835
|
+
details: {
|
|
836
|
+
...detailsBase,
|
|
837
|
+
error: isAbortError(error, callerSignal) ? "aborted" : "call_failed",
|
|
838
|
+
message: guarded.outputGuard ? "output truncated; see outputGuard.fullOutputPath" : message,
|
|
839
|
+
...guardedMcpDetails(guarded),
|
|
840
|
+
},
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
finally {
|
|
844
|
+
if (uiSession?.reused) {
|
|
845
|
+
uiSession.close();
|
|
846
|
+
}
|
|
847
|
+
state.manager.decrementInFlight(serverName);
|
|
848
|
+
state.manager.touch(serverName);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
export async function executeCall(state, toolName, args, serverOverride, getPiTools, signal, onRawResult, identity = {}, beforeDispatch) {
|
|
852
|
+
const ownedSignal = combineAbortSignals(state.owner?.signal, signal);
|
|
853
|
+
throwIfAborted(ownedSignal);
|
|
854
|
+
let serverName = serverOverride;
|
|
855
|
+
let toolMeta;
|
|
856
|
+
let autoAuthAttempted = false;
|
|
857
|
+
const prefixMode = state.config.settings?.toolPrefix ?? "server";
|
|
858
|
+
const disabledCallResult = (disabledServer, metadata) => {
|
|
859
|
+
if (!metadata) {
|
|
860
|
+
const message = `Server "${disabledServer}" is disabled. Run /mcp enable ${disabledServer} and /reload to enable it.`;
|
|
861
|
+
return {
|
|
862
|
+
content: [{ type: "text", text: message }],
|
|
863
|
+
details: { mode: "call", error: "server_disabled", server: disabledServer, requestedTool: toolName, message },
|
|
864
|
+
};
|
|
865
|
+
}
|
|
866
|
+
const message = `Server "${disabledServer}" is disabled. Run /mcp enable ${disabledServer} and /reload to enable it.`;
|
|
867
|
+
const identity = metadata.resourceUri
|
|
868
|
+
? { server: disabledServer, resourceUri: metadata.resourceUri }
|
|
869
|
+
: { server: disabledServer, tool: metadata.originalName };
|
|
870
|
+
return {
|
|
871
|
+
content: [{ type: "text", text: message }],
|
|
872
|
+
details: { mode: "call", error: "server_disabled", ...identity, message },
|
|
873
|
+
};
|
|
874
|
+
};
|
|
875
|
+
if (serverName && !state.config.mcpServers[serverName]) {
|
|
876
|
+
return {
|
|
877
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
878
|
+
details: { mode: "call", error: "server_not_found", server: serverName, requestedTool: toolName },
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
if (serverName) {
|
|
882
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
883
|
+
if (isServerDisabled(state.config.mcpServers[serverName])) {
|
|
884
|
+
return disabledCallResult(serverName, toolMeta);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
let disabledMatch;
|
|
889
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
890
|
+
const found = findToolByName(metadata, toolName);
|
|
891
|
+
if (!found)
|
|
892
|
+
continue;
|
|
893
|
+
if (isServerDisabled(state.config.mcpServers[server])) {
|
|
894
|
+
disabledMatch ??= { serverName: server, toolMeta: found };
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
serverName = server;
|
|
898
|
+
toolMeta = found;
|
|
899
|
+
break;
|
|
900
|
+
}
|
|
901
|
+
if (!toolMeta && disabledMatch)
|
|
902
|
+
return disabledCallResult(disabledMatch.serverName, disabledMatch.toolMeta);
|
|
903
|
+
}
|
|
904
|
+
if (serverName && !toolMeta && !state.toolMetadata.has(serverName)) {
|
|
905
|
+
const connected = await lazyConnect(state, serverName, ownedSignal);
|
|
906
|
+
if (connected) {
|
|
907
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
const needsAuthConnection = state.manager.getConnection(serverName);
|
|
911
|
+
if (needsAuthConnection?.status === "needs-auth") {
|
|
912
|
+
if (!autoAuthAttempted) {
|
|
913
|
+
autoAuthAttempted = true;
|
|
914
|
+
const autoAuth = await attemptAutoAuth(state, serverName, ownedSignal);
|
|
915
|
+
if (autoAuth.status === "failed") {
|
|
916
|
+
return {
|
|
917
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
918
|
+
details: { mode: "call", error: "auth_required", server: serverName, requestedTool: toolName, message: autoAuth.message },
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
if (autoAuth.status === "success") {
|
|
922
|
+
clearFailure(state, serverName);
|
|
923
|
+
let connectedAfterAuth = false;
|
|
924
|
+
try {
|
|
925
|
+
const definition = state.config.mcpServers[serverName];
|
|
926
|
+
if (definition) {
|
|
927
|
+
await state.manager.reconnect(serverName, definition, needsAuthConnection, ownedSignal);
|
|
928
|
+
connectedAfterAuth = true;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
catch (error) {
|
|
932
|
+
if (isAbortError(error, ownedSignal))
|
|
933
|
+
throwIfAborted(ownedSignal);
|
|
934
|
+
recordFailure(state, serverName, error instanceof Error ? error.message : String(error));
|
|
935
|
+
updateStatusBar(state);
|
|
936
|
+
}
|
|
937
|
+
if (connectedAfterAuth)
|
|
938
|
+
connectedAfterAuth = await lazyConnect(state, serverName, ownedSignal);
|
|
939
|
+
if (connectedAfterAuth) {
|
|
940
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
941
|
+
if (!toolMeta) {
|
|
942
|
+
const suggestions = rankSuggestions(state, toolName, 5, serverName);
|
|
943
|
+
const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}` : "";
|
|
944
|
+
return {
|
|
945
|
+
content: [{ type: "text", text: `Tool "${toolName}" not found on "${serverName}" after reconnect.${suggestionText}` }],
|
|
946
|
+
details: { mode: "call", error: "tool_not_found_after_reconnect", server: serverName, requestedTool: toolName, suggestions },
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
if (!toolMeta && state.manager.getConnection(serverName)?.status === "needs-auth") {
|
|
953
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
954
|
+
return {
|
|
955
|
+
content: [{ type: "text", text: message }],
|
|
956
|
+
details: { mode: "call", error: "auth_required", server: serverName, requestedTool: toolName, message },
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (!toolMeta) {
|
|
961
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
962
|
+
if (failedAgo !== null) {
|
|
963
|
+
return {
|
|
964
|
+
content: [{ type: "text", text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
965
|
+
details: { mode: "call", error: "server_backoff", server: serverName, requestedTool: toolName },
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
let prefixMatchedServer;
|
|
972
|
+
if (!serverName && !toolMeta) {
|
|
973
|
+
const candidates = Object.keys(state.config.mcpServers)
|
|
974
|
+
.filter(name => !isServerDisabled(state.config.mcpServers[name]))
|
|
975
|
+
.map(name => ({
|
|
976
|
+
name,
|
|
977
|
+
prefix: getServerPrefix(name, resolveToolPrefix(state.config.mcpServers[name], prefixMode)),
|
|
978
|
+
}))
|
|
979
|
+
.filter(c => c.prefix && toolName.startsWith(c.prefix + "_"))
|
|
980
|
+
.sort((a, b) => b.prefix.length - a.prefix.length)
|
|
981
|
+
.slice(0, 1);
|
|
982
|
+
for (const { name: configuredServer } of candidates) {
|
|
983
|
+
const existingConnection = state.manager.getConnection(configuredServer);
|
|
984
|
+
const failedAgo = getFailureAgeSeconds(state, configuredServer);
|
|
985
|
+
if (failedAgo !== null && existingConnection?.status !== "needs-auth")
|
|
986
|
+
continue;
|
|
987
|
+
if (state.toolMetadata.has(configuredServer)) {
|
|
988
|
+
prefixMatchedServer ??= configuredServer;
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
let connected = await lazyConnect(state, configuredServer, ownedSignal);
|
|
992
|
+
const needsAuthConnection = state.manager.getConnection(configuredServer);
|
|
993
|
+
if (!connected && needsAuthConnection?.status === "needs-auth" && !autoAuthAttempted) {
|
|
994
|
+
autoAuthAttempted = true;
|
|
995
|
+
const autoAuth = await attemptAutoAuth(state, configuredServer, ownedSignal);
|
|
996
|
+
if (autoAuth.status === "failed") {
|
|
997
|
+
return {
|
|
998
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
999
|
+
details: { mode: "call", error: "auth_required", server: configuredServer, requestedTool: toolName, message: autoAuth.message },
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
if (autoAuth.status === "success") {
|
|
1003
|
+
clearFailure(state, configuredServer);
|
|
1004
|
+
try {
|
|
1005
|
+
const definition = state.config.mcpServers[configuredServer];
|
|
1006
|
+
if (definition) {
|
|
1007
|
+
await state.manager.reconnect(configuredServer, definition, needsAuthConnection, ownedSignal);
|
|
1008
|
+
connected = true;
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
catch (error) {
|
|
1012
|
+
if (isAbortError(error, ownedSignal))
|
|
1013
|
+
throwIfAborted(ownedSignal);
|
|
1014
|
+
recordFailure(state, configuredServer, error instanceof Error ? error.message : String(error));
|
|
1015
|
+
updateStatusBar(state);
|
|
1016
|
+
}
|
|
1017
|
+
if (connected)
|
|
1018
|
+
connected = await lazyConnect(state, configuredServer, ownedSignal);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
if (!connected)
|
|
1022
|
+
continue;
|
|
1023
|
+
if (!prefixMatchedServer)
|
|
1024
|
+
prefixMatchedServer = configuredServer;
|
|
1025
|
+
toolMeta = findToolByName(state.toolMetadata.get(configuredServer), toolName);
|
|
1026
|
+
if (toolMeta) {
|
|
1027
|
+
serverName = configuredServer;
|
|
1028
|
+
break;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (!serverName || !toolMeta) {
|
|
1033
|
+
const nativeTool = !serverOverride
|
|
1034
|
+
? getPiTools?.().find((tool) => tool.name === toolName && tool.name !== "mcp")
|
|
1035
|
+
: undefined;
|
|
1036
|
+
if (nativeTool) {
|
|
1037
|
+
return {
|
|
1038
|
+
content: [{ type: "text", text: `"${toolName}" is a native Pi tool. Call ${toolName} directly instead of using mcp({ tool: "${toolName}" }).` }],
|
|
1039
|
+
details: { mode: "call", error: "native_tool", requestedTool: toolName },
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
const hintServer = serverName ?? prefixMatchedServer;
|
|
1043
|
+
const normalizedToolName = toolName.replace(/-/g, "_");
|
|
1044
|
+
const localOriginalName = serverOverride && state.toolMetadata.get(serverOverride)
|
|
1045
|
+
?.some(tool => tool.originalName.replace(/-/g, "_") === normalizedToolName);
|
|
1046
|
+
const suggestedOwner = serverOverride && !localOriginalName
|
|
1047
|
+
? [...state.toolMetadata].find(([candidateServer, metadata]) => candidateServer !== serverOverride
|
|
1048
|
+
&& !isServerDisabled(state.config.mcpServers[candidateServer])
|
|
1049
|
+
&& findToolByName(metadata, toolName))
|
|
1050
|
+
: undefined;
|
|
1051
|
+
const suggestedTool = suggestedOwner ? findToolByName(suggestedOwner[1], toolName) : undefined;
|
|
1052
|
+
if (suggestedOwner && suggestedTool) {
|
|
1053
|
+
const suggestedServer = suggestedOwner[0];
|
|
1054
|
+
return {
|
|
1055
|
+
content: [{ type: "text", 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.` }],
|
|
1056
|
+
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer, suggestedServer, suggestions: [suggestedTool.name] },
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
const suggestions = rankSuggestions(state, toolName, 5, hintServer);
|
|
1060
|
+
let hint;
|
|
1061
|
+
if (suggestions.length > 0) {
|
|
1062
|
+
hint = ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ describe: "${suggestions[0]}" }).`;
|
|
1063
|
+
}
|
|
1064
|
+
else if (hintServer) {
|
|
1065
|
+
hint = ` Search with mcp({ search: ${JSON.stringify(toolName)}, server: ${JSON.stringify(hintServer)} }).`;
|
|
1066
|
+
}
|
|
1067
|
+
else {
|
|
1068
|
+
hint = ` Use mcp({ search: "..." }) to search.`;
|
|
1069
|
+
}
|
|
1070
|
+
const suggestionOnHintServer = hintServer && suggestions.some(suggestion => state.toolMetadata.get(hintServer)?.some(tool => tool.name === suggestion));
|
|
1071
|
+
if (hintServer && (suggestions.length === 0 || suggestionOnHintServer) && state.toolMetadata.has(hintServer) && state.manager.getConnection(hintServer)?.status !== "connected") {
|
|
1072
|
+
hint += ` Refresh a stale catalog with mcp({ connect: ${JSON.stringify(hintServer)} }).`;
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
content: [{ type: "text", text: `Tool "${toolName}" not found.${hint}` }],
|
|
1076
|
+
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer, suggestions },
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
const callIdentity = toolMeta.resourceUri
|
|
1080
|
+
? { server: serverName, resourceUri: toolMeta.resourceUri }
|
|
1081
|
+
: { server: serverName, tool: toolMeta.originalName };
|
|
1082
|
+
let connection = state.manager.getConnection(serverName);
|
|
1083
|
+
let refreshAfterAuth = false;
|
|
1084
|
+
if (connection?.status === "needs-auth") {
|
|
1085
|
+
if (!autoAuthAttempted) {
|
|
1086
|
+
autoAuthAttempted = true;
|
|
1087
|
+
const autoAuth = await attemptAutoAuth(state, serverName, ownedSignal);
|
|
1088
|
+
if (autoAuth.status === "failed") {
|
|
1089
|
+
return {
|
|
1090
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
1091
|
+
details: { mode: "call", error: "auth_required", ...callIdentity, message: autoAuth.message },
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (autoAuth.status === "success") {
|
|
1095
|
+
clearFailure(state, serverName);
|
|
1096
|
+
refreshAfterAuth = true;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
if (connection?.status === "needs-auth" && !refreshAfterAuth) {
|
|
1100
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
1101
|
+
return {
|
|
1102
|
+
content: [{ type: "text", text: message }],
|
|
1103
|
+
details: { mode: "call", error: "auth_required", ...callIdentity, message },
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
if (!connection || connection.status !== "connected") {
|
|
1108
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
1109
|
+
if (failedAgo !== null) {
|
|
1110
|
+
return {
|
|
1111
|
+
content: [{ type: "text", text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
1112
|
+
details: { mode: "call", error: "server_backoff", ...callIdentity },
|
|
1113
|
+
};
|
|
1114
|
+
}
|
|
1115
|
+
const definition = state.config.mcpServers[serverName];
|
|
1116
|
+
if (!definition) {
|
|
1117
|
+
return {
|
|
1118
|
+
content: [{ type: "text", text: `Server "${serverName}" not connected` }],
|
|
1119
|
+
details: { mode: "call", error: "server_not_connected", ...callIdentity },
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
try {
|
|
1123
|
+
if (state.ui) {
|
|
1124
|
+
state.ui.setStatus("mcp", formatMcpStatus(state.config, `connecting to ${serverName}...`));
|
|
1125
|
+
}
|
|
1126
|
+
connection = refreshAfterAuth && connection
|
|
1127
|
+
? await state.manager.reconnect(serverName, definition, connection, ownedSignal)
|
|
1128
|
+
: await state.manager.connect(serverName, definition, ownedSignal);
|
|
1129
|
+
if (connection.status === "needs-auth") {
|
|
1130
|
+
if (!autoAuthAttempted) {
|
|
1131
|
+
autoAuthAttempted = true;
|
|
1132
|
+
const autoAuth = await attemptAutoAuth(state, serverName, ownedSignal);
|
|
1133
|
+
if (autoAuth.status === "failed") {
|
|
1134
|
+
return {
|
|
1135
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
1136
|
+
details: { mode: "call", error: "auth_required", ...callIdentity, message: autoAuth.message },
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1139
|
+
if (autoAuth.status === "success") {
|
|
1140
|
+
connection = await state.manager.reconnect(serverName, definition, connection, ownedSignal);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (connection.status === "needs-auth") {
|
|
1144
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
1145
|
+
return {
|
|
1146
|
+
content: [{ type: "text", text: message }],
|
|
1147
|
+
details: { mode: "call", error: "auth_required", ...callIdentity, message },
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
clearFailure(state, serverName);
|
|
1152
|
+
updateServerMetadata(state, serverName);
|
|
1153
|
+
updateMetadataCache(state, serverName);
|
|
1154
|
+
notifyToolMetadataUpdated(state, serverName, "proxy-call-reconnect");
|
|
1155
|
+
markKeepAliveAfterConnect(state, serverName);
|
|
1156
|
+
updateStatusBar(state);
|
|
1157
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
1158
|
+
if (!toolMeta) {
|
|
1159
|
+
const suggestions = rankSuggestions(state, toolName, 5, serverName);
|
|
1160
|
+
const hint = suggestions.length > 0
|
|
1161
|
+
? ` Did you mean: ${suggestions.join(", ")}. Inspect with mcp({ describe: "${suggestions[0]}" }).`
|
|
1162
|
+
: ` Search with mcp({ search: ${JSON.stringify(toolName)}, server: ${JSON.stringify(serverName)} }).`;
|
|
1163
|
+
return {
|
|
1164
|
+
content: [{ type: "text", text: `Tool "${toolName}" not found on "${serverName}" after reconnect.${hint}` }],
|
|
1165
|
+
details: { mode: "call", error: "tool_not_found_after_reconnect", server: serverName, requestedTool: toolName, suggestions },
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
catch (error) {
|
|
1170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1171
|
+
if (!isAbortError(error, ownedSignal))
|
|
1172
|
+
recordFailure(state, serverName, message);
|
|
1173
|
+
updateStatusBar(state);
|
|
1174
|
+
return {
|
|
1175
|
+
content: [{ type: "text", text: `Failed to connect to "${serverName}": ${message}` }],
|
|
1176
|
+
details: { mode: "call", error: isAbortError(error, ownedSignal) ? "aborted" : "connect_failed", ...callIdentity, message },
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (isServerDisabled(state.config.mcpServers[serverName])) {
|
|
1181
|
+
return disabledCallResult(serverName, toolMeta);
|
|
1182
|
+
}
|
|
1183
|
+
const approval = await ensureToolCallApproved(state, serverName, toolMeta, args, ownedSignal);
|
|
1184
|
+
if (approval.ok === false) {
|
|
1185
|
+
const denied = approval.reason === "denied";
|
|
1186
|
+
const message = denied
|
|
1187
|
+
? `The user declined approval to run MCP tool "${toolMeta.originalName}" on server "${serverName}".`
|
|
1188
|
+
: `MCP tool "${toolMeta.originalName}" on server "${serverName}" is approval-gated and requires an interactive session.`;
|
|
1189
|
+
return {
|
|
1190
|
+
content: [{ type: "text", text: message }],
|
|
1191
|
+
details: {
|
|
1192
|
+
mode: "call",
|
|
1193
|
+
error: denied ? "approval_denied" : "approval_required",
|
|
1194
|
+
server: serverName,
|
|
1195
|
+
tool: toolMeta.originalName,
|
|
1196
|
+
},
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
const recoverAuthConnection = async (_serverName, recoverySignal = ownedSignal, challenge) => {
|
|
1200
|
+
throwIfAborted(recoverySignal);
|
|
1201
|
+
const current = state.manager.getConnection(serverName);
|
|
1202
|
+
if (current?.status === "connected" && current !== challenge?.connection)
|
|
1203
|
+
return current;
|
|
1204
|
+
if (!autoAuthAttempted) {
|
|
1205
|
+
autoAuthAttempted = true;
|
|
1206
|
+
const autoAuth = await attemptAutoAuth(state, serverName, recoverySignal);
|
|
1207
|
+
throwIfAborted(recoverySignal);
|
|
1208
|
+
if (autoAuth.status === "failed") {
|
|
1209
|
+
throw new SessionRecoveryAuthRequiredError(serverName, autoAuth.message);
|
|
1210
|
+
}
|
|
1211
|
+
if (autoAuth.status === "success") {
|
|
1212
|
+
const definition = state.config.mcpServers[serverName];
|
|
1213
|
+
if (!definition)
|
|
1214
|
+
return undefined;
|
|
1215
|
+
if (!current)
|
|
1216
|
+
return undefined;
|
|
1217
|
+
clearFailure(state, serverName);
|
|
1218
|
+
connection = await state.manager.reconnect(serverName, definition, current, recoverySignal);
|
|
1219
|
+
return connection;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
return challenge ? undefined : state.manager.getConnection(serverName);
|
|
1223
|
+
};
|
|
1224
|
+
return runToolCall(state, serverName, toolMeta, args, {
|
|
1225
|
+
...identity,
|
|
1226
|
+
...(beforeDispatch ? { beforeDispatch } : {}),
|
|
1227
|
+
detailsBase: { mode: "call", ...callIdentity },
|
|
1228
|
+
ownedSignal,
|
|
1229
|
+
signal,
|
|
1230
|
+
recoverAuthConnection,
|
|
1231
|
+
authRequiredMessage: () => getAuthRequiredMessage(state, serverName),
|
|
1232
|
+
autoAuthAttempted: () => autoAuthAttempted,
|
|
1233
|
+
...(onRawResult ? { onRawResult } : {}),
|
|
1234
|
+
});
|
|
1235
|
+
}
|