@aexol/spectral 0.2.5 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +10 -47
- package/dist/mcp/agent-dir.js +18 -0
- package/dist/mcp/app-bridge.bundle.js +67 -0
- package/dist/mcp/commands.js +263 -0
- package/dist/mcp/config.js +532 -0
- package/dist/mcp/consent-manager.js +59 -0
- package/dist/mcp/direct-tools.js +354 -0
- package/dist/mcp/errors.js +165 -0
- package/dist/mcp/glimpse-ui.js +67 -0
- package/dist/mcp/host-html-template.js +412 -0
- package/dist/mcp/index.js +291 -0
- package/dist/mcp/init.js +280 -0
- package/dist/mcp/lifecycle.js +79 -0
- package/dist/mcp/logger.js +130 -0
- package/dist/mcp/mcp-auth-flow.js +283 -0
- package/dist/mcp/mcp-auth.js +226 -0
- package/dist/mcp/mcp-callback-server.js +225 -0
- package/dist/mcp/mcp-oauth-provider.js +243 -0
- package/dist/mcp/mcp-panel.js +646 -0
- package/dist/mcp/mcp-setup-panel.js +485 -0
- package/dist/mcp/metadata-cache.js +158 -0
- package/dist/mcp/npx-resolver.js +385 -0
- package/dist/mcp/oauth-handler.js +54 -0
- package/dist/mcp/onboarding-state.js +56 -0
- package/dist/mcp/proxy-modes.js +714 -0
- package/dist/mcp/resource-tools.js +14 -0
- package/dist/mcp/sampling-handler.js +206 -0
- package/dist/mcp/server-manager.js +301 -0
- package/dist/mcp/state.js +1 -0
- package/dist/mcp/tool-metadata.js +128 -0
- package/dist/mcp/tool-registrar.js +43 -0
- package/dist/mcp/types.js +93 -0
- package/dist/mcp/ui-resource-handler.js +113 -0
- package/dist/mcp/ui-server.js +522 -0
- package/dist/mcp/ui-session.js +306 -0
- package/dist/mcp/ui-stream-types.js +58 -0
- package/dist/mcp/utils.js +104 -0
- package/dist/mcp/vitest.config.js +13 -0
- package/dist/server/pi-bridge.js +9 -30
- package/package.json +6 -3
|
@@ -0,0 +1,714 @@
|
|
|
1
|
+
import { getServerPrefix, parseUiPromptHandoff } from "./types.js";
|
|
2
|
+
import { lazyConnect, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar } from "./init.js";
|
|
3
|
+
import { buildToolMetadata, getToolNames, findToolByName, formatSchema } from "./tool-metadata.js";
|
|
4
|
+
import { transformMcpContent } from "./tool-registrar.js";
|
|
5
|
+
import { maybeStartUiSession } from "./ui-session.js";
|
|
6
|
+
import { formatAuthRequiredMessage, truncateAtWord } from "./utils.js";
|
|
7
|
+
import { authenticate, supportsOAuth } from "./mcp-auth-flow.js";
|
|
8
|
+
function getAuthRequiredMessage(state, serverName, defaultMessage = `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} first.`) {
|
|
9
|
+
return formatAuthRequiredMessage(state.config, serverName, defaultMessage);
|
|
10
|
+
}
|
|
11
|
+
function getAuthFailedMessage(state, serverName, message) {
|
|
12
|
+
const customGuidance = state.config.settings?.authRequiredMessage;
|
|
13
|
+
if (customGuidance) {
|
|
14
|
+
return `OAuth authentication failed for "${serverName}": ${message}. ${getAuthRequiredMessage(state, serverName)}`;
|
|
15
|
+
}
|
|
16
|
+
return `OAuth authentication failed for "${serverName}": ${message}. Run /mcp-auth ${serverName} first.`;
|
|
17
|
+
}
|
|
18
|
+
async function attemptAutoAuth(state, serverName) {
|
|
19
|
+
if (state.config.settings?.autoAuth !== true) {
|
|
20
|
+
return { status: "skipped" };
|
|
21
|
+
}
|
|
22
|
+
const definition = state.config.mcpServers[serverName];
|
|
23
|
+
if (!definition || !supportsOAuth(definition) || !definition.url) {
|
|
24
|
+
return { status: "skipped" };
|
|
25
|
+
}
|
|
26
|
+
const oauthConfig = definition.oauth;
|
|
27
|
+
const grantType = oauthConfig?.grantType ?? "authorization_code";
|
|
28
|
+
if (!state.ui && grantType !== "client_credentials") {
|
|
29
|
+
return {
|
|
30
|
+
status: "failed",
|
|
31
|
+
message: getAuthRequiredMessage(state, serverName, `Server "${serverName}" requires OAuth authentication. Run /mcp-auth ${serverName} in an interactive session.`),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await authenticate(serverName, definition.url, definition);
|
|
36
|
+
return { status: "success" };
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
return {
|
|
41
|
+
status: "failed",
|
|
42
|
+
message: getAuthFailedMessage(state, serverName, message),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export function executeUiMessages(state) {
|
|
47
|
+
const sessions = state.completedUiSessions;
|
|
48
|
+
if (sessions.length === 0) {
|
|
49
|
+
return {
|
|
50
|
+
content: [{ type: "text", text: "No UI session messages available." }],
|
|
51
|
+
details: { sessions: 0 },
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
const output = [];
|
|
55
|
+
output.push(`UI Session Messages (${sessions.length} session${sessions.length > 1 ? "s" : ""}):\n`);
|
|
56
|
+
const allPrompts = [];
|
|
57
|
+
const allIntents = sessions.flatMap((session) => session.messages.intents);
|
|
58
|
+
const parsedHandoffs = [];
|
|
59
|
+
for (const session of sessions) {
|
|
60
|
+
const timestamp = session.completedAt.toLocaleTimeString();
|
|
61
|
+
output.push(`\n## ${session.serverName} / ${session.toolName} (${timestamp}, ${session.reason})`);
|
|
62
|
+
const plainPrompts = [];
|
|
63
|
+
for (const prompt of session.messages.prompts) {
|
|
64
|
+
allPrompts.push(prompt);
|
|
65
|
+
const handoff = parseUiPromptHandoff(prompt);
|
|
66
|
+
if (handoff) {
|
|
67
|
+
parsedHandoffs.push(handoff);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
plainPrompts.push(prompt);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (plainPrompts.length > 0) {
|
|
74
|
+
output.push("\n### Prompts:");
|
|
75
|
+
for (const prompt of plainPrompts) {
|
|
76
|
+
output.push(`- ${prompt}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const intentsForSession = [
|
|
80
|
+
...session.messages.intents,
|
|
81
|
+
...session.messages.prompts
|
|
82
|
+
.map((prompt) => parseUiPromptHandoff(prompt))
|
|
83
|
+
.filter((handoff) => !!handoff)
|
|
84
|
+
.map((handoff) => ({ intent: handoff.intent, params: handoff.params })),
|
|
85
|
+
];
|
|
86
|
+
if (intentsForSession.length > 0) {
|
|
87
|
+
output.push("\n### Intents:");
|
|
88
|
+
for (const intent of intentsForSession) {
|
|
89
|
+
const params = intent.params ? ` (${JSON.stringify(intent.params)})` : "";
|
|
90
|
+
output.push(`- ${intent.intent}${params}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (session.messages.notifications.length > 0) {
|
|
94
|
+
output.push("\n### Notifications:");
|
|
95
|
+
for (const notification of session.messages.notifications) {
|
|
96
|
+
output.push(`- ${notification}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const count = sessions.length;
|
|
101
|
+
state.completedUiSessions = [];
|
|
102
|
+
return {
|
|
103
|
+
content: [{ type: "text", text: output.join("\n") }],
|
|
104
|
+
details: {
|
|
105
|
+
sessions: count,
|
|
106
|
+
prompts: allPrompts,
|
|
107
|
+
intents: [...allIntents, ...parsedHandoffs.map(({ intent, params }) => ({ intent, params }))],
|
|
108
|
+
handoffs: parsedHandoffs,
|
|
109
|
+
cleared: true,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function executeStatus(state) {
|
|
114
|
+
const servers = [];
|
|
115
|
+
for (const name of Object.keys(state.config.mcpServers)) {
|
|
116
|
+
const connection = state.manager.getConnection(name);
|
|
117
|
+
const metadata = state.toolMetadata.get(name);
|
|
118
|
+
const toolCount = metadata?.length ?? 0;
|
|
119
|
+
const failedAgo = getFailureAgeSeconds(state, name);
|
|
120
|
+
let status = "not connected";
|
|
121
|
+
if (connection?.status === "connected") {
|
|
122
|
+
status = "connected";
|
|
123
|
+
}
|
|
124
|
+
else if (connection?.status === "needs-auth") {
|
|
125
|
+
status = "needs-auth";
|
|
126
|
+
}
|
|
127
|
+
else if (failedAgo !== null) {
|
|
128
|
+
status = "failed";
|
|
129
|
+
}
|
|
130
|
+
else if (metadata !== undefined) {
|
|
131
|
+
status = "cached";
|
|
132
|
+
}
|
|
133
|
+
servers.push({ name, status, toolCount, failedAgo });
|
|
134
|
+
}
|
|
135
|
+
const totalTools = servers.reduce((sum, s) => sum + s.toolCount, 0);
|
|
136
|
+
const connectedCount = servers.filter(s => s.status === "connected").length;
|
|
137
|
+
let text = `MCP: ${connectedCount}/${servers.length} servers, ${totalTools} tools\n\n`;
|
|
138
|
+
for (const server of servers) {
|
|
139
|
+
if (server.status === "connected") {
|
|
140
|
+
text += `✓ ${server.name} (${server.toolCount} tools)\n`;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (server.status === "needs-auth") {
|
|
144
|
+
text += `⚠ ${server.name} (needs auth)\n`;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (server.status === "cached") {
|
|
148
|
+
text += `○ ${server.name} (${server.toolCount} tools, cached)\n`;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (server.status === "failed") {
|
|
152
|
+
text += `✗ ${server.name} (failed ${server.failedAgo ?? 0}s ago)\n`;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
text += `○ ${server.name} (not connected)\n`;
|
|
156
|
+
}
|
|
157
|
+
if (servers.length > 0) {
|
|
158
|
+
text += `\nmcp({ server: "name" }) to list tools, mcp({ search: "..." }) to search`;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
content: [{ type: "text", text: text.trim() }],
|
|
162
|
+
details: { mode: "status", servers, totalTools, connectedCount },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
export function executeDescribe(state, toolName) {
|
|
166
|
+
let serverName;
|
|
167
|
+
let toolMeta;
|
|
168
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
169
|
+
const found = findToolByName(metadata, toolName);
|
|
170
|
+
if (found) {
|
|
171
|
+
serverName = server;
|
|
172
|
+
toolMeta = found;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (!serverName || !toolMeta) {
|
|
177
|
+
return {
|
|
178
|
+
content: [{ type: "text", text: `Tool "${toolName}" not found. Use mcp({ search: "..." }) to search.` }],
|
|
179
|
+
details: { mode: "describe", error: "tool_not_found", requestedTool: toolName },
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
let text = `${toolMeta.name}\n`;
|
|
183
|
+
text += `Server: ${serverName}\n`;
|
|
184
|
+
if (toolMeta.resourceUri) {
|
|
185
|
+
text += `Type: Resource (reads from ${toolMeta.resourceUri})\n`;
|
|
186
|
+
}
|
|
187
|
+
text += `\n${toolMeta.description || "(no description)"}\n`;
|
|
188
|
+
if (toolMeta.inputSchema && !toolMeta.resourceUri) {
|
|
189
|
+
text += `\nParameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
190
|
+
}
|
|
191
|
+
else if (toolMeta.resourceUri) {
|
|
192
|
+
text += `\nNo parameters required (resource tool).`;
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
text += `\nNo parameters defined.`;
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
content: [{ type: "text", text: text.trim() }],
|
|
199
|
+
details: { mode: "describe", tool: toolMeta, server: serverName },
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
export function executeSearch(state, query, regex, server, includeSchemas) {
|
|
203
|
+
const showSchemas = includeSchemas !== false;
|
|
204
|
+
const matches = [];
|
|
205
|
+
let pattern;
|
|
206
|
+
try {
|
|
207
|
+
if (regex) {
|
|
208
|
+
pattern = new RegExp(query, "i");
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const terms = query.trim().split(/\s+/).filter(t => t.length > 0);
|
|
212
|
+
if (terms.length === 0) {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: "Search query cannot be empty" }],
|
|
215
|
+
details: { mode: "search", error: "empty_query" },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
const escaped = terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
219
|
+
pattern = new RegExp(escaped.join("|"), "i");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
return {
|
|
224
|
+
content: [{ type: "text", text: `Invalid regex: ${query}` }],
|
|
225
|
+
details: { mode: "search", error: "invalid_pattern", query },
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
for (const [serverName, metadata] of state.toolMetadata.entries()) {
|
|
229
|
+
if (server && serverName !== server)
|
|
230
|
+
continue;
|
|
231
|
+
for (const tool of metadata) {
|
|
232
|
+
if (pattern.test(tool.name) || pattern.test(tool.description)) {
|
|
233
|
+
matches.push({
|
|
234
|
+
server: serverName,
|
|
235
|
+
tool,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const totalCount = matches.length;
|
|
241
|
+
if (totalCount === 0) {
|
|
242
|
+
const msg = server
|
|
243
|
+
? `No tools matching "${query}" in "${server}"`
|
|
244
|
+
: `No tools matching "${query}"`;
|
|
245
|
+
return {
|
|
246
|
+
content: [{ type: "text", text: msg }],
|
|
247
|
+
details: { mode: "search", matches: [], count: 0, query },
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
let text = `Found ${totalCount} tool${totalCount === 1 ? "" : "s"} matching "${query}":\n\n`;
|
|
251
|
+
for (const match of matches) {
|
|
252
|
+
if (showSchemas) {
|
|
253
|
+
text += `${match.tool.name}\n`;
|
|
254
|
+
text += ` ${match.tool.description || "(no description)"}\n`;
|
|
255
|
+
if (match.tool.inputSchema && !match.tool.resourceUri) {
|
|
256
|
+
text += `\n Parameters:\n${formatSchema(match.tool.inputSchema, " ")}\n`;
|
|
257
|
+
}
|
|
258
|
+
else if (match.tool.resourceUri) {
|
|
259
|
+
text += ` No parameters (resource tool).\n`;
|
|
260
|
+
}
|
|
261
|
+
text += "\n";
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
text += `- ${match.tool.name}`;
|
|
265
|
+
if (match.tool.description) {
|
|
266
|
+
text += ` - ${truncateAtWord(match.tool.description, 50)}`;
|
|
267
|
+
}
|
|
268
|
+
text += "\n";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
content: [{ type: "text", text: text.trim() }],
|
|
273
|
+
details: {
|
|
274
|
+
mode: "search",
|
|
275
|
+
matches: matches.map(m => ({ server: m.server, tool: m.tool.name })),
|
|
276
|
+
count: totalCount,
|
|
277
|
+
query,
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
export function executeList(state, server) {
|
|
282
|
+
if (!state.config.mcpServers[server]) {
|
|
283
|
+
return {
|
|
284
|
+
content: [{ type: "text", text: `Server "${server}" not found. Use mcp({}) to see available servers.` }],
|
|
285
|
+
details: { mode: "list", server, tools: [], count: 0, error: "not_found" },
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const metadata = state.toolMetadata.get(server);
|
|
289
|
+
const toolNames = metadata?.map(m => m.name) ?? [];
|
|
290
|
+
const connection = state.manager.getConnection(server);
|
|
291
|
+
if (toolNames.length === 0) {
|
|
292
|
+
if (connection?.status === "connected") {
|
|
293
|
+
return {
|
|
294
|
+
content: [{ type: "text", text: `Server "${server}" has no tools.` }],
|
|
295
|
+
details: { mode: "list", server, tools: [], count: 0 },
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (metadata !== undefined) {
|
|
299
|
+
return {
|
|
300
|
+
content: [{ type: "text", text: `Server "${server}" has no cached tools (not connected).` }],
|
|
301
|
+
details: { mode: "list", server, tools: [], count: 0, cached: true },
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: "text", text: `Server "${server}" is configured but not connected. Use mcp({ restart: "${server}" }) to force restart it.` }],
|
|
306
|
+
details: { mode: "list", server, tools: [], count: 0, error: "not_connected" },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const cachedNote = connection?.status === "connected" ? "" : " (not connected, cached)";
|
|
310
|
+
let text = `${server} (${toolNames.length} tools${cachedNote}):\n\n`;
|
|
311
|
+
const descMap = new Map();
|
|
312
|
+
if (metadata) {
|
|
313
|
+
for (const m of metadata) {
|
|
314
|
+
descMap.set(m.name, m.description);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
for (const tool of toolNames) {
|
|
318
|
+
const desc = descMap.get(tool) ?? "";
|
|
319
|
+
const truncated = truncateAtWord(desc, 50);
|
|
320
|
+
text += `- ${tool}`;
|
|
321
|
+
if (truncated)
|
|
322
|
+
text += ` - ${truncated}`;
|
|
323
|
+
text += "\n";
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text", text: text.trim() }],
|
|
327
|
+
details: { mode: "list", server, tools: toolNames, count: toolNames.length },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
export async function executeConnect(state, serverName) {
|
|
331
|
+
const definition = state.config.mcpServers[serverName];
|
|
332
|
+
if (!definition) {
|
|
333
|
+
return {
|
|
334
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
335
|
+
details: { mode: "connect", error: "not_found", server: serverName },
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
if (state.ui) {
|
|
340
|
+
state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
|
|
341
|
+
}
|
|
342
|
+
// Force close any stale/dead connection before reconnecting.
|
|
343
|
+
// The connect() method returns cached connections that may be dead
|
|
344
|
+
// (e.g., after the server process was killed by pkill).
|
|
345
|
+
await state.manager.close(serverName);
|
|
346
|
+
let connection = await state.manager.connect(serverName, definition);
|
|
347
|
+
if (connection.status === "needs-auth") {
|
|
348
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
349
|
+
if (autoAuth.status === "failed") {
|
|
350
|
+
return {
|
|
351
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
352
|
+
details: { mode: "connect", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
if (autoAuth.status === "success") {
|
|
356
|
+
await state.manager.close(serverName);
|
|
357
|
+
connection = await state.manager.connect(serverName, definition);
|
|
358
|
+
}
|
|
359
|
+
if (connection.status === "needs-auth") {
|
|
360
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: "text", text: message }],
|
|
363
|
+
details: { mode: "connect", error: "auth_required", server: serverName, message },
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
368
|
+
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
|
|
369
|
+
state.toolMetadata.set(serverName, metadata);
|
|
370
|
+
updateMetadataCache(state, serverName);
|
|
371
|
+
state.failureTracker.delete(serverName);
|
|
372
|
+
updateStatusBar(state);
|
|
373
|
+
return executeList(state, serverName);
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
state.failureTracker.set(serverName, Date.now());
|
|
377
|
+
updateStatusBar(state);
|
|
378
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
379
|
+
return {
|
|
380
|
+
content: [{ type: "text", text: `Failed to connect to "${serverName}": ${message}` }],
|
|
381
|
+
details: { mode: "connect", error: "connect_failed", server: serverName, message },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
export async function executeCall(state, toolName, args, serverOverride, getPiTools) {
|
|
386
|
+
let serverName = serverOverride;
|
|
387
|
+
let toolMeta;
|
|
388
|
+
let autoAuthAttempted = false;
|
|
389
|
+
const prefixMode = state.config.settings?.toolPrefix ?? "server";
|
|
390
|
+
if (serverName && !state.config.mcpServers[serverName]) {
|
|
391
|
+
return {
|
|
392
|
+
content: [{ type: "text", text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
393
|
+
details: { mode: "call", error: "server_not_found", server: serverName },
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
if (serverName) {
|
|
397
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
401
|
+
const found = findToolByName(metadata, toolName);
|
|
402
|
+
if (found) {
|
|
403
|
+
serverName = server;
|
|
404
|
+
toolMeta = found;
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (serverName && !toolMeta) {
|
|
410
|
+
const connected = await lazyConnect(state, serverName);
|
|
411
|
+
if (connected) {
|
|
412
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
const needsAuthConnection = state.manager.getConnection(serverName);
|
|
416
|
+
if (needsAuthConnection?.status === "needs-auth") {
|
|
417
|
+
if (!autoAuthAttempted) {
|
|
418
|
+
autoAuthAttempted = true;
|
|
419
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
420
|
+
if (autoAuth.status === "failed") {
|
|
421
|
+
return {
|
|
422
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
423
|
+
details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (autoAuth.status === "success") {
|
|
427
|
+
await state.manager.close(serverName);
|
|
428
|
+
state.failureTracker.delete(serverName);
|
|
429
|
+
const connectedAfterAuth = await lazyConnect(state, serverName);
|
|
430
|
+
if (connectedAfterAuth) {
|
|
431
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
432
|
+
if (!toolMeta) {
|
|
433
|
+
return {
|
|
434
|
+
content: [{ type: "text", text: `Tool "${toolName}" not found on "${serverName}" after reconnect.` }],
|
|
435
|
+
details: { mode: "call", error: "tool_not_found_after_reconnect", requestedTool: toolName },
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (!toolMeta && state.manager.getConnection(serverName)?.status === "needs-auth") {
|
|
442
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
443
|
+
return {
|
|
444
|
+
content: [{ type: "text", text: message }],
|
|
445
|
+
details: { mode: "call", error: "auth_required", server: serverName, message },
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (!toolMeta) {
|
|
450
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
451
|
+
if (failedAgo !== null) {
|
|
452
|
+
return {
|
|
453
|
+
content: [{ type: "text", text: `Server "${serverName}" not available (last failed ${failedAgo}s ago). Use mcp({ restart: "${serverName}" }) to force restart it.` }],
|
|
454
|
+
details: { mode: "call", error: "server_backoff", server: serverName },
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
let prefixMatchedServer;
|
|
461
|
+
if (!serverName && !toolMeta && prefixMode !== "none") {
|
|
462
|
+
const candidates = Object.keys(state.config.mcpServers)
|
|
463
|
+
.map(name => ({ name, prefix: getServerPrefix(name, prefixMode) }))
|
|
464
|
+
.filter(c => c.prefix && toolName.startsWith(c.prefix + "_"))
|
|
465
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
466
|
+
for (const { name: configuredServer } of candidates) {
|
|
467
|
+
const existingConnection = state.manager.getConnection(configuredServer);
|
|
468
|
+
const failedAgo = getFailureAgeSeconds(state, configuredServer);
|
|
469
|
+
if (failedAgo !== null && existingConnection?.status !== "needs-auth")
|
|
470
|
+
continue;
|
|
471
|
+
let connected = await lazyConnect(state, configuredServer);
|
|
472
|
+
if (!connected && state.manager.getConnection(configuredServer)?.status === "needs-auth" && !autoAuthAttempted) {
|
|
473
|
+
autoAuthAttempted = true;
|
|
474
|
+
const autoAuth = await attemptAutoAuth(state, configuredServer);
|
|
475
|
+
if (autoAuth.status === "failed") {
|
|
476
|
+
return {
|
|
477
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
478
|
+
details: { mode: "call", error: "auth_required", server: configuredServer, message: autoAuth.message },
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (autoAuth.status === "success") {
|
|
482
|
+
await state.manager.close(configuredServer);
|
|
483
|
+
state.failureTracker.delete(configuredServer);
|
|
484
|
+
connected = await lazyConnect(state, configuredServer);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
if (!connected)
|
|
488
|
+
continue;
|
|
489
|
+
if (!prefixMatchedServer)
|
|
490
|
+
prefixMatchedServer = configuredServer;
|
|
491
|
+
toolMeta = findToolByName(state.toolMetadata.get(configuredServer), toolName);
|
|
492
|
+
if (toolMeta) {
|
|
493
|
+
serverName = configuredServer;
|
|
494
|
+
break;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
if (!serverName || !toolMeta) {
|
|
499
|
+
const nativeTool = !serverOverride
|
|
500
|
+
? getPiTools?.().find((tool) => tool.name === toolName && tool.name !== "mcp")
|
|
501
|
+
: undefined;
|
|
502
|
+
if (nativeTool) {
|
|
503
|
+
return {
|
|
504
|
+
content: [{ type: "text", text: `"${toolName}" is a native Pi tool. Call ${toolName} directly instead of using mcp({ tool: "${toolName}" }).` }],
|
|
505
|
+
details: { mode: "call", error: "native_tool", requestedTool: toolName },
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
const hintServer = serverName ?? prefixMatchedServer;
|
|
509
|
+
const available = hintServer ? getToolNames(state, hintServer) : [];
|
|
510
|
+
let msg = `Tool "${toolName}" not found.`;
|
|
511
|
+
if (available.length > 0) {
|
|
512
|
+
msg += ` Server "${hintServer}" has: ${available.join(", ")}`;
|
|
513
|
+
}
|
|
514
|
+
else {
|
|
515
|
+
msg += ` Use mcp({ search: "..." }) to search.`;
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
content: [{ type: "text", text: msg }],
|
|
519
|
+
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer },
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
let connection = state.manager.getConnection(serverName);
|
|
523
|
+
if (connection?.status === "needs-auth") {
|
|
524
|
+
if (!autoAuthAttempted) {
|
|
525
|
+
autoAuthAttempted = true;
|
|
526
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
527
|
+
if (autoAuth.status === "failed") {
|
|
528
|
+
return {
|
|
529
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
530
|
+
details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (autoAuth.status === "success") {
|
|
534
|
+
await state.manager.close(serverName);
|
|
535
|
+
state.failureTracker.delete(serverName);
|
|
536
|
+
connection = state.manager.getConnection(serverName);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (connection?.status === "needs-auth") {
|
|
540
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
541
|
+
return {
|
|
542
|
+
content: [{ type: "text", text: message }],
|
|
543
|
+
details: { mode: "call", error: "auth_required", server: serverName, message },
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (!connection || connection.status !== "connected") {
|
|
548
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
549
|
+
if (failedAgo !== null) {
|
|
550
|
+
return {
|
|
551
|
+
content: [{ type: "text", text: `Server "${serverName}" not available (last failed ${failedAgo}s ago). Use mcp({ restart: "${serverName}" }) to force restart it.` }],
|
|
552
|
+
details: { mode: "call", error: "server_backoff", server: serverName },
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
const definition = state.config.mcpServers[serverName];
|
|
556
|
+
if (!definition) {
|
|
557
|
+
return {
|
|
558
|
+
content: [{ type: "text", text: `Server "${serverName}" not connected. Use mcp({ restart: "${serverName}" }) to force restart it.` }],
|
|
559
|
+
details: { mode: "call", error: "server_not_connected", server: serverName },
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
if (state.ui) {
|
|
564
|
+
state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
|
|
565
|
+
}
|
|
566
|
+
connection = await state.manager.connect(serverName, definition);
|
|
567
|
+
if (connection.status === "needs-auth") {
|
|
568
|
+
if (!autoAuthAttempted) {
|
|
569
|
+
autoAuthAttempted = true;
|
|
570
|
+
const autoAuth = await attemptAutoAuth(state, serverName);
|
|
571
|
+
if (autoAuth.status === "failed") {
|
|
572
|
+
return {
|
|
573
|
+
content: [{ type: "text", text: autoAuth.message }],
|
|
574
|
+
details: { mode: "call", error: "auth_required", server: serverName, message: autoAuth.message },
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
if (autoAuth.status === "success") {
|
|
578
|
+
await state.manager.close(serverName);
|
|
579
|
+
connection = await state.manager.connect(serverName, definition);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (connection.status === "needs-auth") {
|
|
583
|
+
const message = getAuthRequiredMessage(state, serverName);
|
|
584
|
+
return {
|
|
585
|
+
content: [{ type: "text", text: message }],
|
|
586
|
+
details: { mode: "call", error: "auth_required", server: serverName, message },
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
state.failureTracker.delete(serverName);
|
|
591
|
+
updateServerMetadata(state, serverName);
|
|
592
|
+
updateMetadataCache(state, serverName);
|
|
593
|
+
updateStatusBar(state);
|
|
594
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
595
|
+
if (!toolMeta) {
|
|
596
|
+
const available = getToolNames(state, serverName);
|
|
597
|
+
const hint = available.length > 0
|
|
598
|
+
? `Available tools on "${serverName}": ${available.join(", ")}`
|
|
599
|
+
: `Server "${serverName}" has no tools.`;
|
|
600
|
+
return {
|
|
601
|
+
content: [{ type: "text", text: `Tool "${toolName}" not found on "${serverName}" after reconnect. ${hint}` }],
|
|
602
|
+
details: { mode: "call", error: "tool_not_found_after_reconnect", requestedTool: toolName },
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
catch (error) {
|
|
607
|
+
state.failureTracker.set(serverName, Date.now());
|
|
608
|
+
updateStatusBar(state);
|
|
609
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
610
|
+
return {
|
|
611
|
+
content: [{ type: "text", text: `Failed to connect to "${serverName}": ${message}` }],
|
|
612
|
+
details: { mode: "call", error: "connect_failed", message },
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
let uiSession = null;
|
|
617
|
+
try {
|
|
618
|
+
state.manager.touch(serverName);
|
|
619
|
+
state.manager.incrementInFlight(serverName);
|
|
620
|
+
if (toolMeta.resourceUri) {
|
|
621
|
+
const result = await connection.client.readResource({ uri: toolMeta.resourceUri });
|
|
622
|
+
const content = (result.contents ?? []).map(c => ({
|
|
623
|
+
type: "text",
|
|
624
|
+
text: "text" in c ? c.text : ("blob" in c ? `[Binary data: ${c.mimeType ?? "unknown"}]` : JSON.stringify(c)),
|
|
625
|
+
}));
|
|
626
|
+
return {
|
|
627
|
+
content: content.length > 0 ? content : [{ type: "text", text: "(empty resource)" }],
|
|
628
|
+
details: { mode: "call", resourceUri: toolMeta.resourceUri, server: serverName },
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
uiSession = toolMeta.uiResourceUri
|
|
632
|
+
? await maybeStartUiSession(state, {
|
|
633
|
+
serverName,
|
|
634
|
+
toolName: toolMeta.originalName,
|
|
635
|
+
toolArgs: args ?? {},
|
|
636
|
+
uiResourceUri: toolMeta.uiResourceUri,
|
|
637
|
+
streamMode: toolMeta.uiStreamMode,
|
|
638
|
+
})
|
|
639
|
+
: null;
|
|
640
|
+
const resultPromise = connection.client.callTool({
|
|
641
|
+
name: toolMeta.originalName,
|
|
642
|
+
arguments: args ?? {},
|
|
643
|
+
_meta: uiSession?.requestMeta,
|
|
644
|
+
});
|
|
645
|
+
if (toolMeta.uiResourceUri) {
|
|
646
|
+
const result = await resultPromise;
|
|
647
|
+
uiSession?.sendToolResult(result);
|
|
648
|
+
const mcpContent = (result.content ?? []);
|
|
649
|
+
const content = transformMcpContent(mcpContent);
|
|
650
|
+
const mcpText = content
|
|
651
|
+
.filter((c) => c.type === "text")
|
|
652
|
+
.map((c) => c.text)
|
|
653
|
+
.join("\n");
|
|
654
|
+
if (result.isError) {
|
|
655
|
+
let errorWithSchema = `Error: ${mcpText || "Tool execution failed"}`;
|
|
656
|
+
if (toolMeta.inputSchema) {
|
|
657
|
+
errorWithSchema += `\n\nExpected parameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
658
|
+
}
|
|
659
|
+
return {
|
|
660
|
+
content: [{ type: "text", text: errorWithSchema }],
|
|
661
|
+
details: { mode: "call", error: "tool_error", mcpResult: result },
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
const resultText = mcpText || "(empty result)";
|
|
665
|
+
const uiMessage = uiSession?.reused
|
|
666
|
+
? "Updated the open UI."
|
|
667
|
+
: "📺 Interactive UI is now open in your browser. I'll respond to your prompts and intents as you interact with it.";
|
|
668
|
+
return {
|
|
669
|
+
content: [{ type: "text", text: `${resultText}\n\n${uiMessage}` }],
|
|
670
|
+
details: { mode: "call", mcpResult: result, server: serverName, tool: toolMeta.originalName, uiOpen: true },
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
const result = await resultPromise;
|
|
674
|
+
const mcpContent = (result.content ?? []);
|
|
675
|
+
const content = transformMcpContent(mcpContent);
|
|
676
|
+
if (result.isError) {
|
|
677
|
+
const errorText = content
|
|
678
|
+
.filter((c) => c.type === "text")
|
|
679
|
+
.map((c) => c.text)
|
|
680
|
+
.join("\n") || "Tool execution failed";
|
|
681
|
+
let errorWithSchema = `Error: ${errorText}`;
|
|
682
|
+
if (toolMeta.inputSchema) {
|
|
683
|
+
errorWithSchema += `\n\nExpected parameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
684
|
+
}
|
|
685
|
+
return {
|
|
686
|
+
content: [{ type: "text", text: errorWithSchema }],
|
|
687
|
+
details: { mode: "call", error: "tool_error", mcpResult: result },
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
content: content.length > 0 ? content : [{ type: "text", text: "(empty result)" }],
|
|
692
|
+
details: { mode: "call", mcpResult: result, server: serverName, tool: toolMeta.originalName },
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
catch (error) {
|
|
696
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
697
|
+
uiSession?.sendToolCancelled(message);
|
|
698
|
+
let errorWithSchema = `Failed to call tool: ${message}`;
|
|
699
|
+
if (toolMeta.inputSchema) {
|
|
700
|
+
errorWithSchema += `\n\nExpected parameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
content: [{ type: "text", text: errorWithSchema }],
|
|
704
|
+
details: { mode: "call", error: "call_failed", message },
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
finally {
|
|
708
|
+
if (uiSession?.reused) {
|
|
709
|
+
uiSession.close();
|
|
710
|
+
}
|
|
711
|
+
state.manager.decrementInFlight(serverName);
|
|
712
|
+
state.manager.touch(serverName);
|
|
713
|
+
}
|
|
714
|
+
}
|