@agenticmail/enterprise 0.5.241 → 0.5.243

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.
@@ -1430,8 +1430,9 @@ export function createAgentRoutes(opts: {
1430
1430
  if (!managed) return c.json({ error: 'Agent not found' }, 404);
1431
1431
 
1432
1432
  try {
1433
+ const body = await c.req.json().catch(() => ({})) as any;
1433
1434
  const tracked = meetingBrowsers.get(agentId);
1434
- const port = tracked?.port || (managed.config as any)?.meetingBrowserPort;
1435
+ const port = tracked?.port || (managed.config as any)?.meetingBrowserPort || body.port;
1435
1436
  if (!port) return c.json({ error: 'No meeting browser is tracked for this agent' }, 400);
1436
1437
 
1437
1438
  // Try to close gracefully via CDP
@@ -341,6 +341,219 @@ engine.get('/hierarchy/escalations/:agentId', async (c) => {
341
341
  }
342
342
  });
343
343
 
344
+ // ─── MCP Server Management ──────────────────────────────────────────
345
+ // CRUD for external MCP server connections (stdio, SSE, HTTP)
346
+ engine.get('/mcp-servers', async (c) => {
347
+ try {
348
+ const rows = await engineDb.query(`SELECT * FROM mcp_servers WHERE org_id = $1 ORDER BY created_at DESC`, ['default']);
349
+ const servers = (rows || []).map((r: any) => {
350
+ const config = typeof r.config === 'string' ? JSON.parse(r.config) : (r.config || {});
351
+ return { id: r.id, ...config, status: r.status || 'unknown', toolCount: r.tool_count || 0, tools: r.tools ? (typeof r.tools === 'string' ? JSON.parse(r.tools) : r.tools) : [] };
352
+ });
353
+ return c.json({ servers });
354
+ } catch (e: any) {
355
+ // Table may not exist yet
356
+ if (e.message?.includes('does not exist') || e.message?.includes('no such table')) {
357
+ return c.json({ servers: [] });
358
+ }
359
+ return c.json({ error: e.message }, 500);
360
+ }
361
+ });
362
+
363
+ engine.post('/mcp-servers', async (c) => {
364
+ try {
365
+ await engineDb.exec(`CREATE TABLE IF NOT EXISTS mcp_servers (
366
+ id TEXT PRIMARY KEY DEFAULT gen_random_uuid()::text,
367
+ org_id TEXT NOT NULL DEFAULT 'default',
368
+ config JSONB NOT NULL DEFAULT '{}',
369
+ status TEXT DEFAULT 'unknown',
370
+ tool_count INTEGER DEFAULT 0,
371
+ tools JSONB DEFAULT '[]',
372
+ created_at TIMESTAMPTZ DEFAULT NOW(),
373
+ updated_at TIMESTAMPTZ DEFAULT NOW()
374
+ )`);
375
+ const body = await c.req.json();
376
+ const id = crypto.randomUUID();
377
+ await engineDb.exec(`INSERT INTO mcp_servers (id, org_id, config) VALUES ($1, $2, $3)`, [id, 'default', JSON.stringify(body)]);
378
+ return c.json({ ok: true, id });
379
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
380
+ });
381
+
382
+ engine.put('/mcp-servers/:id', async (c) => {
383
+ try {
384
+ const id = c.req.param('id');
385
+ const body = await c.req.json();
386
+ // Merge with existing config
387
+ const rows = await engineDb.query(`SELECT config FROM mcp_servers WHERE id = $1`, [id]);
388
+ if (!rows?.length) return c.json({ error: 'Server not found' }, 404);
389
+ const existing = typeof rows[0].config === 'string' ? JSON.parse(rows[0].config) : (rows[0].config || {});
390
+ const merged = { ...existing, ...body };
391
+ await engineDb.exec(`UPDATE mcp_servers SET config = $1, updated_at = NOW() WHERE id = $2`, [JSON.stringify(merged), id]);
392
+ return c.json({ ok: true });
393
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
394
+ });
395
+
396
+ engine.delete('/mcp-servers/:id', async (c) => {
397
+ try {
398
+ await engineDb.exec(`DELETE FROM mcp_servers WHERE id = $1`, [c.req.param('id')]);
399
+ return c.json({ ok: true });
400
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
401
+ });
402
+
403
+ engine.post('/mcp-servers/:id/test', async (c) => {
404
+ try {
405
+ const id = c.req.param('id');
406
+ const rows = await engineDb.query(`SELECT config FROM mcp_servers WHERE id = $1`, [id]);
407
+ if (!rows?.length) return c.json({ error: 'Server not found' }, 404);
408
+ const config = typeof rows[0].config === 'string' ? JSON.parse(rows[0].config) : (rows[0].config || {});
409
+
410
+ if (config.type === 'stdio') {
411
+ // Test stdio: spawn process, send initialize, read response
412
+ const { spawn } = await import('node:child_process');
413
+ const args = config.args || [];
414
+ const env = { ...process.env, ...(config.env || {}) };
415
+ const child = spawn(config.command, args, { stdio: ['pipe', 'pipe', 'pipe'], env, timeout: (config.timeout || 30) * 1000 });
416
+
417
+ const initMsg = JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'AgenticMail', version: '1.0' } } });
418
+ child.stdin.write(initMsg + '\n');
419
+
420
+ const result: any = await new Promise((resolve) => {
421
+ let buf = '';
422
+ const timer = setTimeout(() => { child.kill(); resolve({ error: 'Timeout after ' + (config.timeout || 30) + 's' }); }, (config.timeout || 30) * 1000);
423
+ child.stdout.on('data', (chunk: Buffer) => {
424
+ buf += chunk.toString();
425
+ try {
426
+ const parsed = JSON.parse(buf.trim());
427
+ clearTimeout(timer);
428
+ child.kill();
429
+ resolve(parsed);
430
+ } catch { /* wait for more data */ }
431
+ });
432
+ child.on('error', (err: any) => { clearTimeout(timer); resolve({ error: err.message }); });
433
+ child.on('exit', (code: number) => { if (!buf) { clearTimeout(timer); resolve({ error: 'Process exited with code ' + code }); } });
434
+ });
435
+
436
+ if (result.error) return c.json({ error: typeof result.error === 'string' ? result.error : result.error.message || 'Unknown error' });
437
+
438
+ // Now list tools
439
+ const listMsg = JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
440
+ const child2 = spawn(config.command, args, { stdio: ['pipe', 'pipe', 'pipe'], env, timeout: 15000 });
441
+ child2.stdin.write(initMsg + '\n');
442
+
443
+ const tools: any[] = await new Promise((resolve) => {
444
+ let phase = 'init';
445
+ let buf2 = '';
446
+ const timer2 = setTimeout(() => { child2.kill(); resolve([]); }, 15000);
447
+ child2.stdout.on('data', (chunk: Buffer) => {
448
+ buf2 += chunk.toString();
449
+ const lines = buf2.split('\n');
450
+ for (const line of lines) {
451
+ if (!line.trim()) continue;
452
+ try {
453
+ const parsed = JSON.parse(line.trim());
454
+ if (phase === 'init' && parsed.id === 1) {
455
+ phase = 'tools';
456
+ buf2 = '';
457
+ child2.stdin.write(listMsg + '\n');
458
+ } else if (phase === 'tools' && parsed.id === 2) {
459
+ clearTimeout(timer2);
460
+ child2.kill();
461
+ resolve(parsed.result?.tools || []);
462
+ return;
463
+ }
464
+ } catch { /* partial line */ }
465
+ }
466
+ });
467
+ child2.on('error', () => { clearTimeout(timer2); resolve([]); });
468
+ });
469
+
470
+ // Save discovered tools
471
+ await engineDb.exec(`UPDATE mcp_servers SET status = 'connected', tool_count = $1, tools = $2, updated_at = NOW() WHERE id = $3`,
472
+ [tools.length, JSON.stringify(tools.map((t: any) => ({ name: t.name, description: t.description }))), id]);
473
+
474
+ return c.json({ ok: true, tools: tools.length, serverInfo: result.result?.serverInfo });
475
+
476
+ } else {
477
+ // Test HTTP/SSE: send initialize via HTTP
478
+ const url = config.url;
479
+ const headers: Record<string, string> = { 'Content-Type': 'application/json', ...(config.headers || {}) };
480
+ if (config.apiKey) headers['Authorization'] = `Bearer ${config.apiKey}`;
481
+
482
+ const resp = await fetch(url, {
483
+ method: 'POST',
484
+ headers,
485
+ body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'AgenticMail', version: '1.0' } } }),
486
+ signal: AbortSignal.timeout((config.timeout || 30) * 1000),
487
+ });
488
+
489
+ if (!resp.ok) return c.json({ error: `Server returned ${resp.status}` });
490
+ const data = await resp.json() as any;
491
+ if (data.error) return c.json({ error: data.error.message || 'Server error' });
492
+
493
+ // List tools
494
+ const toolResp = await fetch(url, {
495
+ method: 'POST', headers,
496
+ body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }),
497
+ signal: AbortSignal.timeout(15000),
498
+ });
499
+ let tools: any[] = [];
500
+ if (toolResp.ok) {
501
+ const td = await toolResp.json() as any;
502
+ tools = td.result?.tools || [];
503
+ }
504
+
505
+ await engineDb.exec(`UPDATE mcp_servers SET status = 'connected', tool_count = $1, tools = $2, updated_at = NOW() WHERE id = $3`,
506
+ [tools.length, JSON.stringify(tools.map((t: any) => ({ name: t.name, description: t.description }))), id]);
507
+
508
+ return c.json({ ok: true, tools: tools.length, serverInfo: data.result?.serverInfo });
509
+ }
510
+ } catch (e: any) {
511
+ // Update status to error
512
+ try { await engineDb.exec(`UPDATE mcp_servers SET status = 'error', updated_at = NOW() WHERE id = $1`, [c.req.param('id')]); } catch {}
513
+ return c.json({ error: e.message });
514
+ }
515
+ });
516
+
517
+ // ─── Integration Credentials Management ─────────────────
518
+ engine.put('/integrations/:skillId/credentials', async (c) => {
519
+ try {
520
+ const skillId = c.req.param('skillId');
521
+ const body = await c.req.json();
522
+ const orgId = c.req.query('orgId') || 'default';
523
+ // Store each credential field as a vault secret
524
+ for (const [key, value] of Object.entries(body)) {
525
+ if (!value) continue;
526
+ const secretName = `skill:${skillId}:${key}`;
527
+ // Check if exists, update or create
528
+ try {
529
+ const entries = await vault.getSecretsByOrg(orgId, 'skill_credential');
530
+ const existing = entries.find((e: any) => e.name === secretName);
531
+ if (existing) {
532
+ await vault.updateSecret(existing.id, { value: value as string });
533
+ } else {
534
+ await vault.storeSecret({ name: secretName, value: value as string, category: 'skill_credential', orgId });
535
+ }
536
+ } catch {
537
+ await vault.storeSecret({ name: secretName, value: value as string, category: 'skill_credential', orgId });
538
+ }
539
+ }
540
+ return c.json({ ok: true });
541
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
542
+ });
543
+
544
+ engine.delete('/integrations/:skillId/credentials', async (c) => {
545
+ try {
546
+ const skillId = c.req.param('skillId');
547
+ const orgId = c.req.query('orgId') || 'default';
548
+ const entries = await vault.getSecretsByOrg(orgId, 'skill_credential');
549
+ const matching = entries.filter((e: any) => e.name?.startsWith(`skill:${skillId}:`));
550
+ for (const entry of matching) {
551
+ await vault.deleteSecret(entry.id);
552
+ }
553
+ return c.json({ ok: true });
554
+ } catch (e: any) { return c.json({ error: e.message }, 500); }
555
+ });
556
+
344
557
  // ─── Integration catalog (serves all 144 MCP adapter integrations) ──
345
558
  engine.get('/integrations/catalog', async (c) => {
346
559
  try {