@oh-my-pi/pi-coding-agent 6.2.0 → 6.7.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/CHANGELOG.md +46 -0
- package/docs/sdk.md +1 -1
- package/package.json +5 -5
- package/scripts/generate-template.ts +6 -6
- package/src/cli/args.ts +3 -0
- package/src/core/agent-session.ts +39 -0
- package/src/core/bash-executor.ts +3 -3
- package/src/core/cursor/exec-bridge.ts +95 -88
- package/src/core/custom-commands/bundled/review/index.ts +142 -145
- package/src/core/custom-commands/bundled/wt/index.ts +68 -66
- package/src/core/custom-commands/loader.ts +4 -6
- package/src/core/custom-tools/index.ts +2 -2
- package/src/core/custom-tools/loader.ts +66 -61
- package/src/core/custom-tools/types.ts +4 -4
- package/src/core/custom-tools/wrapper.ts +61 -25
- package/src/core/event-bus.ts +19 -47
- package/src/core/extensions/index.ts +8 -4
- package/src/core/extensions/loader.ts +160 -120
- package/src/core/extensions/types.ts +4 -4
- package/src/core/extensions/wrapper.ts +149 -100
- package/src/core/hooks/index.ts +1 -1
- package/src/core/hooks/tool-wrapper.ts +96 -70
- package/src/core/hooks/types.ts +1 -2
- package/src/core/index.ts +1 -0
- package/src/core/mcp/index.ts +6 -2
- package/src/core/mcp/json-rpc.ts +88 -0
- package/src/core/mcp/loader.ts +22 -4
- package/src/core/mcp/manager.ts +202 -48
- package/src/core/mcp/tool-bridge.ts +143 -55
- package/src/core/mcp/tool-cache.ts +122 -0
- package/src/core/python-executor.ts +3 -9
- package/src/core/sdk.ts +33 -32
- package/src/core/session-manager.ts +30 -0
- package/src/core/settings-manager.ts +34 -1
- package/src/core/ssh/ssh-executor.ts +6 -84
- package/src/core/streaming-output.ts +107 -53
- package/src/core/tools/ask.ts +92 -93
- package/src/core/tools/bash.ts +103 -94
- package/src/core/tools/calculator.ts +41 -26
- package/src/core/tools/complete.ts +76 -66
- package/src/core/tools/context.ts +22 -24
- package/src/core/tools/exa/index.ts +1 -1
- package/src/core/tools/exa/mcp-client.ts +56 -101
- package/src/core/tools/find.ts +250 -253
- package/src/core/tools/git.ts +39 -33
- package/src/core/tools/grep.ts +440 -427
- package/src/core/tools/index.ts +62 -61
- package/src/core/tools/ls.ts +119 -114
- package/src/core/tools/lsp/clients/biome-client.ts +5 -7
- package/src/core/tools/lsp/clients/index.ts +4 -4
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +5 -7
- package/src/core/tools/lsp/config.ts +2 -2
- package/src/core/tools/lsp/index.ts +604 -578
- package/src/core/tools/notebook.ts +121 -119
- package/src/core/tools/output.ts +163 -147
- package/src/core/tools/patch/applicator.ts +1100 -0
- package/src/core/tools/patch/diff.ts +362 -0
- package/src/core/tools/patch/fuzzy.ts +647 -0
- package/src/core/tools/patch/index.ts +430 -0
- package/src/core/tools/patch/normalize.ts +220 -0
- package/src/core/tools/patch/normative.ts +49 -0
- package/src/core/tools/patch/parser.ts +528 -0
- package/src/core/tools/patch/shared.ts +228 -0
- package/src/core/tools/patch/types.ts +244 -0
- package/src/core/tools/python.ts +139 -136
- package/src/core/tools/read.ts +237 -216
- package/src/core/tools/render-utils.ts +196 -77
- package/src/core/tools/renderers.ts +1 -1
- package/src/core/tools/ssh.ts +99 -80
- package/src/core/tools/task/executor.ts +11 -7
- package/src/core/tools/task/index.ts +352 -343
- package/src/core/tools/task/worker.ts +13 -23
- package/src/core/tools/todo-write.ts +74 -59
- package/src/core/tools/web-fetch.ts +54 -47
- package/src/core/tools/web-search/index.ts +27 -16
- package/src/core/tools/write.ts +73 -44
- package/src/core/ttsr.ts +106 -152
- package/src/core/voice.ts +49 -39
- package/src/index.ts +16 -12
- package/src/lib/worktree/index.ts +1 -9
- package/src/modes/interactive/components/diff.ts +15 -8
- package/src/modes/interactive/components/settings-defs.ts +24 -0
- package/src/modes/interactive/components/tool-execution.ts +34 -6
- package/src/modes/interactive/controllers/event-controller.ts +6 -19
- package/src/modes/interactive/controllers/input-controller.ts +1 -1
- package/src/modes/interactive/utils/ui-helpers.ts +5 -1
- package/src/modes/rpc/rpc-mode.ts +99 -81
- package/src/prompts/tools/patch.md +76 -0
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/{edit.md → replace.md} +1 -0
- package/src/utils/shell.ts +0 -40
- package/src/core/tools/edit-diff.ts +0 -574
- package/src/core/tools/edit.ts +0 -345
package/src/core/mcp/manager.ts
CHANGED
|
@@ -7,11 +7,48 @@
|
|
|
7
7
|
|
|
8
8
|
import type { TSchema } from "@sinclair/typebox";
|
|
9
9
|
import type { CustomTool } from "../custom-tools/types";
|
|
10
|
+
import { logger } from "../logger";
|
|
10
11
|
import { connectToServer, disconnectServer, listTools } from "./client";
|
|
11
12
|
import { loadAllMCPConfigs, validateServerConfig } from "./config";
|
|
12
13
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
13
|
-
import {
|
|
14
|
-
import type {
|
|
14
|
+
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
15
|
+
import type { MCPToolCache } from "./tool-cache";
|
|
16
|
+
import type { MCPServerConfig, MCPServerConnection, MCPToolDefinition } from "./types";
|
|
17
|
+
|
|
18
|
+
type SourceMeta = import("../../capability/types").SourceMeta;
|
|
19
|
+
|
|
20
|
+
type ToolLoadResult = {
|
|
21
|
+
connection: MCPServerConnection;
|
|
22
|
+
serverTools: MCPToolDefinition[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type TrackedPromise<T> = {
|
|
26
|
+
promise: Promise<T>;
|
|
27
|
+
status: "pending" | "fulfilled" | "rejected";
|
|
28
|
+
value?: T;
|
|
29
|
+
reason?: unknown;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const STARTUP_TIMEOUT_MS = 250;
|
|
33
|
+
|
|
34
|
+
function trackPromise<T>(promise: Promise<T>): TrackedPromise<T> {
|
|
35
|
+
const tracked: TrackedPromise<T> = { promise, status: "pending" };
|
|
36
|
+
promise.then(
|
|
37
|
+
(value) => {
|
|
38
|
+
tracked.status = "fulfilled";
|
|
39
|
+
tracked.value = value;
|
|
40
|
+
},
|
|
41
|
+
(reason) => {
|
|
42
|
+
tracked.status = "rejected";
|
|
43
|
+
tracked.reason = reason;
|
|
44
|
+
},
|
|
45
|
+
);
|
|
46
|
+
return tracked;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function delay(ms: number): Promise<void> {
|
|
50
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
51
|
+
}
|
|
15
52
|
|
|
16
53
|
/** Result of loading MCP tools */
|
|
17
54
|
export interface MCPLoadResult {
|
|
@@ -43,8 +80,14 @@ export interface MCPDiscoverOptions {
|
|
|
43
80
|
export class MCPManager {
|
|
44
81
|
private connections = new Map<string, MCPServerConnection>();
|
|
45
82
|
private tools: CustomTool<TSchema, MCPToolDetails>[] = [];
|
|
83
|
+
private pendingConnections = new Map<string, Promise<MCPServerConnection>>();
|
|
84
|
+
private pendingToolLoads = new Map<string, Promise<ToolLoadResult>>();
|
|
85
|
+
private sources = new Map<string, SourceMeta>();
|
|
46
86
|
|
|
47
|
-
constructor(
|
|
87
|
+
constructor(
|
|
88
|
+
private cwd: string,
|
|
89
|
+
private toolCache: MCPToolCache | null = null,
|
|
90
|
+
) {}
|
|
48
91
|
|
|
49
92
|
/**
|
|
50
93
|
* Discover and connect to all MCP servers from .mcp.json files.
|
|
@@ -66,24 +109,41 @@ export class MCPManager {
|
|
|
66
109
|
*/
|
|
67
110
|
async connectServers(
|
|
68
111
|
configs: Record<string, MCPServerConfig>,
|
|
69
|
-
sources: Record<string,
|
|
112
|
+
sources: Record<string, SourceMeta>,
|
|
70
113
|
onConnecting?: (serverNames: string[]) => void,
|
|
71
114
|
): Promise<MCPLoadResult> {
|
|
115
|
+
type ConnectionTask = {
|
|
116
|
+
name: string;
|
|
117
|
+
config: MCPServerConfig;
|
|
118
|
+
tracked: TrackedPromise<ToolLoadResult>;
|
|
119
|
+
toolsPromise: Promise<ToolLoadResult>;
|
|
120
|
+
};
|
|
121
|
+
|
|
72
122
|
const errors = new Map<string, string>();
|
|
73
|
-
const connectedServers
|
|
123
|
+
const connectedServers = new Set<string>();
|
|
74
124
|
const allTools: CustomTool<TSchema, MCPToolDetails>[] = [];
|
|
125
|
+
const reportedErrors = new Set<string>();
|
|
126
|
+
let allowBackgroundLogging = false;
|
|
75
127
|
|
|
76
128
|
// Prepare connection tasks
|
|
77
|
-
const connectionTasks:
|
|
78
|
-
name: string;
|
|
79
|
-
config: MCPServerConfig;
|
|
80
|
-
validationErrors: string[];
|
|
81
|
-
}> = [];
|
|
129
|
+
const connectionTasks: ConnectionTask[] = [];
|
|
82
130
|
|
|
83
131
|
for (const [name, config] of Object.entries(configs)) {
|
|
132
|
+
if (sources[name]) {
|
|
133
|
+
this.sources.set(name, sources[name]);
|
|
134
|
+
const existing = this.connections.get(name);
|
|
135
|
+
if (existing) {
|
|
136
|
+
existing._source = sources[name];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
84
140
|
// Skip if already connected
|
|
85
141
|
if (this.connections.has(name)) {
|
|
86
|
-
connectedServers.
|
|
142
|
+
connectedServers.add(name);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (this.pendingConnections.has(name) || this.pendingToolLoads.has(name)) {
|
|
87
147
|
continue;
|
|
88
148
|
}
|
|
89
149
|
|
|
@@ -91,59 +151,128 @@ export class MCPManager {
|
|
|
91
151
|
const validationErrors = validateServerConfig(name, config);
|
|
92
152
|
if (validationErrors.length > 0) {
|
|
93
153
|
errors.set(name, validationErrors.join("; "));
|
|
154
|
+
reportedErrors.add(name);
|
|
94
155
|
continue;
|
|
95
156
|
}
|
|
96
157
|
|
|
97
|
-
|
|
158
|
+
const connectionPromise = connectToServer(name, config).then(
|
|
159
|
+
(connection) => {
|
|
160
|
+
if (sources[name]) {
|
|
161
|
+
connection._source = sources[name];
|
|
162
|
+
}
|
|
163
|
+
if (this.pendingConnections.get(name) === connectionPromise) {
|
|
164
|
+
this.pendingConnections.delete(name);
|
|
165
|
+
this.connections.set(name, connection);
|
|
166
|
+
}
|
|
167
|
+
return connection;
|
|
168
|
+
},
|
|
169
|
+
(error) => {
|
|
170
|
+
if (this.pendingConnections.get(name) === connectionPromise) {
|
|
171
|
+
this.pendingConnections.delete(name);
|
|
172
|
+
}
|
|
173
|
+
throw error;
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
this.pendingConnections.set(name, connectionPromise);
|
|
177
|
+
|
|
178
|
+
const toolsPromise = connectionPromise.then(async (connection) => {
|
|
179
|
+
const serverTools = await listTools(connection);
|
|
180
|
+
return { connection, serverTools };
|
|
181
|
+
});
|
|
182
|
+
this.pendingToolLoads.set(name, toolsPromise);
|
|
183
|
+
|
|
184
|
+
const tracked = trackPromise(toolsPromise);
|
|
185
|
+
connectionTasks.push({ name, config, tracked, toolsPromise });
|
|
186
|
+
|
|
187
|
+
void toolsPromise
|
|
188
|
+
.then(({ connection, serverTools }) => {
|
|
189
|
+
if (this.pendingToolLoads.get(name) !== toolsPromise) return;
|
|
190
|
+
this.pendingToolLoads.delete(name);
|
|
191
|
+
const customTools = MCPTool.fromTools(connection, serverTools);
|
|
192
|
+
this.replaceServerTools(name, customTools);
|
|
193
|
+
void this.toolCache?.set(name, config, serverTools);
|
|
194
|
+
})
|
|
195
|
+
.catch((error) => {
|
|
196
|
+
if (this.pendingToolLoads.get(name) !== toolsPromise) return;
|
|
197
|
+
this.pendingToolLoads.delete(name);
|
|
198
|
+
if (!allowBackgroundLogging || reportedErrors.has(name)) return;
|
|
199
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
200
|
+
logger.error("MCP tool load failed", { path: `mcp:${name}`, error: message });
|
|
201
|
+
});
|
|
98
202
|
}
|
|
99
203
|
|
|
100
204
|
// Notify about servers we're connecting to
|
|
101
205
|
if (connectionTasks.length > 0 && onConnecting) {
|
|
102
|
-
onConnecting(connectionTasks.map((
|
|
206
|
+
onConnecting(connectionTasks.map((task) => task.name));
|
|
103
207
|
}
|
|
104
208
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
209
|
+
if (connectionTasks.length > 0) {
|
|
210
|
+
await Promise.race([
|
|
211
|
+
Promise.allSettled(connectionTasks.map((task) => task.tracked.promise)),
|
|
212
|
+
delay(STARTUP_TIMEOUT_MS),
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
const cachedTools = new Map<string, MCPToolDefinition[]>();
|
|
216
|
+
const pendingTasks = connectionTasks.filter((task) => task.tracked.status === "pending");
|
|
217
|
+
|
|
218
|
+
if (pendingTasks.length > 0) {
|
|
219
|
+
if (this.toolCache) {
|
|
220
|
+
await Promise.all(
|
|
221
|
+
pendingTasks.map(async (task) => {
|
|
222
|
+
const cached = await this.toolCache?.get(task.name, task.config);
|
|
223
|
+
if (cached) {
|
|
224
|
+
cachedTools.set(task.name, cached);
|
|
225
|
+
}
|
|
226
|
+
}),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const pendingWithoutCache = pendingTasks.filter((task) => !cachedTools.has(task.name));
|
|
231
|
+
if (pendingWithoutCache.length > 0) {
|
|
232
|
+
await Promise.allSettled(pendingWithoutCache.map((task) => task.tracked.promise));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
for (const task of connectionTasks) {
|
|
237
|
+
const { name } = task;
|
|
238
|
+
if (task.tracked.status === "fulfilled") {
|
|
239
|
+
const value = task.tracked.value;
|
|
240
|
+
if (!value) continue;
|
|
241
|
+
const { connection, serverTools } = value;
|
|
242
|
+
connectedServers.add(name);
|
|
243
|
+
allTools.push(...MCPTool.fromTools(connection, serverTools));
|
|
244
|
+
} else if (task.tracked.status === "rejected") {
|
|
245
|
+
const message =
|
|
246
|
+
task.tracked.reason instanceof Error ? task.tracked.reason.message : String(task.tracked.reason);
|
|
247
|
+
errors.set(name, message);
|
|
248
|
+
reportedErrors.add(name);
|
|
249
|
+
} else {
|
|
250
|
+
const cached = cachedTools.get(name);
|
|
251
|
+
if (cached) {
|
|
252
|
+
const source = this.sources.get(name);
|
|
253
|
+
allTools.push(...DeferredMCPTool.fromTools(name, cached, () => this.waitForConnection(name), source));
|
|
254
|
+
}
|
|
112
255
|
}
|
|
113
|
-
const serverTools = await listTools(connection);
|
|
114
|
-
return { name, connection, serverTools };
|
|
115
|
-
}),
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// Process results
|
|
119
|
-
for (let i = 0; i < results.length; i++) {
|
|
120
|
-
const result = results[i];
|
|
121
|
-
const { name } = connectionTasks[i];
|
|
122
|
-
|
|
123
|
-
if (result.status === "fulfilled") {
|
|
124
|
-
const { connection, serverTools } = result.value;
|
|
125
|
-
this.connections.set(name, connection);
|
|
126
|
-
connectedServers.push(name);
|
|
127
|
-
|
|
128
|
-
const customTools = createMCPTools(connection, serverTools);
|
|
129
|
-
allTools.push(...customTools);
|
|
130
|
-
} else {
|
|
131
|
-
const message = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
132
|
-
errors.set(name, message);
|
|
133
256
|
}
|
|
134
257
|
}
|
|
135
258
|
|
|
136
259
|
// Update cached tools
|
|
137
260
|
this.tools = allTools;
|
|
261
|
+
allowBackgroundLogging = true;
|
|
138
262
|
|
|
139
263
|
return {
|
|
140
264
|
tools: allTools,
|
|
141
265
|
errors,
|
|
142
|
-
connectedServers,
|
|
266
|
+
connectedServers: Array.from(connectedServers),
|
|
143
267
|
exaApiKeys: [], // Will be populated by discoverAndConnect
|
|
144
268
|
};
|
|
145
269
|
}
|
|
146
270
|
|
|
271
|
+
private replaceServerTools(name: string, tools: CustomTool<TSchema, MCPToolDetails>[]): void {
|
|
272
|
+
this.tools = this.tools.filter((t) => !t.name.startsWith(`mcp_${name}_`));
|
|
273
|
+
this.tools.push(...tools);
|
|
274
|
+
}
|
|
275
|
+
|
|
147
276
|
/**
|
|
148
277
|
* Get all loaded tools.
|
|
149
278
|
*/
|
|
@@ -158,6 +287,24 @@ export class MCPManager {
|
|
|
158
287
|
return this.connections.get(name);
|
|
159
288
|
}
|
|
160
289
|
|
|
290
|
+
/**
|
|
291
|
+
* Get the source metadata for a server.
|
|
292
|
+
*/
|
|
293
|
+
getSource(name: string): SourceMeta | undefined {
|
|
294
|
+
return this.sources.get(name) ?? this.connections.get(name)?._source;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Wait for a connection to complete (or fail).
|
|
299
|
+
*/
|
|
300
|
+
async waitForConnection(name: string): Promise<MCPServerConnection> {
|
|
301
|
+
const connection = this.connections.get(name);
|
|
302
|
+
if (connection) return connection;
|
|
303
|
+
const pending = this.pendingConnections.get(name);
|
|
304
|
+
if (pending) return pending;
|
|
305
|
+
throw new Error(`MCP server not connected: ${name}`);
|
|
306
|
+
}
|
|
307
|
+
|
|
161
308
|
/**
|
|
162
309
|
* Get all connected server names.
|
|
163
310
|
*/
|
|
@@ -169,11 +316,15 @@ export class MCPManager {
|
|
|
169
316
|
* Disconnect from a specific server.
|
|
170
317
|
*/
|
|
171
318
|
async disconnectServer(name: string): Promise<void> {
|
|
172
|
-
|
|
173
|
-
|
|
319
|
+
this.pendingConnections.delete(name);
|
|
320
|
+
this.pendingToolLoads.delete(name);
|
|
321
|
+
this.sources.delete(name);
|
|
174
322
|
|
|
175
|
-
|
|
176
|
-
|
|
323
|
+
const connection = this.connections.get(name);
|
|
324
|
+
if (connection) {
|
|
325
|
+
await disconnectServer(connection);
|
|
326
|
+
this.connections.delete(name);
|
|
327
|
+
}
|
|
177
328
|
|
|
178
329
|
// Remove tools from this server
|
|
179
330
|
this.tools = this.tools.filter((t) => !t.name.startsWith(`mcp_${name}_`));
|
|
@@ -186,6 +337,9 @@ export class MCPManager {
|
|
|
186
337
|
const promises = Array.from(this.connections.values()).map((conn) => disconnectServer(conn));
|
|
187
338
|
await Promise.allSettled(promises);
|
|
188
339
|
|
|
340
|
+
this.pendingConnections.clear();
|
|
341
|
+
this.pendingToolLoads.clear();
|
|
342
|
+
this.sources.clear();
|
|
189
343
|
this.connections.clear();
|
|
190
344
|
this.tools = [];
|
|
191
345
|
}
|
|
@@ -202,11 +356,11 @@ export class MCPManager {
|
|
|
202
356
|
|
|
203
357
|
// Reload tools
|
|
204
358
|
const serverTools = await listTools(connection);
|
|
205
|
-
const customTools =
|
|
359
|
+
const customTools = MCPTool.fromTools(connection, serverTools);
|
|
360
|
+
void this.toolCache?.set(name, connection.config, serverTools);
|
|
206
361
|
|
|
207
362
|
// Replace tools from this server
|
|
208
|
-
this.
|
|
209
|
-
this.tools.push(...customTools);
|
|
363
|
+
this.replaceServerTools(name, customTools);
|
|
210
364
|
}
|
|
211
365
|
|
|
212
366
|
/**
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* Converts MCP tool definitions to CustomTool format for the agent.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
7
8
|
import type { TSchema } from "@sinclair/typebox";
|
|
8
|
-
import type {
|
|
9
|
+
import type { SourceMeta } from "../../capability/types";
|
|
10
|
+
import type { CustomTool, CustomToolContext, CustomToolResult } from "../custom-tools/types";
|
|
9
11
|
import { callTool } from "./client";
|
|
10
12
|
import type { MCPContent, MCPServerConnection, MCPToolDefinition } from "./types";
|
|
11
13
|
|
|
@@ -88,69 +90,155 @@ export function parseMCPToolName(name: string): { serverName: string; toolName:
|
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
/**
|
|
91
|
-
*
|
|
93
|
+
* CustomTool wrapping an MCP tool with an active connection.
|
|
92
94
|
*/
|
|
93
|
-
export
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
description: tool.description ?? `MCP tool from ${connection.name}`,
|
|
104
|
-
parameters: schema,
|
|
105
|
-
|
|
106
|
-
async execute(_toolCallId, params, _onUpdate, _ctx, _signal): Promise<CustomToolResult<MCPToolDetails>> {
|
|
107
|
-
try {
|
|
108
|
-
const result = await callTool(connection, tool.name, params as Record<string, unknown>);
|
|
109
|
-
|
|
110
|
-
const text = formatMCPContent(result.content);
|
|
111
|
-
const details: MCPToolDetails = {
|
|
112
|
-
serverName: connection.name,
|
|
113
|
-
mcpToolName: tool.name,
|
|
114
|
-
isError: result.isError,
|
|
115
|
-
rawContent: result.content,
|
|
116
|
-
provider: connection._source?.provider,
|
|
117
|
-
providerName: connection._source?.providerName,
|
|
118
|
-
};
|
|
95
|
+
export class MCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
96
|
+
public readonly name: string;
|
|
97
|
+
public readonly label: string;
|
|
98
|
+
public readonly description: string;
|
|
99
|
+
public readonly parameters: TSchema;
|
|
100
|
+
|
|
101
|
+
/** Create MCPTool instances for all tools from an MCP server connection */
|
|
102
|
+
static fromTools(connection: MCPServerConnection, tools: MCPToolDefinition[]): MCPTool[] {
|
|
103
|
+
return tools.map((tool) => new MCPTool(connection, tool));
|
|
104
|
+
}
|
|
119
105
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
106
|
+
constructor(
|
|
107
|
+
private readonly connection: MCPServerConnection,
|
|
108
|
+
private readonly tool: MCPToolDefinition,
|
|
109
|
+
) {
|
|
110
|
+
this.name = createMCPToolName(connection.name, tool.name);
|
|
111
|
+
this.label = `${connection.name}/${tool.name}`;
|
|
112
|
+
this.description = tool.description ?? `MCP tool from ${connection.name}`;
|
|
113
|
+
this.parameters = convertSchema(tool.inputSchema);
|
|
114
|
+
}
|
|
126
115
|
|
|
116
|
+
async execute(
|
|
117
|
+
_toolCallId: string,
|
|
118
|
+
params: unknown,
|
|
119
|
+
_onUpdate: AgentToolUpdateCallback<MCPToolDetails> | undefined,
|
|
120
|
+
_ctx: CustomToolContext,
|
|
121
|
+
_signal?: AbortSignal,
|
|
122
|
+
): Promise<CustomToolResult<MCPToolDetails>> {
|
|
123
|
+
try {
|
|
124
|
+
const result = await callTool(this.connection, this.tool.name, params as Record<string, unknown>);
|
|
125
|
+
|
|
126
|
+
const text = formatMCPContent(result.content);
|
|
127
|
+
const details: MCPToolDetails = {
|
|
128
|
+
serverName: this.connection.name,
|
|
129
|
+
mcpToolName: this.tool.name,
|
|
130
|
+
isError: result.isError,
|
|
131
|
+
rawContent: result.content,
|
|
132
|
+
provider: this.connection._source?.provider,
|
|
133
|
+
providerName: this.connection._source?.providerName,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (result.isError) {
|
|
127
137
|
return {
|
|
128
|
-
content: [{ type: "text", text }],
|
|
138
|
+
content: [{ type: "text", text: `Error: ${text}` }],
|
|
129
139
|
details,
|
|
130
140
|
};
|
|
131
|
-
} catch (error) {
|
|
132
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
133
|
-
return {
|
|
134
|
-
content: [{ type: "text", text: `MCP error: ${message}` }],
|
|
135
|
-
details: {
|
|
136
|
-
serverName: connection.name,
|
|
137
|
-
mcpToolName: tool.name,
|
|
138
|
-
isError: true,
|
|
139
|
-
provider: connection._source?.provider,
|
|
140
|
-
providerName: connection._source?.providerName,
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
141
|
}
|
|
144
|
-
|
|
145
|
-
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
content: [{ type: "text", text }],
|
|
145
|
+
details,
|
|
146
|
+
};
|
|
147
|
+
} catch (error) {
|
|
148
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: `MCP error: ${message}` }],
|
|
151
|
+
details: {
|
|
152
|
+
serverName: this.connection.name,
|
|
153
|
+
mcpToolName: this.tool.name,
|
|
154
|
+
isError: true,
|
|
155
|
+
provider: this.connection._source?.provider,
|
|
156
|
+
providerName: this.connection._source?.providerName,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
146
161
|
}
|
|
147
162
|
|
|
148
163
|
/**
|
|
149
|
-
*
|
|
164
|
+
* CustomTool wrapping an MCP tool with deferred connection resolution.
|
|
150
165
|
*/
|
|
151
|
-
export
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
166
|
+
export class DeferredMCPTool implements CustomTool<TSchema, MCPToolDetails> {
|
|
167
|
+
public readonly name: string;
|
|
168
|
+
public readonly label: string;
|
|
169
|
+
public readonly description: string;
|
|
170
|
+
public readonly parameters: TSchema;
|
|
171
|
+
private readonly fallbackProvider: string | undefined;
|
|
172
|
+
private readonly fallbackProviderName: string | undefined;
|
|
173
|
+
|
|
174
|
+
/** Create DeferredMCPTool instances for all tools from an MCP server */
|
|
175
|
+
static fromTools(
|
|
176
|
+
serverName: string,
|
|
177
|
+
tools: MCPToolDefinition[],
|
|
178
|
+
getConnection: () => Promise<MCPServerConnection>,
|
|
179
|
+
source?: SourceMeta,
|
|
180
|
+
): DeferredMCPTool[] {
|
|
181
|
+
return tools.map((tool) => new DeferredMCPTool(serverName, tool, getConnection, source));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
constructor(
|
|
185
|
+
private readonly serverName: string,
|
|
186
|
+
private readonly tool: MCPToolDefinition,
|
|
187
|
+
private readonly getConnection: () => Promise<MCPServerConnection>,
|
|
188
|
+
source?: SourceMeta,
|
|
189
|
+
) {
|
|
190
|
+
this.name = createMCPToolName(serverName, tool.name);
|
|
191
|
+
this.label = `${serverName}/${tool.name}`;
|
|
192
|
+
this.description = tool.description ?? `MCP tool from ${serverName}`;
|
|
193
|
+
this.parameters = convertSchema(tool.inputSchema);
|
|
194
|
+
this.fallbackProvider = source?.provider;
|
|
195
|
+
this.fallbackProviderName = source?.providerName;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async execute(
|
|
199
|
+
_toolCallId: string,
|
|
200
|
+
params: unknown,
|
|
201
|
+
_onUpdate: AgentToolUpdateCallback<MCPToolDetails> | undefined,
|
|
202
|
+
_ctx: CustomToolContext,
|
|
203
|
+
_signal?: AbortSignal,
|
|
204
|
+
): Promise<CustomToolResult<MCPToolDetails>> {
|
|
205
|
+
try {
|
|
206
|
+
const connection = await this.getConnection();
|
|
207
|
+
const result = await callTool(connection, this.tool.name, params as Record<string, unknown>);
|
|
208
|
+
|
|
209
|
+
const text = formatMCPContent(result.content);
|
|
210
|
+
const details: MCPToolDetails = {
|
|
211
|
+
serverName: this.serverName,
|
|
212
|
+
mcpToolName: this.tool.name,
|
|
213
|
+
isError: result.isError,
|
|
214
|
+
rawContent: result.content,
|
|
215
|
+
provider: connection._source?.provider ?? this.fallbackProvider,
|
|
216
|
+
providerName: connection._source?.providerName ?? this.fallbackProviderName,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (result.isError) {
|
|
220
|
+
return {
|
|
221
|
+
content: [{ type: "text", text: `Error: ${text}` }],
|
|
222
|
+
details,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
content: [{ type: "text", text }],
|
|
228
|
+
details,
|
|
229
|
+
};
|
|
230
|
+
} catch (error) {
|
|
231
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
232
|
+
return {
|
|
233
|
+
content: [{ type: "text", text: `MCP error: ${message}` }],
|
|
234
|
+
details: {
|
|
235
|
+
serverName: this.serverName,
|
|
236
|
+
mcpToolName: this.tool.name,
|
|
237
|
+
isError: true,
|
|
238
|
+
provider: this.fallbackProvider,
|
|
239
|
+
providerName: this.fallbackProviderName,
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
156
244
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tool cache.
|
|
3
|
+
*
|
|
4
|
+
* Stores tool definitions per server in agent.db for fast startup.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AgentStorage } from "../agent-storage";
|
|
8
|
+
import { logger } from "../logger";
|
|
9
|
+
import type { MCPServerConfig, MCPToolDefinition } from "./types";
|
|
10
|
+
|
|
11
|
+
const CACHE_VERSION = 1;
|
|
12
|
+
const CACHE_PREFIX = "mcp_tools:";
|
|
13
|
+
const CACHE_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
type MCPToolCachePayload = {
|
|
16
|
+
version: number;
|
|
17
|
+
configHash: string;
|
|
18
|
+
tools: MCPToolDefinition[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
22
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function stableClone(value: unknown): unknown {
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return value.map((item) => stableClone(item));
|
|
28
|
+
}
|
|
29
|
+
if (isRecord(value)) {
|
|
30
|
+
const sorted: Record<string, unknown> = {};
|
|
31
|
+
for (const key of Object.keys(value).sort()) {
|
|
32
|
+
sorted[key] = stableClone(value[key]);
|
|
33
|
+
}
|
|
34
|
+
return sorted;
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function stableStringify(value: unknown): string {
|
|
40
|
+
return JSON.stringify(stableClone(value));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function toHex(buffer: ArrayBuffer): string {
|
|
44
|
+
const bytes = new Uint8Array(buffer);
|
|
45
|
+
let output = "";
|
|
46
|
+
for (const byte of bytes) {
|
|
47
|
+
output += byte.toString(16).padStart(2, "0");
|
|
48
|
+
}
|
|
49
|
+
return output;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function hashConfig(config: MCPServerConfig): Promise<string> {
|
|
53
|
+
const stable = stableStringify(config);
|
|
54
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(stable));
|
|
55
|
+
return toHex(digest);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function cacheKey(serverName: string): string {
|
|
59
|
+
return `${CACHE_PREFIX}${serverName}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export class MCPToolCache {
|
|
63
|
+
constructor(private storage: AgentStorage) {}
|
|
64
|
+
|
|
65
|
+
async get(serverName: string, config: MCPServerConfig): Promise<MCPToolDefinition[] | null> {
|
|
66
|
+
const key = cacheKey(serverName);
|
|
67
|
+
const raw = this.storage.getCache(key);
|
|
68
|
+
if (!raw) return null;
|
|
69
|
+
|
|
70
|
+
let parsed: unknown;
|
|
71
|
+
try {
|
|
72
|
+
parsed = JSON.parse(raw);
|
|
73
|
+
} catch (error) {
|
|
74
|
+
logger.warn("MCP tool cache parse failed", { serverName, error: String(error) });
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!isRecord(parsed)) return null;
|
|
79
|
+
if (parsed.version !== CACHE_VERSION) return null;
|
|
80
|
+
if (typeof parsed.configHash !== "string") return null;
|
|
81
|
+
if (!Array.isArray(parsed.tools)) return null;
|
|
82
|
+
|
|
83
|
+
let currentHash: string;
|
|
84
|
+
try {
|
|
85
|
+
currentHash = await hashConfig(config);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
logger.warn("MCP tool cache hash failed", { serverName, error: String(error) });
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (parsed.configHash !== currentHash) return null;
|
|
92
|
+
|
|
93
|
+
return parsed.tools as MCPToolDefinition[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async set(serverName: string, config: MCPServerConfig, tools: MCPToolDefinition[]): Promise<void> {
|
|
97
|
+
let configHash: string;
|
|
98
|
+
try {
|
|
99
|
+
configHash = await hashConfig(config);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
logger.warn("MCP tool cache hash failed", { serverName, error: String(error) });
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const payload: MCPToolCachePayload = {
|
|
106
|
+
version: CACHE_VERSION,
|
|
107
|
+
configHash,
|
|
108
|
+
tools,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
let serialized: string;
|
|
112
|
+
try {
|
|
113
|
+
serialized = JSON.stringify(payload);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
logger.warn("MCP tool cache serialize failed", { serverName, error: String(error) });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const expiresAtSec = Math.floor((Date.now() + CACHE_TTL_MS) / 1000);
|
|
120
|
+
this.storage.setCache(cacheKey(serverName), serialized, expiresAtSec);
|
|
121
|
+
}
|
|
122
|
+
}
|