@dotsetlabs/dotclaw 1.9.0 → 2.1.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/.env.example +6 -0
- package/README.md +13 -8
- package/config-examples/groups/global/CLAUDE.md +6 -14
- package/config-examples/groups/main/CLAUDE.md +8 -39
- package/config-examples/runtime.json +16 -122
- package/config-examples/tool-policy.json +2 -15
- package/container/agent-runner/package-lock.json +258 -0
- package/container/agent-runner/package.json +2 -1
- package/container/agent-runner/src/agent-config.ts +62 -47
- package/container/agent-runner/src/browser.ts +180 -0
- package/container/agent-runner/src/container-protocol.ts +4 -9
- package/container/agent-runner/src/id.ts +3 -2
- package/container/agent-runner/src/index.ts +331 -846
- package/container/agent-runner/src/ipc.ts +3 -33
- package/container/agent-runner/src/mcp-client.ts +222 -0
- package/container/agent-runner/src/mcp-registry.ts +163 -0
- package/container/agent-runner/src/skill-loader.ts +375 -0
- package/container/agent-runner/src/tools.ts +154 -184
- package/container/agent-runner/src/tts.ts +61 -0
- package/dist/admin-commands.d.ts.map +1 -1
- package/dist/admin-commands.js +12 -0
- package/dist/admin-commands.js.map +1 -1
- package/dist/agent-execution.d.ts +5 -9
- package/dist/agent-execution.d.ts.map +1 -1
- package/dist/agent-execution.js +32 -20
- package/dist/agent-execution.js.map +1 -1
- package/dist/cli.js +61 -16
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -4
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -5
- package/dist/config.js.map +1 -1
- package/dist/container-protocol.d.ts +4 -9
- package/dist/container-protocol.d.ts.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +3 -8
- package/dist/container-runner.js.map +1 -1
- package/dist/dashboard.d.ts +5 -6
- package/dist/dashboard.d.ts.map +1 -1
- package/dist/dashboard.js +12 -60
- package/dist/dashboard.js.map +1 -1
- package/dist/db.d.ts +1 -59
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +41 -262
- package/dist/db.js.map +1 -1
- package/dist/error-messages.d.ts.map +1 -1
- package/dist/error-messages.js +5 -1
- package/dist/error-messages.js.map +1 -1
- package/dist/hooks.d.ts +7 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +93 -0
- package/dist/hooks.js.map +1 -0
- package/dist/id.d.ts.map +1 -1
- package/dist/id.js +2 -1
- package/dist/id.js.map +1 -1
- package/dist/index.js +673 -2790
- package/dist/index.js.map +1 -1
- package/dist/ipc-dispatcher.d.ts +26 -0
- package/dist/ipc-dispatcher.d.ts.map +1 -0
- package/dist/ipc-dispatcher.js +861 -0
- package/dist/ipc-dispatcher.js.map +1 -0
- package/dist/local-embeddings.d.ts +7 -0
- package/dist/local-embeddings.d.ts.map +1 -0
- package/dist/local-embeddings.js +60 -0
- package/dist/local-embeddings.js.map +1 -0
- package/dist/maintenance.d.ts.map +1 -1
- package/dist/maintenance.js +3 -7
- package/dist/maintenance.js.map +1 -1
- package/dist/memory-embeddings.d.ts +1 -1
- package/dist/memory-embeddings.d.ts.map +1 -1
- package/dist/memory-embeddings.js +59 -31
- package/dist/memory-embeddings.js.map +1 -1
- package/dist/memory-store.d.ts +0 -10
- package/dist/memory-store.d.ts.map +1 -1
- package/dist/memory-store.js +11 -27
- package/dist/memory-store.js.map +1 -1
- package/dist/message-pipeline.d.ts +47 -0
- package/dist/message-pipeline.d.ts.map +1 -0
- package/dist/message-pipeline.js +652 -0
- package/dist/message-pipeline.js.map +1 -0
- package/dist/metrics.d.ts +7 -10
- package/dist/metrics.d.ts.map +1 -1
- package/dist/metrics.js +2 -33
- package/dist/metrics.js.map +1 -1
- package/dist/model-registry.d.ts +0 -14
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js +0 -36
- package/dist/model-registry.js.map +1 -1
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +2 -0
- package/dist/paths.js.map +1 -1
- package/dist/providers/discord/discord-format.d.ts +16 -0
- package/dist/providers/discord/discord-format.d.ts.map +1 -0
- package/dist/providers/discord/discord-format.js +153 -0
- package/dist/providers/discord/discord-format.js.map +1 -0
- package/dist/providers/discord/discord-provider.d.ts +50 -0
- package/dist/providers/discord/discord-provider.d.ts.map +1 -0
- package/dist/providers/discord/discord-provider.js +607 -0
- package/dist/providers/discord/discord-provider.js.map +1 -0
- package/dist/providers/discord/index.d.ts +4 -0
- package/dist/providers/discord/index.d.ts.map +1 -0
- package/dist/providers/discord/index.js +3 -0
- package/dist/providers/discord/index.js.map +1 -0
- package/dist/providers/registry.d.ts +14 -0
- package/dist/providers/registry.d.ts.map +1 -0
- package/dist/providers/registry.js +49 -0
- package/dist/providers/registry.js.map +1 -0
- package/dist/providers/telegram/index.d.ts +4 -0
- package/dist/providers/telegram/index.d.ts.map +1 -0
- package/dist/providers/telegram/index.js +3 -0
- package/dist/providers/telegram/index.js.map +1 -0
- package/dist/providers/telegram/telegram-format.d.ts +3 -0
- package/dist/providers/telegram/telegram-format.d.ts.map +1 -0
- package/dist/providers/telegram/telegram-format.js +215 -0
- package/dist/providers/telegram/telegram-format.js.map +1 -0
- package/dist/providers/telegram/telegram-provider.d.ts +51 -0
- package/dist/providers/telegram/telegram-provider.d.ts.map +1 -0
- package/dist/providers/telegram/telegram-provider.js +824 -0
- package/dist/providers/telegram/telegram-provider.js.map +1 -0
- package/dist/providers/types.d.ts +107 -0
- package/dist/providers/types.d.ts.map +1 -0
- package/dist/providers/types.js +2 -0
- package/dist/providers/types.js.map +1 -0
- package/dist/request-router.d.ts +9 -31
- package/dist/request-router.d.ts.map +1 -1
- package/dist/request-router.js +12 -142
- package/dist/request-router.js.map +1 -1
- package/dist/runtime-config.d.ts +79 -101
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +140 -208
- package/dist/runtime-config.js.map +1 -1
- package/dist/skill-manager.d.ts +39 -0
- package/dist/skill-manager.d.ts.map +1 -0
- package/dist/skill-manager.js +286 -0
- package/dist/skill-manager.js.map +1 -0
- package/dist/streaming.d.ts +58 -0
- package/dist/streaming.d.ts.map +1 -0
- package/dist/streaming.js +196 -0
- package/dist/streaming.js.map +1 -0
- package/dist/task-scheduler.d.ts.map +1 -1
- package/dist/task-scheduler.js +11 -45
- package/dist/task-scheduler.js.map +1 -1
- package/dist/tool-policy.d.ts.map +1 -1
- package/dist/tool-policy.js +13 -5
- package/dist/tool-policy.js.map +1 -1
- package/dist/transcription.d.ts +8 -0
- package/dist/transcription.d.ts.map +1 -0
- package/dist/transcription.js +174 -0
- package/dist/transcription.js.map +1 -0
- package/dist/types.d.ts +2 -50
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -4
- package/scripts/bootstrap.js +40 -4
- package/scripts/configure.js +129 -7
- package/scripts/doctor.js +30 -4
- package/scripts/init.js +13 -6
- package/scripts/install.sh +1 -1
- package/config-examples/plugin-http.json +0 -18
- package/container/skills/agent-browser.md +0 -159
- package/dist/background-job-classifier.d.ts +0 -20
- package/dist/background-job-classifier.d.ts.map +0 -1
- package/dist/background-job-classifier.js +0 -145
- package/dist/background-job-classifier.js.map +0 -1
- package/dist/background-jobs.d.ts +0 -56
- package/dist/background-jobs.d.ts.map +0 -1
- package/dist/background-jobs.js +0 -550
- package/dist/background-jobs.js.map +0 -1
- package/dist/planner-probe.d.ts +0 -14
- package/dist/planner-probe.d.ts.map +0 -1
- package/dist/planner-probe.js +0 -97
- package/dist/planner-probe.js.map +0 -1
|
@@ -408,38 +408,7 @@ export function createIpcHandlers(ctx: IpcContext, config: IpcConfig) {
|
|
|
408
408
|
},
|
|
409
409
|
|
|
410
410
|
async runTask(taskId: string) {
|
|
411
|
-
return requestResponse('run_task', { task_id: taskId }, config);
|
|
412
|
-
},
|
|
413
|
-
|
|
414
|
-
async spawnJob(args: {
|
|
415
|
-
prompt: string;
|
|
416
|
-
context_mode?: 'group' | 'isolated';
|
|
417
|
-
timeout_ms?: number;
|
|
418
|
-
max_tool_steps?: number;
|
|
419
|
-
tool_allow?: string[];
|
|
420
|
-
tool_deny?: string[];
|
|
421
|
-
model_override?: string;
|
|
422
|
-
priority?: number;
|
|
423
|
-
tags?: string[];
|
|
424
|
-
target_group?: string;
|
|
425
|
-
}) {
|
|
426
|
-
return requestResponse('spawn_job', args as Record<string, unknown>, config);
|
|
427
|
-
},
|
|
428
|
-
|
|
429
|
-
async jobStatus(jobId: string) {
|
|
430
|
-
return requestResponse('job_status', { job_id: jobId }, config);
|
|
431
|
-
},
|
|
432
|
-
|
|
433
|
-
async listJobs(args: { status?: string; limit?: number; target_group?: string }) {
|
|
434
|
-
return requestResponse('list_jobs', args as Record<string, unknown>, config);
|
|
435
|
-
},
|
|
436
|
-
|
|
437
|
-
async cancelJob(jobId: string) {
|
|
438
|
-
return requestResponse('cancel_job', { job_id: jobId }, config);
|
|
439
|
-
},
|
|
440
|
-
|
|
441
|
-
async jobUpdate(args: { job_id: string; message: string; level?: string; notify?: boolean; data?: Record<string, unknown> }) {
|
|
442
|
-
return requestResponse('job_update', args as Record<string, unknown>, config);
|
|
411
|
+
return requestResponse('run_task', { task_id: taskId }, config, 900_000);
|
|
443
412
|
},
|
|
444
413
|
|
|
445
414
|
async setModel(args: { model: string; scope?: 'global' | 'group' | 'user'; target_id?: string }) {
|
|
@@ -500,6 +469,7 @@ export function createIpcHandlers(ctx: IpcContext, config: IpcConfig) {
|
|
|
500
469
|
userId: args.userId,
|
|
501
470
|
target_group: args.target_group
|
|
502
471
|
}, config);
|
|
503
|
-
}
|
|
472
|
+
},
|
|
473
|
+
|
|
504
474
|
};
|
|
505
475
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal MCP (Model Context Protocol) client implementation.
|
|
3
|
+
* Supports stdio transport for connecting to MCP-compatible tool servers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
7
|
+
|
|
8
|
+
export interface McpTool {
|
|
9
|
+
name: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
inputSchema: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface McpCallResult {
|
|
15
|
+
content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
|
|
16
|
+
isError?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type JsonRpcRequest = {
|
|
20
|
+
jsonrpc: '2.0';
|
|
21
|
+
id: number;
|
|
22
|
+
method: string;
|
|
23
|
+
params?: Record<string, unknown>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type JsonRpcResponse = {
|
|
27
|
+
jsonrpc: '2.0';
|
|
28
|
+
id: number;
|
|
29
|
+
result?: unknown;
|
|
30
|
+
error?: { code: number; message: string; data?: unknown };
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export class McpStdioClient {
|
|
34
|
+
private proc: ChildProcess | null = null;
|
|
35
|
+
private nextId = 1;
|
|
36
|
+
private pending = new Map<number, { resolve: (value: unknown) => void; reject: (err: Error) => void }>();
|
|
37
|
+
private buffer = '';
|
|
38
|
+
private connected = false;
|
|
39
|
+
private connectPromise: Promise<void> | null = null;
|
|
40
|
+
private command: string;
|
|
41
|
+
private args: string[];
|
|
42
|
+
private env: Record<string, string>;
|
|
43
|
+
private timeoutMs: number;
|
|
44
|
+
|
|
45
|
+
constructor(options: {
|
|
46
|
+
command: string;
|
|
47
|
+
args?: string[];
|
|
48
|
+
env?: Record<string, string>;
|
|
49
|
+
timeoutMs?: number;
|
|
50
|
+
}) {
|
|
51
|
+
this.command = options.command;
|
|
52
|
+
this.args = options.args || [];
|
|
53
|
+
this.env = options.env || {};
|
|
54
|
+
this.timeoutMs = options.timeoutMs || 10_000;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async connect(): Promise<void> {
|
|
58
|
+
if (this.connected) return;
|
|
59
|
+
if (this.connectPromise) return this.connectPromise;
|
|
60
|
+
|
|
61
|
+
this.connectPromise = this.doConnect();
|
|
62
|
+
try {
|
|
63
|
+
await this.connectPromise;
|
|
64
|
+
} finally {
|
|
65
|
+
this.connectPromise = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
private async doConnect(): Promise<void> {
|
|
70
|
+
// Resolve env vars that reference process.env
|
|
71
|
+
const resolvedEnv: Record<string, string> = {};
|
|
72
|
+
for (const [key, value] of Object.entries(this.env)) {
|
|
73
|
+
resolvedEnv[key] = value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (_match, envKey: string) => {
|
|
74
|
+
return process.env[envKey] || '';
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.proc = spawn(this.command, this.args, {
|
|
79
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
80
|
+
env: { ...process.env, ...resolvedEnv }
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
this.proc.stdout!.on('data', (data: Buffer) => {
|
|
84
|
+
this.buffer += data.toString();
|
|
85
|
+
this.processBuffer();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.proc.stderr!.on('data', (data: Buffer) => {
|
|
89
|
+
console.error(`[mcp-client] stderr: ${data.toString().slice(0, 200)}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
this.proc.on('close', () => {
|
|
93
|
+
this.connected = false;
|
|
94
|
+
for (const [, pending] of this.pending) {
|
|
95
|
+
pending.reject(new Error('MCP server process closed'));
|
|
96
|
+
}
|
|
97
|
+
this.pending.clear();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
this.proc.on('error', (err) => {
|
|
101
|
+
console.error(`[mcp-client] process error: ${err.message}`);
|
|
102
|
+
this.connected = false;
|
|
103
|
+
for (const [, pending] of this.pending) {
|
|
104
|
+
pending.reject(new Error(`MCP server process error: ${err.message}`));
|
|
105
|
+
}
|
|
106
|
+
this.pending.clear();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Initialize MCP connection
|
|
110
|
+
await this.sendRequest('initialize', {
|
|
111
|
+
protocolVersion: '2024-11-05',
|
|
112
|
+
capabilities: {},
|
|
113
|
+
clientInfo: { name: 'dotclaw', version: '2.0.0' }
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// Send initialized notification
|
|
117
|
+
this.sendNotification('notifications/initialized');
|
|
118
|
+
|
|
119
|
+
this.connected = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private processBuffer(): void {
|
|
123
|
+
// Try to parse JSON-RPC messages from the buffer
|
|
124
|
+
// MCP uses newline-delimited JSON
|
|
125
|
+
const lines = this.buffer.split('\n');
|
|
126
|
+
this.buffer = lines.pop() || '';
|
|
127
|
+
|
|
128
|
+
for (const line of lines) {
|
|
129
|
+
const trimmed = line.trim();
|
|
130
|
+
if (!trimmed) continue;
|
|
131
|
+
try {
|
|
132
|
+
const msg = JSON.parse(trimmed) as JsonRpcResponse;
|
|
133
|
+
if (msg.id !== undefined && this.pending.has(msg.id)) {
|
|
134
|
+
const handler = this.pending.get(msg.id)!;
|
|
135
|
+
this.pending.delete(msg.id);
|
|
136
|
+
if (msg.error) {
|
|
137
|
+
handler.reject(new Error(msg.error.message));
|
|
138
|
+
} else {
|
|
139
|
+
handler.resolve(msg.result);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
// Skip malformed lines
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private sendRequest(method: string, params?: Record<string, unknown>): Promise<unknown> {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
if (!this.proc || !this.proc.stdin) {
|
|
151
|
+
reject(new Error('MCP client not connected'));
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const id = this.nextId++;
|
|
156
|
+
const request: JsonRpcRequest = {
|
|
157
|
+
jsonrpc: '2.0',
|
|
158
|
+
id,
|
|
159
|
+
method,
|
|
160
|
+
params
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const timeout = setTimeout(() => {
|
|
164
|
+
this.pending.delete(id);
|
|
165
|
+
reject(new Error(`MCP request timeout: ${method}`));
|
|
166
|
+
}, this.timeoutMs);
|
|
167
|
+
|
|
168
|
+
this.pending.set(id, {
|
|
169
|
+
resolve: (value) => {
|
|
170
|
+
clearTimeout(timeout);
|
|
171
|
+
resolve(value);
|
|
172
|
+
},
|
|
173
|
+
reject: (err) => {
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
reject(err);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
this.proc.stdin.write(JSON.stringify(request) + '\n');
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private sendNotification(method: string, params?: Record<string, unknown>): void {
|
|
184
|
+
if (!this.proc || !this.proc.stdin) return;
|
|
185
|
+
const notification = {
|
|
186
|
+
jsonrpc: '2.0',
|
|
187
|
+
method,
|
|
188
|
+
params
|
|
189
|
+
};
|
|
190
|
+
this.proc.stdin.write(JSON.stringify(notification) + '\n');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async listTools(): Promise<McpTool[]> {
|
|
194
|
+
const result = await this.sendRequest('tools/list') as { tools?: McpTool[] };
|
|
195
|
+
return result?.tools || [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async callTool(name: string, args: Record<string, unknown>): Promise<McpCallResult> {
|
|
199
|
+
const result = await this.sendRequest('tools/call', {
|
|
200
|
+
name,
|
|
201
|
+
arguments: args
|
|
202
|
+
}) as McpCallResult;
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async close(): Promise<void> {
|
|
207
|
+
this.connected = false;
|
|
208
|
+
if (this.proc) {
|
|
209
|
+
this.proc.stdin?.end();
|
|
210
|
+
this.proc.kill('SIGTERM');
|
|
211
|
+
this.proc = null;
|
|
212
|
+
}
|
|
213
|
+
for (const [, pending] of this.pending) {
|
|
214
|
+
pending.reject(new Error('MCP client closed'));
|
|
215
|
+
}
|
|
216
|
+
this.pending.clear();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
get isConnected(): boolean {
|
|
220
|
+
return this.connected;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tool Registry — discovers tools from MCP servers and creates SDK Tool definitions.
|
|
3
|
+
* Tools are prefixed with mcp_ext__<server>__<tool> to avoid collisions with built-in tools.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { tool as sdkTool, type Tool } from '@openrouter/sdk';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { McpStdioClient, type McpTool, type McpCallResult } from './mcp-client.js';
|
|
9
|
+
import type { AgentRuntimeConfig } from './agent-config.js';
|
|
10
|
+
|
|
11
|
+
type ToolConfig = {
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
inputSchema: z.ZodTypeAny;
|
|
15
|
+
outputSchema?: z.ZodTypeAny;
|
|
16
|
+
execute: unknown;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const tool = sdkTool as unknown as (config: ToolConfig) => Tool;
|
|
20
|
+
|
|
21
|
+
type McpServerConfig = NonNullable<AgentRuntimeConfig['agent']['mcp']['servers']>[number];
|
|
22
|
+
|
|
23
|
+
// Convert JSON Schema to a Zod object schema (best-effort)
|
|
24
|
+
function jsonSchemaToZod(schema: Record<string, unknown>): z.ZodTypeAny {
|
|
25
|
+
if (!schema || typeof schema !== 'object') return z.any();
|
|
26
|
+
|
|
27
|
+
const type = schema.type as string | undefined;
|
|
28
|
+
const properties = schema.properties as Record<string, Record<string, unknown>> | undefined;
|
|
29
|
+
const required = (schema.required as string[]) || [];
|
|
30
|
+
|
|
31
|
+
if (type === 'object' && properties) {
|
|
32
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
33
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
34
|
+
let field = jsonSchemaToZod(propSchema);
|
|
35
|
+
if (!required.includes(key)) {
|
|
36
|
+
field = field.optional();
|
|
37
|
+
}
|
|
38
|
+
if (propSchema.description && typeof propSchema.description === 'string') {
|
|
39
|
+
field = field.describe(propSchema.description);
|
|
40
|
+
}
|
|
41
|
+
shape[key] = field;
|
|
42
|
+
}
|
|
43
|
+
return z.object(shape).passthrough();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
switch (type) {
|
|
47
|
+
case 'string': {
|
|
48
|
+
if (schema.enum && Array.isArray(schema.enum) && schema.enum.length > 0) {
|
|
49
|
+
return z.enum(schema.enum as [string, ...string[]]);
|
|
50
|
+
}
|
|
51
|
+
return z.string();
|
|
52
|
+
}
|
|
53
|
+
case 'number':
|
|
54
|
+
case 'integer':
|
|
55
|
+
return z.number();
|
|
56
|
+
case 'boolean':
|
|
57
|
+
return z.boolean();
|
|
58
|
+
case 'array': {
|
|
59
|
+
const items = schema.items as Record<string, unknown> | undefined;
|
|
60
|
+
return z.array(items ? jsonSchemaToZod(items) : z.any());
|
|
61
|
+
}
|
|
62
|
+
default:
|
|
63
|
+
return z.any();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class McpToolRegistry {
|
|
68
|
+
private clients = new Map<string, McpStdioClient>();
|
|
69
|
+
private serverConfigs: McpServerConfig[];
|
|
70
|
+
private connectionTimeoutMs: number;
|
|
71
|
+
private discoveredTools: Tool[] = [];
|
|
72
|
+
|
|
73
|
+
constructor(config: AgentRuntimeConfig['agent']['mcp']) {
|
|
74
|
+
this.serverConfigs = config.servers || [];
|
|
75
|
+
this.connectionTimeoutMs = config.connectionTimeoutMs;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async discoverTools(wrapExecute: <TInput, TOutput>(name: string, execute: (args: TInput) => Promise<TOutput>) => (args: TInput) => Promise<TOutput>): Promise<Tool[]> {
|
|
79
|
+
if (this.serverConfigs.length === 0) return [];
|
|
80
|
+
|
|
81
|
+
const tools: Tool[] = [];
|
|
82
|
+
|
|
83
|
+
for (const serverConfig of this.serverConfigs) {
|
|
84
|
+
if (!serverConfig.command) continue;
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const client = new McpStdioClient({
|
|
88
|
+
command: serverConfig.command,
|
|
89
|
+
args: serverConfig.args,
|
|
90
|
+
env: serverConfig.env,
|
|
91
|
+
timeoutMs: this.connectionTimeoutMs
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
this.clients.set(serverConfig.name, client);
|
|
95
|
+
|
|
96
|
+
const mcpTools = await this.discoverServerTools(client);
|
|
97
|
+
|
|
98
|
+
for (const mcpTool of mcpTools) {
|
|
99
|
+
const toolName = `mcp_ext__${serverConfig.name}__${mcpTool.name}`;
|
|
100
|
+
const inputSchema = jsonSchemaToZod(mcpTool.inputSchema);
|
|
101
|
+
|
|
102
|
+
const sdkToolInstance = tool({
|
|
103
|
+
name: toolName,
|
|
104
|
+
description: mcpTool.description || `MCP tool from ${serverConfig.name}`,
|
|
105
|
+
inputSchema,
|
|
106
|
+
outputSchema: z.object({
|
|
107
|
+
content: z.array(z.object({
|
|
108
|
+
type: z.string(),
|
|
109
|
+
text: z.string().optional()
|
|
110
|
+
})).optional(),
|
|
111
|
+
isError: z.boolean().optional(),
|
|
112
|
+
error: z.string().optional()
|
|
113
|
+
}),
|
|
114
|
+
execute: wrapExecute(toolName, async (args: Record<string, unknown>) => {
|
|
115
|
+
return this.callMcpTool(serverConfig.name, mcpTool.name, args);
|
|
116
|
+
})
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
tools.push(sdkToolInstance as Tool);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
console.log(`[mcp-registry] Discovered ${mcpTools.length} tools from ${serverConfig.name}`);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
console.error(`[mcp-registry] Failed to connect to MCP server ${serverConfig.name}: ${err instanceof Error ? err.message : String(err)}`);
|
|
125
|
+
const failedClient = this.clients.get(serverConfig.name);
|
|
126
|
+
this.clients.delete(serverConfig.name);
|
|
127
|
+
if (failedClient) { try { await failedClient.close(); } catch { /* ignore */ } }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
this.discoveredTools = tools;
|
|
132
|
+
return tools;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private async discoverServerTools(client: McpStdioClient): Promise<McpTool[]> {
|
|
136
|
+
await client.connect();
|
|
137
|
+
return client.listTools();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async callMcpTool(serverName: string, toolName: string, args: Record<string, unknown>): Promise<McpCallResult & { error?: string }> {
|
|
141
|
+
const client = this.clients.get(serverName);
|
|
142
|
+
if (!client) {
|
|
143
|
+
return { content: [], isError: true, error: `MCP server ${serverName} not found` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!client.isConnected) {
|
|
147
|
+
await client.connect();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return client.callTool(toolName, args);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async closeAll(): Promise<void> {
|
|
154
|
+
for (const [, client] of this.clients) {
|
|
155
|
+
await client.close();
|
|
156
|
+
}
|
|
157
|
+
this.clients.clear();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getDiscoveredTools(): Tool[] {
|
|
161
|
+
return this.discoveredTools;
|
|
162
|
+
}
|
|
163
|
+
}
|