@agenticmail/enterprise 0.5.248 → 0.5.249
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/dist/agent-heartbeat-SLOAWBEN.js +510 -0
- package/dist/agent-tools-V3J26M57.js +13871 -0
- package/dist/chunk-PNBWEEJZ.js +1224 -0
- package/dist/chunk-RNREJCPG.js +4466 -0
- package/dist/chunk-XWVKQO6P.js +3778 -0
- package/dist/cli-agent-6DT3UZHY.js +1735 -0
- package/dist/cli-serve-SUPVRGDZ.js +114 -0
- package/dist/cli.js +3 -3
- package/dist/dashboard/pages/skill-connections.js +48 -4
- package/dist/index.js +3 -3
- package/dist/mcp-process-manager-J2XATJVY.js +425 -0
- package/dist/mcp-server-tools-EDTW6FE2.js +61 -0
- package/dist/routes-D65Q3W4Z.js +13509 -0
- package/dist/runtime-M2GFJRL3.js +45 -0
- package/dist/server-DT5K3D55.js +15 -0
- package/dist/setup-6EUJP3OZ.js +20 -0
- package/package.json +1 -1
- package/src/agent-tools/index.ts +20 -0
- package/src/agent-tools/tools/mcp-server-tools.ts +91 -0
- package/src/cli-agent.ts +19 -0
- package/src/dashboard/pages/skill-connections.js +48 -4
- package/src/engine/mcp-process-manager.ts +574 -0
- package/src/engine/routes.ts +23 -0
- package/src/runtime/index.ts +4 -0
- package/src/runtime/types.ts +2 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import "./chunk-KFQGP6VL.js";
|
|
2
|
+
|
|
3
|
+
// src/cli-serve.ts
|
|
4
|
+
import { existsSync, readFileSync } from "fs";
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
function loadEnvFile() {
|
|
8
|
+
const candidates = [
|
|
9
|
+
join(process.cwd(), ".env"),
|
|
10
|
+
join(homedir(), ".agenticmail", ".env")
|
|
11
|
+
];
|
|
12
|
+
for (const envPath of candidates) {
|
|
13
|
+
if (!existsSync(envPath)) continue;
|
|
14
|
+
try {
|
|
15
|
+
const content = readFileSync(envPath, "utf8");
|
|
16
|
+
for (const line of content.split("\n")) {
|
|
17
|
+
const trimmed = line.trim();
|
|
18
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
19
|
+
const eq = trimmed.indexOf("=");
|
|
20
|
+
if (eq < 0) continue;
|
|
21
|
+
const key = trimmed.slice(0, eq).trim();
|
|
22
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
23
|
+
if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
|
|
24
|
+
val = val.slice(1, -1);
|
|
25
|
+
}
|
|
26
|
+
if (!process.env[key]) process.env[key] = val;
|
|
27
|
+
}
|
|
28
|
+
console.log(`Loaded config from ${envPath}`);
|
|
29
|
+
return;
|
|
30
|
+
} catch {
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async function ensureSecrets() {
|
|
35
|
+
const { randomUUID } = await import("crypto");
|
|
36
|
+
const envDir = join(homedir(), ".agenticmail");
|
|
37
|
+
const envPath = join(envDir, ".env");
|
|
38
|
+
let dirty = false;
|
|
39
|
+
if (!process.env.JWT_SECRET) {
|
|
40
|
+
process.env.JWT_SECRET = randomUUID() + randomUUID();
|
|
41
|
+
dirty = true;
|
|
42
|
+
console.log("[startup] Generated new JWT_SECRET (existing sessions will need to re-login)");
|
|
43
|
+
}
|
|
44
|
+
if (!process.env.AGENTICMAIL_VAULT_KEY) {
|
|
45
|
+
process.env.AGENTICMAIL_VAULT_KEY = randomUUID() + randomUUID();
|
|
46
|
+
dirty = true;
|
|
47
|
+
console.log("[startup] Generated new AGENTICMAIL_VAULT_KEY");
|
|
48
|
+
console.log("[startup] \u26A0\uFE0F Previously encrypted credentials will need to be re-entered in the dashboard");
|
|
49
|
+
}
|
|
50
|
+
if (dirty) {
|
|
51
|
+
try {
|
|
52
|
+
if (!existsSync(envDir)) {
|
|
53
|
+
const { mkdirSync } = await import("fs");
|
|
54
|
+
mkdirSync(envDir, { recursive: true });
|
|
55
|
+
}
|
|
56
|
+
const { appendFileSync } = await import("fs");
|
|
57
|
+
const lines = [];
|
|
58
|
+
let existing = "";
|
|
59
|
+
if (existsSync(envPath)) {
|
|
60
|
+
existing = readFileSync(envPath, "utf8");
|
|
61
|
+
}
|
|
62
|
+
if (!existing.includes("JWT_SECRET=")) {
|
|
63
|
+
lines.push(`JWT_SECRET=${process.env.JWT_SECRET}`);
|
|
64
|
+
}
|
|
65
|
+
if (!existing.includes("AGENTICMAIL_VAULT_KEY=")) {
|
|
66
|
+
lines.push(`AGENTICMAIL_VAULT_KEY=${process.env.AGENTICMAIL_VAULT_KEY}`);
|
|
67
|
+
}
|
|
68
|
+
if (lines.length) {
|
|
69
|
+
appendFileSync(envPath, "\n" + lines.join("\n") + "\n", { mode: 384 });
|
|
70
|
+
console.log(`[startup] Saved secrets to ${envPath}`);
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
console.warn(`[startup] Could not save secrets to ${envPath}: ${e.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function runServe(_args) {
|
|
78
|
+
loadEnvFile();
|
|
79
|
+
const DATABASE_URL = process.env.DATABASE_URL;
|
|
80
|
+
const PORT = parseInt(process.env.PORT || "8080", 10);
|
|
81
|
+
await ensureSecrets();
|
|
82
|
+
const JWT_SECRET = process.env.JWT_SECRET;
|
|
83
|
+
const VAULT_KEY = process.env.AGENTICMAIL_VAULT_KEY;
|
|
84
|
+
if (!DATABASE_URL) {
|
|
85
|
+
console.error("ERROR: DATABASE_URL is required.");
|
|
86
|
+
console.error("");
|
|
87
|
+
console.error("Set it via environment variable or .env file:");
|
|
88
|
+
console.error(" DATABASE_URL=postgresql://user:pass@host:5432/db npx @agenticmail/enterprise start");
|
|
89
|
+
console.error("");
|
|
90
|
+
console.error("Or create a .env file (in cwd or ~/.agenticmail/.env):");
|
|
91
|
+
console.error(" DATABASE_URL=postgresql://user:pass@host:5432/db");
|
|
92
|
+
console.error(" JWT_SECRET=your-secret-here");
|
|
93
|
+
console.error(" PORT=3200");
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
const { createAdapter } = await import("./factory-672W7A5B.js");
|
|
97
|
+
const { createServer } = await import("./server-DT5K3D55.js");
|
|
98
|
+
const db = await createAdapter({
|
|
99
|
+
type: DATABASE_URL.startsWith("postgres") ? "postgres" : "sqlite",
|
|
100
|
+
connectionString: DATABASE_URL
|
|
101
|
+
});
|
|
102
|
+
await db.migrate();
|
|
103
|
+
const server = createServer({
|
|
104
|
+
port: PORT,
|
|
105
|
+
db,
|
|
106
|
+
jwtSecret: JWT_SECRET,
|
|
107
|
+
corsOrigins: ["*"]
|
|
108
|
+
});
|
|
109
|
+
await server.start();
|
|
110
|
+
console.log(`AgenticMail Enterprise server running on :${PORT}`);
|
|
111
|
+
}
|
|
112
|
+
export {
|
|
113
|
+
runServe
|
|
114
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -53,14 +53,14 @@ Skill Development:
|
|
|
53
53
|
break;
|
|
54
54
|
case "serve":
|
|
55
55
|
case "start":
|
|
56
|
-
import("./cli-serve-
|
|
56
|
+
import("./cli-serve-SUPVRGDZ.js").then((m) => m.runServe(args.slice(1))).catch(fatal);
|
|
57
57
|
break;
|
|
58
58
|
case "agent":
|
|
59
|
-
import("./cli-agent-
|
|
59
|
+
import("./cli-agent-6DT3UZHY.js").then((m) => m.runAgent(args.slice(1))).catch(fatal);
|
|
60
60
|
break;
|
|
61
61
|
case "setup":
|
|
62
62
|
default:
|
|
63
|
-
import("./setup-
|
|
63
|
+
import("./setup-6EUJP3OZ.js").then((m) => m.runSetupWizard()).catch(fatal);
|
|
64
64
|
break;
|
|
65
65
|
}
|
|
66
66
|
function fatal(err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h, useState, useEffect, useCallback, Fragment, useApp, engineCall } from '../components/utils.js';
|
|
1
|
+
import { h, useState, useEffect, useCallback, Fragment, useApp, engineCall, adminCall } from '../components/utils.js';
|
|
2
2
|
import { I } from '../components/icons.js';
|
|
3
3
|
import { Modal } from '../components/modal.js';
|
|
4
4
|
import { HelpButton } from '../components/help-button.js';
|
|
@@ -44,9 +44,10 @@ function McpServersSection() {
|
|
|
44
44
|
var _showAdd = useState(false); var showAdd = _showAdd[0]; var setShowAdd = _showAdd[1];
|
|
45
45
|
var _editServer = useState(null); var editServer = _editServer[0]; var setEditServer = _editServer[1];
|
|
46
46
|
var _testing = useState(null); var testing = _testing[0]; var setTesting = _testing[1];
|
|
47
|
+
var _agents = useState([]); var agents = _agents[0]; var setAgents = _agents[1];
|
|
47
48
|
|
|
48
49
|
// Add/edit form
|
|
49
|
-
var _form = useState({ name: '', type: 'stdio', command: '', args: '', url: '', apiKey: '', headers: '{}', env: '{}', enabled: true, description: '', autoRestart: true, timeout: 30 });
|
|
50
|
+
var _form = useState({ name: '', type: 'stdio', command: '', args: '', url: '', apiKey: '', headers: '{}', env: '{}', enabled: true, description: '', autoRestart: true, timeout: 30, assignedAgents: [] });
|
|
50
51
|
var form = _form[0]; var setForm = _form[1];
|
|
51
52
|
|
|
52
53
|
var load = useCallback(function() {
|
|
@@ -58,9 +59,12 @@ function McpServersSection() {
|
|
|
58
59
|
}, []);
|
|
59
60
|
|
|
60
61
|
useEffect(function() { load(); }, [load]);
|
|
62
|
+
useEffect(function() {
|
|
63
|
+
adminCall('/agents').then(function(d) { setAgents((d.agents || d || []).filter(function(a) { return a.status !== 'archived'; })); }).catch(function() {});
|
|
64
|
+
}, []);
|
|
61
65
|
|
|
62
66
|
var resetForm = function() {
|
|
63
|
-
setForm({ name: '', type: 'stdio', command: '', args: '', url: '', apiKey: '', headers: '{}', env: '{}', enabled: true, description: '', autoRestart: true, timeout: 30 });
|
|
67
|
+
setForm({ name: '', type: 'stdio', command: '', args: '', url: '', apiKey: '', headers: '{}', env: '{}', enabled: true, description: '', autoRestart: true, timeout: 30, assignedAgents: [] });
|
|
64
68
|
};
|
|
65
69
|
|
|
66
70
|
var openAdd = function() { resetForm(); setEditServer(null); setShowAdd(true); };
|
|
@@ -79,6 +83,7 @@ function McpServersSection() {
|
|
|
79
83
|
description: server.description || '',
|
|
80
84
|
autoRestart: server.autoRestart !== false,
|
|
81
85
|
timeout: server.timeout || 30,
|
|
86
|
+
assignedAgents: server.assignedAgents || [],
|
|
82
87
|
});
|
|
83
88
|
setEditServer(server);
|
|
84
89
|
setShowAdd(true);
|
|
@@ -92,6 +97,7 @@ function McpServersSection() {
|
|
|
92
97
|
description: form.description.trim(),
|
|
93
98
|
autoRestart: form.autoRestart,
|
|
94
99
|
timeout: parseInt(form.timeout) || 30,
|
|
100
|
+
assignedAgents: form.assignedAgents || [],
|
|
95
101
|
};
|
|
96
102
|
if (form.type === 'stdio') {
|
|
97
103
|
payload.command = form.command.trim();
|
|
@@ -210,7 +216,13 @@ function McpServersSection() {
|
|
|
210
216
|
? (server.command + ' ' + (server.args || []).join(' ')).trim()
|
|
211
217
|
: server.url || ''
|
|
212
218
|
),
|
|
213
|
-
server.description && h('div', { style: { fontSize: 11, color: 'var(--text-secondary)', marginTop: 2 } }, server.description)
|
|
219
|
+
server.description && h('div', { style: { fontSize: 11, color: 'var(--text-secondary)', marginTop: 2 } }, server.description),
|
|
220
|
+
server.assignedAgents && server.assignedAgents.length > 0 && h('div', { style: { fontSize: 10, color: 'var(--text-muted)', marginTop: 3 } },
|
|
221
|
+
'Agents: ' + server.assignedAgents.map(function(aid) {
|
|
222
|
+
var a = agents.find(function(x) { return x.id === aid; });
|
|
223
|
+
return a ? (a.display_name || a.name) : aid.slice(0, 8);
|
|
224
|
+
}).join(', ')
|
|
225
|
+
)
|
|
214
226
|
),
|
|
215
227
|
h('div', { style: { display: 'flex', gap: 4, flexShrink: 0 } },
|
|
216
228
|
h('button', { className: 'btn btn-ghost btn-sm', title: 'Test connection', disabled: isTesting, onClick: function() { testServer(server); } },
|
|
@@ -400,6 +412,38 @@ function McpServersSection() {
|
|
|
400
412
|
h('p', { style: { marginTop: 8 } }, 'Keep this on unless you have a reason to disable it.')
|
|
401
413
|
)
|
|
402
414
|
)
|
|
415
|
+
),
|
|
416
|
+
// Agent assignment
|
|
417
|
+
agents.length > 0 && h('div', { className: 'form-group', style: { marginTop: 16 } },
|
|
418
|
+
h('label', { className: 'form-label', style: { display: 'flex', alignItems: 'center' } }, 'Agent Access', h(HelpButton, { label: 'Agent Access' },
|
|
419
|
+
h('p', null, 'Choose which agents can use this MCP server\'s tools. If none are selected, ALL agents can use it.'),
|
|
420
|
+
h('p', { style: { marginTop: 8 } }, 'Use this to restrict sensitive tools (like database access) to specific agents only.')
|
|
421
|
+
)),
|
|
422
|
+
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6 } },
|
|
423
|
+
agents.map(function(a) {
|
|
424
|
+
var isSelected = (form.assignedAgents || []).includes(a.id);
|
|
425
|
+
return h('button', {
|
|
426
|
+
key: a.id,
|
|
427
|
+
type: 'button',
|
|
428
|
+
style: {
|
|
429
|
+
padding: '4px 10px', borderRadius: 6, fontSize: 12, cursor: 'pointer',
|
|
430
|
+
border: '1px solid ' + (isSelected ? 'var(--primary)' : 'var(--border)'),
|
|
431
|
+
background: isSelected ? 'var(--primary)' : 'var(--bg-secondary)',
|
|
432
|
+
color: isSelected ? '#fff' : 'var(--text-primary)',
|
|
433
|
+
},
|
|
434
|
+
onClick: function() {
|
|
435
|
+
var current = form.assignedAgents || [];
|
|
436
|
+
var next = isSelected ? current.filter(function(x) { return x !== a.id; }) : current.concat(a.id);
|
|
437
|
+
setForm(Object.assign({}, form, { assignedAgents: next }));
|
|
438
|
+
}
|
|
439
|
+
}, a.display_name || a.name);
|
|
440
|
+
})
|
|
441
|
+
),
|
|
442
|
+
h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } },
|
|
443
|
+
form.assignedAgents && form.assignedAgents.length > 0
|
|
444
|
+
? form.assignedAgents.length + ' agent(s) selected — only they can use this server'
|
|
445
|
+
: 'No agents selected — all agents can use this server'
|
|
446
|
+
)
|
|
403
447
|
)
|
|
404
448
|
),
|
|
405
449
|
// Preset templates
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import {
|
|
8
8
|
provision,
|
|
9
9
|
runSetupWizard
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-PNBWEEJZ.js";
|
|
11
11
|
import {
|
|
12
12
|
AgenticMailManager,
|
|
13
13
|
GoogleEmailProvider,
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
executeTool,
|
|
29
29
|
runAgentLoop,
|
|
30
30
|
toolsToDefinitions
|
|
31
|
-
} from "./chunk-
|
|
31
|
+
} from "./chunk-RNREJCPG.js";
|
|
32
32
|
import {
|
|
33
33
|
ValidationError,
|
|
34
34
|
auditLogger,
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
requireRole,
|
|
43
43
|
securityHeaders,
|
|
44
44
|
validate
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-XWVKQO6P.js";
|
|
46
46
|
import "./chunk-OF4MUWWS.js";
|
|
47
47
|
import {
|
|
48
48
|
PROVIDER_REGISTRY,
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import "./chunk-KFQGP6VL.js";
|
|
2
|
+
|
|
3
|
+
// src/engine/mcp-process-manager.ts
|
|
4
|
+
import { spawn } from "child_process";
|
|
5
|
+
import { EventEmitter } from "events";
|
|
6
|
+
var McpProcessManager = class extends EventEmitter {
|
|
7
|
+
db;
|
|
8
|
+
orgId;
|
|
9
|
+
servers = /* @__PURE__ */ new Map();
|
|
10
|
+
maxRestarts;
|
|
11
|
+
restartDelayMs;
|
|
12
|
+
discoveryTimeoutMs;
|
|
13
|
+
started = false;
|
|
14
|
+
healthTimer = null;
|
|
15
|
+
constructor(config) {
|
|
16
|
+
super();
|
|
17
|
+
this.db = config.engineDb;
|
|
18
|
+
this.orgId = config.orgId || "default";
|
|
19
|
+
this.maxRestarts = config.maxRestarts ?? 5;
|
|
20
|
+
this.restartDelayMs = config.restartDelayMs ?? 3e3;
|
|
21
|
+
this.discoveryTimeoutMs = config.discoveryTimeoutMs ?? 3e4;
|
|
22
|
+
}
|
|
23
|
+
/** Start the manager — load all enabled MCP servers from DB and connect */
|
|
24
|
+
async start() {
|
|
25
|
+
if (this.started) return;
|
|
26
|
+
this.started = true;
|
|
27
|
+
try {
|
|
28
|
+
const rows = await this.db.query(
|
|
29
|
+
`SELECT * FROM mcp_servers WHERE org_id = $1`,
|
|
30
|
+
[this.orgId]
|
|
31
|
+
);
|
|
32
|
+
const servers = (rows || []).map((r) => {
|
|
33
|
+
const config = typeof r.config === "string" ? JSON.parse(r.config) : r.config || {};
|
|
34
|
+
return { ...config, id: r.id };
|
|
35
|
+
});
|
|
36
|
+
const enabled = servers.filter((s) => s.enabled !== false);
|
|
37
|
+
if (enabled.length === 0) {
|
|
38
|
+
console.log("[mcp-manager] No enabled MCP servers found");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(`[mcp-manager] Starting ${enabled.length} MCP server(s)...`);
|
|
42
|
+
await Promise.allSettled(enabled.map((s) => this.connectServer(s)));
|
|
43
|
+
this.healthTimer = setInterval(() => this.healthCheck(), 6e4);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
if (e.message?.includes("does not exist") || e.message?.includes("no such table")) {
|
|
46
|
+
console.log("[mcp-manager] mcp_servers table does not exist yet \u2014 skipping");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
console.error(`[mcp-manager] Start failed: ${e.message}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Stop all servers and clean up */
|
|
53
|
+
async stop() {
|
|
54
|
+
this.started = false;
|
|
55
|
+
if (this.healthTimer) {
|
|
56
|
+
clearInterval(this.healthTimer);
|
|
57
|
+
this.healthTimer = null;
|
|
58
|
+
}
|
|
59
|
+
for (const [id, state] of Array.from(this.servers)) {
|
|
60
|
+
this.killProcess(state);
|
|
61
|
+
console.log(`[mcp-manager] Stopped server: ${state.config.name} (${id})`);
|
|
62
|
+
}
|
|
63
|
+
this.servers.clear();
|
|
64
|
+
}
|
|
65
|
+
/** Connect a single MCP server (stdio spawn or HTTP/SSE connect) */
|
|
66
|
+
async connectServer(config) {
|
|
67
|
+
const existing = this.servers.get(config.id);
|
|
68
|
+
if (existing) this.killProcess(existing);
|
|
69
|
+
const state = {
|
|
70
|
+
config,
|
|
71
|
+
status: "starting",
|
|
72
|
+
tools: [],
|
|
73
|
+
restartCount: 0,
|
|
74
|
+
rpcId: 0,
|
|
75
|
+
pendingRpc: /* @__PURE__ */ new Map(),
|
|
76
|
+
stdoutBuffer: ""
|
|
77
|
+
};
|
|
78
|
+
this.servers.set(config.id, state);
|
|
79
|
+
try {
|
|
80
|
+
if (config.type === "stdio") {
|
|
81
|
+
await this.connectStdio(state);
|
|
82
|
+
} else {
|
|
83
|
+
await this.connectHttp(state);
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
state.status = "error";
|
|
87
|
+
state.error = e.message;
|
|
88
|
+
console.error(`[mcp-manager] Failed to connect ${config.name}: ${e.message}`);
|
|
89
|
+
this.updateDbStatus(config.id, "error", 0, []);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** Disconnect and remove a server */
|
|
93
|
+
async disconnectServer(serverId) {
|
|
94
|
+
const state = this.servers.get(serverId);
|
|
95
|
+
if (state) {
|
|
96
|
+
this.killProcess(state);
|
|
97
|
+
this.servers.delete(serverId);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** Hot-reload: add or update a server config without restarting the whole manager */
|
|
101
|
+
async reloadServer(serverId) {
|
|
102
|
+
try {
|
|
103
|
+
const rows = await this.db.query(`SELECT * FROM mcp_servers WHERE id = $1`, [serverId]);
|
|
104
|
+
if (!rows?.length) {
|
|
105
|
+
await this.disconnectServer(serverId);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const config = typeof rows[0].config === "string" ? JSON.parse(rows[0].config) : rows[0].config || {};
|
|
109
|
+
const serverConfig = { ...config, id: rows[0].id };
|
|
110
|
+
if (serverConfig.enabled === false) {
|
|
111
|
+
await this.disconnectServer(serverId);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await this.connectServer(serverConfig);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
console.error(`[mcp-manager] Reload failed for ${serverId}: ${e.message}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// ─── Tool Access ───────────────────────────────────────
|
|
120
|
+
/** Get all discovered tools across all connected servers, optionally filtered by agent */
|
|
121
|
+
getToolsForAgent(agentId) {
|
|
122
|
+
const tools = [];
|
|
123
|
+
for (const [id, state] of Array.from(this.servers)) {
|
|
124
|
+
if (state.status !== "connected") continue;
|
|
125
|
+
if (agentId && state.config.assignedAgents?.length) {
|
|
126
|
+
if (!state.config.assignedAgents.includes(agentId)) continue;
|
|
127
|
+
}
|
|
128
|
+
for (const tool of state.tools) {
|
|
129
|
+
tools.push({ ...tool, serverId: id, serverName: state.config.name });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return tools;
|
|
133
|
+
}
|
|
134
|
+
/** Get all connected server statuses */
|
|
135
|
+
getServerStatuses() {
|
|
136
|
+
return Array.from(this.servers.values()).map((s) => ({
|
|
137
|
+
id: s.config.id,
|
|
138
|
+
name: s.config.name,
|
|
139
|
+
status: s.status,
|
|
140
|
+
toolCount: s.tools.length,
|
|
141
|
+
error: s.error
|
|
142
|
+
}));
|
|
143
|
+
}
|
|
144
|
+
/** Call a tool on its MCP server */
|
|
145
|
+
async callTool(toolName, args, agentId) {
|
|
146
|
+
for (const [_id, state] of Array.from(this.servers)) {
|
|
147
|
+
if (state.status !== "connected") continue;
|
|
148
|
+
if (agentId && state.config.assignedAgents?.length) {
|
|
149
|
+
if (!state.config.assignedAgents.includes(agentId)) continue;
|
|
150
|
+
}
|
|
151
|
+
const tool = state.tools.find((t) => t.name === toolName);
|
|
152
|
+
if (!tool) continue;
|
|
153
|
+
if (state.config.type === "stdio") {
|
|
154
|
+
return this.callToolStdio(state, toolName, args);
|
|
155
|
+
} else {
|
|
156
|
+
return this.callToolHttp(state, toolName, args);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { content: `Tool "${toolName}" not found on any connected MCP server`, isError: true };
|
|
160
|
+
}
|
|
161
|
+
// ─── Stdio Transport ──────────────────────────────────
|
|
162
|
+
async connectStdio(state) {
|
|
163
|
+
const { config } = state;
|
|
164
|
+
if (!config.command) throw new Error("No command specified for stdio MCP server");
|
|
165
|
+
const env = { ...process.env, ...config.env || {} };
|
|
166
|
+
const child = spawn(config.command, config.args || [], {
|
|
167
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
168
|
+
env
|
|
169
|
+
});
|
|
170
|
+
state.process = child;
|
|
171
|
+
state.lastStarted = /* @__PURE__ */ new Date();
|
|
172
|
+
state.stdoutBuffer = "";
|
|
173
|
+
child.stdout.on("data", (chunk) => {
|
|
174
|
+
state.stdoutBuffer += chunk.toString();
|
|
175
|
+
this.processStdoutBuffer(state);
|
|
176
|
+
});
|
|
177
|
+
child.stderr.on("data", (chunk) => {
|
|
178
|
+
const msg = chunk.toString().trim();
|
|
179
|
+
if (msg) console.log(`[mcp:${config.name}:stderr] ${msg.slice(0, 200)}`);
|
|
180
|
+
});
|
|
181
|
+
child.on("exit", (code) => {
|
|
182
|
+
if (state.status === "connected" && config.autoRestart !== false && this.started) {
|
|
183
|
+
console.warn(`[mcp-manager] ${config.name} exited with code ${code} \u2014 restarting...`);
|
|
184
|
+
this.scheduleRestart(state);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
child.on("error", (err) => {
|
|
188
|
+
state.status = "error";
|
|
189
|
+
state.error = err.message;
|
|
190
|
+
console.error(`[mcp-manager] ${config.name} process error: ${err.message}`);
|
|
191
|
+
});
|
|
192
|
+
const initResult = await this.sendRpc(state, "initialize", {
|
|
193
|
+
protocolVersion: "2024-11-05",
|
|
194
|
+
capabilities: {},
|
|
195
|
+
clientInfo: { name: "AgenticMail-Enterprise", version: "1.0" }
|
|
196
|
+
});
|
|
197
|
+
if (!initResult?.result) {
|
|
198
|
+
throw new Error(`Initialize failed: ${JSON.stringify(initResult?.error || "no response")}`);
|
|
199
|
+
}
|
|
200
|
+
this.sendNotification(state, "notifications/initialized", {});
|
|
201
|
+
const toolsResult = await this.sendRpc(state, "tools/list", {});
|
|
202
|
+
state.tools = (toolsResult?.result?.tools || []).map((t) => ({
|
|
203
|
+
name: t.name,
|
|
204
|
+
description: t.description,
|
|
205
|
+
inputSchema: t.inputSchema || { type: "object", properties: {} }
|
|
206
|
+
}));
|
|
207
|
+
state.status = "connected";
|
|
208
|
+
state.error = void 0;
|
|
209
|
+
console.log(`[mcp-manager] ${config.name} connected (stdio) \u2014 ${state.tools.length} tools discovered`);
|
|
210
|
+
this.updateDbStatus(config.id, "connected", state.tools.length, state.tools);
|
|
211
|
+
this.emit("server:connected", { serverId: config.id, tools: state.tools });
|
|
212
|
+
}
|
|
213
|
+
async callToolStdio(state, toolName, args) {
|
|
214
|
+
try {
|
|
215
|
+
const result = await this.sendRpc(state, "tools/call", { name: toolName, arguments: args });
|
|
216
|
+
if (result?.error) {
|
|
217
|
+
return { content: result.error.message || JSON.stringify(result.error), isError: true };
|
|
218
|
+
}
|
|
219
|
+
const contents = result?.result?.content || [];
|
|
220
|
+
const textParts = contents.map((c) => c.type === "text" ? c.text : JSON.stringify(c)).join("\n");
|
|
221
|
+
return { content: textParts || "OK", isError: result?.result?.isError };
|
|
222
|
+
} catch (e) {
|
|
223
|
+
return { content: `MCP call failed: ${e.message}`, isError: true };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// ─── HTTP/SSE Transport ────────────────────────────────
|
|
227
|
+
async connectHttp(state) {
|
|
228
|
+
const { config } = state;
|
|
229
|
+
if (!config.url) throw new Error("No URL specified for HTTP/SSE MCP server");
|
|
230
|
+
const headers = {
|
|
231
|
+
"Content-Type": "application/json",
|
|
232
|
+
...config.headers || {}
|
|
233
|
+
};
|
|
234
|
+
if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
235
|
+
const timeout = (config.timeout || 30) * 1e3;
|
|
236
|
+
const initResp = await fetch(config.url, {
|
|
237
|
+
method: "POST",
|
|
238
|
+
headers,
|
|
239
|
+
body: JSON.stringify({
|
|
240
|
+
jsonrpc: "2.0",
|
|
241
|
+
id: 1,
|
|
242
|
+
method: "initialize",
|
|
243
|
+
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "AgenticMail-Enterprise", version: "1.0" } }
|
|
244
|
+
}),
|
|
245
|
+
signal: AbortSignal.timeout(timeout)
|
|
246
|
+
});
|
|
247
|
+
if (!initResp.ok) throw new Error(`HTTP ${initResp.status}: ${await initResp.text().catch(() => "")}`);
|
|
248
|
+
const initData = await initResp.json();
|
|
249
|
+
if (initData.error) throw new Error(initData.error.message || "Initialize error");
|
|
250
|
+
const toolResp = await fetch(config.url, {
|
|
251
|
+
method: "POST",
|
|
252
|
+
headers,
|
|
253
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }),
|
|
254
|
+
signal: AbortSignal.timeout(15e3)
|
|
255
|
+
});
|
|
256
|
+
let tools = [];
|
|
257
|
+
if (toolResp.ok) {
|
|
258
|
+
const td = await toolResp.json();
|
|
259
|
+
tools = (td.result?.tools || []).map((t) => ({
|
|
260
|
+
name: t.name,
|
|
261
|
+
description: t.description,
|
|
262
|
+
inputSchema: t.inputSchema || { type: "object", properties: {} }
|
|
263
|
+
}));
|
|
264
|
+
}
|
|
265
|
+
state.tools = tools;
|
|
266
|
+
state.status = "connected";
|
|
267
|
+
state.error = void 0;
|
|
268
|
+
state.lastStarted = /* @__PURE__ */ new Date();
|
|
269
|
+
console.log(`[mcp-manager] ${config.name} connected (${config.type}) \u2014 ${tools.length} tools discovered`);
|
|
270
|
+
this.updateDbStatus(config.id, "connected", tools.length, tools);
|
|
271
|
+
this.emit("server:connected", { serverId: config.id, tools });
|
|
272
|
+
}
|
|
273
|
+
async callToolHttp(state, toolName, args) {
|
|
274
|
+
const { config } = state;
|
|
275
|
+
const headers = {
|
|
276
|
+
"Content-Type": "application/json",
|
|
277
|
+
...config.headers || {}
|
|
278
|
+
};
|
|
279
|
+
if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
280
|
+
try {
|
|
281
|
+
const resp = await fetch(config.url, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers,
|
|
284
|
+
body: JSON.stringify({
|
|
285
|
+
jsonrpc: "2.0",
|
|
286
|
+
id: Date.now(),
|
|
287
|
+
method: "tools/call",
|
|
288
|
+
params: { name: toolName, arguments: args }
|
|
289
|
+
}),
|
|
290
|
+
signal: AbortSignal.timeout((config.timeout || 30) * 1e3)
|
|
291
|
+
});
|
|
292
|
+
if (!resp.ok) return { content: `HTTP ${resp.status}`, isError: true };
|
|
293
|
+
const data = await resp.json();
|
|
294
|
+
if (data.error) return { content: data.error.message || JSON.stringify(data.error), isError: true };
|
|
295
|
+
const contents = data.result?.content || [];
|
|
296
|
+
const textParts = contents.map((c) => c.type === "text" ? c.text : JSON.stringify(c)).join("\n");
|
|
297
|
+
return { content: textParts || "OK", isError: data.result?.isError };
|
|
298
|
+
} catch (e) {
|
|
299
|
+
return { content: `MCP HTTP call failed: ${e.message}`, isError: true };
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// ─── JSON-RPC Helpers (stdio) ──────────────────────────
|
|
303
|
+
sendRpc(state, method, params) {
|
|
304
|
+
return new Promise((resolve, reject) => {
|
|
305
|
+
if (!state.process?.stdin?.writable) {
|
|
306
|
+
return reject(new Error("Process stdin not writable"));
|
|
307
|
+
}
|
|
308
|
+
const id = ++state.rpcId;
|
|
309
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
310
|
+
const timer = setTimeout(() => {
|
|
311
|
+
state.pendingRpc.delete(id);
|
|
312
|
+
reject(new Error(`RPC timeout for ${method} after ${this.discoveryTimeoutMs}ms`));
|
|
313
|
+
}, this.discoveryTimeoutMs);
|
|
314
|
+
state.pendingRpc.set(id, { resolve, reject, timer });
|
|
315
|
+
try {
|
|
316
|
+
state.process.stdin.write(msg + "\n");
|
|
317
|
+
} catch (e) {
|
|
318
|
+
state.pendingRpc.delete(id);
|
|
319
|
+
clearTimeout(timer);
|
|
320
|
+
reject(e);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
sendNotification(state, method, params) {
|
|
325
|
+
if (!state.process?.stdin?.writable) return;
|
|
326
|
+
try {
|
|
327
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
328
|
+
state.process.stdin.write(msg + "\n");
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
processStdoutBuffer(state) {
|
|
333
|
+
const lines = state.stdoutBuffer.split("\n");
|
|
334
|
+
state.stdoutBuffer = lines.pop() || "";
|
|
335
|
+
for (const line of lines) {
|
|
336
|
+
const trimmed = line.trim();
|
|
337
|
+
if (!trimmed) continue;
|
|
338
|
+
try {
|
|
339
|
+
const parsed = JSON.parse(trimmed);
|
|
340
|
+
if (parsed.id !== void 0 && state.pendingRpc.has(parsed.id)) {
|
|
341
|
+
const pending = state.pendingRpc.get(parsed.id);
|
|
342
|
+
state.pendingRpc.delete(parsed.id);
|
|
343
|
+
clearTimeout(pending.timer);
|
|
344
|
+
pending.resolve(parsed);
|
|
345
|
+
} else if (!parsed.id && parsed.method) {
|
|
346
|
+
this.emit("server:notification", {
|
|
347
|
+
serverId: state.config.id,
|
|
348
|
+
method: parsed.method,
|
|
349
|
+
params: parsed.params
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
} catch {
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
// ─── Restart / Health ──────────────────────────────────
|
|
357
|
+
scheduleRestart(state) {
|
|
358
|
+
if (state.restartCount >= this.maxRestarts) {
|
|
359
|
+
state.status = "error";
|
|
360
|
+
state.error = `Max restarts (${this.maxRestarts}) exceeded`;
|
|
361
|
+
console.error(`[mcp-manager] ${state.config.name} exceeded max restarts`);
|
|
362
|
+
this.updateDbStatus(state.config.id, "error", 0, []);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
state.restartCount++;
|
|
366
|
+
const delay = this.restartDelayMs * state.restartCount;
|
|
367
|
+
setTimeout(async () => {
|
|
368
|
+
if (!this.started) return;
|
|
369
|
+
console.log(`[mcp-manager] Restarting ${state.config.name} (attempt ${state.restartCount})...`);
|
|
370
|
+
try {
|
|
371
|
+
await this.connectServer(state.config);
|
|
372
|
+
} catch (e) {
|
|
373
|
+
console.error(`[mcp-manager] Restart failed: ${e.message}`);
|
|
374
|
+
}
|
|
375
|
+
}, delay);
|
|
376
|
+
}
|
|
377
|
+
healthCheck() {
|
|
378
|
+
for (const [_id, state] of Array.from(this.servers)) {
|
|
379
|
+
if (state.status === "connected" && state.config.type === "stdio") {
|
|
380
|
+
if (state.process && state.process.exitCode !== null) {
|
|
381
|
+
console.warn(`[mcp-manager] ${state.config.name} process died (exit ${state.process.exitCode})`);
|
|
382
|
+
if (state.config.autoRestart !== false) {
|
|
383
|
+
this.scheduleRestart(state);
|
|
384
|
+
} else {
|
|
385
|
+
state.status = "error";
|
|
386
|
+
state.error = "Process exited";
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
killProcess(state) {
|
|
393
|
+
state.status = "stopped";
|
|
394
|
+
for (const [_id, pending] of Array.from(state.pendingRpc)) {
|
|
395
|
+
clearTimeout(pending.timer);
|
|
396
|
+
pending.reject(new Error("Server stopped"));
|
|
397
|
+
}
|
|
398
|
+
state.pendingRpc.clear();
|
|
399
|
+
if (state.process) {
|
|
400
|
+
try {
|
|
401
|
+
state.process.kill("SIGTERM");
|
|
402
|
+
} catch {
|
|
403
|
+
}
|
|
404
|
+
setTimeout(() => {
|
|
405
|
+
try {
|
|
406
|
+
state.process?.kill("SIGKILL");
|
|
407
|
+
} catch {
|
|
408
|
+
}
|
|
409
|
+
}, 3e3);
|
|
410
|
+
state.process = void 0;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async updateDbStatus(serverId, status, toolCount, tools) {
|
|
414
|
+
try {
|
|
415
|
+
await this.db.exec(
|
|
416
|
+
`UPDATE mcp_servers SET status = $1, tool_count = $2, tools = $3, updated_at = NOW() WHERE id = $4`,
|
|
417
|
+
[status, toolCount, JSON.stringify(tools.map((t) => ({ name: t.name, description: t.description }))), serverId]
|
|
418
|
+
);
|
|
419
|
+
} catch {
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
export {
|
|
424
|
+
McpProcessManager
|
|
425
|
+
};
|