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,64 @@
|
|
|
1
|
+
import { ConsentError } from "./errors.ts";
|
|
2
|
+
import { logger } from "./logger.ts";
|
|
3
|
+
|
|
4
|
+
export type ToolConsentMode = "never" | "once-per-server" | "always";
|
|
5
|
+
|
|
6
|
+
export class ConsentManager {
|
|
7
|
+
private approvedServers = new Set<string>();
|
|
8
|
+
private deniedServers = new Set<string>();
|
|
9
|
+
private log = logger.child({ component: "ConsentManager" });
|
|
10
|
+
|
|
11
|
+
constructor(private mode: ToolConsentMode = "once-per-server") {
|
|
12
|
+
this.log.debug("Initialized", { mode });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
requiresPrompt(serverName: string): boolean {
|
|
16
|
+
if (this.mode === "never") return false;
|
|
17
|
+
if (this.deniedServers.has(serverName)) return true;
|
|
18
|
+
if (this.mode === "always") return true;
|
|
19
|
+
return !this.approvedServers.has(serverName);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
shouldCacheConsent(): boolean {
|
|
23
|
+
return this.mode !== "always";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
registerDecision(serverName: string, approved: boolean): void {
|
|
27
|
+
this.deniedServers.delete(serverName);
|
|
28
|
+
this.approvedServers.delete(serverName);
|
|
29
|
+
|
|
30
|
+
if (approved) {
|
|
31
|
+
this.approvedServers.add(serverName);
|
|
32
|
+
this.log.debug("Consent granted", { server: serverName });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
this.deniedServers.add(serverName);
|
|
37
|
+
this.log.debug("Consent denied", { server: serverName });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ensureApproved(serverName: string): void {
|
|
41
|
+
if (this.mode === "never") return;
|
|
42
|
+
if (this.deniedServers.has(serverName)) {
|
|
43
|
+
throw new ConsentError(serverName, { denied: true });
|
|
44
|
+
}
|
|
45
|
+
if (!this.approvedServers.has(serverName)) {
|
|
46
|
+
throw new ConsentError(serverName, { requiresApproval: true });
|
|
47
|
+
}
|
|
48
|
+
if (this.mode === "always") {
|
|
49
|
+
this.approvedServers.delete(serverName);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
clear(serverName?: string): void {
|
|
54
|
+
if (serverName) {
|
|
55
|
+
this.approvedServers.delete(serverName);
|
|
56
|
+
this.deniedServers.delete(serverName);
|
|
57
|
+
this.log.debug("Cleared consent for server", { server: serverName });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.approvedServers.clear();
|
|
61
|
+
this.deniedServers.clear();
|
|
62
|
+
this.log.debug("Cleared all consent records");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
import type { AgentToolResult, AgentToolUpdateCallback, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { UrlElicitationRequiredError } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import type { McpExtensionState } from "./state.ts";
|
|
4
|
+
import type { DirectToolSpec, McpConfig, McpContent } from "./types.ts";
|
|
5
|
+
import type { MetadataCache } from "./metadata-cache.ts";
|
|
6
|
+
import { lazyConnect, getFailureAgeSeconds } from "./init.ts";
|
|
7
|
+
import { isServerCacheValid } from "./metadata-cache.ts";
|
|
8
|
+
import { formatSchema } from "./tool-metadata.ts";
|
|
9
|
+
import { transformMcpContent } from "./tool-registrar.ts";
|
|
10
|
+
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.ts";
|
|
11
|
+
import { formatToolName, isToolExcluded } from "./types.ts";
|
|
12
|
+
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
13
|
+
import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
14
|
+
import { formatAuthRequiredMessage } from "./utils.ts";
|
|
15
|
+
|
|
16
|
+
const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
17
|
+
|
|
18
|
+
type DirectAutoAuthResult =
|
|
19
|
+
| { status: "skipped" }
|
|
20
|
+
| { status: "success" }
|
|
21
|
+
| { status: "failed"; message: string };
|
|
22
|
+
|
|
23
|
+
function getDirectAuthRequiredMessage(
|
|
24
|
+
state: McpExtensionState,
|
|
25
|
+
serverName: string,
|
|
26
|
+
defaultMessage = `MCP 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.`,
|
|
27
|
+
): string {
|
|
28
|
+
return formatAuthRequiredMessage(state.config, serverName, defaultMessage);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getDirectAuthFailedMessage(state: McpExtensionState, serverName: string, message: string): string {
|
|
32
|
+
const customGuidance = state.config.settings?.authRequiredMessage;
|
|
33
|
+
if (customGuidance) {
|
|
34
|
+
return `OAuth authentication failed for "${serverName}": ${message}. ${getDirectAuthRequiredMessage(state, serverName)}`;
|
|
35
|
+
}
|
|
36
|
+
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.`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function attemptDirectAutoAuth(
|
|
40
|
+
state: McpExtensionState,
|
|
41
|
+
serverName: string,
|
|
42
|
+
): Promise<DirectAutoAuthResult> {
|
|
43
|
+
if (state.config.settings?.autoAuth !== true) {
|
|
44
|
+
return { status: "skipped" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const definition = state.config.mcpServers[serverName];
|
|
48
|
+
if (!definition || !supportsOAuth(definition) || !definition.url) {
|
|
49
|
+
return { status: "skipped" };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const grantType = definition.oauth ? definition.oauth.grantType ?? "authorization_code" : "authorization_code";
|
|
53
|
+
if (!state.ui && grantType !== "client_credentials") {
|
|
54
|
+
return {
|
|
55
|
+
status: "failed",
|
|
56
|
+
message: getDirectAuthRequiredMessage(
|
|
57
|
+
state,
|
|
58
|
+
serverName,
|
|
59
|
+
`MCP 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.`,
|
|
60
|
+
),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await authenticate(serverName, definition.url, definition);
|
|
66
|
+
return { status: "success" };
|
|
67
|
+
} catch (error) {
|
|
68
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
69
|
+
return {
|
|
70
|
+
status: "failed",
|
|
71
|
+
message: getDirectAuthFailedMessage(state, serverName, message),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function resolveDirectTools(
|
|
77
|
+
config: McpConfig,
|
|
78
|
+
cache: MetadataCache | null,
|
|
79
|
+
prefix: "server" | "none" | "short",
|
|
80
|
+
envOverride?: string[],
|
|
81
|
+
): DirectToolSpec[] {
|
|
82
|
+
const specs: DirectToolSpec[] = [];
|
|
83
|
+
if (!cache) return specs;
|
|
84
|
+
|
|
85
|
+
const seenNames = new Set<string>();
|
|
86
|
+
|
|
87
|
+
const envServers = new Set<string>();
|
|
88
|
+
const envTools = new Map<string, Set<string>>();
|
|
89
|
+
if (envOverride) {
|
|
90
|
+
for (let item of envOverride) {
|
|
91
|
+
item = item.replace(/\/+$/, "");
|
|
92
|
+
if (item.includes("/")) {
|
|
93
|
+
const [server, tool] = item.split("/", 2);
|
|
94
|
+
if (server && tool) {
|
|
95
|
+
if (!envTools.has(server)) envTools.set(server, new Set());
|
|
96
|
+
envTools.get(server)!.add(tool);
|
|
97
|
+
} else if (server) {
|
|
98
|
+
envServers.add(server);
|
|
99
|
+
}
|
|
100
|
+
} else if (item) {
|
|
101
|
+
envServers.add(item);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const globalDirect = config.settings?.directTools;
|
|
107
|
+
|
|
108
|
+
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
109
|
+
const serverCache = cache.servers[serverName];
|
|
110
|
+
if (!serverCache || !isServerCacheValid(serverCache, definition)) continue;
|
|
111
|
+
|
|
112
|
+
let toolFilter: true | string[] | false = false;
|
|
113
|
+
|
|
114
|
+
if (envOverride) {
|
|
115
|
+
if (envServers.has(serverName)) {
|
|
116
|
+
toolFilter = true;
|
|
117
|
+
} else if (envTools.has(serverName)) {
|
|
118
|
+
toolFilter = [...envTools.get(serverName)!];
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
if (definition.directTools !== undefined) {
|
|
122
|
+
toolFilter = definition.directTools;
|
|
123
|
+
} else if (globalDirect) {
|
|
124
|
+
toolFilter = globalDirect;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!toolFilter) continue;
|
|
129
|
+
|
|
130
|
+
for (const tool of serverCache.tools ?? []) {
|
|
131
|
+
if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
|
|
132
|
+
if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) continue;
|
|
133
|
+
const prefixedName = formatToolName(tool.name, serverName, prefix);
|
|
134
|
+
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
135
|
+
console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (seenNames.has(prefixedName)) {
|
|
139
|
+
console.warn(`MCP: skipping duplicate direct tool "${prefixedName}" from "${serverName}"`);
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
seenNames.add(prefixedName);
|
|
143
|
+
specs.push({
|
|
144
|
+
serverName,
|
|
145
|
+
originalName: tool.name,
|
|
146
|
+
prefixedName,
|
|
147
|
+
description: tool.description ?? "",
|
|
148
|
+
inputSchema: tool.inputSchema,
|
|
149
|
+
uiResourceUri: tool.uiResourceUri,
|
|
150
|
+
uiStreamMode: tool.uiStreamMode,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (definition.exposeResources !== false) {
|
|
155
|
+
for (const resource of serverCache.resources ?? []) {
|
|
156
|
+
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
157
|
+
if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
|
|
158
|
+
if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) continue;
|
|
159
|
+
const prefixedName = formatToolName(baseName, serverName, prefix);
|
|
160
|
+
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
161
|
+
console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (seenNames.has(prefixedName)) {
|
|
165
|
+
console.warn(`MCP: skipping duplicate direct resource tool "${prefixedName}" from "${serverName}"`);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
seenNames.add(prefixedName);
|
|
169
|
+
specs.push({
|
|
170
|
+
serverName,
|
|
171
|
+
originalName: baseName,
|
|
172
|
+
prefixedName,
|
|
173
|
+
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
174
|
+
resourceUri: resource.uri,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return specs;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function getMissingConfiguredDirectToolServers(
|
|
184
|
+
config: McpConfig,
|
|
185
|
+
cache: MetadataCache | null,
|
|
186
|
+
): string[] {
|
|
187
|
+
const missing: string[] = [];
|
|
188
|
+
const globalDirect = config.settings?.directTools;
|
|
189
|
+
|
|
190
|
+
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
191
|
+
const hasDirectTools = definition.directTools !== undefined
|
|
192
|
+
? !!definition.directTools
|
|
193
|
+
: !!globalDirect;
|
|
194
|
+
|
|
195
|
+
if (!hasDirectTools) continue;
|
|
196
|
+
|
|
197
|
+
const serverCache = cache?.servers?.[serverName];
|
|
198
|
+
if (!serverCache || !isServerCacheValid(serverCache, definition)) {
|
|
199
|
+
missing.push(serverName);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return missing;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function buildProxyDescription(
|
|
207
|
+
config: McpConfig,
|
|
208
|
+
cache: MetadataCache | null,
|
|
209
|
+
directSpecs: DirectToolSpec[],
|
|
210
|
+
): string {
|
|
211
|
+
const prefix = config.settings?.toolPrefix ?? "server";
|
|
212
|
+
let desc = `MCP gateway - connect to MCP servers and call their tools. Non-MCP Pi tools should be called directly, not through mcp.\n`;
|
|
213
|
+
|
|
214
|
+
const directByServer = new Map<string, number>();
|
|
215
|
+
for (const spec of directSpecs) {
|
|
216
|
+
directByServer.set(spec.serverName, (directByServer.get(spec.serverName) ?? 0) + 1);
|
|
217
|
+
}
|
|
218
|
+
if (directByServer.size > 0) {
|
|
219
|
+
const parts = [...directByServer.entries()].map(
|
|
220
|
+
([server, count]) => `${server} (${count})`,
|
|
221
|
+
);
|
|
222
|
+
desc += `\nDirect tools available (call as normal tools): ${parts.join(", ")}\n`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const serverSummaries: string[] = [];
|
|
226
|
+
for (const serverName of Object.keys(config.mcpServers)) {
|
|
227
|
+
const entry = cache?.servers?.[serverName];
|
|
228
|
+
const definition = config.mcpServers[serverName];
|
|
229
|
+
const toolCount = (entry?.tools ?? []).filter(
|
|
230
|
+
(tool) => !isToolExcluded(tool.name, serverName, prefix, definition.excludeTools),
|
|
231
|
+
).length;
|
|
232
|
+
const resourceCount = definition?.exposeResources !== false
|
|
233
|
+
? (entry?.resources ?? []).filter((resource) => {
|
|
234
|
+
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
235
|
+
return !isToolExcluded(baseName, serverName, prefix, definition.excludeTools);
|
|
236
|
+
}).length
|
|
237
|
+
: 0;
|
|
238
|
+
const totalItems = toolCount + resourceCount;
|
|
239
|
+
if (totalItems === 0) continue;
|
|
240
|
+
const directCount = directByServer.get(serverName) ?? 0;
|
|
241
|
+
const proxyCount = totalItems - directCount;
|
|
242
|
+
if (proxyCount > 0) {
|
|
243
|
+
serverSummaries.push(`${serverName} (${proxyCount} tools)`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (serverSummaries.length > 0) {
|
|
248
|
+
desc += `\nServers: ${serverSummaries.join(", ")}\n`;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
desc += `\nUsage:\n`;
|
|
252
|
+
desc += ` mcp({ }) → Show server status\n`;
|
|
253
|
+
desc += ` mcp({ server: "name" }) → List tools from server\n`;
|
|
254
|
+
desc += ` mcp({ search: "query" }) → Search MCP tools by name/description\n`;
|
|
255
|
+
desc += ` mcp({ describe: "tool_name" }) → Show tool details and parameters\n`;
|
|
256
|
+
desc += ` mcp({ connect: "server-name" }) → Connect to a server and refresh metadata\n`;
|
|
257
|
+
desc += ` mcp({ tool: "name", args: '{"key": "value"}' }) → Call a tool (args is JSON string)\n`;
|
|
258
|
+
desc += ` mcp({ action: "ui-messages" }) → Retrieve accumulated messages from completed UI sessions\n`;
|
|
259
|
+
desc += ` mcp({ action: "auth-start", server: "name" }) → Start manual OAuth and get a browser URL\n`;
|
|
260
|
+
desc += ` mcp({ action: "auth-complete", server: "name", args: '{"redirectUrl":"..."}' }) → Complete manual OAuth\n`;
|
|
261
|
+
desc += `\nMode: action > tool (call) > connect > describe > search > server (list) > nothing (status)`;
|
|
262
|
+
|
|
263
|
+
return desc;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
type DirectToolExecute = (
|
|
267
|
+
toolCallId: string,
|
|
268
|
+
params: Record<string, unknown>,
|
|
269
|
+
signal: AbortSignal | undefined,
|
|
270
|
+
onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
|
|
271
|
+
ctx: ExtensionContext,
|
|
272
|
+
) => Promise<AgentToolResult<Record<string, unknown>>>;
|
|
273
|
+
|
|
274
|
+
export function createDirectToolExecutor(
|
|
275
|
+
getState: () => McpExtensionState | null,
|
|
276
|
+
getInitPromise: () => Promise<McpExtensionState> | null,
|
|
277
|
+
spec: DirectToolSpec
|
|
278
|
+
): DirectToolExecute {
|
|
279
|
+
return async function execute(_toolCallId, params) {
|
|
280
|
+
let state = getState();
|
|
281
|
+
const initPromise = getInitPromise();
|
|
282
|
+
|
|
283
|
+
if (!state && initPromise) {
|
|
284
|
+
try {
|
|
285
|
+
state = await initPromise;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
288
|
+
return {
|
|
289
|
+
content: [{ type: "text" as const, text: `MCP initialization failed: ${message}` }],
|
|
290
|
+
details: { error: "init_failed", message },
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (!state) {
|
|
295
|
+
return {
|
|
296
|
+
content: [{ type: "text" as const, text: "MCP not initialized" }],
|
|
297
|
+
details: { error: "not_initialized" },
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
let connected = await lazyConnect(state, spec.serverName);
|
|
302
|
+
let autoAuthAttempted = false;
|
|
303
|
+
|
|
304
|
+
if (!connected && state.manager.getConnection(spec.serverName)?.status === "needs-auth") {
|
|
305
|
+
autoAuthAttempted = true;
|
|
306
|
+
const autoAuth = await attemptDirectAutoAuth(state, spec.serverName);
|
|
307
|
+
if (autoAuth.status === "failed") {
|
|
308
|
+
return {
|
|
309
|
+
content: [{ type: "text" as const, text: autoAuth.message }],
|
|
310
|
+
details: { error: "auth_required", server: spec.serverName, message: autoAuth.message },
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (autoAuth.status === "success") {
|
|
314
|
+
await state.manager.close(spec.serverName);
|
|
315
|
+
state.failureTracker.delete(spec.serverName);
|
|
316
|
+
connected = await lazyConnect(state, spec.serverName);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (!connected) {
|
|
321
|
+
const authConnection = state.manager.getConnection(spec.serverName);
|
|
322
|
+
if (authConnection?.status === "needs-auth") {
|
|
323
|
+
const message = getDirectAuthRequiredMessage(state, spec.serverName);
|
|
324
|
+
return {
|
|
325
|
+
content: [{ type: "text" as const, text: message }],
|
|
326
|
+
details: { error: "auth_required", server: spec.serverName, message, autoAuthAttempted },
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const failedAgo = getFailureAgeSeconds(state, spec.serverName);
|
|
330
|
+
return {
|
|
331
|
+
content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not available${failedAgo !== null ? ` (failed ${failedAgo}s ago)` : ""}` }],
|
|
332
|
+
details: { error: "server_unavailable", server: spec.serverName },
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const connection = state.manager.getConnection(spec.serverName);
|
|
337
|
+
if (!connection || connection.status !== "connected") {
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not connected` }],
|
|
340
|
+
details: { error: "not_connected", server: spec.serverName },
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
let uiSession: UiSessionRuntime | null = null;
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
state.manager.touch(spec.serverName);
|
|
348
|
+
state.manager.incrementInFlight(spec.serverName);
|
|
349
|
+
|
|
350
|
+
if (spec.resourceUri) {
|
|
351
|
+
const result = await connection.client.readResource({ uri: spec.resourceUri });
|
|
352
|
+
const content = (result.contents ?? []).map(c => ({
|
|
353
|
+
type: "text" as const,
|
|
354
|
+
text: "text" in c ? c.text : ("blob" in c ? `[Binary data: ${(c as { mimeType?: string }).mimeType ?? "unknown"}]` : JSON.stringify(c)),
|
|
355
|
+
}));
|
|
356
|
+
return {
|
|
357
|
+
content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty resource)" }],
|
|
358
|
+
details: { server: spec.serverName, resourceUri: spec.resourceUri },
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const hasUi = !!spec.uiResourceUri;
|
|
363
|
+
uiSession = hasUi
|
|
364
|
+
? await maybeStartUiSession(state, {
|
|
365
|
+
serverName: spec.serverName,
|
|
366
|
+
toolName: spec.originalName,
|
|
367
|
+
toolArgs: params ?? {},
|
|
368
|
+
uiResourceUri: spec.uiResourceUri!,
|
|
369
|
+
streamMode: spec.uiStreamMode,
|
|
370
|
+
})
|
|
371
|
+
: null;
|
|
372
|
+
|
|
373
|
+
const result = await state.manager.callTool(spec.serverName, {
|
|
374
|
+
name: spec.originalName,
|
|
375
|
+
arguments: params ?? {},
|
|
376
|
+
_meta: uiSession?.requestMeta,
|
|
377
|
+
}, { _bookkeep: false });
|
|
378
|
+
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/sdk/types.js").CallToolResult);
|
|
379
|
+
|
|
380
|
+
const mcpContent = (result.content ?? []) as McpContent[];
|
|
381
|
+
const content = transformMcpContent(mcpContent);
|
|
382
|
+
|
|
383
|
+
if (result.isError) {
|
|
384
|
+
let errorText = content.filter(c => c.type === "text").map(c => (c as { text: string }).text).join("\n") || "Tool execution failed";
|
|
385
|
+
if (spec.inputSchema) {
|
|
386
|
+
errorText += `\n\nExpected parameters:\n${formatSchema(spec.inputSchema)}`;
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
content: [{ type: "text" as const, text: `Error: ${errorText}` }],
|
|
390
|
+
details: { error: "tool_error", server: spec.serverName },
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const resultText = content.filter(c => c.type === "text").map(c => (c as { text: string }).text).join("\n") || "(empty result)";
|
|
395
|
+
if (hasUi) {
|
|
396
|
+
const uiMessage = uiSession?.reused
|
|
397
|
+
? "Updated the open UI."
|
|
398
|
+
: "📺 Interactive UI is now open in your browser. I'll respond to your prompts and intents as you interact with it.";
|
|
399
|
+
return {
|
|
400
|
+
content: [{ type: "text" as const, text: `${resultText}\n\n${uiMessage}` }],
|
|
401
|
+
details: { server: spec.serverName, tool: spec.originalName, uiOpen: true },
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }],
|
|
407
|
+
details: { server: spec.serverName, tool: spec.originalName },
|
|
408
|
+
};
|
|
409
|
+
} catch (error) {
|
|
410
|
+
if (error instanceof UrlElicitationRequiredError) {
|
|
411
|
+
const action = await state.manager.handleUrlElicitationRequired(spec.serverName, error);
|
|
412
|
+
const message = action === "accept"
|
|
413
|
+
? "The original MCP tool did not run. Complete the opened browser interaction, then retry the tool."
|
|
414
|
+
: `The URL interaction was ${action === "decline" ? "declined" : "cancelled"}.`;
|
|
415
|
+
uiSession?.sendToolCancelled(message);
|
|
416
|
+
return {
|
|
417
|
+
content: [{ type: "text" as const, text: message }],
|
|
418
|
+
details: { error: "url_elicitation_required", server: spec.serverName, action },
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
422
|
+
uiSession?.sendToolCancelled(message);
|
|
423
|
+
let errorText = `Failed to call tool: ${message}`;
|
|
424
|
+
if (spec.inputSchema) {
|
|
425
|
+
errorText += `\n\nExpected parameters:\n${formatSchema(spec.inputSchema)}`;
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
content: [{ type: "text" as const, text: errorText }],
|
|
429
|
+
details: { error: "call_failed", server: spec.serverName },
|
|
430
|
+
};
|
|
431
|
+
} finally {
|
|
432
|
+
if (uiSession?.reused) {
|
|
433
|
+
uiSession.close();
|
|
434
|
+
}
|
|
435
|
+
state.manager.decrementInFlight(spec.serverName);
|
|
436
|
+
state.manager.touch(spec.serverName);
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
}
|