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.
@@ -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
+ };