@agenticmail/enterprise 0.5.250 → 0.5.252
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/cli-agent-4I4UUABZ.js +1735 -0
- package/dist/cli.js +1 -1
- package/dist/dashboard/pages/skill-connections.js +6 -6
- package/dist/mcp-process-manager-PPCP4RPZ.js +424 -0
- package/package.json +1 -1
- package/src/dashboard/pages/skill-connections.js +6 -6
- package/src/engine/mcp-process-manager.ts +6 -7
package/dist/cli.js
CHANGED
|
@@ -56,7 +56,7 @@ Skill Development:
|
|
|
56
56
|
import("./cli-serve-GBITKZXM.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-4I4UUABZ.js").then((m) => m.runAgent(args.slice(1))).catch(fatal);
|
|
60
60
|
break;
|
|
61
61
|
case "setup":
|
|
62
62
|
default:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h, useState, useEffect, useCallback, Fragment, useApp, engineCall,
|
|
1
|
+
import { h, useState, useEffect, useCallback, Fragment, useApp, engineCall, apiCall } 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';
|
|
@@ -60,7 +60,7 @@ function McpServersSection() {
|
|
|
60
60
|
|
|
61
61
|
useEffect(function() { load(); }, [load]);
|
|
62
62
|
useEffect(function() {
|
|
63
|
-
|
|
63
|
+
apiCall('/agents').then(function(d) { setAgents((d.agents || d || []).filter(function(a) { return a.status !== 'archived'; })); }).catch(function() {});
|
|
64
64
|
}, []);
|
|
65
65
|
|
|
66
66
|
var resetForm = function() {
|
|
@@ -416,8 +416,8 @@ function McpServersSection() {
|
|
|
416
416
|
// Agent assignment
|
|
417
417
|
agents.length > 0 && h('div', { className: 'form-group', style: { marginTop: 16 } },
|
|
418
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.
|
|
420
|
-
h('p', { style: { marginTop: 8 } }, '
|
|
419
|
+
h('p', null, 'Choose which agents can use this MCP server\'s tools. You must select at least one agent — no agent has access until explicitly granted.'),
|
|
420
|
+
h('p', { style: { marginTop: 8 } }, 'This ensures sensitive tools (like database access) are never accidentally exposed to the wrong agent.')
|
|
421
421
|
)),
|
|
422
422
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6 } },
|
|
423
423
|
agents.map(function(a) {
|
|
@@ -441,8 +441,8 @@ function McpServersSection() {
|
|
|
441
441
|
),
|
|
442
442
|
h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } },
|
|
443
443
|
form.assignedAgents && form.assignedAgents.length > 0
|
|
444
|
-
? form.assignedAgents.length + ' agent(s) selected
|
|
445
|
-
: 'No agents selected —
|
|
444
|
+
? form.assignedAgents.length + ' agent(s) selected'
|
|
445
|
+
: 'No agents selected — no agent can use this server yet'
|
|
446
446
|
)
|
|
447
447
|
)
|
|
448
448
|
),
|
|
@@ -0,0 +1,424 @@
|
|
|
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 (!state.config.assignedAgents?.length) continue;
|
|
126
|
+
if (agentId && !state.config.assignedAgents.includes(agentId)) continue;
|
|
127
|
+
if (!agentId) continue;
|
|
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 (!state.config.assignedAgents?.length) continue;
|
|
149
|
+
if (!agentId || !state.config.assignedAgents.includes(agentId)) continue;
|
|
150
|
+
const tool = state.tools.find((t) => t.name === toolName);
|
|
151
|
+
if (!tool) continue;
|
|
152
|
+
if (state.config.type === "stdio") {
|
|
153
|
+
return this.callToolStdio(state, toolName, args);
|
|
154
|
+
} else {
|
|
155
|
+
return this.callToolHttp(state, toolName, args);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return { content: `Tool "${toolName}" not found on any connected MCP server`, isError: true };
|
|
159
|
+
}
|
|
160
|
+
// ─── Stdio Transport ──────────────────────────────────
|
|
161
|
+
async connectStdio(state) {
|
|
162
|
+
const { config } = state;
|
|
163
|
+
if (!config.command) throw new Error("No command specified for stdio MCP server");
|
|
164
|
+
const env = { ...process.env, ...config.env || {} };
|
|
165
|
+
const child = spawn(config.command, config.args || [], {
|
|
166
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
167
|
+
env
|
|
168
|
+
});
|
|
169
|
+
state.process = child;
|
|
170
|
+
state.lastStarted = /* @__PURE__ */ new Date();
|
|
171
|
+
state.stdoutBuffer = "";
|
|
172
|
+
child.stdout.on("data", (chunk) => {
|
|
173
|
+
state.stdoutBuffer += chunk.toString();
|
|
174
|
+
this.processStdoutBuffer(state);
|
|
175
|
+
});
|
|
176
|
+
child.stderr.on("data", (chunk) => {
|
|
177
|
+
const msg = chunk.toString().trim();
|
|
178
|
+
if (msg) console.log(`[mcp:${config.name}:stderr] ${msg.slice(0, 200)}`);
|
|
179
|
+
});
|
|
180
|
+
child.on("exit", (code) => {
|
|
181
|
+
if (state.status === "connected" && config.autoRestart !== false && this.started) {
|
|
182
|
+
console.warn(`[mcp-manager] ${config.name} exited with code ${code} \u2014 restarting...`);
|
|
183
|
+
this.scheduleRestart(state);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
child.on("error", (err) => {
|
|
187
|
+
state.status = "error";
|
|
188
|
+
state.error = err.message;
|
|
189
|
+
console.error(`[mcp-manager] ${config.name} process error: ${err.message}`);
|
|
190
|
+
});
|
|
191
|
+
const initResult = await this.sendRpc(state, "initialize", {
|
|
192
|
+
protocolVersion: "2024-11-05",
|
|
193
|
+
capabilities: {},
|
|
194
|
+
clientInfo: { name: "AgenticMail-Enterprise", version: "1.0" }
|
|
195
|
+
});
|
|
196
|
+
if (!initResult?.result) {
|
|
197
|
+
throw new Error(`Initialize failed: ${JSON.stringify(initResult?.error || "no response")}`);
|
|
198
|
+
}
|
|
199
|
+
this.sendNotification(state, "notifications/initialized", {});
|
|
200
|
+
const toolsResult = await this.sendRpc(state, "tools/list", {});
|
|
201
|
+
state.tools = (toolsResult?.result?.tools || []).map((t) => ({
|
|
202
|
+
name: t.name,
|
|
203
|
+
description: t.description,
|
|
204
|
+
inputSchema: t.inputSchema || { type: "object", properties: {} }
|
|
205
|
+
}));
|
|
206
|
+
state.status = "connected";
|
|
207
|
+
state.error = void 0;
|
|
208
|
+
console.log(`[mcp-manager] ${config.name} connected (stdio) \u2014 ${state.tools.length} tools discovered`);
|
|
209
|
+
this.updateDbStatus(config.id, "connected", state.tools.length, state.tools);
|
|
210
|
+
this.emit("server:connected", { serverId: config.id, tools: state.tools });
|
|
211
|
+
}
|
|
212
|
+
async callToolStdio(state, toolName, args) {
|
|
213
|
+
try {
|
|
214
|
+
const result = await this.sendRpc(state, "tools/call", { name: toolName, arguments: args });
|
|
215
|
+
if (result?.error) {
|
|
216
|
+
return { content: result.error.message || JSON.stringify(result.error), isError: true };
|
|
217
|
+
}
|
|
218
|
+
const contents = result?.result?.content || [];
|
|
219
|
+
const textParts = contents.map((c) => c.type === "text" ? c.text : JSON.stringify(c)).join("\n");
|
|
220
|
+
return { content: textParts || "OK", isError: result?.result?.isError };
|
|
221
|
+
} catch (e) {
|
|
222
|
+
return { content: `MCP call failed: ${e.message}`, isError: true };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// ─── HTTP/SSE Transport ────────────────────────────────
|
|
226
|
+
async connectHttp(state) {
|
|
227
|
+
const { config } = state;
|
|
228
|
+
if (!config.url) throw new Error("No URL specified for HTTP/SSE MCP server");
|
|
229
|
+
const headers = {
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
...config.headers || {}
|
|
232
|
+
};
|
|
233
|
+
if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
234
|
+
const timeout = (config.timeout || 30) * 1e3;
|
|
235
|
+
const initResp = await fetch(config.url, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers,
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
jsonrpc: "2.0",
|
|
240
|
+
id: 1,
|
|
241
|
+
method: "initialize",
|
|
242
|
+
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "AgenticMail-Enterprise", version: "1.0" } }
|
|
243
|
+
}),
|
|
244
|
+
signal: AbortSignal.timeout(timeout)
|
|
245
|
+
});
|
|
246
|
+
if (!initResp.ok) throw new Error(`HTTP ${initResp.status}: ${await initResp.text().catch(() => "")}`);
|
|
247
|
+
const initData = await initResp.json();
|
|
248
|
+
if (initData.error) throw new Error(initData.error.message || "Initialize error");
|
|
249
|
+
const toolResp = await fetch(config.url, {
|
|
250
|
+
method: "POST",
|
|
251
|
+
headers,
|
|
252
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} }),
|
|
253
|
+
signal: AbortSignal.timeout(15e3)
|
|
254
|
+
});
|
|
255
|
+
let tools = [];
|
|
256
|
+
if (toolResp.ok) {
|
|
257
|
+
const td = await toolResp.json();
|
|
258
|
+
tools = (td.result?.tools || []).map((t) => ({
|
|
259
|
+
name: t.name,
|
|
260
|
+
description: t.description,
|
|
261
|
+
inputSchema: t.inputSchema || { type: "object", properties: {} }
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
state.tools = tools;
|
|
265
|
+
state.status = "connected";
|
|
266
|
+
state.error = void 0;
|
|
267
|
+
state.lastStarted = /* @__PURE__ */ new Date();
|
|
268
|
+
console.log(`[mcp-manager] ${config.name} connected (${config.type}) \u2014 ${tools.length} tools discovered`);
|
|
269
|
+
this.updateDbStatus(config.id, "connected", tools.length, tools);
|
|
270
|
+
this.emit("server:connected", { serverId: config.id, tools });
|
|
271
|
+
}
|
|
272
|
+
async callToolHttp(state, toolName, args) {
|
|
273
|
+
const { config } = state;
|
|
274
|
+
const headers = {
|
|
275
|
+
"Content-Type": "application/json",
|
|
276
|
+
...config.headers || {}
|
|
277
|
+
};
|
|
278
|
+
if (config.apiKey) headers["Authorization"] = `Bearer ${config.apiKey}`;
|
|
279
|
+
try {
|
|
280
|
+
const resp = await fetch(config.url, {
|
|
281
|
+
method: "POST",
|
|
282
|
+
headers,
|
|
283
|
+
body: JSON.stringify({
|
|
284
|
+
jsonrpc: "2.0",
|
|
285
|
+
id: Date.now(),
|
|
286
|
+
method: "tools/call",
|
|
287
|
+
params: { name: toolName, arguments: args }
|
|
288
|
+
}),
|
|
289
|
+
signal: AbortSignal.timeout((config.timeout || 30) * 1e3)
|
|
290
|
+
});
|
|
291
|
+
if (!resp.ok) return { content: `HTTP ${resp.status}`, isError: true };
|
|
292
|
+
const data = await resp.json();
|
|
293
|
+
if (data.error) return { content: data.error.message || JSON.stringify(data.error), isError: true };
|
|
294
|
+
const contents = data.result?.content || [];
|
|
295
|
+
const textParts = contents.map((c) => c.type === "text" ? c.text : JSON.stringify(c)).join("\n");
|
|
296
|
+
return { content: textParts || "OK", isError: data.result?.isError };
|
|
297
|
+
} catch (e) {
|
|
298
|
+
return { content: `MCP HTTP call failed: ${e.message}`, isError: true };
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
// ─── JSON-RPC Helpers (stdio) ──────────────────────────
|
|
302
|
+
sendRpc(state, method, params) {
|
|
303
|
+
return new Promise((resolve, reject) => {
|
|
304
|
+
if (!state.process?.stdin?.writable) {
|
|
305
|
+
return reject(new Error("Process stdin not writable"));
|
|
306
|
+
}
|
|
307
|
+
const id = ++state.rpcId;
|
|
308
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", id, method, params });
|
|
309
|
+
const timer = setTimeout(() => {
|
|
310
|
+
state.pendingRpc.delete(id);
|
|
311
|
+
reject(new Error(`RPC timeout for ${method} after ${this.discoveryTimeoutMs}ms`));
|
|
312
|
+
}, this.discoveryTimeoutMs);
|
|
313
|
+
state.pendingRpc.set(id, { resolve, reject, timer });
|
|
314
|
+
try {
|
|
315
|
+
state.process.stdin.write(msg + "\n");
|
|
316
|
+
} catch (e) {
|
|
317
|
+
state.pendingRpc.delete(id);
|
|
318
|
+
clearTimeout(timer);
|
|
319
|
+
reject(e);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
sendNotification(state, method, params) {
|
|
324
|
+
if (!state.process?.stdin?.writable) return;
|
|
325
|
+
try {
|
|
326
|
+
const msg = JSON.stringify({ jsonrpc: "2.0", method, params });
|
|
327
|
+
state.process.stdin.write(msg + "\n");
|
|
328
|
+
} catch {
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
processStdoutBuffer(state) {
|
|
332
|
+
const lines = state.stdoutBuffer.split("\n");
|
|
333
|
+
state.stdoutBuffer = lines.pop() || "";
|
|
334
|
+
for (const line of lines) {
|
|
335
|
+
const trimmed = line.trim();
|
|
336
|
+
if (!trimmed) continue;
|
|
337
|
+
try {
|
|
338
|
+
const parsed = JSON.parse(trimmed);
|
|
339
|
+
if (parsed.id !== void 0 && state.pendingRpc.has(parsed.id)) {
|
|
340
|
+
const pending = state.pendingRpc.get(parsed.id);
|
|
341
|
+
state.pendingRpc.delete(parsed.id);
|
|
342
|
+
clearTimeout(pending.timer);
|
|
343
|
+
pending.resolve(parsed);
|
|
344
|
+
} else if (!parsed.id && parsed.method) {
|
|
345
|
+
this.emit("server:notification", {
|
|
346
|
+
serverId: state.config.id,
|
|
347
|
+
method: parsed.method,
|
|
348
|
+
params: parsed.params
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
// ─── Restart / Health ──────────────────────────────────
|
|
356
|
+
scheduleRestart(state) {
|
|
357
|
+
if (state.restartCount >= this.maxRestarts) {
|
|
358
|
+
state.status = "error";
|
|
359
|
+
state.error = `Max restarts (${this.maxRestarts}) exceeded`;
|
|
360
|
+
console.error(`[mcp-manager] ${state.config.name} exceeded max restarts`);
|
|
361
|
+
this.updateDbStatus(state.config.id, "error", 0, []);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
state.restartCount++;
|
|
365
|
+
const delay = this.restartDelayMs * state.restartCount;
|
|
366
|
+
setTimeout(async () => {
|
|
367
|
+
if (!this.started) return;
|
|
368
|
+
console.log(`[mcp-manager] Restarting ${state.config.name} (attempt ${state.restartCount})...`);
|
|
369
|
+
try {
|
|
370
|
+
await this.connectServer(state.config);
|
|
371
|
+
} catch (e) {
|
|
372
|
+
console.error(`[mcp-manager] Restart failed: ${e.message}`);
|
|
373
|
+
}
|
|
374
|
+
}, delay);
|
|
375
|
+
}
|
|
376
|
+
healthCheck() {
|
|
377
|
+
for (const [_id, state] of Array.from(this.servers)) {
|
|
378
|
+
if (state.status === "connected" && state.config.type === "stdio") {
|
|
379
|
+
if (state.process && state.process.exitCode !== null) {
|
|
380
|
+
console.warn(`[mcp-manager] ${state.config.name} process died (exit ${state.process.exitCode})`);
|
|
381
|
+
if (state.config.autoRestart !== false) {
|
|
382
|
+
this.scheduleRestart(state);
|
|
383
|
+
} else {
|
|
384
|
+
state.status = "error";
|
|
385
|
+
state.error = "Process exited";
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
killProcess(state) {
|
|
392
|
+
state.status = "stopped";
|
|
393
|
+
for (const [_id, pending] of Array.from(state.pendingRpc)) {
|
|
394
|
+
clearTimeout(pending.timer);
|
|
395
|
+
pending.reject(new Error("Server stopped"));
|
|
396
|
+
}
|
|
397
|
+
state.pendingRpc.clear();
|
|
398
|
+
if (state.process) {
|
|
399
|
+
try {
|
|
400
|
+
state.process.kill("SIGTERM");
|
|
401
|
+
} catch {
|
|
402
|
+
}
|
|
403
|
+
setTimeout(() => {
|
|
404
|
+
try {
|
|
405
|
+
state.process?.kill("SIGKILL");
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
}, 3e3);
|
|
409
|
+
state.process = void 0;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
async updateDbStatus(serverId, status, toolCount, tools) {
|
|
413
|
+
try {
|
|
414
|
+
await this.db.exec(
|
|
415
|
+
`UPDATE mcp_servers SET status = $1, tool_count = $2, tools = $3, updated_at = NOW() WHERE id = $4`,
|
|
416
|
+
[status, toolCount, JSON.stringify(tools.map((t) => ({ name: t.name, description: t.description }))), serverId]
|
|
417
|
+
);
|
|
418
|
+
} catch {
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
export {
|
|
423
|
+
McpProcessManager
|
|
424
|
+
};
|
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { h, useState, useEffect, useCallback, Fragment, useApp, engineCall,
|
|
1
|
+
import { h, useState, useEffect, useCallback, Fragment, useApp, engineCall, apiCall } 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';
|
|
@@ -60,7 +60,7 @@ function McpServersSection() {
|
|
|
60
60
|
|
|
61
61
|
useEffect(function() { load(); }, [load]);
|
|
62
62
|
useEffect(function() {
|
|
63
|
-
|
|
63
|
+
apiCall('/agents').then(function(d) { setAgents((d.agents || d || []).filter(function(a) { return a.status !== 'archived'; })); }).catch(function() {});
|
|
64
64
|
}, []);
|
|
65
65
|
|
|
66
66
|
var resetForm = function() {
|
|
@@ -416,8 +416,8 @@ function McpServersSection() {
|
|
|
416
416
|
// Agent assignment
|
|
417
417
|
agents.length > 0 && h('div', { className: 'form-group', style: { marginTop: 16 } },
|
|
418
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.
|
|
420
|
-
h('p', { style: { marginTop: 8 } }, '
|
|
419
|
+
h('p', null, 'Choose which agents can use this MCP server\'s tools. You must select at least one agent — no agent has access until explicitly granted.'),
|
|
420
|
+
h('p', { style: { marginTop: 8 } }, 'This ensures sensitive tools (like database access) are never accidentally exposed to the wrong agent.')
|
|
421
421
|
)),
|
|
422
422
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6 } },
|
|
423
423
|
agents.map(function(a) {
|
|
@@ -441,8 +441,8 @@ function McpServersSection() {
|
|
|
441
441
|
),
|
|
442
442
|
h('div', { style: { fontSize: 11, color: 'var(--text-muted)', marginTop: 4 } },
|
|
443
443
|
form.assignedAgents && form.assignedAgents.length > 0
|
|
444
|
-
? form.assignedAgents.length + ' agent(s) selected
|
|
445
|
-
: 'No agents selected —
|
|
444
|
+
? form.assignedAgents.length + ' agent(s) selected'
|
|
445
|
+
: 'No agents selected — no agent can use this server yet'
|
|
446
446
|
)
|
|
447
447
|
)
|
|
448
448
|
),
|
|
@@ -213,10 +213,10 @@ export class McpProcessManager extends EventEmitter {
|
|
|
213
213
|
for (const [id, state] of Array.from(this.servers)) {
|
|
214
214
|
if (state.status !== 'connected') continue;
|
|
215
215
|
|
|
216
|
-
// Check agent assignment
|
|
217
|
-
if (
|
|
218
|
-
|
|
219
|
-
|
|
216
|
+
// Check agent assignment — empty/missing means NO agents have access
|
|
217
|
+
if (!state.config.assignedAgents?.length) continue;
|
|
218
|
+
if (agentId && !state.config.assignedAgents.includes(agentId)) continue;
|
|
219
|
+
if (!agentId) continue; // anonymous callers get nothing
|
|
220
220
|
|
|
221
221
|
for (const tool of state.tools) {
|
|
222
222
|
tools.push({ ...tool, serverId: id, serverName: state.config.name });
|
|
@@ -241,9 +241,8 @@ export class McpProcessManager extends EventEmitter {
|
|
|
241
241
|
// Find which server owns this tool
|
|
242
242
|
for (const [_id, state] of Array.from(this.servers)) {
|
|
243
243
|
if (state.status !== 'connected') continue;
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
}
|
|
244
|
+
if (!state.config.assignedAgents?.length) continue;
|
|
245
|
+
if (!agentId || !state.config.assignedAgents.includes(agentId)) continue;
|
|
247
246
|
|
|
248
247
|
const tool = state.tools.find(t => t.name === toolName);
|
|
249
248
|
if (!tool) continue;
|