pi-soly 1.9.3 → 1.11.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/ask/index.ts +12 -11
- package/ask/picker.ts +356 -76
- package/ask/prompt.ts +6 -2
- package/ask/tests/picker.test.ts +273 -82
- package/codemap.ts +276 -0
- package/hotreload.ts +239 -0
- package/init.ts +302 -0
- package/mcp/CHANGELOG.md +384 -0
- package/mcp/LICENSE +21 -0
- package/mcp/OAUTH.md +355 -0
- package/mcp/README.md +410 -0
- package/mcp/__tests__/agent-dir-paths.upstream-test.ts +80 -0
- package/mcp/__tests__/cli.upstream-test.ts +97 -0
- package/mcp/__tests__/commands-onboarding.upstream-test.ts +157 -0
- package/mcp/__tests__/config.upstream-test.ts +303 -0
- package/mcp/__tests__/consent-manager.upstream-test.ts +151 -0
- package/mcp/__tests__/direct-tools-auto-auth.upstream-test.ts +218 -0
- package/mcp/__tests__/direct-tools.upstream-test.ts +299 -0
- package/mcp/__tests__/elicitation-handler.upstream-test.ts +346 -0
- package/mcp/__tests__/elicitation-sdk-integration.upstream-test.ts +140 -0
- package/mcp/__tests__/errors.upstream-test.ts +218 -0
- package/mcp/__tests__/fixtures/elicitation-server.mjs +98 -0
- package/mcp/__tests__/host-html-template.upstream-test.ts +278 -0
- package/mcp/__tests__/index-lifecycle.upstream-test.ts +484 -0
- package/mcp/__tests__/init-elicitation.upstream-test.ts +86 -0
- package/mcp/__tests__/interactive-visualizer-server.upstream-test.ts +28 -0
- package/mcp/__tests__/logger.upstream-test.ts +175 -0
- package/mcp/__tests__/mcp-auth-flow-client-credentials.upstream-test.ts +612 -0
- package/mcp/__tests__/mcp-auth-storage.upstream-test.ts +47 -0
- package/mcp/__tests__/mcp-callback-server-unref.upstream-test.ts +264 -0
- package/mcp/__tests__/mcp-oauth-provider.upstream-test.ts +216 -0
- package/mcp/__tests__/mcp-panel-auth.upstream-test.ts +206 -0
- package/mcp/__tests__/mcp-panel-exclude-tools.upstream-test.ts +69 -0
- package/mcp/__tests__/mcp-panel-keybindings.upstream-test.ts +186 -0
- package/mcp/__tests__/npx-resolver.upstream-test.ts +54 -0
- package/mcp/__tests__/oauth-handler.upstream-test.ts +77 -0
- package/mcp/__tests__/onboarding-state.upstream-test.ts +52 -0
- package/mcp/__tests__/package-manifest.upstream-test.ts +22 -0
- package/mcp/__tests__/proxy-modes-auto-auth.upstream-test.ts +253 -0
- package/mcp/__tests__/proxy-modes-discovery.upstream-test.ts +84 -0
- package/mcp/__tests__/proxy-modes-manual-auth.upstream-test.ts +91 -0
- package/mcp/__tests__/proxy-modes-ui-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/sampling-handler.upstream-test.ts +285 -0
- package/mcp/__tests__/server-manager-http-auth.upstream-test.ts +144 -0
- package/mcp/__tests__/server-manager-sampling.upstream-test.ts +236 -0
- package/mcp/__tests__/tool-metadata.upstream-test.ts +111 -0
- package/mcp/__tests__/tool-result-renderer.upstream-test.ts +147 -0
- package/mcp/__tests__/ui-integration.upstream-test.ts +551 -0
- package/mcp/__tests__/ui-resource-handler.upstream-test.ts +303 -0
- package/mcp/__tests__/ui-server.upstream-test.ts +967 -0
- package/mcp/__tests__/ui-session-messages.upstream-test.ts +72 -0
- package/mcp/__tests__/ui-streaming.upstream-test.ts +543 -0
- package/mcp/agent-dir.ts +20 -0
- package/mcp/app-bridge.bundle.js +67 -0
- package/mcp/cli.js +184 -0
- package/mcp/commands.ts +422 -0
- package/mcp/config.ts +666 -0
- package/mcp/consent-manager.ts +64 -0
- package/mcp/direct-tools.ts +439 -0
- package/mcp/elicitation-handler.ts +347 -0
- package/mcp/errors.ts +219 -0
- package/mcp/glimpse-ui.ts +80 -0
- package/mcp/host-html-template.ts +427 -0
- package/mcp/index.ts +362 -0
- package/mcp/init.ts +362 -0
- package/mcp/lifecycle.ts +93 -0
- package/mcp/logger.ts +169 -0
- package/mcp/mcp-auth-flow.ts +559 -0
- package/mcp/mcp-auth-flow.upstream-test.ts +259 -0
- package/mcp/mcp-auth.ts +302 -0
- package/mcp/mcp-auth.upstream-test.ts +373 -0
- package/mcp/mcp-callback-server.ts +372 -0
- package/mcp/mcp-callback-server.upstream-test.ts +416 -0
- package/mcp/mcp-oauth-provider.ts +369 -0
- package/mcp/mcp-oauth-provider.upstream-test.ts +518 -0
- package/mcp/mcp-panel.ts +829 -0
- package/mcp/mcp-setup-panel.ts +580 -0
- package/mcp/metadata-cache.ts +201 -0
- package/mcp/notify.ts +111 -0
- package/mcp/npx-resolver.ts +424 -0
- package/mcp/oauth-handler.ts +57 -0
- package/mcp/onboarding-state.ts +68 -0
- package/mcp/package.json +106 -0
- package/mcp/panel-keys.ts +37 -0
- package/mcp/proxy-modes.ts +949 -0
- package/mcp/resource-tools.ts +17 -0
- package/mcp/sampling-handler.ts +268 -0
- package/mcp/server-manager.ts +545 -0
- package/mcp/state.ts +41 -0
- package/mcp/tool-metadata.ts +216 -0
- package/mcp/tool-registrar.ts +46 -0
- package/mcp/tool-result-renderer.ts +161 -0
- package/mcp/types.ts +448 -0
- package/mcp/ui-resource-handler.ts +146 -0
- package/mcp/ui-server.ts +623 -0
- package/mcp/ui-session.ts +386 -0
- package/mcp/ui-stream-types.ts +89 -0
- package/mcp/utils.ts +129 -0
- package/mcp/vitest.config.ts +14 -0
- package/migrate.ts +258 -0
- package/notification.ts +218 -0
- package/notifications-log.ts +83 -0
- package/package.json +20 -3
- package/status.ts +140 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
import type { AgentToolResult, ToolInfo } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { checkSync } from "recheck";
|
|
4
|
+
import type { McpExtensionState } from "./state.ts";
|
|
5
|
+
import type { ToolMetadata, McpContent } from "./types.ts";
|
|
6
|
+
import { getServerPrefix, parseUiPromptHandoff } from "./types.ts";
|
|
7
|
+
import { lazyConnect, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar } from "./init.ts";
|
|
8
|
+
import { buildToolMetadata, getToolNames, findToolByName, formatSchema } from "./tool-metadata.ts";
|
|
9
|
+
import { transformMcpContent } from "./tool-registrar.ts";
|
|
10
|
+
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.ts";
|
|
11
|
+
import { formatAuthRequiredMessage, truncateAtWord } from "./utils.ts";
|
|
12
|
+
import { authenticate, completeAuthFromInput, startAuth, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
13
|
+
|
|
14
|
+
type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
|
|
15
|
+
|
|
16
|
+
const MAX_REGEX_SEARCH_QUERY_LENGTH = 256;
|
|
17
|
+
const REGEX_SAFETY_CHECK_PARAMS = {
|
|
18
|
+
attackTimeout: 50,
|
|
19
|
+
incubationTimeout: 50,
|
|
20
|
+
timeout: 250,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
type AutoAuthResult =
|
|
24
|
+
| { status: "skipped" }
|
|
25
|
+
| { status: "success" }
|
|
26
|
+
| { status: "failed"; message: string };
|
|
27
|
+
|
|
28
|
+
function getAuthRequiredMessage(
|
|
29
|
+
state: McpExtensionState,
|
|
30
|
+
serverName: string,
|
|
31
|
+
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.`,
|
|
32
|
+
): string {
|
|
33
|
+
return formatAuthRequiredMessage(state.config, serverName, defaultMessage);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function getAuthFailedMessage(state: McpExtensionState, serverName: string, message: string): string {
|
|
37
|
+
const customGuidance = state.config.settings?.authRequiredMessage;
|
|
38
|
+
if (customGuidance) {
|
|
39
|
+
return `OAuth authentication failed for "${serverName}": ${message}. ${getAuthRequiredMessage(state, serverName)}`;
|
|
40
|
+
}
|
|
41
|
+
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.`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getRedirectPort(authorizationUrl: string): number | undefined {
|
|
45
|
+
try {
|
|
46
|
+
const redirectUri = new URL(authorizationUrl).searchParams.get("redirect_uri");
|
|
47
|
+
if (!redirectUri) return undefined;
|
|
48
|
+
const port = Number.parseInt(new URL(redirectUri).port, 10);
|
|
49
|
+
return Number.isInteger(port) ? port : undefined;
|
|
50
|
+
} catch {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatManualAuthInstructions(serverName: string, authorizationUrl: string): string {
|
|
56
|
+
const port = getRedirectPort(authorizationUrl);
|
|
57
|
+
const portNote = port
|
|
58
|
+
? `\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.`
|
|
59
|
+
: "";
|
|
60
|
+
|
|
61
|
+
return [
|
|
62
|
+
`MCP OAuth required for "${serverName}".`,
|
|
63
|
+
"",
|
|
64
|
+
"Open this URL in your local browser:",
|
|
65
|
+
"",
|
|
66
|
+
authorizationUrl,
|
|
67
|
+
"",
|
|
68
|
+
"After approving, copy the full redirected localhost URL from your browser address bar and send it back with:",
|
|
69
|
+
`mcp({ action: "auth-complete", server: "${serverName}", args: '{"redirectUrl":"PASTE_REDIRECT_URL_HERE"}' })`,
|
|
70
|
+
"",
|
|
71
|
+
"You can also pass just the `code` query parameter as `args: '{\"code\":\"PASTE_CODE_HERE\"}'`.",
|
|
72
|
+
portNote.trimEnd(),
|
|
73
|
+
].filter(Boolean).join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function attemptAutoAuth(
|
|
77
|
+
state: McpExtensionState,
|
|
78
|
+
serverName: string,
|
|
79
|
+
): Promise<AutoAuthResult> {
|
|
80
|
+
if (state.config.settings?.autoAuth !== true) {
|
|
81
|
+
return { status: "skipped" };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const definition = state.config.mcpServers[serverName];
|
|
85
|
+
if (!definition || !supportsOAuth(definition) || !definition.url) {
|
|
86
|
+
return { status: "skipped" };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const grantType = definition.oauth ? definition.oauth.grantType ?? "authorization_code" : "authorization_code";
|
|
90
|
+
if (!state.ui && grantType !== "client_credentials") {
|
|
91
|
+
return {
|
|
92
|
+
status: "failed",
|
|
93
|
+
message: getAuthRequiredMessage(
|
|
94
|
+
state,
|
|
95
|
+
serverName,
|
|
96
|
+
`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.`,
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await authenticate(serverName, definition.url, definition);
|
|
103
|
+
return { status: "success" };
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
return {
|
|
107
|
+
status: "failed",
|
|
108
|
+
message: getAuthFailedMessage(state, serverName, message),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function executeUiMessages(state: McpExtensionState): ProxyToolResult {
|
|
114
|
+
const sessions = state.completedUiSessions;
|
|
115
|
+
|
|
116
|
+
if (sessions.length === 0) {
|
|
117
|
+
return {
|
|
118
|
+
content: [{ type: "text" as const, text: "No UI session messages available." }],
|
|
119
|
+
details: { sessions: 0 },
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const output: string[] = [];
|
|
124
|
+
output.push(`UI Session Messages (${sessions.length} session${sessions.length > 1 ? "s" : ""}):\n`);
|
|
125
|
+
|
|
126
|
+
const allPrompts: string[] = [];
|
|
127
|
+
const allIntents = sessions.flatMap((session) => session.messages.intents);
|
|
128
|
+
const parsedHandoffs: Array<{ intent: string; params: Record<string, unknown>; raw: string }> = [];
|
|
129
|
+
|
|
130
|
+
for (const session of sessions) {
|
|
131
|
+
const timestamp = session.completedAt.toLocaleTimeString();
|
|
132
|
+
output.push(`\n## ${session.serverName} / ${session.toolName} (${timestamp}, ${session.reason})`);
|
|
133
|
+
|
|
134
|
+
const plainPrompts: string[] = [];
|
|
135
|
+
for (const prompt of session.messages.prompts) {
|
|
136
|
+
allPrompts.push(prompt);
|
|
137
|
+
const handoff = parseUiPromptHandoff(prompt);
|
|
138
|
+
if (handoff) {
|
|
139
|
+
parsedHandoffs.push(handoff);
|
|
140
|
+
} else {
|
|
141
|
+
plainPrompts.push(prompt);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (plainPrompts.length > 0) {
|
|
146
|
+
output.push("\n### Prompts:");
|
|
147
|
+
for (const prompt of plainPrompts) {
|
|
148
|
+
output.push(`- ${prompt}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const intentsForSession = [
|
|
153
|
+
...session.messages.intents,
|
|
154
|
+
...session.messages.prompts
|
|
155
|
+
.map((prompt) => parseUiPromptHandoff(prompt))
|
|
156
|
+
.filter((handoff): handoff is NonNullable<typeof handoff> => !!handoff)
|
|
157
|
+
.map((handoff) => ({ intent: handoff.intent, params: handoff.params })),
|
|
158
|
+
];
|
|
159
|
+
|
|
160
|
+
if (intentsForSession.length > 0) {
|
|
161
|
+
output.push("\n### Intents:");
|
|
162
|
+
for (const intent of intentsForSession) {
|
|
163
|
+
const params = intent.params ? ` (${JSON.stringify(intent.params)})` : "";
|
|
164
|
+
output.push(`- ${intent.intent}${params}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (session.messages.notifications.length > 0) {
|
|
169
|
+
output.push("\n### Notifications:");
|
|
170
|
+
for (const notification of session.messages.notifications) {
|
|
171
|
+
output.push(`- ${notification}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const count = sessions.length;
|
|
177
|
+
state.completedUiSessions = [];
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
content: [{ type: "text" as const, text: output.join("\n") }],
|
|
181
|
+
details: {
|
|
182
|
+
sessions: count,
|
|
183
|
+
prompts: allPrompts,
|
|
184
|
+
intents: [...allIntents, ...parsedHandoffs.map(({ intent, params }) => ({ intent, params }))],
|
|
185
|
+
handoffs: parsedHandoffs,
|
|
186
|
+
cleared: true,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function executeStatus(state: McpExtensionState): ProxyToolResult {
|
|
192
|
+
const servers: Array<{ name: string; status: string; toolCount: number; failedAgo: number | null }> = [];
|
|
193
|
+
|
|
194
|
+
for (const name of Object.keys(state.config.mcpServers)) {
|
|
195
|
+
const connection = state.manager.getConnection(name);
|
|
196
|
+
const metadata = state.toolMetadata.get(name);
|
|
197
|
+
const toolCount = metadata?.length ?? 0;
|
|
198
|
+
const failedAgo = getFailureAgeSeconds(state, name);
|
|
199
|
+
let status = "not connected";
|
|
200
|
+
if (connection?.status === "connected") {
|
|
201
|
+
status = "connected";
|
|
202
|
+
} else if (connection?.status === "needs-auth") {
|
|
203
|
+
status = "needs-auth";
|
|
204
|
+
} else if (failedAgo !== null) {
|
|
205
|
+
status = "failed";
|
|
206
|
+
} else if (metadata !== undefined) {
|
|
207
|
+
status = "cached";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
servers.push({ name, status, toolCount, failedAgo });
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const totalTools = servers.reduce((sum, s) => sum + s.toolCount, 0);
|
|
214
|
+
const connectedCount = servers.filter(s => s.status === "connected").length;
|
|
215
|
+
|
|
216
|
+
let text = `MCP: ${connectedCount}/${servers.length} servers, ${totalTools} tools\n\n`;
|
|
217
|
+
for (const server of servers) {
|
|
218
|
+
if (server.status === "connected") {
|
|
219
|
+
text += `✓ ${server.name} (${server.toolCount} tools)\n`;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (server.status === "needs-auth") {
|
|
223
|
+
text += `⚠ ${server.name} (needs auth)\n`;
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (server.status === "cached") {
|
|
227
|
+
text += `○ ${server.name} (${server.toolCount} tools, cached)\n`;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
if (server.status === "failed") {
|
|
231
|
+
text += `✗ ${server.name} (failed ${server.failedAgo ?? 0}s ago)\n`;
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
text += `○ ${server.name} (not connected)\n`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (servers.length > 0) {
|
|
238
|
+
text += `\nmcp({ server: "name" }) to list tools, mcp({ search: "..." }) to search`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
content: [{ type: "text" as const, text: text.trim() }],
|
|
243
|
+
details: { mode: "status", servers, totalTools, connectedCount },
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function executeAuthStart(state: McpExtensionState, serverName: string): Promise<ProxyToolResult> {
|
|
248
|
+
const definition = state.config.mcpServers[serverName];
|
|
249
|
+
if (!definition) {
|
|
250
|
+
return {
|
|
251
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
252
|
+
details: { mode: "auth-start", error: "not_found", server: serverName },
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!definition.url || !supportsOAuth(definition)) {
|
|
257
|
+
return {
|
|
258
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" is not configured for OAuth over HTTP.` }],
|
|
259
|
+
details: { mode: "auth-start", error: "oauth_not_supported", server: serverName },
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const { authorizationUrl } = await startAuth(serverName, definition.url, definition);
|
|
265
|
+
if (!authorizationUrl) {
|
|
266
|
+
return {
|
|
267
|
+
content: [{ type: "text" as const, text: `OAuth authentication successful for "${serverName}".` }],
|
|
268
|
+
details: { mode: "auth-start", server: serverName, authenticated: true },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: "text" as const, text: formatManualAuthInstructions(serverName, authorizationUrl) }],
|
|
274
|
+
details: { mode: "auth-start", server: serverName, authorizationUrl },
|
|
275
|
+
};
|
|
276
|
+
} catch (error) {
|
|
277
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
278
|
+
return {
|
|
279
|
+
content: [{ type: "text" as const, text: `Failed to start OAuth for "${serverName}": ${message}` }],
|
|
280
|
+
details: { mode: "auth-start", error: "auth_start_failed", server: serverName, message },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export async function executeAuthComplete(state: McpExtensionState, serverName: string, input: string): Promise<ProxyToolResult> {
|
|
286
|
+
if (!state.config.mcpServers[serverName]) {
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
289
|
+
details: { mode: "auth-complete", error: "not_found", server: serverName },
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const status = await completeAuthFromInput(serverName, input);
|
|
295
|
+
if (status !== "authenticated") {
|
|
296
|
+
return {
|
|
297
|
+
content: [{ type: "text" as const, text: `OAuth authentication did not complete for "${serverName}".` }],
|
|
298
|
+
details: { mode: "auth-complete", error: "not_authenticated", server: serverName, status },
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
await state.manager.close(serverName);
|
|
303
|
+
state.failureTracker.delete(serverName);
|
|
304
|
+
updateStatusBar(state);
|
|
305
|
+
return {
|
|
306
|
+
content: [{ type: "text" as const, text: `OAuth authentication successful for "${serverName}". Run mcp({ connect: "${serverName}" }) to connect with the new token.` }],
|
|
307
|
+
details: { mode: "auth-complete", server: serverName, authenticated: true },
|
|
308
|
+
};
|
|
309
|
+
} catch (error) {
|
|
310
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
311
|
+
return {
|
|
312
|
+
content: [{ type: "text" as const, text: `Failed to complete OAuth for "${serverName}": ${message}` }],
|
|
313
|
+
details: { mode: "auth-complete", error: "auth_complete_failed", server: serverName, message },
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function executeDescribe(state: McpExtensionState, toolName: string): ProxyToolResult {
|
|
319
|
+
let serverName: string | undefined;
|
|
320
|
+
let toolMeta: ToolMetadata | undefined;
|
|
321
|
+
|
|
322
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
323
|
+
const found = findToolByName(metadata, toolName);
|
|
324
|
+
if (found) {
|
|
325
|
+
serverName = server;
|
|
326
|
+
toolMeta = found;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!serverName || !toolMeta) {
|
|
332
|
+
return {
|
|
333
|
+
content: [{ type: "text" as const, text: `Tool "${toolName}" not found. Use mcp({ search: "..." }) to search.` }],
|
|
334
|
+
details: { mode: "describe", error: "tool_not_found", requestedTool: toolName },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let text = `${toolMeta.name}\n`;
|
|
339
|
+
text += `Server: ${serverName}\n`;
|
|
340
|
+
if (toolMeta.resourceUri) {
|
|
341
|
+
text += `Type: Resource (reads from ${toolMeta.resourceUri})\n`;
|
|
342
|
+
}
|
|
343
|
+
text += `\n${toolMeta.description || "(no description)"}\n`;
|
|
344
|
+
|
|
345
|
+
if (toolMeta.inputSchema && !toolMeta.resourceUri) {
|
|
346
|
+
text += `\nParameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
347
|
+
} else if (toolMeta.resourceUri) {
|
|
348
|
+
text += `\nNo parameters required (resource tool).`;
|
|
349
|
+
} else {
|
|
350
|
+
text += `\nNo parameters defined.`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
content: [{ type: "text" as const, text: text.trim() }],
|
|
355
|
+
details: { mode: "describe", tool: toolMeta, server: serverName },
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function executeSearch(
|
|
360
|
+
state: McpExtensionState,
|
|
361
|
+
query: string,
|
|
362
|
+
regex?: boolean,
|
|
363
|
+
server?: string,
|
|
364
|
+
includeSchemas?: boolean,
|
|
365
|
+
): ProxyToolResult {
|
|
366
|
+
const showSchemas = includeSchemas !== false;
|
|
367
|
+
|
|
368
|
+
const matches: Array<{ server: string; tool: ToolMetadata }> = [];
|
|
369
|
+
|
|
370
|
+
let pattern: RegExp;
|
|
371
|
+
try {
|
|
372
|
+
if (regex) {
|
|
373
|
+
if (query.length > MAX_REGEX_SEARCH_QUERY_LENGTH) {
|
|
374
|
+
return {
|
|
375
|
+
content: [{ type: "text" as const, text: `Regex query is too long; maximum length is ${MAX_REGEX_SEARCH_QUERY_LENGTH} characters.` }],
|
|
376
|
+
details: { mode: "search", error: "query_too_long", query, maxLength: MAX_REGEX_SEARCH_QUERY_LENGTH },
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
pattern = new RegExp(query, "i");
|
|
381
|
+
let safety;
|
|
382
|
+
try {
|
|
383
|
+
safety = checkSync(query, "i", REGEX_SAFETY_CHECK_PARAMS);
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
386
|
+
return {
|
|
387
|
+
content: [{ type: "text" as const, text: "Regex query rejected because safety analysis failed." }],
|
|
388
|
+
details: { mode: "search", error: "unsafe_pattern", query, reason: message },
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
if (safety.status !== "safe") {
|
|
392
|
+
return {
|
|
393
|
+
content: [{ type: "text" as const, text: `Regex query rejected as unsafe (${safety.status}).` }],
|
|
394
|
+
details: { mode: "search", error: "unsafe_pattern", query, safetyStatus: safety.status },
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
const terms = query.trim().split(/\s+/).filter(t => t.length > 0);
|
|
399
|
+
if (terms.length === 0) {
|
|
400
|
+
return {
|
|
401
|
+
content: [{ type: "text" as const, text: "Search query cannot be empty" }],
|
|
402
|
+
details: { mode: "search", error: "empty_query" },
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
const escaped = terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
406
|
+
pattern = new RegExp(escaped.join("|"), "i");
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
return {
|
|
410
|
+
content: [{ type: "text" as const, text: `Invalid regex: ${query}` }],
|
|
411
|
+
details: { mode: "search", error: "invalid_pattern", query },
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
for (const [serverName, metadata] of state.toolMetadata.entries()) {
|
|
416
|
+
if (server && serverName !== server) continue;
|
|
417
|
+
for (const tool of metadata) {
|
|
418
|
+
if (pattern.test(tool.name) || pattern.test(tool.description)) {
|
|
419
|
+
matches.push({
|
|
420
|
+
server: serverName,
|
|
421
|
+
tool,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const totalCount = matches.length;
|
|
428
|
+
|
|
429
|
+
if (totalCount === 0) {
|
|
430
|
+
const msg = server
|
|
431
|
+
? `No tools matching "${query}" in "${server}"`
|
|
432
|
+
: `No tools matching "${query}"`;
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: "text" as const, text: msg }],
|
|
435
|
+
details: { mode: "search", matches: [], count: 0, query },
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
let text = `Found ${totalCount} tool${totalCount === 1 ? "" : "s"} matching "${query}":\n\n`;
|
|
440
|
+
|
|
441
|
+
for (const match of matches) {
|
|
442
|
+
if (showSchemas) {
|
|
443
|
+
text += `${match.tool.name}\n`;
|
|
444
|
+
text += ` ${match.tool.description || "(no description)"}\n`;
|
|
445
|
+
if (match.tool.inputSchema && !match.tool.resourceUri) {
|
|
446
|
+
text += `\n Parameters:\n${formatSchema(match.tool.inputSchema, " ")}\n`;
|
|
447
|
+
} else if (match.tool.resourceUri) {
|
|
448
|
+
text += ` No parameters (resource tool).\n`;
|
|
449
|
+
}
|
|
450
|
+
text += "\n";
|
|
451
|
+
} else {
|
|
452
|
+
text += `- ${match.tool.name}`;
|
|
453
|
+
if (match.tool.description) {
|
|
454
|
+
text += ` - ${truncateAtWord(match.tool.description, 50)}`;
|
|
455
|
+
}
|
|
456
|
+
text += "\n";
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
return {
|
|
461
|
+
content: [{ type: "text" as const, text: text.trim() }],
|
|
462
|
+
details: {
|
|
463
|
+
mode: "search",
|
|
464
|
+
matches: matches.map(m => ({ server: m.server, tool: m.tool.name })),
|
|
465
|
+
count: totalCount,
|
|
466
|
+
query,
|
|
467
|
+
},
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function executeList(state: McpExtensionState, server: string): ProxyToolResult {
|
|
472
|
+
if (!state.config.mcpServers[server]) {
|
|
473
|
+
return {
|
|
474
|
+
content: [{ type: "text" as const, text: `Server "${server}" not found. Use mcp({}) to see available servers.` }],
|
|
475
|
+
details: { mode: "list", server, tools: [], count: 0, error: "not_found" },
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const metadata = state.toolMetadata.get(server);
|
|
480
|
+
const toolNames = metadata?.map(m => m.name) ?? [];
|
|
481
|
+
const connection = state.manager.getConnection(server);
|
|
482
|
+
|
|
483
|
+
if (toolNames.length === 0) {
|
|
484
|
+
if (connection?.status === "connected") {
|
|
485
|
+
return {
|
|
486
|
+
content: [{ type: "text" as const, text: `Server "${server}" has no tools.` }],
|
|
487
|
+
details: { mode: "list", server, tools: [], count: 0 },
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
if (metadata !== undefined) {
|
|
491
|
+
return {
|
|
492
|
+
content: [{ type: "text" as const, text: `Server "${server}" has no cached tools (not connected).` }],
|
|
493
|
+
details: { mode: "list", server, tools: [], count: 0, cached: true },
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
return {
|
|
497
|
+
content: [{ type: "text" as const, text: `Server "${server}" is configured but not connected. Use mcp({ connect: "${server}" }) or /mcp reconnect ${server} to retry.` }],
|
|
498
|
+
details: { mode: "list", server, tools: [], count: 0, error: "not_connected" },
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const cachedNote = connection?.status === "connected" ? "" : " (not connected, cached)";
|
|
503
|
+
let text = `${server} (${toolNames.length} tools${cachedNote}):\n\n`;
|
|
504
|
+
|
|
505
|
+
const descMap = new Map<string, string>();
|
|
506
|
+
if (metadata) {
|
|
507
|
+
for (const m of metadata) {
|
|
508
|
+
descMap.set(m.name, m.description);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
for (const tool of toolNames) {
|
|
513
|
+
const desc = descMap.get(tool) ?? "";
|
|
514
|
+
const truncated = truncateAtWord(desc, 50);
|
|
515
|
+
text += `- ${tool}`;
|
|
516
|
+
if (truncated) text += ` - ${truncated}`;
|
|
517
|
+
text += "\n";
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
content: [{ type: "text" as const, text: text.trim() }],
|
|
522
|
+
details: { mode: "list", server, tools: toolNames, count: toolNames.length },
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export async function executeConnect(state: McpExtensionState, serverName: string): Promise<ProxyToolResult> {
|
|
527
|
+
const definition = state.config.mcpServers[serverName];
|
|
528
|
+
if (!definition) {
|
|
529
|
+
return {
|
|
530
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
531
|
+
details: { mode: "connect", error: "not_found", server: serverName },
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
try {
|
|
536
|
+
if (state.ui) {
|
|
537
|
+
state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
|
|
538
|
+
}
|
|
539
|
+
let connection = await state.manager.connect(serverName, definition);
|
|
540
|
+
if (connection.status === "needs-auth") {
|
|
541
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
542
|
+
if (autoAuth.status === "failed") {
|
|
543
|
+
return {
|
|
544
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
545
|
+
details: { mode: "connect", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (autoAuth.status === "success") {
|
|
549
|
+
await state.manager.close(serverName);
|
|
550
|
+
connection = await state.manager.connect(serverName, definition);
|
|
551
|
+
}
|
|
552
|
+
if (connection.status === "needs-auth") {
|
|
553
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
554
|
+
return {
|
|
555
|
+
content: [{ type: "text" as const, text: message }],
|
|
556
|
+
details: { mode: "connect", error: "auth_required", server: serverName, message },
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
561
|
+
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
|
|
562
|
+
state.toolMetadata.set(serverName, metadata);
|
|
563
|
+
updateMetadataCache(state, serverName);
|
|
564
|
+
state.failureTracker.delete(serverName);
|
|
565
|
+
updateStatusBar(state);
|
|
566
|
+
return executeList(state, serverName);
|
|
567
|
+
} catch (error) {
|
|
568
|
+
state.failureTracker.set(serverName, Date.now());
|
|
569
|
+
updateStatusBar(state);
|
|
570
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
571
|
+
return {
|
|
572
|
+
content: [{ type: "text" as const, text: `Failed to connect to "${serverName}": ${message}` }],
|
|
573
|
+
details: { mode: "connect", error: "connect_failed", server: serverName, message },
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export async function executeCall(
|
|
579
|
+
state: McpExtensionState,
|
|
580
|
+
toolName: string,
|
|
581
|
+
args?: Record<string, unknown>,
|
|
582
|
+
serverOverride?: string,
|
|
583
|
+
getPiTools?: () => ToolInfo[],
|
|
584
|
+
): Promise<ProxyToolResult> {
|
|
585
|
+
let serverName: string | undefined = serverOverride;
|
|
586
|
+
let toolMeta: ToolMetadata | undefined;
|
|
587
|
+
let autoAuthAttempted = false;
|
|
588
|
+
const prefixMode = state.config.settings?.toolPrefix ?? "server";
|
|
589
|
+
|
|
590
|
+
if (serverName && !state.config.mcpServers[serverName]) {
|
|
591
|
+
return {
|
|
592
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
593
|
+
details: { mode: "call", error: "server_not_found", server: serverName },
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (serverName) {
|
|
598
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
599
|
+
} else {
|
|
600
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
601
|
+
const found = findToolByName(metadata, toolName);
|
|
602
|
+
if (found) {
|
|
603
|
+
serverName = server;
|
|
604
|
+
toolMeta = found;
|
|
605
|
+
break;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (serverName && !toolMeta) {
|
|
611
|
+
const connected = await lazyConnect(state, serverName);
|
|
612
|
+
if (connected) {
|
|
613
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
614
|
+
} else {
|
|
615
|
+
const needsAuthConnection = state.manager.getConnection(serverName);
|
|
616
|
+
if (needsAuthConnection?.status === "needs-auth") {
|
|
617
|
+
if (!autoAuthAttempted) {
|
|
618
|
+
autoAuthAttempted = true;
|
|
619
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
620
|
+
if (autoAuth.status === "failed") {
|
|
621
|
+
return {
|
|
622
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
623
|
+
details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
if (autoAuth.status === "success") {
|
|
627
|
+
await state.manager.close(serverName);
|
|
628
|
+
state.failureTracker.delete(serverName);
|
|
629
|
+
const connectedAfterAuth = await lazyConnect(state, serverName);
|
|
630
|
+
if (connectedAfterAuth) {
|
|
631
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
632
|
+
if (!toolMeta) {
|
|
633
|
+
return {
|
|
634
|
+
content: [{ type: "text" as const, text: `Tool "${toolName}" not found on "${serverName}" after reconnect.` }],
|
|
635
|
+
details: { mode: "call", error: "tool_not_found_after_reconnect", requestedTool: toolName },
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (!toolMeta && state.manager.getConnection(serverName)?.status === "needs-auth") {
|
|
643
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
644
|
+
return {
|
|
645
|
+
content: [{ type: "text" as const, text: message }],
|
|
646
|
+
details: { mode: "call", error: "auth_required", server: serverName, message },
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!toolMeta) {
|
|
652
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
653
|
+
if (failedAgo !== null) {
|
|
654
|
+
return {
|
|
655
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
656
|
+
details: { mode: "call", error: "server_backoff", server: serverName },
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
let prefixMatchedServer: string | undefined;
|
|
664
|
+
|
|
665
|
+
if (!serverName && !toolMeta && prefixMode !== "none") {
|
|
666
|
+
const candidates = Object.keys(state.config.mcpServers)
|
|
667
|
+
.map(name => ({ name, prefix: getServerPrefix(name, prefixMode) }))
|
|
668
|
+
.filter(c => c.prefix && toolName.startsWith(c.prefix + "_"))
|
|
669
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
670
|
+
|
|
671
|
+
for (const { name: configuredServer } of candidates) {
|
|
672
|
+
const existingConnection = state.manager.getConnection(configuredServer);
|
|
673
|
+
const failedAgo = getFailureAgeSeconds(state, configuredServer);
|
|
674
|
+
if (failedAgo !== null && existingConnection?.status !== "needs-auth") continue;
|
|
675
|
+
|
|
676
|
+
let connected = await lazyConnect(state, configuredServer);
|
|
677
|
+
if (!connected && state.manager.getConnection(configuredServer)?.status === "needs-auth" && !autoAuthAttempted) {
|
|
678
|
+
autoAuthAttempted = true;
|
|
679
|
+
const autoAuth = await attemptAutoAuth(state, configuredServer);
|
|
680
|
+
if (autoAuth.status === "failed") {
|
|
681
|
+
return {
|
|
682
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
683
|
+
details: { mode: "call", error: "auth_required", server: configuredServer, message: autoAuth.message },
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
if (autoAuth.status === "success") {
|
|
687
|
+
await state.manager.close(configuredServer);
|
|
688
|
+
state.failureTracker.delete(configuredServer);
|
|
689
|
+
connected = await lazyConnect(state, configuredServer);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (!connected) continue;
|
|
694
|
+
if (!prefixMatchedServer) prefixMatchedServer = configuredServer;
|
|
695
|
+
toolMeta = findToolByName(state.toolMetadata.get(configuredServer), toolName);
|
|
696
|
+
if (toolMeta) {
|
|
697
|
+
serverName = configuredServer;
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (!serverName || !toolMeta) {
|
|
704
|
+
const nativeTool = !serverOverride
|
|
705
|
+
? getPiTools?.().find((tool) => tool.name === toolName && tool.name !== "mcp")
|
|
706
|
+
: undefined;
|
|
707
|
+
if (nativeTool) {
|
|
708
|
+
return {
|
|
709
|
+
content: [{ type: "text" as const, text: `"${toolName}" is a native Pi tool. Call ${toolName} directly instead of using mcp({ tool: "${toolName}" }).` }],
|
|
710
|
+
details: { mode: "call", error: "native_tool", requestedTool: toolName },
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
const hintServer = serverName ?? prefixMatchedServer;
|
|
715
|
+
const available = hintServer ? getToolNames(state, hintServer) : [];
|
|
716
|
+
let msg = `Tool "${toolName}" not found.`;
|
|
717
|
+
if (available.length > 0) {
|
|
718
|
+
msg += ` Server "${hintServer}" has: ${available.join(", ")}`;
|
|
719
|
+
} else {
|
|
720
|
+
msg += ` Use mcp({ search: "..." }) to search.`;
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
content: [{ type: "text" as const, text: msg }],
|
|
724
|
+
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer },
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
let connection = state.manager.getConnection(serverName);
|
|
729
|
+
if (connection?.status === "needs-auth") {
|
|
730
|
+
if (!autoAuthAttempted) {
|
|
731
|
+
autoAuthAttempted = true;
|
|
732
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
733
|
+
if (autoAuth.status === "failed") {
|
|
734
|
+
return {
|
|
735
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
736
|
+
details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
if (autoAuth.status === "success") {
|
|
740
|
+
await state.manager.close(serverName);
|
|
741
|
+
state.failureTracker.delete(serverName);
|
|
742
|
+
connection = state.manager.getConnection(serverName);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (connection?.status === "needs-auth") {
|
|
747
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
748
|
+
return {
|
|
749
|
+
content: [{ type: "text" as const, text: message }],
|
|
750
|
+
details: { mode: "call", error: "auth_required", server: serverName, message },
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (!connection || connection.status !== "connected") {
|
|
755
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
756
|
+
if (failedAgo !== null) {
|
|
757
|
+
return {
|
|
758
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
759
|
+
details: { mode: "call", error: "server_backoff", server: serverName },
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const definition = state.config.mcpServers[serverName];
|
|
764
|
+
if (!definition) {
|
|
765
|
+
return {
|
|
766
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not connected` }],
|
|
767
|
+
details: { mode: "call", error: "server_not_connected", server: serverName },
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
try {
|
|
772
|
+
if (state.ui) {
|
|
773
|
+
state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
|
|
774
|
+
}
|
|
775
|
+
connection = await state.manager.connect(serverName, definition);
|
|
776
|
+
if (connection.status === "needs-auth") {
|
|
777
|
+
if (!autoAuthAttempted) {
|
|
778
|
+
autoAuthAttempted = true;
|
|
779
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
780
|
+
if (autoAuth.status === "failed") {
|
|
781
|
+
return {
|
|
782
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
783
|
+
details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
if (autoAuth.status === "success") {
|
|
787
|
+
await state.manager.close(serverName);
|
|
788
|
+
connection = await state.manager.connect(serverName, definition);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
if (connection.status === "needs-auth") {
|
|
793
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
794
|
+
return {
|
|
795
|
+
content: [{ type: "text" as const, text: message }],
|
|
796
|
+
details: { mode: "call", error: "auth_required", server: serverName, message },
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
state.failureTracker.delete(serverName);
|
|
801
|
+
updateServerMetadata(state, serverName);
|
|
802
|
+
updateMetadataCache(state, serverName);
|
|
803
|
+
updateStatusBar(state);
|
|
804
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
805
|
+
if (!toolMeta) {
|
|
806
|
+
const available = getToolNames(state, serverName);
|
|
807
|
+
const hint = available.length > 0
|
|
808
|
+
? `Available tools on "${serverName}": ${available.join(", ")}`
|
|
809
|
+
: `Server "${serverName}" has no tools.`;
|
|
810
|
+
return {
|
|
811
|
+
content: [{ type: "text" as const, text: `Tool "${toolName}" not found on "${serverName}" after reconnect. ${hint}` }],
|
|
812
|
+
details: { mode: "call", error: "tool_not_found_after_reconnect", requestedTool: toolName },
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
} catch (error) {
|
|
816
|
+
state.failureTracker.set(serverName, Date.now());
|
|
817
|
+
updateStatusBar(state);
|
|
818
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
819
|
+
return {
|
|
820
|
+
content: [{ type: "text" as const, text: `Failed to connect to "${serverName}": ${message}` }],
|
|
821
|
+
details: { mode: "call", error: "connect_failed", message },
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
let uiSession: UiSessionRuntime | null = null;
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
state.manager.touch(serverName);
|
|
830
|
+
state.manager.incrementInFlight(serverName);
|
|
831
|
+
|
|
832
|
+
if (toolMeta.resourceUri) {
|
|
833
|
+
const result = await connection.client.readResource({ uri: toolMeta.resourceUri });
|
|
834
|
+
const content = (result.contents ?? []).map(c => ({
|
|
835
|
+
type: "text" as const,
|
|
836
|
+
text: "text" in c ? c.text : ("blob" in c ? `[Binary data: ${(c as { mimeType?: string }).mimeType ?? "unknown"}]` : JSON.stringify(c)),
|
|
837
|
+
}));
|
|
838
|
+
return {
|
|
839
|
+
content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty resource)" }],
|
|
840
|
+
details: { mode: "call", resourceUri: toolMeta.resourceUri, server: serverName },
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
uiSession = toolMeta.uiResourceUri
|
|
845
|
+
? await maybeStartUiSession(state, {
|
|
846
|
+
serverName,
|
|
847
|
+
toolName: toolMeta.originalName,
|
|
848
|
+
toolArgs: args ?? {},
|
|
849
|
+
uiResourceUri: toolMeta.uiResourceUri,
|
|
850
|
+
streamMode: toolMeta.uiStreamMode,
|
|
851
|
+
})
|
|
852
|
+
: null;
|
|
853
|
+
|
|
854
|
+
const resultPromise = connection.client.callTool({
|
|
855
|
+
name: toolMeta.originalName,
|
|
856
|
+
arguments: args ?? {},
|
|
857
|
+
_meta: uiSession?.requestMeta,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
if (toolMeta.uiResourceUri) {
|
|
861
|
+
const result = await resultPromise;
|
|
862
|
+
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/sdk/types.js").CallToolResult);
|
|
863
|
+
const mcpContent = (result.content ?? []) as McpContent[];
|
|
864
|
+
const content = transformMcpContent(mcpContent);
|
|
865
|
+
|
|
866
|
+
const mcpText = content
|
|
867
|
+
.filter((c) => c.type === "text")
|
|
868
|
+
.map((c) => (c as { text: string }).text)
|
|
869
|
+
.join("\n");
|
|
870
|
+
|
|
871
|
+
if (result.isError) {
|
|
872
|
+
let errorWithSchema = `Error: ${mcpText || "Tool execution failed"}`;
|
|
873
|
+
if (toolMeta.inputSchema) {
|
|
874
|
+
errorWithSchema += `\n\nExpected parameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
875
|
+
}
|
|
876
|
+
return {
|
|
877
|
+
content: [{ type: "text" as const, text: errorWithSchema }],
|
|
878
|
+
details: { mode: "call", error: "tool_error", mcpResult: result },
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const resultText = mcpText || "(empty result)";
|
|
883
|
+
const uiMessage = uiSession?.reused
|
|
884
|
+
? "Updated the open UI."
|
|
885
|
+
: "📺 Interactive UI is now open in your browser. I'll respond to your prompts and intents as you interact with it.";
|
|
886
|
+
return {
|
|
887
|
+
content: [{ type: "text" as const, text: `${resultText}\n\n${uiMessage}` }],
|
|
888
|
+
details: { mode: "call", mcpResult: result, server: serverName, tool: toolMeta.originalName, uiOpen: true },
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const result = await resultPromise;
|
|
893
|
+
|
|
894
|
+
const mcpContent = (result.content ?? []) as McpContent[];
|
|
895
|
+
const content = transformMcpContent(mcpContent);
|
|
896
|
+
|
|
897
|
+
if (result.isError) {
|
|
898
|
+
const errorText = content
|
|
899
|
+
.filter((c) => c.type === "text")
|
|
900
|
+
.map((c) => (c as { text: string }).text)
|
|
901
|
+
.join("\n") || "Tool execution failed";
|
|
902
|
+
|
|
903
|
+
let errorWithSchema = `Error: ${errorText}`;
|
|
904
|
+
if (toolMeta.inputSchema) {
|
|
905
|
+
errorWithSchema += `\n\nExpected parameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
return {
|
|
909
|
+
content: [{ type: "text" as const, text: errorWithSchema }],
|
|
910
|
+
details: { mode: "call", error: "tool_error", mcpResult: result },
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return {
|
|
915
|
+
content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }],
|
|
916
|
+
details: { mode: "call", mcpResult: result, server: serverName, tool: toolMeta.originalName },
|
|
917
|
+
};
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if (error instanceof UrlElicitationRequiredError) {
|
|
920
|
+
const action = await state.manager.handleUrlElicitationRequired(serverName, error);
|
|
921
|
+
const message = action === "accept"
|
|
922
|
+
? "The original MCP tool did not run. Complete the opened browser interaction, then retry the tool."
|
|
923
|
+
: `The URL interaction was ${action === "decline" ? "declined" : "cancelled"}.`;
|
|
924
|
+
uiSession?.sendToolCancelled(message);
|
|
925
|
+
return {
|
|
926
|
+
content: [{ type: "text" as const, text: message }],
|
|
927
|
+
details: { mode: "call", error: "url_elicitation_required", server: serverName, action },
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
931
|
+
uiSession?.sendToolCancelled(message);
|
|
932
|
+
|
|
933
|
+
let errorWithSchema = `Failed to call tool: ${message}`;
|
|
934
|
+
if (toolMeta.inputSchema) {
|
|
935
|
+
errorWithSchema += `\n\nExpected parameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return {
|
|
939
|
+
content: [{ type: "text" as const, text: errorWithSchema }],
|
|
940
|
+
details: { mode: "call", error: "call_failed", message },
|
|
941
|
+
};
|
|
942
|
+
} finally {
|
|
943
|
+
if (uiSession?.reused) {
|
|
944
|
+
uiSession.close();
|
|
945
|
+
}
|
|
946
|
+
state.manager.decrementInFlight(serverName);
|
|
947
|
+
state.manager.touch(serverName);
|
|
948
|
+
}
|
|
949
|
+
}
|