mixdog 0.9.105 → 0.9.106
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/package.json +1 -1
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +3 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +145 -46
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +16 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -4
- package/src/session-runtime/lifecycle-api.mjs +8 -4
- package/src/session-runtime/mcp-glue.mjs +10 -8
- package/src/session-runtime/runtime-core.mjs +6 -0
- package/src/session-runtime/session-lifecycle.mjs +3 -1
- package/src/session-runtime/session-turn-api.mjs +2 -2
- package/src/session-runtime/tool-catalog.mjs +3 -3
- package/src/session-runtime/tool-surface.mjs +3 -2
- package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
- package/src/standalone/agent-tool.mjs +2 -0
package/package.json
CHANGED
|
@@ -64,6 +64,7 @@ function normalizeAgentCompactionConfig(value = {}, { memoryEnabled = true } = {
|
|
|
64
64
|
* @param {number} [opts.maxLoopIterations]
|
|
65
65
|
* @param {string} [opts.parentSessionId]
|
|
66
66
|
* @param {string|null} [opts.ownerSessionId] - owning Mixdog MCP instance id for statusline isolation
|
|
67
|
+
* @param {string|null} [opts.mcpScopeId] - owning session-runtime MCP registry scope
|
|
67
68
|
* @returns {{ session: object, effectiveCwd: string|null }}
|
|
68
69
|
*/
|
|
69
70
|
export function prepareAgentSession({
|
|
@@ -86,6 +87,7 @@ export function prepareAgentSession({
|
|
|
86
87
|
cacheKeyOverride,
|
|
87
88
|
schemaAllowedTools,
|
|
88
89
|
sessionId,
|
|
90
|
+
mcpScopeId,
|
|
89
91
|
}) {
|
|
90
92
|
const effectivePermission = resolveAgentSessionPermission(agent, permission);
|
|
91
93
|
// No per-agent loop caps: sessions either pin maxLoopIterations explicitly
|
|
@@ -121,6 +123,7 @@ export function prepareAgentSession({
|
|
|
121
123
|
ownerSessionId: effectiveOwnerSessionId || null,
|
|
122
124
|
clientHostPid: clientHostPid || null,
|
|
123
125
|
compaction: compaction || undefined,
|
|
126
|
+
mcpScopeId: mcpScopeId || null,
|
|
124
127
|
};
|
|
125
128
|
if (agentTag) sessionOpts.agentTag = agentTag;
|
|
126
129
|
if (effectivePermission) sessionOpts.permission = effectivePermission;
|
|
@@ -28,6 +28,10 @@ const DEFAULT_MCP_STARTUP_TIMEOUT_MS = 10000;
|
|
|
28
28
|
const servers = new Map();
|
|
29
29
|
const reconnects = createKeyedSingleflight();
|
|
30
30
|
const callAdmissions = new Map();
|
|
31
|
+
const DEFAULT_MCP_SCOPE_ID = 'global';
|
|
32
|
+
const _knownMcpScopes = new Set([DEFAULT_MCP_SCOPE_ID]);
|
|
33
|
+
const _connectAbortGenerations = new Map();
|
|
34
|
+
const _pendingConnects = new Set();
|
|
31
35
|
let mcpSdkPromise = null;
|
|
32
36
|
// Memo for mcpToolHasField(name, field) — keyed by `${toolName}|${field}`.
|
|
33
37
|
// The lookup (regex parse + servers Map get + tools.find + schema property
|
|
@@ -39,6 +43,30 @@ const _mcpToolFieldMemo = new Map();
|
|
|
39
43
|
function _invalidateMcpToolFieldMemo() {
|
|
40
44
|
_mcpToolFieldMemo.clear();
|
|
41
45
|
}
|
|
46
|
+
function normalizeMcpScopeId(value) {
|
|
47
|
+
const raw = value && typeof value === 'object' ? value.scopeId : value;
|
|
48
|
+
const scopeId = String(raw || '').trim();
|
|
49
|
+
return scopeId || DEFAULT_MCP_SCOPE_ID;
|
|
50
|
+
}
|
|
51
|
+
function mcpServerRegistryKey(scopeId, name) {
|
|
52
|
+
return `${normalizeMcpScopeId(scopeId)}\u0000${String(name || '')}`;
|
|
53
|
+
}
|
|
54
|
+
function scopedServer(scopeId, name) {
|
|
55
|
+
return servers.get(mcpServerRegistryKey(scopeId, name));
|
|
56
|
+
}
|
|
57
|
+
function scopedServerEntries(scopeId) {
|
|
58
|
+
const normalized = normalizeMcpScopeId(scopeId);
|
|
59
|
+
return [...servers.entries()].filter(([, server]) => server?.scopeId === normalized);
|
|
60
|
+
}
|
|
61
|
+
function currentConnectAbortGeneration(scopeId) {
|
|
62
|
+
return _connectAbortGenerations.get(normalizeMcpScopeId(scopeId)) || 0;
|
|
63
|
+
}
|
|
64
|
+
function bumpConnectAbortGeneration(scopeId) {
|
|
65
|
+
const normalized = normalizeMcpScopeId(scopeId);
|
|
66
|
+
const next = currentConnectAbortGeneration(normalized) + 1;
|
|
67
|
+
_connectAbortGenerations.set(normalized, next);
|
|
68
|
+
return next;
|
|
69
|
+
}
|
|
42
70
|
function mcpLog(line) {
|
|
43
71
|
if (process.env.MIXDOG_QUIET_MCP_LOG) return;
|
|
44
72
|
process.stderr.write(line);
|
|
@@ -106,12 +134,14 @@ export function resolveMcpTransportKind(cfg) {
|
|
|
106
134
|
* Connect to MCP servers defined in config.
|
|
107
135
|
* Supports stdio (child process) and http (Streamable HTTP) transports.
|
|
108
136
|
*/
|
|
109
|
-
export async function connectMcpServers(config) {
|
|
137
|
+
export async function connectMcpServers(config, options = {}) {
|
|
138
|
+
const scopeId = normalizeMcpScopeId(options);
|
|
139
|
+
_knownMcpScopes.add(scopeId);
|
|
110
140
|
// Capture the abort generation SYNCHRONOUSLY at entry: the boot path fires
|
|
111
141
|
// this un-awaited, so a runtime close can land while connectServer is still
|
|
112
142
|
// loading the SDK. A capture taken any later would already see the bumped
|
|
113
143
|
// generation and register the server anyway (leaking its stdio child).
|
|
114
|
-
const genAtStart =
|
|
144
|
+
const genAtStart = currentConnectAbortGeneration(scopeId);
|
|
115
145
|
const failures = [];
|
|
116
146
|
const entries = Object.entries(config).filter(([name, cfg]) => {
|
|
117
147
|
if (cfg?.enabled === false) {
|
|
@@ -123,7 +153,7 @@ export async function connectMcpServers(config) {
|
|
|
123
153
|
// Connect all servers in PARALLEL: a slow/hung server (bounded by its
|
|
124
154
|
// per-server startup timeout) must never delay the others' handshakes.
|
|
125
155
|
const settled = await Promise.allSettled(
|
|
126
|
-
entries.map(([name, cfg]) => connectServer(name, cfg, genAtStart)),
|
|
156
|
+
entries.map(([name, cfg]) => connectServer(name, cfg, scopeId, genAtStart)),
|
|
127
157
|
);
|
|
128
158
|
settled.forEach((res, i) => {
|
|
129
159
|
if (res.status !== 'rejected') return;
|
|
@@ -143,15 +173,15 @@ export async function connectMcpServers(config) {
|
|
|
143
173
|
* Get all tool definitions from connected MCP servers.
|
|
144
174
|
* Tool names are prefixed: `mcp__{serverName}__{toolName}`
|
|
145
175
|
*/
|
|
146
|
-
export function getMcpTools() {
|
|
176
|
+
export function getMcpTools(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
147
177
|
const tools = [];
|
|
148
|
-
for (const server of
|
|
178
|
+
for (const [, server] of scopedServerEntries(scopeId)) {
|
|
149
179
|
tools.push(...server.tools);
|
|
150
180
|
}
|
|
151
181
|
return tools;
|
|
152
182
|
}
|
|
153
|
-
export function getMcpServerStatus() {
|
|
154
|
-
return
|
|
183
|
+
export function getMcpServerStatus(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
184
|
+
return scopedServerEntries(scopeId).map(([, server]) => ({
|
|
155
185
|
name: server.name,
|
|
156
186
|
connected: true,
|
|
157
187
|
toolCount: Array.isArray(server.tools) ? server.tools.length : 0,
|
|
@@ -173,7 +203,8 @@ function positiveInt(value, fallback) {
|
|
|
173
203
|
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
174
204
|
}
|
|
175
205
|
function callAdmissionFor(server) {
|
|
176
|
-
|
|
206
|
+
const registryKey = server.registryKey || mcpServerRegistryKey(server.scopeId, server.name);
|
|
207
|
+
let gate = callAdmissions.get(registryKey);
|
|
177
208
|
if (gate) return gate;
|
|
178
209
|
const cfg = server?.cfg || {};
|
|
179
210
|
gate = createOwnerFairGate({
|
|
@@ -192,44 +223,53 @@ function callAdmissionFor(server) {
|
|
|
192
223
|
30_000,
|
|
193
224
|
),
|
|
194
225
|
});
|
|
195
|
-
|
|
226
|
+
gate.mcpServerName = server.name;
|
|
227
|
+
gate.mcpScopeId = server.scopeId;
|
|
228
|
+
callAdmissions.set(registryKey, gate);
|
|
196
229
|
return gate;
|
|
197
230
|
}
|
|
198
|
-
function closeCallAdmission(name, reason) {
|
|
199
|
-
const
|
|
231
|
+
function closeCallAdmission(scopeId, name, reason) {
|
|
232
|
+
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
233
|
+
const gate = callAdmissions.get(registryKey);
|
|
200
234
|
if (!gate) return;
|
|
201
|
-
callAdmissions.delete(
|
|
235
|
+
callAdmissions.delete(registryKey);
|
|
202
236
|
gate.close(reason || `MCP ${name} disconnected`);
|
|
203
237
|
}
|
|
204
|
-
export function getMcpAdmissionSnapshot() {
|
|
205
|
-
|
|
206
|
-
|
|
238
|
+
export function getMcpAdmissionSnapshot(options = undefined) {
|
|
239
|
+
const scoped = options !== undefined;
|
|
240
|
+
const scopeId = scoped ? normalizeMcpScopeId(options) : null;
|
|
241
|
+
return [...callAdmissions.values()]
|
|
242
|
+
.filter((gate) => !scoped || gate.mcpScopeId === scopeId)
|
|
243
|
+
.map((gate) => ({
|
|
244
|
+
name: gate.mcpServerName,
|
|
245
|
+
scopeId: gate.mcpScopeId,
|
|
207
246
|
...gate.snapshot(),
|
|
208
247
|
}));
|
|
209
248
|
}
|
|
210
249
|
|
|
211
250
|
/** Snapshot of MCP initialize `instructions` per connected server (handshake time). */
|
|
212
|
-
export function getMcpServerInstructionsMap() {
|
|
251
|
+
export function getMcpServerInstructionsMap(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
213
252
|
const out = {};
|
|
214
|
-
for (const server of
|
|
253
|
+
for (const [, server] of scopedServerEntries(scopeId)) {
|
|
215
254
|
const text = typeof server.instructions === 'string' ? server.instructions.trim() : '';
|
|
216
255
|
if (text) out[server.name] = text;
|
|
217
256
|
}
|
|
218
257
|
return out;
|
|
219
258
|
}
|
|
220
259
|
|
|
221
|
-
async function reconnectMcpServer(serverName, failedServer) {
|
|
222
|
-
|
|
223
|
-
|
|
260
|
+
async function reconnectMcpServer(scopeId, serverName, failedServer) {
|
|
261
|
+
const registryKey = mcpServerRegistryKey(scopeId, serverName);
|
|
262
|
+
return reconnects.run(registryKey, async () => {
|
|
263
|
+
const current = servers.get(registryKey);
|
|
224
264
|
// A peer already completed the replacement while this failed call was
|
|
225
265
|
// unwinding. Reuse it immediately instead of closing a fresh transport.
|
|
226
266
|
if (current && current !== failedServer) return current;
|
|
227
267
|
if (current) {
|
|
228
268
|
await _closeServer(current);
|
|
229
|
-
if (servers.get(
|
|
269
|
+
if (servers.get(registryKey) === current) servers.delete(registryKey);
|
|
230
270
|
}
|
|
231
|
-
await connectServer(serverName, failedServer.cfg);
|
|
232
|
-
const replacement = servers.get(
|
|
271
|
+
await connectServer(serverName, failedServer.cfg, scopeId);
|
|
272
|
+
const replacement = servers.get(registryKey);
|
|
233
273
|
if (!replacement) {
|
|
234
274
|
throw new Error(`reconnect succeeded but server "${serverName}" entry is missing from registry`);
|
|
235
275
|
}
|
|
@@ -247,7 +287,8 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
247
287
|
if (!match)
|
|
248
288
|
throw new Error(`Not an MCP tool name: ${name}`);
|
|
249
289
|
const [, serverName, toolName] = match;
|
|
250
|
-
const
|
|
290
|
+
const scopeId = normalizeMcpScopeId(options);
|
|
291
|
+
const server = scopedServer(scopeId, serverName);
|
|
251
292
|
if (!server)
|
|
252
293
|
throw new Error(`MCP server "${serverName}" not connected`);
|
|
253
294
|
const gate = callAdmissionFor(server);
|
|
@@ -264,7 +305,7 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
264
305
|
mcpLog(`[mcp-client] Tool call failed, attempting shared reconnect...\n`);
|
|
265
306
|
let retryServer;
|
|
266
307
|
try {
|
|
267
|
-
retryServer = await reconnectMcpServer(serverName, server);
|
|
308
|
+
retryServer = await reconnectMcpServer(scopeId, serverName, server);
|
|
268
309
|
} catch (reconnectErr) {
|
|
269
310
|
const reconnectMsg = reconnectErr instanceof Error ? reconnectErr.message : String(reconnectErr);
|
|
270
311
|
throw new Error(`Tool call failed: ${firstMsg}; reconnect also failed: ${reconnectMsg}`);
|
|
@@ -415,12 +456,12 @@ export function isMcpTool(name) {
|
|
|
415
456
|
return name.startsWith('mcp__');
|
|
416
457
|
}
|
|
417
458
|
/** True when the prefixed name exists on a connected MCP server. */
|
|
418
|
-
export function isRegisteredMcpTool(name) {
|
|
459
|
+
export function isRegisteredMcpTool(name, scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
419
460
|
if (!isMcpTool(name)) return false;
|
|
420
461
|
const match = name.match(/^mcp__(.+?)__(.+)$/);
|
|
421
462
|
if (!match) return false;
|
|
422
463
|
const [, serverName] = match;
|
|
423
|
-
const server =
|
|
464
|
+
const server = scopedServer(scopeId, serverName);
|
|
424
465
|
if (!server || !Array.isArray(server.tools)) return false;
|
|
425
466
|
return server.tools.some((t) => t?.name === name);
|
|
426
467
|
}
|
|
@@ -430,14 +471,15 @@ export function isRegisteredMcpTool(name) {
|
|
|
430
471
|
* (e.g. cwd) into the args before dispatch — schemas that don't declare the
|
|
431
472
|
* field would reject the unknown argument.
|
|
432
473
|
*/
|
|
433
|
-
export function mcpToolHasField(name, field) {
|
|
434
|
-
const
|
|
474
|
+
export function mcpToolHasField(name, field, scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
475
|
+
const normalizedScopeId = normalizeMcpScopeId(scopeId);
|
|
476
|
+
const memoKey = `${normalizedScopeId}|${name}|${field}`;
|
|
435
477
|
const memoized = _mcpToolFieldMemo.get(memoKey);
|
|
436
478
|
if (memoized !== undefined) return memoized;
|
|
437
479
|
const match = name.match(/^mcp__(.+?)__(.+)$/);
|
|
438
480
|
if (!match) { _mcpToolFieldMemo.set(memoKey, false); return false; }
|
|
439
481
|
const [, serverName] = match;
|
|
440
|
-
const server =
|
|
482
|
+
const server = scopedServer(normalizedScopeId, serverName);
|
|
441
483
|
if (!server) { _mcpToolFieldMemo.set(memoKey, false); return false; }
|
|
442
484
|
const tool = server.tools.find((t) => t.name === name);
|
|
443
485
|
if (!tool) { _mcpToolFieldMemo.set(memoKey, false); return false; }
|
|
@@ -449,14 +491,24 @@ export function mcpToolHasField(name, field) {
|
|
|
449
491
|
/**
|
|
450
492
|
* Disconnect all MCP servers.
|
|
451
493
|
*/
|
|
452
|
-
export async function disconnectAll() {
|
|
494
|
+
export async function disconnectAll(options = undefined) {
|
|
495
|
+
const hasExplicitScope = options && typeof options === 'object'
|
|
496
|
+
&& Object.prototype.hasOwnProperty.call(options, 'scopeId');
|
|
497
|
+
const scopes = hasExplicitScope
|
|
498
|
+
? new Set([normalizeMcpScopeId(options)])
|
|
499
|
+
: new Set([
|
|
500
|
+
..._knownMcpScopes,
|
|
501
|
+
...[...servers.values()].map((server) => server.scopeId),
|
|
502
|
+
...[..._pendingConnects].map((entry) => entry.scopeId),
|
|
503
|
+
]);
|
|
453
504
|
// Abort handshakes still in flight: bump the generation so a connect that
|
|
454
505
|
// completes after this point tears itself down instead of registering, and
|
|
455
506
|
// reap any already-spawned stdio child now so its ref'd ChildProcess handle
|
|
456
507
|
// can't keep the event loop alive (close-during-connect previously leaked
|
|
457
508
|
// the uvx/npx wrapper tree and hung process exit).
|
|
458
|
-
|
|
509
|
+
for (const scopeId of scopes) bumpConnectAbortGeneration(scopeId);
|
|
459
510
|
for (const entry of [..._pendingConnects]) {
|
|
511
|
+
if (!scopes.has(entry.scopeId)) continue;
|
|
460
512
|
_pendingConnects.delete(entry);
|
|
461
513
|
// Mid-handshake child: nothing to shut down gracefully — hard-kill the
|
|
462
514
|
// tree without holding the event loop (this path runs during process
|
|
@@ -466,13 +518,14 @@ export async function disconnectAll() {
|
|
|
466
518
|
try { void entry.client.close().catch(() => { /* ignore */ }); }
|
|
467
519
|
catch { /* ignore */ }
|
|
468
520
|
}
|
|
469
|
-
for (const [
|
|
521
|
+
for (const [registryKey, server] of [...servers]) {
|
|
522
|
+
if (!scopes.has(server.scopeId)) continue;
|
|
470
523
|
try {
|
|
471
524
|
await _closeServer(server);
|
|
472
525
|
}
|
|
473
526
|
catch { /* ignore */ }
|
|
474
|
-
servers.delete(
|
|
475
|
-
closeCallAdmission(name, `MCP ${name} disconnected`);
|
|
527
|
+
servers.delete(registryKey);
|
|
528
|
+
closeCallAdmission(server.scopeId, server.name, `MCP ${server.name} disconnected`);
|
|
476
529
|
}
|
|
477
530
|
_invalidateMcpToolFieldMemo();
|
|
478
531
|
}
|
|
@@ -482,15 +535,17 @@ export async function disconnectAll() {
|
|
|
482
535
|
* it, and invalidates the tool-field memo. Lets callers toggle one server
|
|
483
536
|
* without a full disconnectAll()/reconnect cycle.
|
|
484
537
|
*/
|
|
485
|
-
export async function disconnectMcpServer(name) {
|
|
486
|
-
const
|
|
538
|
+
export async function disconnectMcpServer(name, options = {}) {
|
|
539
|
+
const scopeId = normalizeMcpScopeId(options);
|
|
540
|
+
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
541
|
+
const server = servers.get(registryKey);
|
|
487
542
|
if (!server) return false;
|
|
488
543
|
try {
|
|
489
544
|
await _closeServer(server);
|
|
490
545
|
}
|
|
491
546
|
catch { /* ignore */ }
|
|
492
|
-
servers.delete(
|
|
493
|
-
closeCallAdmission(name, `MCP ${name} disconnected`);
|
|
547
|
+
servers.delete(registryKey);
|
|
548
|
+
closeCallAdmission(scopeId, name, `MCP ${name} disconnected`);
|
|
494
549
|
_invalidateMcpToolFieldMemo();
|
|
495
550
|
return true;
|
|
496
551
|
}
|
|
@@ -515,9 +570,9 @@ async function _closeServer(server) {
|
|
|
515
570
|
// to see (and tear down) their transports, because `servers` only lists fully
|
|
516
571
|
// handshaken entries. Generation token aborts a connect that outlives a
|
|
517
572
|
// disconnectAll() issued mid-handshake (runtime close during boot connect).
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
573
|
+
async function connectServer(name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID, genAtStart = currentConnectAbortGeneration(scopeId)) {
|
|
574
|
+
scopeId = normalizeMcpScopeId(scopeId);
|
|
575
|
+
_knownMcpScopes.add(scopeId);
|
|
521
576
|
const {
|
|
522
577
|
Client,
|
|
523
578
|
StdioClientTransport,
|
|
@@ -525,7 +580,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
|
|
|
525
580
|
SSEClientTransport,
|
|
526
581
|
WebSocketClientTransport,
|
|
527
582
|
} = await loadMcpSdk();
|
|
528
|
-
if (genAtStart !==
|
|
583
|
+
if (genAtStart !== currentConnectAbortGeneration(scopeId)) {
|
|
529
584
|
// disconnectAll() ran while the SDK was loading: nothing spawned yet —
|
|
530
585
|
// abort before creating a transport/child at all.
|
|
531
586
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
@@ -623,7 +678,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
|
|
|
623
678
|
else {
|
|
624
679
|
throw new Error(`Invalid config for "${name}": need autoDetect, type (stdio/http/sse/ws), url (http), or command (stdio)`);
|
|
625
680
|
}
|
|
626
|
-
const pending = { name, client, transport };
|
|
681
|
+
const pending = { scopeId, name, client, transport };
|
|
627
682
|
_pendingConnects.add(pending);
|
|
628
683
|
try {
|
|
629
684
|
// Bound the connect + listTools handshake so a slow/hung server can't
|
|
@@ -682,7 +737,7 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
|
|
|
682
737
|
if (!toolsResult || !Array.isArray(toolsResult.tools)) {
|
|
683
738
|
throw new Error(`[mcp-client] ListTools returned invalid shape for "${name}": missing or non-array tools field`);
|
|
684
739
|
}
|
|
685
|
-
if (genAtStart !==
|
|
740
|
+
if (genAtStart !== currentConnectAbortGeneration(scopeId)) {
|
|
686
741
|
// disconnectAll() ran mid-handshake: never register — tear down.
|
|
687
742
|
try { await _closeServer({ client, transport }); }
|
|
688
743
|
catch { /* ignore */ }
|
|
@@ -695,7 +750,18 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
|
|
|
695
750
|
...(t.annotations && typeof t.annotations === 'object' ? { annotations: t.annotations } : {}),
|
|
696
751
|
}));
|
|
697
752
|
const toolNames = tools.map(t => t.name);
|
|
698
|
-
|
|
753
|
+
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
754
|
+
servers.set(registryKey, {
|
|
755
|
+
scopeId,
|
|
756
|
+
registryKey,
|
|
757
|
+
name,
|
|
758
|
+
client,
|
|
759
|
+
transport,
|
|
760
|
+
tools,
|
|
761
|
+
cfg,
|
|
762
|
+
instructions,
|
|
763
|
+
generation: genAtStart,
|
|
764
|
+
});
|
|
699
765
|
_invalidateMcpToolFieldMemo();
|
|
700
766
|
mcpLog(`[mcp] connected: ${tools.length} tools — ${toolNames.join(', ')}\n`);
|
|
701
767
|
}
|
|
@@ -703,3 +769,36 @@ async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
|
|
|
703
769
|
_pendingConnects.delete(pending);
|
|
704
770
|
}
|
|
705
771
|
}
|
|
772
|
+
|
|
773
|
+
// Test seam for registry scoping without launching an MCP transport.
|
|
774
|
+
export function _registerMcpServerForTest(scopeId, name, rawTools = [], options = {}) {
|
|
775
|
+
const normalizedScopeId = normalizeMcpScopeId(scopeId);
|
|
776
|
+
const registryKey = mcpServerRegistryKey(normalizedScopeId, name);
|
|
777
|
+
const tools = rawTools.map((tool) => ({
|
|
778
|
+
...tool,
|
|
779
|
+
name: String(tool?.name || '').startsWith('mcp__')
|
|
780
|
+
? String(tool.name)
|
|
781
|
+
: `mcp__${name}__${String(tool?.name || '')}`,
|
|
782
|
+
inputSchema: tool?.inputSchema || { type: 'object', properties: {} },
|
|
783
|
+
}));
|
|
784
|
+
const server = {
|
|
785
|
+
scopeId: normalizedScopeId,
|
|
786
|
+
registryKey,
|
|
787
|
+
name,
|
|
788
|
+
tools,
|
|
789
|
+
cfg: options.cfg || {},
|
|
790
|
+
instructions: options.instructions || '',
|
|
791
|
+
transport: null,
|
|
792
|
+
client: {
|
|
793
|
+
callTool: typeof options.callTool === 'function'
|
|
794
|
+
? options.callTool
|
|
795
|
+
: async () => ({ content: [{ type: 'text', text: 'ok' }] }),
|
|
796
|
+
close: async () => {},
|
|
797
|
+
},
|
|
798
|
+
generation: currentConnectAbortGeneration(normalizedScopeId),
|
|
799
|
+
};
|
|
800
|
+
_knownMcpScopes.add(normalizedScopeId);
|
|
801
|
+
servers.set(registryKey, server);
|
|
802
|
+
_invalidateMcpToolFieldMemo();
|
|
803
|
+
return server;
|
|
804
|
+
}
|
|
@@ -178,7 +178,8 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
|
|
|
178
178
|
return viewSkill(cwd, args?.name);
|
|
179
179
|
}
|
|
180
180
|
if (isMcpTool(name)) {
|
|
181
|
-
|
|
181
|
+
const mcpScopeId = sessionRef?.mcpScopeId || null;
|
|
182
|
+
if (!isOnDeferredToolSurface(sessionRef, name) && !isRegisteredMcpTool(name, mcpScopeId)) {
|
|
182
183
|
return formatUnknownBuiltinToolMessage(name, args, 'tool');
|
|
183
184
|
}
|
|
184
185
|
// 24h trace data shows ~24% of external MCP calls are cwd-sensitive
|
|
@@ -187,12 +188,13 @@ async function executeToolOwned(name, args, cwd, callerSessionId, sessionRef, ex
|
|
|
187
188
|
// inputSchema declares the field — schemas without it would reject
|
|
188
189
|
// an unknown argument.
|
|
189
190
|
const needsCwdInjection = cwd
|
|
190
|
-
&& mcpToolHasField(name, 'cwd')
|
|
191
|
+
&& mcpToolHasField(name, 'cwd', mcpScopeId)
|
|
191
192
|
&& (args == null || args.cwd == null);
|
|
192
193
|
const finalArgs = needsCwdInjection ? { ...(args || {}), cwd } : args;
|
|
193
194
|
return executeMcpTool(name, finalArgs, {
|
|
194
195
|
signal: executeOpts.signal || null,
|
|
195
196
|
ownerKey: callerSessionId,
|
|
197
|
+
scopeId: mcpScopeId,
|
|
196
198
|
});
|
|
197
199
|
}
|
|
198
200
|
if (name === 'code_graph') {
|
|
@@ -249,7 +249,10 @@ export function createSession(opts) {
|
|
|
249
249
|
const toolSpec = ownerIsAgent
|
|
250
250
|
? (isReadOnlyAgentBundle ? 'readonly' : 'full')
|
|
251
251
|
: (Array.isArray(profile?.tools) ? profile.tools : toolPreset);
|
|
252
|
-
let toolsForRouting = resolveSessionTools(toolSpec, skills, {
|
|
252
|
+
let toolsForRouting = resolveSessionTools(toolSpec, skills, {
|
|
253
|
+
ownerIsAgentSession: ownerIsAgent,
|
|
254
|
+
mcpScopeId: opts.mcpScopeId || null,
|
|
255
|
+
});
|
|
253
256
|
// Fail-closed permission intersection: when a session declares an explicit
|
|
254
257
|
// object-form permission, intersect the
|
|
255
258
|
// resolved tool list with the permission's allow/deny lists. If the
|
|
@@ -371,6 +374,7 @@ export function createSession(opts) {
|
|
|
371
374
|
agent: opts.agent,
|
|
372
375
|
owner: opts.owner || 'user',
|
|
373
376
|
mcpPid: process.pid,
|
|
377
|
+
mcpScopeId: opts.mcpScopeId || null,
|
|
374
378
|
scopeKey: opts.scopeKey || null,
|
|
375
379
|
lane: opts.lane || 'agent',
|
|
376
380
|
cwd: opts.cwd,
|
|
@@ -530,7 +534,10 @@ function _prepareResumeTools(session, preset) {
|
|
|
530
534
|
if (!ownerIsAgent && Array.isArray(profile?.tools)) toolSpec = profile.tools;
|
|
531
535
|
} catch { /* ignore lookup failures, keep preset fallback */ }
|
|
532
536
|
}
|
|
533
|
-
let toolsForRouting = resolveSessionTools(toolSpec, skills, {
|
|
537
|
+
let toolsForRouting = resolveSessionTools(toolSpec, skills, {
|
|
538
|
+
ownerIsAgentSession: ownerIsAgent,
|
|
539
|
+
mcpScopeId: session.mcpScopeId || null,
|
|
540
|
+
});
|
|
534
541
|
if (ownerIsAgent) {
|
|
535
542
|
toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.agent || null);
|
|
536
543
|
}
|
|
@@ -563,8 +570,10 @@ function _rememberPreparedResume(sessionId, prepared) {
|
|
|
563
570
|
|
|
564
571
|
function _preparedResumeForSession(session, preset) {
|
|
565
572
|
const cached = _preparedResumes.get(session.id);
|
|
566
|
-
if (cached?.session === session && cached?.preset === preset
|
|
573
|
+
if (cached?.session === session && cached?.preset === preset
|
|
574
|
+
&& cached?.mcpScopeId === (session.mcpScopeId || null)) return cached;
|
|
567
575
|
const prepared = _prepareResumeTools(session, preset);
|
|
576
|
+
prepared.mcpScopeId = session.mcpScopeId || null;
|
|
568
577
|
_rememberPreparedResume(session.id, prepared);
|
|
569
578
|
return prepared;
|
|
570
579
|
}
|
|
@@ -745,6 +754,9 @@ export async function resumeSession(sessionId, preset, options = {}) {
|
|
|
745
754
|
session.desktopSession = expectedDesktop;
|
|
746
755
|
}
|
|
747
756
|
if (!session.owner) session.owner = 'user';
|
|
757
|
+
if (Object.prototype.hasOwnProperty.call(options, 'mcpScopeId')) {
|
|
758
|
+
session.mcpScopeId = String(options.mcpScopeId || '').trim() || null;
|
|
759
|
+
}
|
|
748
760
|
if (_isActivelyOwnedElsewhere(session, sessionId)) {
|
|
749
761
|
// ATTACH (viewer mode, zero ownership): hand back the live transcript
|
|
750
762
|
// under the SAME id, flagged remoteAttached. No tool refresh, no save,
|
|
@@ -764,6 +776,7 @@ export async function resumeSession(sessionId, preset, options = {}) {
|
|
|
764
776
|
const cached = _preparedResumes.get(sessionId);
|
|
765
777
|
_preparedResumes.delete(sessionId);
|
|
766
778
|
const prepared = cached?.session === session && cached?.preset === preset
|
|
779
|
+
&& cached?.mcpScopeId === (session.mcpScopeId || null)
|
|
767
780
|
? cached
|
|
768
781
|
: _prepareResumeTools(session, preset);
|
|
769
782
|
// Keep the persisted tool mode in sync on resume (see createSession note).
|
|
@@ -18,8 +18,8 @@ import { getHiddenAgent, listHiddenAgentNames } from '../../internal-agents.mjs'
|
|
|
18
18
|
// tools array verbatim, so any reorder rewrites the prefix.
|
|
19
19
|
// No cache: getMcpTools() and getInternalTools() are O(n) in-memory reads;
|
|
20
20
|
// the sort overhead on ~30 tools is negligible.
|
|
21
|
-
function _getMcpTools() {
|
|
22
|
-
const mcp = getMcpTools() || [];
|
|
21
|
+
function _getMcpTools(mcpScopeId = null) {
|
|
22
|
+
const mcp = getMcpTools(mcpScopeId) || [];
|
|
23
23
|
// `public:false` tools stay registered in internal-tools for runtime
|
|
24
24
|
// rewrites/dispatch, but must never enter any model-visible schema (Lead
|
|
25
25
|
// full/mcp included). Filter before mapping because the projection below
|
|
@@ -229,8 +229,8 @@ const ALL_BUILTIN_SESSION_TOOLS = orderSessionTools(_dedupByName([
|
|
|
229
229
|
...CODE_GRAPH_TOOL_DEFS,
|
|
230
230
|
]));
|
|
231
231
|
|
|
232
|
-
export function resolveSessionTools(toolSpec, skills, { ownerIsAgentSession = false } = {}) {
|
|
233
|
-
const mcp = _getMcpTools();
|
|
232
|
+
export function resolveSessionTools(toolSpec, skills, { ownerIsAgentSession = false, mcpScopeId = null } = {}) {
|
|
233
|
+
const mcp = _getMcpTools(mcpScopeId);
|
|
234
234
|
// Agent sessions freeze the skill meta-tool into the schema
|
|
235
235
|
// unconditionally — concrete skill resolution is cwd-scoped at tool-call
|
|
236
236
|
// time (loop.mjs), so the schema bytes stay bit-identical across roles /
|
|
@@ -40,6 +40,7 @@ export function createLifecycleApi(deps) {
|
|
|
40
40
|
const cancelBackgroundTasksForLifecycle = deps.cancelBackgroundTasks || cancelBackgroundTasks;
|
|
41
41
|
const {
|
|
42
42
|
getSession, setSession, getRoute, setRoute, getConfig, getMode, getCurrentCwd,
|
|
43
|
+
getMcpScopeId,
|
|
43
44
|
getDesktopSession, setDesktopSession,
|
|
44
45
|
setCloseRequested, getMemoryModPromise, setMemoryModPromise,
|
|
45
46
|
setSessionNeedsCwdRefresh,
|
|
@@ -263,7 +264,7 @@ export function createLifecycleApi(deps) {
|
|
|
263
264
|
const channelStop = channels.stop(reason, detach ? { waitForExit: false } : undefined);
|
|
264
265
|
try { agentTool.closeAll(reason); } catch {}
|
|
265
266
|
let mcpStop = null;
|
|
266
|
-
try { mcpStop = mcpClient.disconnectAll?.(); } catch {}
|
|
267
|
+
try { mcpStop = mcpClient.disconnectAll?.({ scopeId: getMcpScopeId?.() }); } catch {}
|
|
267
268
|
const openaiWsStop = isProcessExit && globalThis.__mixdogOpenaiWsRuntimeLoaded === true
|
|
268
269
|
? import('../runtime/agent/orchestrator/providers/openai-oauth-ws.mjs')
|
|
269
270
|
.then((mod) => mod?.drainOpenaiWsPool?.(reason))
|
|
@@ -520,9 +521,12 @@ export function createLifecycleApi(deps) {
|
|
|
520
521
|
const activeDesktopSession = typeof getDesktopSession === 'function'
|
|
521
522
|
? getDesktopSession()
|
|
522
523
|
: desktopSession;
|
|
523
|
-
const resumeOptions =
|
|
524
|
-
|
|
525
|
-
|
|
524
|
+
const resumeOptions = {
|
|
525
|
+
...(activeDesktopSession && typeof activeDesktopSession === 'object'
|
|
526
|
+
? { desktopSession: activeDesktopSession }
|
|
527
|
+
: {}),
|
|
528
|
+
mcpScopeId: getMcpScopeId?.() || null,
|
|
529
|
+
};
|
|
526
530
|
const resumed = await mgr.resumeSession(id, toolSpecForMode(getMode()), resumeOptions);
|
|
527
531
|
if (!resumed) return null;
|
|
528
532
|
if (previousId && previousId !== resumed.id) {
|
|
@@ -33,8 +33,10 @@ export function createMcpGlue({
|
|
|
33
33
|
mcpClient,
|
|
34
34
|
getConfig,
|
|
35
35
|
getCurrentCwd,
|
|
36
|
+
getMcpScopeId = () => null,
|
|
36
37
|
state,
|
|
37
38
|
}) {
|
|
39
|
+
const scopeOptions = () => ({ scopeId: getMcpScopeId() });
|
|
38
40
|
function mcpTransportLabel(cfg = {}) {
|
|
39
41
|
if (cfg.autoDetect) return `autoDetect:${cfg.autoDetect}`;
|
|
40
42
|
try {
|
|
@@ -77,7 +79,7 @@ export function createMcpGlue({
|
|
|
77
79
|
return { servers: [], configuredCount: 0, connectedCount: 0, failedCount: 0 };
|
|
78
80
|
}
|
|
79
81
|
const { servers: configured, sources } = resolveEffectiveMcpServers();
|
|
80
|
-
const connected = new Map((mcpClient.getMcpServerStatus?.() || []).map((row) => [row.name, row]));
|
|
82
|
+
const connected = new Map((mcpClient.getMcpServerStatus?.(getMcpScopeId()) || []).map((row) => [row.name, row]));
|
|
81
83
|
const failures = new Map((state.mcpFailures || []).map((row) => [row.name, row]));
|
|
82
84
|
const servers = [];
|
|
83
85
|
for (const [name, cfg] of Object.entries(configured)) {
|
|
@@ -125,16 +127,16 @@ export function createMcpGlue({
|
|
|
125
127
|
state.mcpFailures = state.mcpFailures.filter((row) => row.name !== target);
|
|
126
128
|
}
|
|
127
129
|
if (enabled === false) {
|
|
128
|
-
await mcpClient.disconnectMcpServer?.(target);
|
|
130
|
+
await mcpClient.disconnectMcpServer?.(target, scopeOptions());
|
|
129
131
|
return;
|
|
130
132
|
}
|
|
131
133
|
const cfg = servers[target];
|
|
132
134
|
if (!cfg) return;
|
|
133
135
|
// Drop any existing live entry first so connectMcpServers doesn't overwrite
|
|
134
136
|
// the registry Map entry and leak the old transport/process.
|
|
135
|
-
await mcpClient.disconnectMcpServer?.(target);
|
|
137
|
+
await mcpClient.disconnectMcpServer?.(target, scopeOptions());
|
|
136
138
|
try {
|
|
137
|
-
await mcpClient.connectMcpServers({ [target]: cfg });
|
|
139
|
+
await mcpClient.connectMcpServers({ [target]: cfg }, scopeOptions());
|
|
138
140
|
} catch (error) {
|
|
139
141
|
const failures = Array.isArray(error?.failures)
|
|
140
142
|
? error.failures
|
|
@@ -147,8 +149,8 @@ export function createMcpGlue({
|
|
|
147
149
|
if (envFlag('MIXDOG_DISABLE_MCP')) {
|
|
148
150
|
++state.mcpConnectGeneration;
|
|
149
151
|
state.mcpFailures = [];
|
|
150
|
-
if (only) await mcpClient.disconnectMcpServer?.(only);
|
|
151
|
-
else await mcpClient.disconnectAll?.();
|
|
152
|
+
if (only) await mcpClient.disconnectMcpServer?.(only, scopeOptions());
|
|
153
|
+
else await mcpClient.disconnectAll?.(scopeOptions());
|
|
152
154
|
return mcpStatus();
|
|
153
155
|
}
|
|
154
156
|
// Scoped single-server toggle: non-superseding. It must NEVER cancel a
|
|
@@ -192,12 +194,12 @@ export function createMcpGlue({
|
|
|
192
194
|
}
|
|
193
195
|
if (gen !== state.mcpConnectGeneration) return mcpStatus();
|
|
194
196
|
const run = (async () => {
|
|
195
|
-
if (reset) await mcpClient.disconnectAll?.();
|
|
197
|
+
if (reset) await mcpClient.disconnectAll?.(scopeOptions());
|
|
196
198
|
state.mcpFailures = [];
|
|
197
199
|
const { servers } = resolveEffectiveMcpServers();
|
|
198
200
|
if (Object.keys(servers).length === 0) return;
|
|
199
201
|
try {
|
|
200
|
-
await mcpClient.connectMcpServers(servers);
|
|
202
|
+
await mcpClient.connectMcpServers(servers, scopeOptions());
|
|
201
203
|
} catch (error) {
|
|
202
204
|
state.mcpFailures = Array.isArray(error?.failures)
|
|
203
205
|
? error.failures
|
|
@@ -4,6 +4,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSy
|
|
|
4
4
|
import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
|
|
5
5
|
import { performance } from 'node:perf_hooks';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
7
8
|
import keychain from '../lib/keychain-cjs.cjs';
|
|
8
9
|
import './hitch-profile.mjs';
|
|
9
10
|
import { ensureStandaloneEnvironment } from '../standalone/seeds.mjs';
|
|
@@ -335,6 +336,7 @@ export async function createMixdogSessionRuntime({
|
|
|
335
336
|
// Shared mutable runtime state, promoted from closure `let`s so extracted
|
|
336
337
|
// modules can read/write live values through one reference.
|
|
337
338
|
const rt = {};
|
|
339
|
+
rt.mcpScopeId = randomUUID();
|
|
338
340
|
rt.desktopSession = initialDesktopSession;
|
|
339
341
|
// Agent shard spread: a daemon-hosted worker runtime carries the resolved
|
|
340
342
|
// agent session spec; session creation routes through prepareAgentSession
|
|
@@ -656,6 +658,7 @@ export async function createMixdogSessionRuntime({
|
|
|
656
658
|
mcpClient,
|
|
657
659
|
getConfig: () => rt.config,
|
|
658
660
|
getCurrentCwd: () => rt.currentCwd,
|
|
661
|
+
getMcpScopeId: () => rt.mcpScopeId,
|
|
659
662
|
getDesktopSession: () => rt.desktopSession,
|
|
660
663
|
setDesktopSession: (v) => { rt.desktopSession = v; },
|
|
661
664
|
state: mcpState,
|
|
@@ -750,6 +753,7 @@ export async function createMixdogSessionRuntime({
|
|
|
750
753
|
mgr,
|
|
751
754
|
dataDir: cfgMod.getPluginData(),
|
|
752
755
|
cwd,
|
|
756
|
+
mcpScopeId: rt.mcpScopeId,
|
|
753
757
|
awaitKeychainPrewarm,
|
|
754
758
|
isKeychainPrewarmReady: () => rt.keychainPrewarmWaitDone,
|
|
755
759
|
// SubagentStart/SubagentStop: bridge internal worker spawn/finish to the
|
|
@@ -843,6 +847,7 @@ export async function createMixdogSessionRuntime({
|
|
|
843
847
|
getSession: () => rt.session,
|
|
844
848
|
getRoute: () => rt.route,
|
|
845
849
|
getConfig: () => rt.config,
|
|
850
|
+
getMcpScopeId: () => rt.mcpScopeId,
|
|
846
851
|
cfgMod,
|
|
847
852
|
loadWorkflowPack,
|
|
848
853
|
activeWorkflowId,
|
|
@@ -854,6 +859,7 @@ export async function createMixdogSessionRuntime({
|
|
|
854
859
|
getSession: () => rt.session,
|
|
855
860
|
getRoute: () => rt.route,
|
|
856
861
|
getCurrentCwd: () => rt.currentCwd,
|
|
862
|
+
getMcpScopeId: () => rt.mcpScopeId,
|
|
857
863
|
getMode: () => rt.mode,
|
|
858
864
|
});
|
|
859
865
|
const computeContextStatusForSession = (session) => {
|
|
@@ -183,6 +183,7 @@ export function createSessionLifecycle({
|
|
|
183
183
|
if (rt.closeRequested) throw new Error('runtime is closing');
|
|
184
184
|
const { session } = prepareAgentSession({
|
|
185
185
|
...rt.agentSessionSpec,
|
|
186
|
+
mcpScopeId: rt.mcpScopeId,
|
|
186
187
|
...(rt.reservedSessionId ? { sessionId: rt.reservedSessionId } : {}),
|
|
187
188
|
});
|
|
188
189
|
rt.session = session;
|
|
@@ -232,6 +233,7 @@ export function createSessionLifecycle({
|
|
|
232
233
|
sourceType: 'lead',
|
|
233
234
|
sourceName: 'main',
|
|
234
235
|
clientHostPid: process.pid,
|
|
236
|
+
mcpScopeId: rt.mcpScopeId,
|
|
235
237
|
disallowedTools: [...LEAD_DISALLOWED_TOOLS, ...featureDisallowedTools()],
|
|
236
238
|
cwd: rt.currentCwd,
|
|
237
239
|
...(rt.desktopSession && typeof rt.desktopSession === 'object' ? { desktopSession: rt.desktopSession } : {}),
|
|
@@ -258,7 +260,7 @@ export function createSessionLifecycle({
|
|
|
258
260
|
// prompt renders. This fold keeps recreate paths (cwd change with MCP
|
|
259
261
|
// already connected) seeding their manifest instead of re-announcing late.
|
|
260
262
|
let connectedMcpTools = [];
|
|
261
|
-
try { connectedMcpTools = mcpClient.getMcpTools?.() || []; }
|
|
263
|
+
try { connectedMcpTools = mcpClient.getMcpTools?.(rt.mcpScopeId) || []; }
|
|
262
264
|
catch { connectedMcpTools = []; }
|
|
263
265
|
applyDeferredToolSurface(
|
|
264
266
|
rt.session,
|
|
@@ -246,7 +246,7 @@ export function createSessionTurnApi(deps) {
|
|
|
246
246
|
// Slower servers remain available through the late-tool path.
|
|
247
247
|
try { await awaitTurn(() => awaitMcpGrace()); }
|
|
248
248
|
catch { /* gate must never break the turn */ }
|
|
249
|
-
try { refreshInitialDeferredMcpSurface(session0, getMcpTools()); }
|
|
249
|
+
try { refreshInitialDeferredMcpSurface(session0, getMcpTools(session0.mcpScopeId)); }
|
|
250
250
|
catch { /* first-turn MCP fold must never break the turn */ }
|
|
251
251
|
} else {
|
|
252
252
|
// AFTER FIRST TURN: fold in MCP tools whose servers finished their
|
|
@@ -263,7 +263,7 @@ export function createSessionTurnApi(deps) {
|
|
|
263
263
|
try { await awaitTurn(() => awaitMcpGrace()); }
|
|
264
264
|
catch { /* gate must never break the turn */ }
|
|
265
265
|
try {
|
|
266
|
-
reconcileDeferredMcpToolCatalog(session0, getMcpTools(), {
|
|
266
|
+
reconcileDeferredMcpToolCatalog(session0, getMcpTools(session0.mcpScopeId), {
|
|
267
267
|
// Deliver the late-tool announcement through the pending-message
|
|
268
268
|
// queue so it rides inside the next real user turn as a persisted
|
|
269
269
|
// system-reminder (no synthetic user + '.' assistant pair).
|
|
@@ -311,7 +311,7 @@ export function applyDeferredToolSurface(session, mode, extraTools = [], options
|
|
|
311
311
|
}
|
|
312
312
|
if (!session.deferredToolBp1Applied && session.messages?.some((m) => m?.role === 'system')) {
|
|
313
313
|
if (!session.mcpServerInstructions || typeof session.mcpServerInstructions !== 'object') {
|
|
314
|
-
session.mcpServerInstructions = getMcpServerInstructionsMap();
|
|
314
|
+
session.mcpServerInstructions = getMcpServerInstructionsMap(session.mcpScopeId);
|
|
315
315
|
}
|
|
316
316
|
applyInitialDeferredToolManifestToBp1(session, deferredPoolToolNames(session));
|
|
317
317
|
}
|
|
@@ -352,7 +352,7 @@ export function rebuildDeferredToolSurfaceForProvider(session, provider) {
|
|
|
352
352
|
}
|
|
353
353
|
if (previousMode && previousMode !== session.deferredProviderMode) {
|
|
354
354
|
if (session.deferredProviderMode === 'native') {
|
|
355
|
-
session.mcpServerInstructions = getMcpServerInstructionsMap();
|
|
355
|
+
session.mcpServerInstructions = getMcpServerInstructionsMap(session.mcpScopeId);
|
|
356
356
|
applyInitialDeferredToolManifestToBp1(session, deferredPoolToolNames(session), { rebuild: true });
|
|
357
357
|
const rendered = session.messages?.find((message) => message?.role === 'system')?.content;
|
|
358
358
|
session.deferredAnnouncedTools = deferredPoolToolNames(session)
|
|
@@ -409,7 +409,7 @@ export function refreshInitialDeferredMcpSurface(session, liveMcpTools) {
|
|
|
409
409
|
}
|
|
410
410
|
// Refresh MCP server instructions so a newly-connected server's block is
|
|
411
411
|
// included when BP1 is re-rendered below.
|
|
412
|
-
session.mcpServerInstructions = getMcpServerInstructionsMap();
|
|
412
|
+
session.mcpServerInstructions = getMcpServerInstructionsMap(session.mcpScopeId);
|
|
413
413
|
const applied = applyInitialDeferredToolManifestToBp1(session, deferredPoolToolNames(session), { rebuild: true });
|
|
414
414
|
if (!applied) return false;
|
|
415
415
|
// Pre-mark ONLY the names that ACTUALLY landed in the rebuilt BP1 manifest as
|
|
@@ -17,6 +17,7 @@ export function createToolSurface({
|
|
|
17
17
|
getSession,
|
|
18
18
|
getRoute,
|
|
19
19
|
getConfig,
|
|
20
|
+
getMcpScopeId = () => null,
|
|
20
21
|
cfgMod,
|
|
21
22
|
loadWorkflowPack,
|
|
22
23
|
activeWorkflowId,
|
|
@@ -62,13 +63,13 @@ export function createToolSurface({
|
|
|
62
63
|
|
|
63
64
|
function buildPreSessionSurface() {
|
|
64
65
|
const previewTools = typeof mgr.previewSessionTools === 'function'
|
|
65
|
-
? mgr.previewSessionTools(toolSpecForMode(mode), [])
|
|
66
|
+
? mgr.previewSessionTools(toolSpecForMode(mode), [], { mcpScopeId: getMcpScopeId() })
|
|
66
67
|
: [];
|
|
67
68
|
const tools = filterDisallowedTools(previewTools, [
|
|
68
69
|
...LEAD_DISALLOWED_TOOLS,
|
|
69
70
|
...getFeatureDisallowedTools(),
|
|
70
71
|
]);
|
|
71
|
-
const surface = { tools: Array.isArray(tools) ? tools.slice() : [] };
|
|
72
|
+
const surface = { tools: Array.isArray(tools) ? tools.slice() : [], mcpScopeId: getMcpScopeId() };
|
|
72
73
|
applyDeferredToolSurface(surface, deferredSurfaceModeForLead(mode), modelStandaloneTools(), {
|
|
73
74
|
provider: getRoute().provider,
|
|
74
75
|
});
|
|
@@ -25,6 +25,7 @@ export function createSpawnFlow({
|
|
|
25
25
|
upsertWorkerSessionDeferred,
|
|
26
26
|
refreshTagsFromSessions,
|
|
27
27
|
defaultCwd,
|
|
28
|
+
mcpScopeId = null,
|
|
28
29
|
nextTag,
|
|
29
30
|
cancelReap,
|
|
30
31
|
bindTag,
|
|
@@ -228,6 +229,7 @@ export function createSpawnFlow({
|
|
|
228
229
|
maxLoopIterations: positiveInt(args.maxLoopIterations) || undefined,
|
|
229
230
|
permission: agentPerm || undefined,
|
|
230
231
|
cacheKeyOverride: args.cacheKey || undefined,
|
|
232
|
+
mcpScopeId,
|
|
231
233
|
};
|
|
232
234
|
}
|
|
233
235
|
|
|
@@ -89,6 +89,7 @@ export function createStandaloneAgent({
|
|
|
89
89
|
mgr: baseMgr,
|
|
90
90
|
dataDir,
|
|
91
91
|
cwd: defaultCwd,
|
|
92
|
+
mcpScopeId = null,
|
|
92
93
|
onSubagentEvent,
|
|
93
94
|
awaitKeychainPrewarm = async () => {},
|
|
94
95
|
isKeychainPrewarmReady = () => true,
|
|
@@ -213,6 +214,7 @@ export function createStandaloneAgent({
|
|
|
213
214
|
refreshTagsFromSessions,
|
|
214
215
|
readWorkerRows,
|
|
215
216
|
defaultCwd,
|
|
217
|
+
mcpScopeId,
|
|
216
218
|
nextTag,
|
|
217
219
|
cancelReap,
|
|
218
220
|
bindTag,
|