@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.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-BB4GKQAM.js").then((m) => m.runAgent(args.slice(1))).catch(fatal);
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, adminCall } from '../components/utils.js';
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
- adminCall('/agents').then(function(d) { setAgents((d.agents || d || []).filter(function(a) { return a.status !== 'archived'; })); }).catch(function() {});
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. 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.')
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 — only they can use this server'
445
- : 'No agents selected — all agents can use this server'
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@agenticmail/enterprise",
3
- "version": "0.5.250",
3
+ "version": "0.5.252",
4
4
  "description": "AgenticMail Enterprise — cloud-hosted AI agent identity, email, auth & compliance for organizations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,4 +1,4 @@
1
- import { h, useState, useEffect, useCallback, Fragment, useApp, engineCall, adminCall } from '../components/utils.js';
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
- adminCall('/agents').then(function(d) { setAgents((d.agents || d || []).filter(function(a) { return a.status !== 'archived'; })); }).catch(function() {});
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. 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.')
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 — only they can use this server'
445
- : 'No agents selected — all agents can use this server'
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 (agentId && state.config.assignedAgents?.length) {
218
- if (!state.config.assignedAgents.includes(agentId)) continue;
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 (agentId && state.config.assignedAgents?.length) {
245
- if (!state.config.assignedAgents.includes(agentId)) continue;
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;