ultra-dex 3.2.0 → 3.3.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/bin/ultra-dex.js +14 -2
- package/lib/commands/cloud.js +780 -0
- package/lib/commands/exec.js +434 -0
- package/lib/commands/github.js +475 -0
- package/lib/commands/search.js +477 -0
- package/lib/mcp/client.js +502 -0
- package/lib/providers/agent-sdk.js +630 -0
- package/lib/providers/anthropic-agents.js +580 -0
- package/lib/utils/browser.js +373 -0
- package/package.json +10 -4
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Client - Consume External MCP Servers
|
|
3
|
+
* This allows Ultra-Dex to connect to GitHub MCP, Linear MCP, Notion MCP, etc.
|
|
4
|
+
* Transforms Ultra-Dex from isolated tool to connected ecosystem hub
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import fs from 'fs/promises';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import { EventEmitter } from 'events';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// MCP CLIENT CONFIGURATION
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
const MCP_CLIENT_CONFIG = {
|
|
18
|
+
// Known MCP servers
|
|
19
|
+
servers: {
|
|
20
|
+
github: {
|
|
21
|
+
name: 'GitHub MCP',
|
|
22
|
+
command: 'npx',
|
|
23
|
+
args: ['-y', '@modelcontextprotocol/server-github'],
|
|
24
|
+
env: { GITHUB_PERSONAL_ACCESS_TOKEN: process.env.GITHUB_TOKEN },
|
|
25
|
+
description: 'GitHub issues, PRs, and repository operations',
|
|
26
|
+
},
|
|
27
|
+
filesystem: {
|
|
28
|
+
name: 'Filesystem MCP',
|
|
29
|
+
command: 'npx',
|
|
30
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', '.'],
|
|
31
|
+
description: 'File system operations',
|
|
32
|
+
},
|
|
33
|
+
postgres: {
|
|
34
|
+
name: 'PostgreSQL MCP',
|
|
35
|
+
command: 'npx',
|
|
36
|
+
args: ['-y', '@modelcontextprotocol/server-postgres'],
|
|
37
|
+
env: { POSTGRES_CONNECTION_STRING: process.env.DATABASE_URL },
|
|
38
|
+
description: 'PostgreSQL database operations',
|
|
39
|
+
},
|
|
40
|
+
slack: {
|
|
41
|
+
name: 'Slack MCP',
|
|
42
|
+
command: 'npx',
|
|
43
|
+
args: ['-y', '@modelcontextprotocol/server-slack'],
|
|
44
|
+
env: { SLACK_BOT_TOKEN: process.env.SLACK_TOKEN },
|
|
45
|
+
description: 'Slack messaging and channels',
|
|
46
|
+
},
|
|
47
|
+
brave: {
|
|
48
|
+
name: 'Brave Search MCP',
|
|
49
|
+
command: 'npx',
|
|
50
|
+
args: ['-y', '@anthropic/mcp-server-brave-search'],
|
|
51
|
+
env: { BRAVE_API_KEY: process.env.BRAVE_API_KEY },
|
|
52
|
+
description: 'Web search via Brave',
|
|
53
|
+
},
|
|
54
|
+
memory: {
|
|
55
|
+
name: 'Memory MCP',
|
|
56
|
+
command: 'npx',
|
|
57
|
+
args: ['-y', '@modelcontextprotocol/server-memory'],
|
|
58
|
+
description: 'Persistent memory storage',
|
|
59
|
+
},
|
|
60
|
+
puppeteer: {
|
|
61
|
+
name: 'Puppeteer MCP',
|
|
62
|
+
command: 'npx',
|
|
63
|
+
args: ['-y', '@anthropic/mcp-server-puppeteer'],
|
|
64
|
+
description: 'Browser automation',
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// State file
|
|
69
|
+
stateFile: '.ultra-dex/mcp-connections.json',
|
|
70
|
+
|
|
71
|
+
// Timeouts
|
|
72
|
+
connectionTimeout: 30000,
|
|
73
|
+
requestTimeout: 60000,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// MCP CONNECTION CLASS
|
|
78
|
+
// ============================================================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Connection to an MCP server
|
|
82
|
+
*/
|
|
83
|
+
export class MCPConnection extends EventEmitter {
|
|
84
|
+
constructor(serverConfig) {
|
|
85
|
+
super();
|
|
86
|
+
this.config = serverConfig;
|
|
87
|
+
this.process = null;
|
|
88
|
+
this.connected = false;
|
|
89
|
+
this.tools = [];
|
|
90
|
+
this.resources = [];
|
|
91
|
+
this.requestId = 0;
|
|
92
|
+
this.pendingRequests = new Map();
|
|
93
|
+
this.buffer = '';
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Connect to MCP server
|
|
98
|
+
*/
|
|
99
|
+
async connect() {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const env = { ...process.env, ...this.config.env };
|
|
102
|
+
|
|
103
|
+
this.process = spawn(this.config.command, this.config.args, {
|
|
104
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
105
|
+
env,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const timeout = setTimeout(() => {
|
|
109
|
+
reject(new Error('Connection timeout'));
|
|
110
|
+
this.disconnect();
|
|
111
|
+
}, MCP_CLIENT_CONFIG.connectionTimeout);
|
|
112
|
+
|
|
113
|
+
this.process.stdout.on('data', (data) => {
|
|
114
|
+
this.buffer += data.toString();
|
|
115
|
+
this._processBuffer();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
this.process.stderr.on('data', (data) => {
|
|
119
|
+
this.emit('error', data.toString());
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
this.process.on('close', (code) => {
|
|
123
|
+
this.connected = false;
|
|
124
|
+
this.emit('close', code);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
this.process.on('error', (err) => {
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
reject(err);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Initialize connection
|
|
133
|
+
this._sendRequest('initialize', {
|
|
134
|
+
protocolVersion: '2024-11-05',
|
|
135
|
+
capabilities: {
|
|
136
|
+
roots: { listChanged: true },
|
|
137
|
+
sampling: {},
|
|
138
|
+
},
|
|
139
|
+
clientInfo: {
|
|
140
|
+
name: 'ultra-dex',
|
|
141
|
+
version: '3.2.0',
|
|
142
|
+
},
|
|
143
|
+
}).then(async (result) => {
|
|
144
|
+
clearTimeout(timeout);
|
|
145
|
+
this.connected = true;
|
|
146
|
+
|
|
147
|
+
// Send initialized notification
|
|
148
|
+
this._sendNotification('notifications/initialized', {});
|
|
149
|
+
|
|
150
|
+
// List available tools
|
|
151
|
+
try {
|
|
152
|
+
const toolsResult = await this._sendRequest('tools/list', {});
|
|
153
|
+
this.tools = toolsResult.tools || [];
|
|
154
|
+
} catch {
|
|
155
|
+
this.tools = [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// List available resources
|
|
159
|
+
try {
|
|
160
|
+
const resourcesResult = await this._sendRequest('resources/list', {});
|
|
161
|
+
this.resources = resourcesResult.resources || [];
|
|
162
|
+
} catch {
|
|
163
|
+
this.resources = [];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
resolve({
|
|
167
|
+
tools: this.tools,
|
|
168
|
+
resources: this.resources,
|
|
169
|
+
serverInfo: result.serverInfo,
|
|
170
|
+
});
|
|
171
|
+
}).catch(reject);
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Disconnect from MCP server
|
|
177
|
+
*/
|
|
178
|
+
async disconnect() {
|
|
179
|
+
if (this.process) {
|
|
180
|
+
this.process.kill();
|
|
181
|
+
this.process = null;
|
|
182
|
+
}
|
|
183
|
+
this.connected = false;
|
|
184
|
+
this.pendingRequests.clear();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Call a tool
|
|
189
|
+
*/
|
|
190
|
+
async callTool(name, arguments_) {
|
|
191
|
+
if (!this.connected) {
|
|
192
|
+
throw new Error('Not connected');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return this._sendRequest('tools/call', {
|
|
196
|
+
name,
|
|
197
|
+
arguments: arguments_,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Read a resource
|
|
203
|
+
*/
|
|
204
|
+
async readResource(uri) {
|
|
205
|
+
if (!this.connected) {
|
|
206
|
+
throw new Error('Not connected');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return this._sendRequest('resources/read', { uri });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Send JSON-RPC request
|
|
214
|
+
*/
|
|
215
|
+
_sendRequest(method, params) {
|
|
216
|
+
return new Promise((resolve, reject) => {
|
|
217
|
+
const id = ++this.requestId;
|
|
218
|
+
const request = {
|
|
219
|
+
jsonrpc: '2.0',
|
|
220
|
+
id,
|
|
221
|
+
method,
|
|
222
|
+
params,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
this.pendingRequests.set(id, { resolve, reject });
|
|
226
|
+
|
|
227
|
+
const timeout = setTimeout(() => {
|
|
228
|
+
this.pendingRequests.delete(id);
|
|
229
|
+
reject(new Error('Request timeout'));
|
|
230
|
+
}, MCP_CLIENT_CONFIG.requestTimeout);
|
|
231
|
+
|
|
232
|
+
this.pendingRequests.get(id).timeout = timeout;
|
|
233
|
+
|
|
234
|
+
this.process.stdin.write(JSON.stringify(request) + '\n');
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Send notification (no response expected)
|
|
240
|
+
*/
|
|
241
|
+
_sendNotification(method, params) {
|
|
242
|
+
const notification = {
|
|
243
|
+
jsonrpc: '2.0',
|
|
244
|
+
method,
|
|
245
|
+
params,
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
this.process.stdin.write(JSON.stringify(notification) + '\n');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Process incoming data buffer
|
|
253
|
+
*/
|
|
254
|
+
_processBuffer() {
|
|
255
|
+
const lines = this.buffer.split('\n');
|
|
256
|
+
this.buffer = lines.pop() || '';
|
|
257
|
+
|
|
258
|
+
for (const line of lines) {
|
|
259
|
+
if (!line.trim()) continue;
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
const message = JSON.parse(line);
|
|
263
|
+
this._handleMessage(message);
|
|
264
|
+
} catch {
|
|
265
|
+
// Ignore non-JSON lines
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Handle incoming message
|
|
272
|
+
*/
|
|
273
|
+
_handleMessage(message) {
|
|
274
|
+
// Response to our request
|
|
275
|
+
if (message.id && this.pendingRequests.has(message.id)) {
|
|
276
|
+
const { resolve, reject, timeout } = this.pendingRequests.get(message.id);
|
|
277
|
+
clearTimeout(timeout);
|
|
278
|
+
this.pendingRequests.delete(message.id);
|
|
279
|
+
|
|
280
|
+
if (message.error) {
|
|
281
|
+
reject(new Error(message.error.message || 'Unknown error'));
|
|
282
|
+
} else {
|
|
283
|
+
resolve(message.result);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Notification from server
|
|
289
|
+
if (message.method) {
|
|
290
|
+
this.emit('notification', message);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// MCP CLIENT HUB
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Hub for managing multiple MCP connections
|
|
301
|
+
*/
|
|
302
|
+
export class MCPHub {
|
|
303
|
+
constructor() {
|
|
304
|
+
this.connections = new Map();
|
|
305
|
+
this.stateFile = MCP_CLIENT_CONFIG.stateFile;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Connect to an MCP server
|
|
310
|
+
*/
|
|
311
|
+
async connect(serverName, customConfig = null) {
|
|
312
|
+
// Check if already connected
|
|
313
|
+
if (this.connections.has(serverName) && this.connections.get(serverName).connected) {
|
|
314
|
+
return this.connections.get(serverName);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Get server config
|
|
318
|
+
const config = customConfig || MCP_CLIENT_CONFIG.servers[serverName];
|
|
319
|
+
if (!config) {
|
|
320
|
+
throw new Error(`Unknown MCP server: ${serverName}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const connection = new MCPConnection(config);
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const result = await connection.connect();
|
|
327
|
+
this.connections.set(serverName, connection);
|
|
328
|
+
|
|
329
|
+
console.log(chalk.green(`Connected to ${config.name}`));
|
|
330
|
+
console.log(chalk.gray(` Tools: ${result.tools.map(t => t.name).join(', ') || 'none'}`));
|
|
331
|
+
|
|
332
|
+
return connection;
|
|
333
|
+
} catch (err) {
|
|
334
|
+
console.log(chalk.red(`Failed to connect to ${config.name}: ${err.message}`));
|
|
335
|
+
throw err;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Disconnect from an MCP server
|
|
341
|
+
*/
|
|
342
|
+
async disconnect(serverName) {
|
|
343
|
+
const connection = this.connections.get(serverName);
|
|
344
|
+
if (connection) {
|
|
345
|
+
await connection.disconnect();
|
|
346
|
+
this.connections.delete(serverName);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Disconnect from all servers
|
|
352
|
+
*/
|
|
353
|
+
async disconnectAll() {
|
|
354
|
+
for (const [name] of this.connections) {
|
|
355
|
+
await this.disconnect(name);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Call a tool on a connected server
|
|
361
|
+
*/
|
|
362
|
+
async callTool(serverName, toolName, args) {
|
|
363
|
+
const connection = this.connections.get(serverName);
|
|
364
|
+
if (!connection || !connection.connected) {
|
|
365
|
+
throw new Error(`Not connected to ${serverName}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return connection.callTool(toolName, args);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Read a resource from a connected server
|
|
373
|
+
*/
|
|
374
|
+
async readResource(serverName, uri) {
|
|
375
|
+
const connection = this.connections.get(serverName);
|
|
376
|
+
if (!connection || !connection.connected) {
|
|
377
|
+
throw new Error(`Not connected to ${serverName}`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
return connection.readResource(uri);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* List all available tools across connections
|
|
385
|
+
*/
|
|
386
|
+
listAllTools() {
|
|
387
|
+
const tools = [];
|
|
388
|
+
|
|
389
|
+
for (const [serverName, connection] of this.connections) {
|
|
390
|
+
for (const tool of connection.tools) {
|
|
391
|
+
tools.push({
|
|
392
|
+
server: serverName,
|
|
393
|
+
...tool,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return tools;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* List all available resources across connections
|
|
403
|
+
*/
|
|
404
|
+
listAllResources() {
|
|
405
|
+
const resources = [];
|
|
406
|
+
|
|
407
|
+
for (const [serverName, connection] of this.connections) {
|
|
408
|
+
for (const resource of connection.resources) {
|
|
409
|
+
resources.push({
|
|
410
|
+
server: serverName,
|
|
411
|
+
...resource,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return resources;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Get connection status
|
|
421
|
+
*/
|
|
422
|
+
getStatus() {
|
|
423
|
+
const status = {};
|
|
424
|
+
|
|
425
|
+
for (const [name, connection] of this.connections) {
|
|
426
|
+
status[name] = {
|
|
427
|
+
connected: connection.connected,
|
|
428
|
+
tools: connection.tools.length,
|
|
429
|
+
resources: connection.resources.length,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
return status;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Save connection state
|
|
438
|
+
*/
|
|
439
|
+
async saveState() {
|
|
440
|
+
const state = {
|
|
441
|
+
connections: Array.from(this.connections.keys()),
|
|
442
|
+
timestamp: new Date().toISOString(),
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
await fs.mkdir(path.dirname(this.stateFile), { recursive: true });
|
|
446
|
+
await fs.writeFile(this.stateFile, JSON.stringify(state, null, 2));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Load and restore connections from state
|
|
451
|
+
*/
|
|
452
|
+
async restoreState() {
|
|
453
|
+
try {
|
|
454
|
+
const state = JSON.parse(await fs.readFile(this.stateFile, 'utf8'));
|
|
455
|
+
|
|
456
|
+
for (const serverName of state.connections) {
|
|
457
|
+
try {
|
|
458
|
+
await this.connect(serverName);
|
|
459
|
+
} catch {
|
|
460
|
+
// Skip failed connections
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
return state.connections;
|
|
465
|
+
} catch {
|
|
466
|
+
return [];
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ============================================================================
|
|
472
|
+
// GLOBAL HUB INSTANCE
|
|
473
|
+
// ============================================================================
|
|
474
|
+
|
|
475
|
+
export const mcpHub = new MCPHub();
|
|
476
|
+
|
|
477
|
+
// ============================================================================
|
|
478
|
+
// CLI HELPERS
|
|
479
|
+
// ============================================================================
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* List available MCP servers
|
|
483
|
+
*/
|
|
484
|
+
export function listAvailableServers() {
|
|
485
|
+
return Object.entries(MCP_CLIENT_CONFIG.servers).map(([name, config]) => ({
|
|
486
|
+
name,
|
|
487
|
+
description: config.description,
|
|
488
|
+
command: `${config.command} ${config.args.join(' ')}`,
|
|
489
|
+
}));
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// ============================================================================
|
|
493
|
+
// EXPORTS
|
|
494
|
+
// ============================================================================
|
|
495
|
+
|
|
496
|
+
export default {
|
|
497
|
+
MCPConnection,
|
|
498
|
+
MCPHub,
|
|
499
|
+
mcpHub,
|
|
500
|
+
listAvailableServers,
|
|
501
|
+
MCP_CLIENT_CONFIG,
|
|
502
|
+
};
|