@docyrus/docyrus 0.0.19 → 0.0.21
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/agent-loader.js +37 -3
- package/agent-loader.js.map +2 -2
- package/main.js +498 -93
- package/main.js.map +4 -4
- package/package.json +14 -4
- package/resources/chrome-tools/browser-content.js +103 -0
- package/resources/chrome-tools/browser-cookies.js +35 -0
- package/resources/chrome-tools/browser-eval.js +53 -0
- package/resources/chrome-tools/browser-hn-scraper.js +108 -0
- package/resources/chrome-tools/browser-nav.js +44 -0
- package/resources/chrome-tools/browser-pick.js +162 -0
- package/resources/chrome-tools/browser-screenshot.js +34 -0
- package/resources/chrome-tools/browser-start.js +86 -0
- package/resources/pi-agent/extensions/answer.ts +532 -0
- package/resources/pi-agent/extensions/context.ts +578 -0
- package/resources/pi-agent/extensions/control.ts +1779 -0
- package/resources/pi-agent/extensions/diff.ts +218 -0
- package/resources/pi-agent/extensions/files.ts +199 -0
- package/resources/pi-agent/extensions/loop.ts +446 -0
- package/resources/pi-agent/extensions/multi-edit.ts +835 -0
- package/resources/pi-agent/extensions/notify.ts +88 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/CHANGELOG.md +192 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/LICENSE +21 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/README.md +296 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/app-bridge.bundle.js +67 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/cli.js +108 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/commands.ts +211 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/config.ts +227 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/consent-manager.ts +64 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/direct-tools.ts +301 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/errors.ts +219 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/glimpse-ui.ts +80 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/host-html-template.ts +427 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/index.ts +232 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/init.ts +319 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/lifecycle.ts +93 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/logger.ts +169 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/mcp-panel.ts +713 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/metadata-cache.ts +191 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/npx-resolver.ts +419 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/oauth-handler.ts +56 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/package.json +85 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/paths.ts +29 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/proxy-modes.ts +635 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/resource-tools.ts +17 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/server-manager.ts +330 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/state.ts +41 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-metadata.ts +144 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/tool-registrar.ts +46 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/types.ts +367 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-resource-handler.ts +145 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-server.ts +623 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-session.ts +384 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/ui-stream-types.ts +89 -0
- package/resources/pi-agent/extensions/pi-mcp-adapter/utils.ts +75 -0
- package/resources/pi-agent/extensions/prompt-editor.ts +1315 -0
- package/resources/pi-agent/extensions/prompt-url-widget.ts +158 -0
- package/resources/pi-agent/extensions/redraws.ts +24 -0
- package/resources/pi-agent/extensions/review.ts +2160 -0
- package/resources/pi-agent/extensions/todos.ts +2076 -0
- package/resources/pi-agent/extensions/tps.ts +47 -0
- package/resources/pi-agent/extensions/whimsical.ts +474 -0
- package/resources/pi-agent/prompts/coder-system.md +106 -0
- package/resources/pi-agent/skills/changelog-generator/SKILL.md +425 -0
- package/resources/pi-agent/skills/docyrus-chrome-devtools-cli/SKILL.md +80 -0
- package/resources/pi-agent/skills/docyrus-platform/SKILL.md +71 -0
- package/resources/pi-agent/skills/docyrus-platform/references/ai-capabilities.md +43 -0
- package/resources/pi-agent/skills/docyrus-platform/references/auth-and-multi-tenancy.md +35 -0
- package/resources/pi-agent/skills/docyrus-platform/references/automation-and-workflows.md +30 -0
- package/resources/pi-agent/skills/docyrus-platform/references/core-building-blocks.md +53 -0
- package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/data-source-query-guide.md +32 -28
- package/resources/pi-agent/skills/docyrus-platform/references/developer-tools.md +28 -0
- package/resources/pi-agent/skills/docyrus-platform/references/docyrus-cli-usage.md +554 -0
- package/resources/pi-agent/skills/{docyrus-api-dev → docyrus-platform}/references/formula-design-guide-llm.md +15 -23
- package/resources/pi-agent/skills/docyrus-platform/references/integrations-and-events.md +60 -0
- package/resources/pi-agent/skills/docyrus-platform/references/platform-services.md +58 -0
- package/resources/pi-agent/skills/docyrus-platform/references/querying-and-data-operations.md +27 -0
- package/resources/pi-agent/prompts/coder-append-system.md +0 -19
- package/resources/pi-agent/skills/docyrus-ai/SKILL.md +0 -28
- package/resources/pi-agent/skills/docyrus-api-dev/SKILL.md +0 -161
- package/resources/pi-agent/skills/docyrus-api-dev/references/api-client.md +0 -349
- package/resources/pi-agent/skills/docyrus-api-dev/references/authentication.md +0 -238
- package/resources/pi-agent/skills/docyrus-api-dev/references/query-and-formulas.md +0 -592
- package/resources/pi-agent/skills/docyrus-api-doctor/SKILL.md +0 -70
- package/resources/pi-agent/skills/docyrus-api-doctor/references/checklist-details.md +0 -588
- package/resources/pi-agent/skills/docyrus-app-dev/SKILL.md +0 -159
- package/resources/pi-agent/skills/docyrus-app-dev/references/api-client-and-auth.md +0 -275
- package/resources/pi-agent/skills/docyrus-app-dev/references/collections-and-patterns.md +0 -352
- package/resources/pi-agent/skills/docyrus-app-dev/references/data-source-query-guide.md +0 -2059
- package/resources/pi-agent/skills/docyrus-app-dev/references/formula-design-guide-llm.md +0 -320
- package/resources/pi-agent/skills/docyrus-app-dev/references/query-guide.md +0 -525
- package/resources/pi-agent/skills/docyrus-app-ui-design/SKILL.md +0 -466
- package/resources/pi-agent/skills/docyrus-app-ui-design/references/component-selection-guide.md +0 -602
- package/resources/pi-agent/skills/docyrus-app-ui-design/references/icon-usage-guide.md +0 -463
- package/resources/pi-agent/skills/docyrus-app-ui-design/references/preferred-components-catalog.md +0 -242
- package/resources/pi-agent/skills/docyrus-apps/SKILL.md +0 -54
- package/resources/pi-agent/skills/docyrus-architect/SKILL.md +0 -174
- package/resources/pi-agent/skills/docyrus-architect/references/custom-query-guide.md +0 -410
- package/resources/pi-agent/skills/docyrus-architect/references/data-source-query-guide.md +0 -2059
- package/resources/pi-agent/skills/docyrus-architect/references/formula-design-guide-llm.md +0 -320
- package/resources/pi-agent/skills/docyrus-architect/references/formula-reference.md +0 -145
- package/resources/pi-agent/skills/docyrus-auth/SKILL.md +0 -100
- package/resources/pi-agent/skills/docyrus-cli-app/SKILL.md +0 -279
- package/resources/pi-agent/skills/docyrus-cli-app/references/cli-manifest.md +0 -532
- package/resources/pi-agent/skills/docyrus-cli-app/references/list-query-examples.md +0 -248
- package/resources/pi-agent/skills/docyrus-curl/SKILL.md +0 -32
- package/resources/pi-agent/skills/docyrus-discover/SKILL.md +0 -63
- package/resources/pi-agent/skills/docyrus-ds/SKILL.md +0 -95
- package/resources/pi-agent/skills/docyrus-env/SKILL.md +0 -21
- package/resources/pi-agent/skills/docyrus-studio/SKILL.md +0 -369
- package/resources/pi-agent/skills/docyrus-tui/SKILL.md +0 -15
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
import type { AgentToolResult, ToolInfo } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { McpExtensionState } from "./state.js";
|
|
3
|
+
import type { ToolMetadata, McpContent } from "./types.js";
|
|
4
|
+
import { getServerPrefix, parseUiPromptHandoff } from "./types.js";
|
|
5
|
+
import { lazyConnect, updateServerMetadata, updateMetadataCache, getFailureAgeSeconds, updateStatusBar } from "./init.js";
|
|
6
|
+
import { buildToolMetadata, getToolNames, findToolByName, formatSchema } from "./tool-metadata.js";
|
|
7
|
+
import { transformMcpContent } from "./tool-registrar.js";
|
|
8
|
+
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.js";
|
|
9
|
+
import { truncateAtWord } from "./utils.js";
|
|
10
|
+
|
|
11
|
+
type ProxyToolResult = AgentToolResult<Record<string, unknown>>;
|
|
12
|
+
|
|
13
|
+
export function executeUiMessages(state: McpExtensionState): ProxyToolResult {
|
|
14
|
+
const sessions = state.completedUiSessions;
|
|
15
|
+
|
|
16
|
+
if (sessions.length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
content: [{ type: "text" as const, text: "No UI session messages available." }],
|
|
19
|
+
details: { sessions: 0 },
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const output: string[] = [];
|
|
24
|
+
output.push(`UI Session Messages (${sessions.length} session${sessions.length > 1 ? "s" : ""}):\n`);
|
|
25
|
+
|
|
26
|
+
const allPrompts: string[] = [];
|
|
27
|
+
const allIntents = sessions.flatMap((session) => session.messages.intents);
|
|
28
|
+
const parsedHandoffs: Array<{ intent: string; params: Record<string, unknown>; raw: string }> = [];
|
|
29
|
+
|
|
30
|
+
for (const session of sessions) {
|
|
31
|
+
const timestamp = session.completedAt.toLocaleTimeString();
|
|
32
|
+
output.push(`\n## ${session.serverName} / ${session.toolName} (${timestamp}, ${session.reason})`);
|
|
33
|
+
|
|
34
|
+
const plainPrompts: string[] = [];
|
|
35
|
+
for (const prompt of session.messages.prompts) {
|
|
36
|
+
allPrompts.push(prompt);
|
|
37
|
+
const handoff = parseUiPromptHandoff(prompt);
|
|
38
|
+
if (handoff) {
|
|
39
|
+
parsedHandoffs.push(handoff);
|
|
40
|
+
} else {
|
|
41
|
+
plainPrompts.push(prompt);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (plainPrompts.length > 0) {
|
|
46
|
+
output.push("\n### Prompts:");
|
|
47
|
+
for (const prompt of plainPrompts) {
|
|
48
|
+
output.push(`- ${prompt}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const intentsForSession = [
|
|
53
|
+
...session.messages.intents,
|
|
54
|
+
...session.messages.prompts
|
|
55
|
+
.map((prompt) => parseUiPromptHandoff(prompt))
|
|
56
|
+
.filter((handoff): handoff is NonNullable<typeof handoff> => !!handoff)
|
|
57
|
+
.map((handoff) => ({ intent: handoff.intent, params: handoff.params })),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
if (intentsForSession.length > 0) {
|
|
61
|
+
output.push("\n### Intents:");
|
|
62
|
+
for (const intent of intentsForSession) {
|
|
63
|
+
const params = intent.params ? ` (${JSON.stringify(intent.params)})` : "";
|
|
64
|
+
output.push(`- ${intent.intent}${params}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (session.messages.notifications.length > 0) {
|
|
69
|
+
output.push("\n### Notifications:");
|
|
70
|
+
for (const notification of session.messages.notifications) {
|
|
71
|
+
output.push(`- ${notification}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const count = sessions.length;
|
|
77
|
+
state.completedUiSessions = [];
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
content: [{ type: "text" as const, text: output.join("\n") }],
|
|
81
|
+
details: {
|
|
82
|
+
sessions: count,
|
|
83
|
+
prompts: allPrompts,
|
|
84
|
+
intents: [...allIntents, ...parsedHandoffs.map(({ intent, params }) => ({ intent, params }))],
|
|
85
|
+
handoffs: parsedHandoffs,
|
|
86
|
+
cleared: true,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function executeStatus(state: McpExtensionState): ProxyToolResult {
|
|
92
|
+
const servers: Array<{ name: string; status: string; toolCount: number; failedAgo: number | null }> = [];
|
|
93
|
+
|
|
94
|
+
for (const name of Object.keys(state.config.mcpServers)) {
|
|
95
|
+
const connection = state.manager.getConnection(name);
|
|
96
|
+
const metadata = state.toolMetadata.get(name);
|
|
97
|
+
const toolCount = metadata?.length ?? 0;
|
|
98
|
+
const failedAgo = getFailureAgeSeconds(state, name);
|
|
99
|
+
let status = "not connected";
|
|
100
|
+
if (connection?.status === "connected") {
|
|
101
|
+
status = "connected";
|
|
102
|
+
} else if (failedAgo !== null) {
|
|
103
|
+
status = "failed";
|
|
104
|
+
} else if (metadata !== undefined) {
|
|
105
|
+
status = "cached";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
servers.push({ name, status, toolCount, failedAgo });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const totalTools = servers.reduce((sum, s) => sum + s.toolCount, 0);
|
|
112
|
+
const connectedCount = servers.filter(s => s.status === "connected").length;
|
|
113
|
+
|
|
114
|
+
let text = `MCP: ${connectedCount}/${servers.length} servers, ${totalTools} tools\n\n`;
|
|
115
|
+
for (const server of servers) {
|
|
116
|
+
if (server.status === "connected") {
|
|
117
|
+
text += `✓ ${server.name} (${server.toolCount} tools)\n`;
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (server.status === "cached") {
|
|
121
|
+
text += `○ ${server.name} (${server.toolCount} tools, cached)\n`;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (server.status === "failed") {
|
|
125
|
+
text += `✗ ${server.name} (failed ${server.failedAgo ?? 0}s ago)\n`;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
text += `○ ${server.name} (not connected)\n`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (servers.length > 0) {
|
|
132
|
+
text += `\nmcp({ server: "name" }) to list tools, mcp({ search: "..." }) to search`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
content: [{ type: "text" as const, text: text.trim() }],
|
|
137
|
+
details: { mode: "status", servers, totalTools, connectedCount },
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function executeDescribe(state: McpExtensionState, toolName: string): ProxyToolResult {
|
|
142
|
+
let serverName: string | undefined;
|
|
143
|
+
let toolMeta: ToolMetadata | undefined;
|
|
144
|
+
|
|
145
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
146
|
+
const found = findToolByName(metadata, toolName);
|
|
147
|
+
if (found) {
|
|
148
|
+
serverName = server;
|
|
149
|
+
toolMeta = found;
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (!serverName || !toolMeta) {
|
|
155
|
+
return {
|
|
156
|
+
content: [{ type: "text" as const, text: `Tool "${toolName}" not found. Use mcp({ search: "..." }) to search.` }],
|
|
157
|
+
details: { mode: "describe", error: "tool_not_found", requestedTool: toolName },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
let text = `${toolMeta.name}\n`;
|
|
162
|
+
text += `Server: ${serverName}\n`;
|
|
163
|
+
if (toolMeta.resourceUri) {
|
|
164
|
+
text += `Type: Resource (reads from ${toolMeta.resourceUri})\n`;
|
|
165
|
+
}
|
|
166
|
+
text += `\n${toolMeta.description || "(no description)"}\n`;
|
|
167
|
+
|
|
168
|
+
if (toolMeta.inputSchema && !toolMeta.resourceUri) {
|
|
169
|
+
text += `\nParameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
170
|
+
} else if (toolMeta.resourceUri) {
|
|
171
|
+
text += `\nNo parameters required (resource tool).`;
|
|
172
|
+
} else {
|
|
173
|
+
text += `\nNo parameters defined.`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
content: [{ type: "text" as const, text: text.trim() }],
|
|
178
|
+
details: { mode: "describe", tool: toolMeta, server: serverName },
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function executeSearch(
|
|
183
|
+
state: McpExtensionState,
|
|
184
|
+
query: string,
|
|
185
|
+
regex?: boolean,
|
|
186
|
+
server?: string,
|
|
187
|
+
includeSchemas?: boolean,
|
|
188
|
+
getPiTools?: () => ToolInfo[]
|
|
189
|
+
): ProxyToolResult {
|
|
190
|
+
const showSchemas = includeSchemas !== false;
|
|
191
|
+
|
|
192
|
+
const matches: Array<{ server: string; tool: ToolMetadata }> = [];
|
|
193
|
+
|
|
194
|
+
let pattern: RegExp;
|
|
195
|
+
try {
|
|
196
|
+
if (regex) {
|
|
197
|
+
pattern = new RegExp(query, "i");
|
|
198
|
+
} else {
|
|
199
|
+
const terms = query.trim().split(/\s+/).filter(t => t.length > 0);
|
|
200
|
+
if (terms.length === 0) {
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text" as const, text: "Search query cannot be empty" }],
|
|
203
|
+
details: { mode: "search", error: "empty_query" },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const escaped = terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
207
|
+
pattern = new RegExp(escaped.join("|"), "i");
|
|
208
|
+
}
|
|
209
|
+
} catch {
|
|
210
|
+
return {
|
|
211
|
+
content: [{ type: "text" as const, text: `Invalid regex: ${query}` }],
|
|
212
|
+
details: { mode: "search", error: "invalid_pattern", query },
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const piMatches: Array<{ name: string; description: string }> = [];
|
|
217
|
+
if (!server && getPiTools) {
|
|
218
|
+
const piTools = getPiTools();
|
|
219
|
+
for (const tool of piTools) {
|
|
220
|
+
if (tool.name === "mcp") continue;
|
|
221
|
+
|
|
222
|
+
if (pattern.test(tool.name) || pattern.test(tool.description ?? "")) {
|
|
223
|
+
piMatches.push({
|
|
224
|
+
name: tool.name,
|
|
225
|
+
description: tool.description ?? "",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const [serverName, metadata] of state.toolMetadata.entries()) {
|
|
232
|
+
if (server && serverName !== server) continue;
|
|
233
|
+
for (const tool of metadata) {
|
|
234
|
+
if (pattern.test(tool.name) || pattern.test(tool.description)) {
|
|
235
|
+
matches.push({
|
|
236
|
+
server: serverName,
|
|
237
|
+
tool,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const totalCount = piMatches.length + matches.length;
|
|
244
|
+
|
|
245
|
+
if (totalCount === 0) {
|
|
246
|
+
const msg = server
|
|
247
|
+
? `No tools matching "${query}" in "${server}"`
|
|
248
|
+
: `No tools matching "${query}"`;
|
|
249
|
+
return {
|
|
250
|
+
content: [{ type: "text" as const, text: msg }],
|
|
251
|
+
details: { mode: "search", matches: [], count: 0, query },
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let text = `Found ${totalCount} tool${totalCount === 1 ? "" : "s"} matching "${query}":\n\n`;
|
|
256
|
+
|
|
257
|
+
for (const match of piMatches) {
|
|
258
|
+
if (showSchemas) {
|
|
259
|
+
text += `[pi tool] ${match.name}\n`;
|
|
260
|
+
text += ` ${match.description || "(no description)"}\n`;
|
|
261
|
+
text += ` No parameters (call directly).\n`;
|
|
262
|
+
text += "\n";
|
|
263
|
+
} else {
|
|
264
|
+
text += `[pi tool] ${match.name}`;
|
|
265
|
+
if (match.description) {
|
|
266
|
+
text += ` - ${truncateAtWord(match.description, 50)}`;
|
|
267
|
+
}
|
|
268
|
+
text += "\n";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const match of matches) {
|
|
273
|
+
if (showSchemas) {
|
|
274
|
+
text += `${match.tool.name}\n`;
|
|
275
|
+
text += ` ${match.tool.description || "(no description)"}\n`;
|
|
276
|
+
if (match.tool.inputSchema && !match.tool.resourceUri) {
|
|
277
|
+
text += `\n Parameters:\n${formatSchema(match.tool.inputSchema, " ")}\n`;
|
|
278
|
+
} else if (match.tool.resourceUri) {
|
|
279
|
+
text += ` No parameters (resource tool).\n`;
|
|
280
|
+
}
|
|
281
|
+
text += "\n";
|
|
282
|
+
} else {
|
|
283
|
+
text += `- ${match.tool.name}`;
|
|
284
|
+
if (match.tool.description) {
|
|
285
|
+
text += ` - ${truncateAtWord(match.tool.description, 50)}`;
|
|
286
|
+
}
|
|
287
|
+
text += "\n";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return {
|
|
292
|
+
content: [{ type: "text" as const, text: text.trim() }],
|
|
293
|
+
details: {
|
|
294
|
+
mode: "search",
|
|
295
|
+
matches: [
|
|
296
|
+
...piMatches.map(m => ({ server: "pi", tool: m.name })),
|
|
297
|
+
...matches.map(m => ({ server: m.server, tool: m.tool.name })),
|
|
298
|
+
],
|
|
299
|
+
count: totalCount,
|
|
300
|
+
query,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function executeList(state: McpExtensionState, server: string): ProxyToolResult {
|
|
306
|
+
if (!state.config.mcpServers[server]) {
|
|
307
|
+
return {
|
|
308
|
+
content: [{ type: "text" as const, text: `Server "${server}" not found. Use mcp({}) to see available servers.` }],
|
|
309
|
+
details: { mode: "list", server, tools: [], count: 0, error: "not_found" },
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const metadata = state.toolMetadata.get(server);
|
|
314
|
+
const toolNames = metadata?.map(m => m.name) ?? [];
|
|
315
|
+
const connection = state.manager.getConnection(server);
|
|
316
|
+
|
|
317
|
+
if (toolNames.length === 0) {
|
|
318
|
+
if (connection?.status === "connected") {
|
|
319
|
+
return {
|
|
320
|
+
content: [{ type: "text" as const, text: `Server "${server}" has no tools.` }],
|
|
321
|
+
details: { mode: "list", server, tools: [], count: 0 },
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (metadata !== undefined) {
|
|
325
|
+
return {
|
|
326
|
+
content: [{ type: "text" as const, text: `Server "${server}" has no cached tools (not connected).` }],
|
|
327
|
+
details: { mode: "list", server, tools: [], count: 0, cached: true },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text" as const, text: `Server "${server}" is configured but not connected. Use mcp({ connect: "${server}" }) or /mcp reconnect ${server} to retry.` }],
|
|
332
|
+
details: { mode: "list", server, tools: [], count: 0, error: "not_connected" },
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const cachedNote = connection?.status === "connected" ? "" : " (not connected, cached)";
|
|
337
|
+
let text = `${server} (${toolNames.length} tools${cachedNote}):\n\n`;
|
|
338
|
+
|
|
339
|
+
const descMap = new Map<string, string>();
|
|
340
|
+
if (metadata) {
|
|
341
|
+
for (const m of metadata) {
|
|
342
|
+
descMap.set(m.name, m.description);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
for (const tool of toolNames) {
|
|
347
|
+
const desc = descMap.get(tool) ?? "";
|
|
348
|
+
const truncated = truncateAtWord(desc, 50);
|
|
349
|
+
text += `- ${tool}`;
|
|
350
|
+
if (truncated) text += ` - ${truncated}`;
|
|
351
|
+
text += "\n";
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
content: [{ type: "text" as const, text: text.trim() }],
|
|
356
|
+
details: { mode: "list", server, tools: toolNames, count: toolNames.length },
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function executeConnect(state: McpExtensionState, serverName: string): Promise<ProxyToolResult> {
|
|
361
|
+
const definition = state.config.mcpServers[serverName];
|
|
362
|
+
if (!definition) {
|
|
363
|
+
return {
|
|
364
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
365
|
+
details: { mode: "connect", error: "not_found", server: serverName },
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
if (state.ui) {
|
|
371
|
+
state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
|
|
372
|
+
}
|
|
373
|
+
const connection = await state.manager.connect(serverName, definition);
|
|
374
|
+
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
375
|
+
const { metadata } = buildToolMetadata(connection.tools, connection.resources, definition, serverName, prefix);
|
|
376
|
+
state.toolMetadata.set(serverName, metadata);
|
|
377
|
+
updateMetadataCache(state, serverName);
|
|
378
|
+
state.failureTracker.delete(serverName);
|
|
379
|
+
updateStatusBar(state);
|
|
380
|
+
return executeList(state, serverName);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
state.failureTracker.set(serverName, Date.now());
|
|
383
|
+
updateStatusBar(state);
|
|
384
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
385
|
+
return {
|
|
386
|
+
content: [{ type: "text" as const, text: `Failed to connect to "${serverName}": ${message}` }],
|
|
387
|
+
details: { mode: "connect", error: "connect_failed", server: serverName, message },
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
export async function executeCall(
|
|
393
|
+
state: McpExtensionState,
|
|
394
|
+
toolName: string,
|
|
395
|
+
args?: Record<string, unknown>,
|
|
396
|
+
serverOverride?: string,
|
|
397
|
+
): Promise<ProxyToolResult> {
|
|
398
|
+
let serverName: string | undefined = serverOverride;
|
|
399
|
+
let toolMeta: ToolMetadata | undefined;
|
|
400
|
+
const prefixMode = state.config.settings?.toolPrefix ?? "server";
|
|
401
|
+
|
|
402
|
+
if (serverName && !state.config.mcpServers[serverName]) {
|
|
403
|
+
return {
|
|
404
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not found. Use mcp({}) to see available servers.` }],
|
|
405
|
+
details: { mode: "call", error: "server_not_found", server: serverName },
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (serverName) {
|
|
410
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
411
|
+
} else {
|
|
412
|
+
for (const [server, metadata] of state.toolMetadata.entries()) {
|
|
413
|
+
const found = findToolByName(metadata, toolName);
|
|
414
|
+
if (found) {
|
|
415
|
+
serverName = server;
|
|
416
|
+
toolMeta = found;
|
|
417
|
+
break;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (serverName && !toolMeta) {
|
|
423
|
+
const connected = await lazyConnect(state, serverName);
|
|
424
|
+
if (connected) {
|
|
425
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
426
|
+
} else {
|
|
427
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
428
|
+
if (failedAgo !== null) {
|
|
429
|
+
return {
|
|
430
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
431
|
+
details: { mode: "call", error: "server_backoff", server: serverName },
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
let prefixMatchedServer: string | undefined;
|
|
438
|
+
|
|
439
|
+
if (!serverName && !toolMeta && prefixMode !== "none") {
|
|
440
|
+
const candidates = Object.keys(state.config.mcpServers)
|
|
441
|
+
.map(name => ({ name, prefix: getServerPrefix(name, prefixMode) }))
|
|
442
|
+
.filter(c => c.prefix && toolName.startsWith(c.prefix + "_"))
|
|
443
|
+
.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
444
|
+
|
|
445
|
+
for (const { name: configuredServer } of candidates) {
|
|
446
|
+
const failedAgo = getFailureAgeSeconds(state, configuredServer);
|
|
447
|
+
if (failedAgo !== null) continue;
|
|
448
|
+
const connected = await lazyConnect(state, configuredServer);
|
|
449
|
+
if (!connected) continue;
|
|
450
|
+
if (!prefixMatchedServer) prefixMatchedServer = configuredServer;
|
|
451
|
+
toolMeta = findToolByName(state.toolMetadata.get(configuredServer), toolName);
|
|
452
|
+
if (toolMeta) {
|
|
453
|
+
serverName = configuredServer;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!serverName || !toolMeta) {
|
|
460
|
+
const hintServer = serverName ?? prefixMatchedServer;
|
|
461
|
+
const available = hintServer ? getToolNames(state, hintServer) : [];
|
|
462
|
+
let msg = `Tool "${toolName}" not found.`;
|
|
463
|
+
if (available.length > 0) {
|
|
464
|
+
msg += ` Server "${hintServer}" has: ${available.join(", ")}`;
|
|
465
|
+
} else {
|
|
466
|
+
msg += ` Use mcp({ search: "..." }) to search.`;
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
content: [{ type: "text" as const, text: msg }],
|
|
470
|
+
details: { mode: "call", error: "tool_not_found", requestedTool: toolName, hintServer },
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
let connection = state.manager.getConnection(serverName);
|
|
475
|
+
if (!connection || connection.status !== "connected") {
|
|
476
|
+
const failedAgo = getFailureAgeSeconds(state, serverName);
|
|
477
|
+
if (failedAgo !== null) {
|
|
478
|
+
return {
|
|
479
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not available (last failed ${failedAgo}s ago)` }],
|
|
480
|
+
details: { mode: "call", error: "server_backoff", server: serverName },
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const definition = state.config.mcpServers[serverName];
|
|
485
|
+
if (!definition) {
|
|
486
|
+
return {
|
|
487
|
+
content: [{ type: "text" as const, text: `Server "${serverName}" not connected` }],
|
|
488
|
+
details: { mode: "call", error: "server_not_connected", server: serverName },
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
if (state.ui) {
|
|
494
|
+
state.ui.setStatus("mcp", `MCP: connecting to ${serverName}...`);
|
|
495
|
+
}
|
|
496
|
+
connection = await state.manager.connect(serverName, definition);
|
|
497
|
+
state.failureTracker.delete(serverName);
|
|
498
|
+
updateServerMetadata(state, serverName);
|
|
499
|
+
updateMetadataCache(state, serverName);
|
|
500
|
+
updateStatusBar(state);
|
|
501
|
+
toolMeta = findToolByName(state.toolMetadata.get(serverName), toolName);
|
|
502
|
+
if (!toolMeta) {
|
|
503
|
+
const available = getToolNames(state, serverName);
|
|
504
|
+
const hint = available.length > 0
|
|
505
|
+
? `Available tools on "${serverName}": ${available.join(", ")}`
|
|
506
|
+
: `Server "${serverName}" has no tools.`;
|
|
507
|
+
return {
|
|
508
|
+
content: [{ type: "text" as const, text: `Tool "${toolName}" not found on "${serverName}" after reconnect. ${hint}` }],
|
|
509
|
+
details: { mode: "call", error: "tool_not_found_after_reconnect", requestedTool: toolName },
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
} catch (error) {
|
|
513
|
+
state.failureTracker.set(serverName, Date.now());
|
|
514
|
+
updateStatusBar(state);
|
|
515
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
516
|
+
return {
|
|
517
|
+
content: [{ type: "text" as const, text: `Failed to connect to "${serverName}": ${message}` }],
|
|
518
|
+
details: { mode: "call", error: "connect_failed", message },
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
let uiSession: UiSessionRuntime | null = null;
|
|
524
|
+
|
|
525
|
+
try {
|
|
526
|
+
state.manager.touch(serverName);
|
|
527
|
+
state.manager.incrementInFlight(serverName);
|
|
528
|
+
|
|
529
|
+
if (toolMeta.resourceUri) {
|
|
530
|
+
const result = await connection.client.readResource({ uri: toolMeta.resourceUri });
|
|
531
|
+
const content = (result.contents ?? []).map(c => ({
|
|
532
|
+
type: "text" as const,
|
|
533
|
+
text: "text" in c ? c.text : ("blob" in c ? `[Binary data: ${(c as { mimeType?: string }).mimeType ?? "unknown"}]` : JSON.stringify(c)),
|
|
534
|
+
}));
|
|
535
|
+
return {
|
|
536
|
+
content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty resource)" }],
|
|
537
|
+
details: { mode: "call", resourceUri: toolMeta.resourceUri, server: serverName },
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
uiSession = toolMeta.uiResourceUri
|
|
542
|
+
? await maybeStartUiSession(state, {
|
|
543
|
+
serverName,
|
|
544
|
+
toolName: toolMeta.originalName,
|
|
545
|
+
toolArgs: args ?? {},
|
|
546
|
+
uiResourceUri: toolMeta.uiResourceUri,
|
|
547
|
+
streamMode: toolMeta.uiStreamMode,
|
|
548
|
+
})
|
|
549
|
+
: null;
|
|
550
|
+
|
|
551
|
+
const resultPromise = connection.client.callTool({
|
|
552
|
+
name: toolMeta.originalName,
|
|
553
|
+
arguments: args ?? {},
|
|
554
|
+
_meta: uiSession?.requestMeta,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
if (toolMeta.uiResourceUri) {
|
|
558
|
+
const result = await resultPromise;
|
|
559
|
+
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/sdk/types.js").CallToolResult);
|
|
560
|
+
const mcpContent = (result.content ?? []) as McpContent[];
|
|
561
|
+
const content = transformMcpContent(mcpContent);
|
|
562
|
+
|
|
563
|
+
const mcpText = content
|
|
564
|
+
.filter((c) => c.type === "text")
|
|
565
|
+
.map((c) => (c as { text: string }).text)
|
|
566
|
+
.join("\n");
|
|
567
|
+
|
|
568
|
+
if (result.isError) {
|
|
569
|
+
let errorWithSchema = `Error: ${mcpText || "Tool execution failed"}`;
|
|
570
|
+
if (toolMeta.inputSchema) {
|
|
571
|
+
errorWithSchema += `\n\nExpected parameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
content: [{ type: "text" as const, text: errorWithSchema }],
|
|
575
|
+
details: { mode: "call", error: "tool_error", mcpResult: result },
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const resultText = mcpText || "(empty result)";
|
|
580
|
+
const uiMessage = uiSession?.reused
|
|
581
|
+
? "Updated the open UI."
|
|
582
|
+
: "📺 Interactive UI is now open in your browser. I'll respond to your prompts and intents as you interact with it.";
|
|
583
|
+
return {
|
|
584
|
+
content: [{ type: "text" as const, text: `${resultText}\n\n${uiMessage}` }],
|
|
585
|
+
details: { mode: "call", mcpResult: result, server: serverName, tool: toolMeta.originalName, uiOpen: true },
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const result = await resultPromise;
|
|
590
|
+
|
|
591
|
+
const mcpContent = (result.content ?? []) as McpContent[];
|
|
592
|
+
const content = transformMcpContent(mcpContent);
|
|
593
|
+
|
|
594
|
+
if (result.isError) {
|
|
595
|
+
const errorText = content
|
|
596
|
+
.filter((c) => c.type === "text")
|
|
597
|
+
.map((c) => (c as { text: string }).text)
|
|
598
|
+
.join("\n") || "Tool execution failed";
|
|
599
|
+
|
|
600
|
+
let errorWithSchema = `Error: ${errorText}`;
|
|
601
|
+
if (toolMeta.inputSchema) {
|
|
602
|
+
errorWithSchema += `\n\nExpected parameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
content: [{ type: "text" as const, text: errorWithSchema }],
|
|
607
|
+
details: { mode: "call", error: "tool_error", mcpResult: result },
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }],
|
|
613
|
+
details: { mode: "call", mcpResult: result, server: serverName, tool: toolMeta.originalName },
|
|
614
|
+
};
|
|
615
|
+
} catch (error) {
|
|
616
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
617
|
+
uiSession?.sendToolCancelled(message);
|
|
618
|
+
|
|
619
|
+
let errorWithSchema = `Failed to call tool: ${message}`;
|
|
620
|
+
if (toolMeta.inputSchema) {
|
|
621
|
+
errorWithSchema += `\n\nExpected parameters:\n${formatSchema(toolMeta.inputSchema)}`;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return {
|
|
625
|
+
content: [{ type: "text" as const, text: errorWithSchema }],
|
|
626
|
+
details: { mode: "call", error: "call_failed", message },
|
|
627
|
+
};
|
|
628
|
+
} finally {
|
|
629
|
+
if (uiSession?.reused) {
|
|
630
|
+
uiSession.close();
|
|
631
|
+
}
|
|
632
|
+
state.manager.decrementInFlight(serverName);
|
|
633
|
+
state.manager.touch(serverName);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// resource-tools.ts - MCP resource name utilities
|
|
2
|
+
|
|
3
|
+
export function resourceNameToToolName(name: string): string {
|
|
4
|
+
let result = name
|
|
5
|
+
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
6
|
+
.replace(/_+/g, "_")
|
|
7
|
+
.replace(/^_+/, "") // Remove leading underscores
|
|
8
|
+
.replace(/_+$/, "") // Remove trailing underscores
|
|
9
|
+
.toLowerCase();
|
|
10
|
+
|
|
11
|
+
// Ensure we have a valid name
|
|
12
|
+
if (!result || /^\d/.test(result)) {
|
|
13
|
+
result = "resource" + (result ? "_" + result : "");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return result;
|
|
17
|
+
}
|