pi-mcp-adapter 2.1.2 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +60 -2
- package/app-bridge.bundle.js +67 -0
- package/cli.js +0 -1
- package/commands.ts +210 -0
- package/consent-manager.ts +64 -0
- package/direct-tools.ts +301 -0
- package/errors.ts +219 -0
- package/glimpse-ui.ts +80 -0
- package/host-html-template.ts +427 -0
- package/index.ts +38 -1512
- package/init.ts +319 -0
- package/lifecycle.ts +2 -2
- package/logger.ts +169 -0
- package/metadata-cache.ts +16 -0
- package/package.json +27 -4
- package/proxy-modes.ts +635 -0
- package/server-manager.ts +48 -4
- package/state.ts +41 -0
- package/tool-metadata.ts +144 -0
- package/types.ts +211 -0
- package/ui-resource-handler.ts +145 -0
- package/ui-server.ts +623 -0
- package/ui-session.ts +384 -0
- package/ui-stream-types.ts +89 -0
- package/utils.ts +75 -0
- package/ARCHITECTURE.md +0 -630
package/cli.js
CHANGED
package/commands.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { McpExtensionState } from "./state.js";
|
|
3
|
+
import type { McpConfig, ServerEntry, McpPanelCallbacks, McpPanelResult } from "./types.js";
|
|
4
|
+
import { getServerProvenance, writeDirectToolsConfig } from "./config.js";
|
|
5
|
+
import { lazyConnect, updateMetadataCache, updateStatusBar, getFailureAgeSeconds } from "./init.js";
|
|
6
|
+
import { loadMetadataCache } from "./metadata-cache.js";
|
|
7
|
+
import { getStoredTokens } from "./oauth-handler.js";
|
|
8
|
+
import { buildToolMetadata } from "./tool-metadata.js";
|
|
9
|
+
|
|
10
|
+
export async function showStatus(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
|
|
11
|
+
if (!ctx.hasUI) return;
|
|
12
|
+
|
|
13
|
+
const lines: string[] = ["MCP Server Status:", ""];
|
|
14
|
+
|
|
15
|
+
for (const name of Object.keys(state.config.mcpServers)) {
|
|
16
|
+
const connection = state.manager.getConnection(name);
|
|
17
|
+
const metadata = state.toolMetadata.get(name);
|
|
18
|
+
const toolCount = metadata?.length ?? 0;
|
|
19
|
+
const failedAgo = getFailureAgeSeconds(state, name);
|
|
20
|
+
let status = "not connected";
|
|
21
|
+
let statusIcon = "○";
|
|
22
|
+
let failed = false;
|
|
23
|
+
|
|
24
|
+
if (connection?.status === "connected") {
|
|
25
|
+
status = "connected";
|
|
26
|
+
statusIcon = "✓";
|
|
27
|
+
} else if (failedAgo !== null) {
|
|
28
|
+
status = `failed ${failedAgo}s ago`;
|
|
29
|
+
statusIcon = "✗";
|
|
30
|
+
failed = true;
|
|
31
|
+
} else if (metadata !== undefined) {
|
|
32
|
+
status = "cached";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const toolSuffix = failed ? "" : ` (${toolCount} tools${status === "cached" ? ", cached" : ""})`;
|
|
36
|
+
lines.push(`${statusIcon} ${name}: ${status}${toolSuffix}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (Object.keys(state.config.mcpServers).length === 0) {
|
|
40
|
+
lines.push("No MCP servers configured");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function showTools(state: McpExtensionState, ctx: ExtensionContext): Promise<void> {
|
|
47
|
+
if (!ctx.hasUI) return;
|
|
48
|
+
|
|
49
|
+
const allTools = [...state.toolMetadata.values()].flat().map(m => m.name);
|
|
50
|
+
|
|
51
|
+
if (allTools.length === 0) {
|
|
52
|
+
ctx.ui.notify("No MCP tools available", "info");
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lines = [
|
|
57
|
+
"MCP Tools:",
|
|
58
|
+
"",
|
|
59
|
+
...allTools.map(t => ` ${t}`),
|
|
60
|
+
"",
|
|
61
|
+
`Total: ${allTools.length} tools`,
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function reconnectServers(
|
|
68
|
+
state: McpExtensionState,
|
|
69
|
+
ctx: ExtensionContext,
|
|
70
|
+
targetServer?: string
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
if (targetServer && !state.config.mcpServers[targetServer]) {
|
|
73
|
+
if (ctx.hasUI) {
|
|
74
|
+
ctx.ui.notify(`Server "${targetServer}" not found in config`, "error");
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const entries = targetServer
|
|
80
|
+
? [[targetServer, state.config.mcpServers[targetServer]] as [string, ServerEntry]]
|
|
81
|
+
: Object.entries(state.config.mcpServers);
|
|
82
|
+
|
|
83
|
+
for (const [name, definition] of entries) {
|
|
84
|
+
try {
|
|
85
|
+
await state.manager.close(name);
|
|
86
|
+
|
|
87
|
+
const connection = await state.manager.connect(name, definition);
|
|
88
|
+
const prefix = state.config.settings?.toolPrefix ?? "server";
|
|
89
|
+
|
|
90
|
+
const { metadata, failedTools } = buildToolMetadata(connection.tools, connection.resources, definition, name, prefix);
|
|
91
|
+
state.toolMetadata.set(name, metadata);
|
|
92
|
+
updateMetadataCache(state, name);
|
|
93
|
+
state.failureTracker.delete(name);
|
|
94
|
+
|
|
95
|
+
if (ctx.hasUI) {
|
|
96
|
+
ctx.ui.notify(
|
|
97
|
+
`MCP: Reconnected to ${name} (${connection.tools.length} tools, ${connection.resources.length} resources)`,
|
|
98
|
+
"info"
|
|
99
|
+
);
|
|
100
|
+
if (failedTools.length > 0) {
|
|
101
|
+
ctx.ui.notify(`MCP: ${name} - ${failedTools.length} tools skipped`, "warning");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
+
state.failureTracker.set(name, Date.now());
|
|
107
|
+
if (ctx.hasUI) {
|
|
108
|
+
ctx.ui.notify(`MCP: Failed to reconnect to ${name}: ${message}`, "error");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
updateStatusBar(state);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function authenticateServer(
|
|
117
|
+
serverName: string,
|
|
118
|
+
config: McpConfig,
|
|
119
|
+
ctx: ExtensionContext
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
if (!ctx.hasUI) return;
|
|
122
|
+
|
|
123
|
+
const definition = config.mcpServers[serverName];
|
|
124
|
+
if (!definition) {
|
|
125
|
+
ctx.ui.notify(`Server "${serverName}" not found in config`, "error");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (definition.auth !== "oauth") {
|
|
130
|
+
ctx.ui.notify(
|
|
131
|
+
`Server "${serverName}" does not use OAuth authentication.\n` +
|
|
132
|
+
`Current auth mode: ${definition.auth ?? "none"}`,
|
|
133
|
+
"error"
|
|
134
|
+
);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!definition.url) {
|
|
139
|
+
ctx.ui.notify(
|
|
140
|
+
`Server "${serverName}" has no URL configured (OAuth requires HTTP transport)`,
|
|
141
|
+
"error"
|
|
142
|
+
);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const tokenPath = `~/.pi/agent/mcp-oauth/${serverName}/tokens.json`;
|
|
147
|
+
|
|
148
|
+
ctx.ui.notify(
|
|
149
|
+
`OAuth setup for "${serverName}":\n\n` +
|
|
150
|
+
`1. Obtain an access token from your OAuth provider\n` +
|
|
151
|
+
`2. Create the token file:\n` +
|
|
152
|
+
` ${tokenPath}\n\n` +
|
|
153
|
+
`3. Add your token:\n` +
|
|
154
|
+
` {\n` +
|
|
155
|
+
` "access_token": "your-token-here",\n` +
|
|
156
|
+
` "token_type": "bearer"\n` +
|
|
157
|
+
` }\n\n` +
|
|
158
|
+
`4. Run /mcp reconnect to connect with the token`,
|
|
159
|
+
"info"
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function openMcpPanel(
|
|
164
|
+
state: McpExtensionState,
|
|
165
|
+
pi: ExtensionAPI,
|
|
166
|
+
ctx: ExtensionContext,
|
|
167
|
+
configOverridePath?: string,
|
|
168
|
+
): Promise<void> {
|
|
169
|
+
const config = state.config;
|
|
170
|
+
const cache = loadMetadataCache();
|
|
171
|
+
const provenanceMap = getServerProvenance(pi.getFlag("mcp-config") as string | undefined ?? configOverridePath);
|
|
172
|
+
|
|
173
|
+
const callbacks: McpPanelCallbacks = {
|
|
174
|
+
reconnect: async (serverName: string) => {
|
|
175
|
+
return lazyConnect(state, serverName);
|
|
176
|
+
},
|
|
177
|
+
getConnectionStatus: (serverName: string) => {
|
|
178
|
+
const definition = config.mcpServers[serverName];
|
|
179
|
+
if (definition?.auth === "oauth" && getStoredTokens(serverName) === undefined) {
|
|
180
|
+
return "needs-auth";
|
|
181
|
+
}
|
|
182
|
+
const connection = state.manager.getConnection(serverName);
|
|
183
|
+
if (connection?.status === "connected") return "connected";
|
|
184
|
+
if (getFailureAgeSeconds(state, serverName) !== null) return "failed";
|
|
185
|
+
return "idle";
|
|
186
|
+
},
|
|
187
|
+
refreshCacheAfterReconnect: (serverName: string) => {
|
|
188
|
+
const freshCache = loadMetadataCache();
|
|
189
|
+
return freshCache?.servers?.[serverName] ?? null;
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const { createMcpPanel } = await import("./mcp-panel.js");
|
|
194
|
+
|
|
195
|
+
return new Promise<void>((resolve) => {
|
|
196
|
+
ctx.ui.custom(
|
|
197
|
+
(tui, _theme, _keybindings, done) => {
|
|
198
|
+
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, (result: McpPanelResult) => {
|
|
199
|
+
if (!result.cancelled && result.changes.size > 0) {
|
|
200
|
+
writeDirectToolsConfig(result.changes, provenanceMap, config);
|
|
201
|
+
ctx.ui.notify("Direct tools updated. Restart pi to apply.", "info");
|
|
202
|
+
}
|
|
203
|
+
done();
|
|
204
|
+
resolve();
|
|
205
|
+
});
|
|
206
|
+
},
|
|
207
|
+
{ overlay: true, overlayOptions: { anchor: "center", width: 82 } },
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { ConsentError } from "./errors.js";
|
|
2
|
+
import { logger } from "./logger.js";
|
|
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
|
+
}
|
package/direct-tools.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type { ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { McpExtensionState } from "./state.js";
|
|
3
|
+
import type { DirectToolSpec, McpConfig, McpContent } from "./types.js";
|
|
4
|
+
import type { MetadataCache } from "./metadata-cache.js";
|
|
5
|
+
import { lazyConnect, getFailureAgeSeconds } from "./init.js";
|
|
6
|
+
import { isServerCacheValid } from "./metadata-cache.js";
|
|
7
|
+
import { formatSchema } from "./tool-metadata.js";
|
|
8
|
+
import { transformMcpContent } from "./tool-registrar.js";
|
|
9
|
+
import { maybeStartUiSession, type UiSessionRuntime } from "./ui-session.js";
|
|
10
|
+
import { formatToolName } from "./types.js";
|
|
11
|
+
import { resourceNameToToolName } from "./resource-tools.js";
|
|
12
|
+
|
|
13
|
+
const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
14
|
+
|
|
15
|
+
export function resolveDirectTools(
|
|
16
|
+
config: McpConfig,
|
|
17
|
+
cache: MetadataCache | null,
|
|
18
|
+
prefix: "server" | "none" | "short",
|
|
19
|
+
envOverride?: string[],
|
|
20
|
+
): DirectToolSpec[] {
|
|
21
|
+
const specs: DirectToolSpec[] = [];
|
|
22
|
+
if (!cache) return specs;
|
|
23
|
+
|
|
24
|
+
const seenNames = new Set<string>();
|
|
25
|
+
|
|
26
|
+
const envServers = new Set<string>();
|
|
27
|
+
const envTools = new Map<string, Set<string>>();
|
|
28
|
+
if (envOverride) {
|
|
29
|
+
for (let item of envOverride) {
|
|
30
|
+
item = item.replace(/\/+$/, "");
|
|
31
|
+
if (item.includes("/")) {
|
|
32
|
+
const [server, tool] = item.split("/", 2);
|
|
33
|
+
if (server && tool) {
|
|
34
|
+
if (!envTools.has(server)) envTools.set(server, new Set());
|
|
35
|
+
envTools.get(server)!.add(tool);
|
|
36
|
+
} else if (server) {
|
|
37
|
+
envServers.add(server);
|
|
38
|
+
}
|
|
39
|
+
} else if (item) {
|
|
40
|
+
envServers.add(item);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const globalDirect = config.settings?.directTools;
|
|
46
|
+
|
|
47
|
+
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
48
|
+
const serverCache = cache.servers[serverName];
|
|
49
|
+
if (!serverCache || !isServerCacheValid(serverCache, definition)) continue;
|
|
50
|
+
|
|
51
|
+
let toolFilter: true | string[] | false = false;
|
|
52
|
+
|
|
53
|
+
if (envOverride) {
|
|
54
|
+
if (envServers.has(serverName)) {
|
|
55
|
+
toolFilter = true;
|
|
56
|
+
} else if (envTools.has(serverName)) {
|
|
57
|
+
toolFilter = [...envTools.get(serverName)!];
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
if (definition.directTools !== undefined) {
|
|
61
|
+
toolFilter = definition.directTools;
|
|
62
|
+
} else if (globalDirect) {
|
|
63
|
+
toolFilter = globalDirect;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!toolFilter) continue;
|
|
68
|
+
|
|
69
|
+
for (const tool of serverCache.tools ?? []) {
|
|
70
|
+
if (toolFilter !== true && !toolFilter.includes(tool.name)) continue;
|
|
71
|
+
const prefixedName = formatToolName(tool.name, serverName, prefix);
|
|
72
|
+
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
73
|
+
console.warn(`MCP: skipping direct tool "${prefixedName}" (collides with builtin)`);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (seenNames.has(prefixedName)) {
|
|
77
|
+
console.warn(`MCP: skipping duplicate direct tool "${prefixedName}" from "${serverName}"`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
seenNames.add(prefixedName);
|
|
81
|
+
specs.push({
|
|
82
|
+
serverName,
|
|
83
|
+
originalName: tool.name,
|
|
84
|
+
prefixedName,
|
|
85
|
+
description: tool.description ?? "",
|
|
86
|
+
inputSchema: tool.inputSchema,
|
|
87
|
+
uiResourceUri: tool.uiResourceUri,
|
|
88
|
+
uiStreamMode: tool.uiStreamMode,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (definition.exposeResources !== false) {
|
|
93
|
+
for (const resource of serverCache.resources ?? []) {
|
|
94
|
+
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
95
|
+
if (toolFilter !== true && !toolFilter.includes(baseName)) continue;
|
|
96
|
+
const prefixedName = formatToolName(baseName, serverName, prefix);
|
|
97
|
+
if (BUILTIN_NAMES.has(prefixedName)) {
|
|
98
|
+
console.warn(`MCP: skipping direct resource tool "${prefixedName}" (collides with builtin)`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (seenNames.has(prefixedName)) {
|
|
102
|
+
console.warn(`MCP: skipping duplicate direct resource tool "${prefixedName}" from "${serverName}"`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
seenNames.add(prefixedName);
|
|
106
|
+
specs.push({
|
|
107
|
+
serverName,
|
|
108
|
+
originalName: baseName,
|
|
109
|
+
prefixedName,
|
|
110
|
+
description: resource.description ?? `Read resource: ${resource.uri}`,
|
|
111
|
+
resourceUri: resource.uri,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return specs;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildProxyDescription(
|
|
121
|
+
config: McpConfig,
|
|
122
|
+
cache: MetadataCache | null,
|
|
123
|
+
directSpecs: DirectToolSpec[],
|
|
124
|
+
): string {
|
|
125
|
+
let desc = `MCP gateway - connect to MCP servers and call their tools.\n`;
|
|
126
|
+
|
|
127
|
+
const directByServer = new Map<string, number>();
|
|
128
|
+
for (const spec of directSpecs) {
|
|
129
|
+
directByServer.set(spec.serverName, (directByServer.get(spec.serverName) ?? 0) + 1);
|
|
130
|
+
}
|
|
131
|
+
if (directByServer.size > 0) {
|
|
132
|
+
const parts = [...directByServer.entries()].map(
|
|
133
|
+
([server, count]) => `${server} (${count})`,
|
|
134
|
+
);
|
|
135
|
+
desc += `\nDirect tools available (call as normal tools): ${parts.join(", ")}\n`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const serverSummaries: string[] = [];
|
|
139
|
+
for (const serverName of Object.keys(config.mcpServers)) {
|
|
140
|
+
const entry = cache?.servers?.[serverName];
|
|
141
|
+
const definition = config.mcpServers[serverName];
|
|
142
|
+
const toolCount = entry?.tools?.length ?? 0;
|
|
143
|
+
const resourceCount = definition?.exposeResources !== false ? (entry?.resources?.length ?? 0) : 0;
|
|
144
|
+
const totalItems = toolCount + resourceCount;
|
|
145
|
+
if (totalItems === 0) continue;
|
|
146
|
+
const directCount = directByServer.get(serverName) ?? 0;
|
|
147
|
+
const proxyCount = totalItems - directCount;
|
|
148
|
+
if (proxyCount > 0) {
|
|
149
|
+
serverSummaries.push(`${serverName} (${proxyCount} tools)`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (serverSummaries.length > 0) {
|
|
154
|
+
desc += `\nServers: ${serverSummaries.join(", ")}\n`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
desc += `\nUsage:\n`;
|
|
158
|
+
desc += ` mcp({ }) → Show server status\n`;
|
|
159
|
+
desc += ` mcp({ server: "name" }) → List tools from server\n`;
|
|
160
|
+
desc += ` mcp({ search: "query" }) → Search for tools (MCP + pi, space-separated words OR'd)\n`;
|
|
161
|
+
desc += ` mcp({ describe: "tool_name" }) → Show tool details and parameters\n`;
|
|
162
|
+
desc += ` mcp({ connect: "server-name" }) → Connect to a server and refresh metadata\n`;
|
|
163
|
+
desc += ` mcp({ tool: "name", args: '{"key": "value"}' }) → Call a tool (args is JSON string)\n`;
|
|
164
|
+
desc += ` mcp({ action: "ui-messages" }) → Retrieve accumulated messages from completed UI sessions\n`;
|
|
165
|
+
desc += `\nMode: tool (call) > connect > describe > search > server (list) > action > nothing (status)`;
|
|
166
|
+
|
|
167
|
+
return desc;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
type DirectToolExecute = ToolDefinition["execute"];
|
|
171
|
+
|
|
172
|
+
export function createDirectToolExecutor(
|
|
173
|
+
getState: () => McpExtensionState | null,
|
|
174
|
+
getInitPromise: () => Promise<McpExtensionState> | null,
|
|
175
|
+
spec: DirectToolSpec
|
|
176
|
+
): DirectToolExecute {
|
|
177
|
+
return async function execute(_toolCallId, params) {
|
|
178
|
+
let state = getState();
|
|
179
|
+
const initPromise = getInitPromise();
|
|
180
|
+
|
|
181
|
+
if (!state && initPromise) {
|
|
182
|
+
try {
|
|
183
|
+
state = await initPromise;
|
|
184
|
+
} catch {
|
|
185
|
+
return {
|
|
186
|
+
content: [{ type: "text" as const, text: "MCP initialization failed" }],
|
|
187
|
+
details: { error: "init_failed" },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!state) {
|
|
192
|
+
return {
|
|
193
|
+
content: [{ type: "text" as const, text: "MCP not initialized" }],
|
|
194
|
+
details: { error: "not_initialized" },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const connected = await lazyConnect(state, spec.serverName);
|
|
199
|
+
if (!connected) {
|
|
200
|
+
const failedAgo = getFailureAgeSeconds(state, spec.serverName);
|
|
201
|
+
return {
|
|
202
|
+
content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not available${failedAgo !== null ? ` (failed ${failedAgo}s ago)` : ""}` }],
|
|
203
|
+
details: { error: "server_unavailable", server: spec.serverName },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const connection = state.manager.getConnection(spec.serverName);
|
|
208
|
+
if (!connection || connection.status !== "connected") {
|
|
209
|
+
return {
|
|
210
|
+
content: [{ type: "text" as const, text: `MCP server "${spec.serverName}" not connected` }],
|
|
211
|
+
details: { error: "not_connected", server: spec.serverName },
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let uiSession: UiSessionRuntime | null = null;
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
state.manager.touch(spec.serverName);
|
|
219
|
+
state.manager.incrementInFlight(spec.serverName);
|
|
220
|
+
|
|
221
|
+
if (spec.resourceUri) {
|
|
222
|
+
const result = await connection.client.readResource({ uri: spec.resourceUri });
|
|
223
|
+
const content = (result.contents ?? []).map(c => ({
|
|
224
|
+
type: "text" as const,
|
|
225
|
+
text: "text" in c ? c.text : ("blob" in c ? `[Binary data: ${(c as { mimeType?: string }).mimeType ?? "unknown"}]` : JSON.stringify(c)),
|
|
226
|
+
}));
|
|
227
|
+
return {
|
|
228
|
+
content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty resource)" }],
|
|
229
|
+
details: { server: spec.serverName, resourceUri: spec.resourceUri },
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const hasUi = !!spec.uiResourceUri;
|
|
234
|
+
uiSession = hasUi
|
|
235
|
+
? await maybeStartUiSession(state, {
|
|
236
|
+
serverName: spec.serverName,
|
|
237
|
+
toolName: spec.originalName,
|
|
238
|
+
toolArgs: params ?? {},
|
|
239
|
+
uiResourceUri: spec.uiResourceUri!,
|
|
240
|
+
streamMode: spec.uiStreamMode,
|
|
241
|
+
})
|
|
242
|
+
: null;
|
|
243
|
+
|
|
244
|
+
const resultPromise = connection.client.callTool({
|
|
245
|
+
name: spec.originalName,
|
|
246
|
+
arguments: params ?? {},
|
|
247
|
+
_meta: uiSession?.requestMeta,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const result = await resultPromise;
|
|
251
|
+
uiSession?.sendToolResult(result as unknown as import("@modelcontextprotocol/sdk/types.js").CallToolResult);
|
|
252
|
+
|
|
253
|
+
const mcpContent = (result.content ?? []) as McpContent[];
|
|
254
|
+
const content = transformMcpContent(mcpContent);
|
|
255
|
+
|
|
256
|
+
if (result.isError) {
|
|
257
|
+
let errorText = content.filter(c => c.type === "text").map(c => (c as { text: string }).text).join("\n") || "Tool execution failed";
|
|
258
|
+
if (spec.inputSchema) {
|
|
259
|
+
errorText += `\n\nExpected parameters:\n${formatSchema(spec.inputSchema)}`;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
content: [{ type: "text" as const, text: `Error: ${errorText}` }],
|
|
263
|
+
details: { error: "tool_error", server: spec.serverName },
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const resultText = content.filter(c => c.type === "text").map(c => (c as { text: string }).text).join("\n") || "(empty result)";
|
|
268
|
+
if (hasUi) {
|
|
269
|
+
const uiMessage = uiSession?.reused
|
|
270
|
+
? "Updated the open UI."
|
|
271
|
+
: "📺 Interactive UI is now open in your browser. I'll respond to your prompts and intents as you interact with it.";
|
|
272
|
+
return {
|
|
273
|
+
content: [{ type: "text" as const, text: `${resultText}\n\n${uiMessage}` }],
|
|
274
|
+
details: { server: spec.serverName, tool: spec.originalName, uiOpen: true },
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }],
|
|
280
|
+
details: { server: spec.serverName, tool: spec.originalName },
|
|
281
|
+
};
|
|
282
|
+
} catch (error) {
|
|
283
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
284
|
+
uiSession?.sendToolCancelled(message);
|
|
285
|
+
let errorText = `Failed to call tool: ${message}`;
|
|
286
|
+
if (spec.inputSchema) {
|
|
287
|
+
errorText += `\n\nExpected parameters:\n${formatSchema(spec.inputSchema)}`;
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
content: [{ type: "text" as const, text: errorText }],
|
|
291
|
+
details: { error: "call_failed", server: spec.serverName },
|
|
292
|
+
};
|
|
293
|
+
} finally {
|
|
294
|
+
if (uiSession?.reused) {
|
|
295
|
+
uiSession.close();
|
|
296
|
+
}
|
|
297
|
+
state.manager.decrementInFlight(spec.serverName);
|
|
298
|
+
state.manager.touch(spec.serverName);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|