@agenticmail/enterprise 0.5.247 → 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.
@@ -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
+ };
@@ -0,0 +1,61 @@
1
+ import {
2
+ errorResult,
3
+ jsonResult
4
+ } from "./chunk-ZB3VC2MR.js";
5
+ import "./chunk-KFQGP6VL.js";
6
+
7
+ // src/agent-tools/tools/mcp-server-tools.ts
8
+ function createMcpServerTools(config) {
9
+ const { mcpManager, agentId } = config;
10
+ const discoveredTools = mcpManager.getToolsForAgent(agentId);
11
+ if (discoveredTools.length === 0) return [];
12
+ console.log(`[mcp-server-tools] Creating ${discoveredTools.length} tools from MCP servers for agent ${agentId || "all"}`);
13
+ if (config.permissionEngine && discoveredTools.length > 0) {
14
+ try {
15
+ const toolDefs = discoveredTools.map((t) => ({
16
+ id: `mcp_${t.name}`,
17
+ name: `mcp_${t.name}`,
18
+ description: t.description || t.name,
19
+ category: "utility",
20
+ risk: "medium",
21
+ skillId: "mcp-servers",
22
+ sideEffects: ["external_api"]
23
+ }));
24
+ config.permissionEngine.registerDynamicTools("mcp-servers", toolDefs);
25
+ } catch (e) {
26
+ console.warn(`[mcp-server-tools] Permission engine registration failed: ${e.message}`);
27
+ }
28
+ }
29
+ return discoveredTools.map((tool) => {
30
+ const toolName = `mcp_${tool.name}`;
31
+ return {
32
+ name: toolName,
33
+ description: `[${tool.serverName}] ${tool.description || tool.name}`,
34
+ category: "utility",
35
+ parameters: {
36
+ type: "object",
37
+ properties: tool.inputSchema?.properties || {},
38
+ required: tool.inputSchema?.required || []
39
+ },
40
+ async execute(_callId, params) {
41
+ try {
42
+ const result = await mcpManager.callTool(tool.name, params, agentId);
43
+ if (result.isError) {
44
+ return errorResult(result.content);
45
+ }
46
+ try {
47
+ const parsed = JSON.parse(result.content);
48
+ return jsonResult(parsed);
49
+ } catch {
50
+ return { content: [{ type: "text", text: result.content }] };
51
+ }
52
+ } catch (e) {
53
+ return errorResult(`MCP tool ${tool.name} error: ${e.message}`);
54
+ }
55
+ }
56
+ };
57
+ });
58
+ }
59
+ export {
60
+ createMcpServerTools
61
+ };