mixdog 0.9.105 → 0.9.107
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/approval-mode.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +4 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -4
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -4
- package/src/runtime/shared/automation-workflow.mjs +7 -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 +8 -0
- package/src/session-runtime/session-lifecycle.mjs +4 -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
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const IMPLICIT_APPROVAL_MODE = 'implicit';
|
|
2
|
+
export const IMPLICIT_APPROVAL_CONTEXT =
|
|
3
|
+
'Non-interactive execution: treat the initial user request as the approved plan and proceed without requesting approval.';
|
|
4
|
+
|
|
5
|
+
export function workflowContextForApprovalMode(workflowContext, approvalMode) {
|
|
6
|
+
if (approvalMode !== IMPLICIT_APPROVAL_MODE) return workflowContext || null;
|
|
7
|
+
const workflow = typeof workflowContext === 'string' ? workflowContext.trim() : '';
|
|
8
|
+
return [workflow, IMPLICIT_APPROVAL_CONTEXT].filter(Boolean).join('\n\n');
|
|
9
|
+
}
|
|
@@ -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') {
|
|
@@ -34,6 +34,10 @@ import { getAgentRuntimeSync, warnAgentRuntimeResolveFailureOnce } from './agent
|
|
|
34
34
|
import { mintSessionId } from './session-id.mjs';
|
|
35
35
|
import { providerCacheKey } from './provider-cache-key.mjs';
|
|
36
36
|
import { clearTurnCheckpoint, recoverTurnCheckpoint } from './turn-checkpoint.mjs';
|
|
37
|
+
import {
|
|
38
|
+
IMPLICIT_APPROVAL_MODE,
|
|
39
|
+
workflowContextForApprovalMode,
|
|
40
|
+
} from '../approval-mode.mjs';
|
|
37
41
|
|
|
38
42
|
function buildSessionProviderCacheOpts(providerName, sessionId, agent = null) {
|
|
39
43
|
// Keep this in sync with createSession's provider-cache policy: only
|
|
@@ -249,7 +253,10 @@ export function createSession(opts) {
|
|
|
249
253
|
const toolSpec = ownerIsAgent
|
|
250
254
|
? (isReadOnlyAgentBundle ? 'readonly' : 'full')
|
|
251
255
|
: (Array.isArray(profile?.tools) ? profile.tools : toolPreset);
|
|
252
|
-
let toolsForRouting = resolveSessionTools(toolSpec, skills, {
|
|
256
|
+
let toolsForRouting = resolveSessionTools(toolSpec, skills, {
|
|
257
|
+
ownerIsAgentSession: ownerIsAgent,
|
|
258
|
+
mcpScopeId: opts.mcpScopeId || null,
|
|
259
|
+
});
|
|
253
260
|
// Fail-closed permission intersection: when a session declares an explicit
|
|
254
261
|
// object-form permission, intersect the
|
|
255
262
|
// resolved tool list with the permission's allow/deny lists. If the
|
|
@@ -271,7 +278,7 @@ export function createSession(opts) {
|
|
|
271
278
|
skipRoleCatalog: !ownerIsAgent,
|
|
272
279
|
profile: profile || undefined,
|
|
273
280
|
agent: resolvedAgent,
|
|
274
|
-
workflowContext: opts.workflowContext
|
|
281
|
+
workflowContext: workflowContextForApprovalMode(opts.workflowContext, opts.approvalMode),
|
|
275
282
|
workspaceContext: opts.workspaceContext || null,
|
|
276
283
|
coreMemoryContext: opts.coreMemoryContext || null,
|
|
277
284
|
skillManifest: buildSkillManifest(skills),
|
|
@@ -370,7 +377,11 @@ export function createSession(opts) {
|
|
|
370
377
|
fast,
|
|
371
378
|
agent: opts.agent,
|
|
372
379
|
owner: opts.owner || 'user',
|
|
380
|
+
...(opts.approvalMode === IMPLICIT_APPROVAL_MODE
|
|
381
|
+
? { approvalMode: IMPLICIT_APPROVAL_MODE }
|
|
382
|
+
: {}),
|
|
373
383
|
mcpPid: process.pid,
|
|
384
|
+
mcpScopeId: opts.mcpScopeId || null,
|
|
374
385
|
scopeKey: opts.scopeKey || null,
|
|
375
386
|
lane: opts.lane || 'agent',
|
|
376
387
|
cwd: opts.cwd,
|
|
@@ -530,7 +541,10 @@ function _prepareResumeTools(session, preset) {
|
|
|
530
541
|
if (!ownerIsAgent && Array.isArray(profile?.tools)) toolSpec = profile.tools;
|
|
531
542
|
} catch { /* ignore lookup failures, keep preset fallback */ }
|
|
532
543
|
}
|
|
533
|
-
let toolsForRouting = resolveSessionTools(toolSpec, skills, {
|
|
544
|
+
let toolsForRouting = resolveSessionTools(toolSpec, skills, {
|
|
545
|
+
ownerIsAgentSession: ownerIsAgent,
|
|
546
|
+
mcpScopeId: session.mcpScopeId || null,
|
|
547
|
+
});
|
|
534
548
|
if (ownerIsAgent) {
|
|
535
549
|
toolsForRouting = applyToolPermissionNarrowing(toolsForRouting, session.toolPermission, session.agent || null);
|
|
536
550
|
}
|
|
@@ -563,8 +577,10 @@ function _rememberPreparedResume(sessionId, prepared) {
|
|
|
563
577
|
|
|
564
578
|
function _preparedResumeForSession(session, preset) {
|
|
565
579
|
const cached = _preparedResumes.get(session.id);
|
|
566
|
-
if (cached?.session === session && cached?.preset === preset
|
|
580
|
+
if (cached?.session === session && cached?.preset === preset
|
|
581
|
+
&& cached?.mcpScopeId === (session.mcpScopeId || null)) return cached;
|
|
567
582
|
const prepared = _prepareResumeTools(session, preset);
|
|
583
|
+
prepared.mcpScopeId = session.mcpScopeId || null;
|
|
568
584
|
_rememberPreparedResume(session.id, prepared);
|
|
569
585
|
return prepared;
|
|
570
586
|
}
|
|
@@ -745,6 +761,9 @@ export async function resumeSession(sessionId, preset, options = {}) {
|
|
|
745
761
|
session.desktopSession = expectedDesktop;
|
|
746
762
|
}
|
|
747
763
|
if (!session.owner) session.owner = 'user';
|
|
764
|
+
if (Object.prototype.hasOwnProperty.call(options, 'mcpScopeId')) {
|
|
765
|
+
session.mcpScopeId = String(options.mcpScopeId || '').trim() || null;
|
|
766
|
+
}
|
|
748
767
|
if (_isActivelyOwnedElsewhere(session, sessionId)) {
|
|
749
768
|
// ATTACH (viewer mode, zero ownership): hand back the live transcript
|
|
750
769
|
// under the SAME id, flagged remoteAttached. No tool refresh, no save,
|
|
@@ -764,6 +783,7 @@ export async function resumeSession(sessionId, preset, options = {}) {
|
|
|
764
783
|
const cached = _preparedResumes.get(sessionId);
|
|
765
784
|
_preparedResumes.delete(sessionId);
|
|
766
785
|
const prepared = cached?.session === session && cached?.preset === preset
|
|
786
|
+
&& cached?.mcpScopeId === (session.mcpScopeId || null)
|
|
767
787
|
? cached
|
|
768
788
|
: _prepareResumeTools(session, preset);
|
|
769
789
|
// 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 /
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { createWorkflowHelpers } from '../../session-runtime/workflow.mjs';
|
|
10
10
|
import { STANDALONE_ROOT, STANDALONE_DATA_DIR } from '../../session-runtime/runtime-paths.mjs';
|
|
11
11
|
import { readMarkdownDocument, normalizeAgentPermissionOrNone } from './markdown-frontmatter.mjs';
|
|
12
|
+
import { IMPLICIT_APPROVAL_MODE } from '../agent/orchestrator/session/approval-mode.mjs';
|
|
12
13
|
|
|
13
14
|
let _helpers = null;
|
|
14
15
|
function helpers() {
|
|
@@ -23,19 +24,21 @@ function helpers() {
|
|
|
23
24
|
return _helpers;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
|
-
/**
|
|
27
|
+
/** Non-interactive createSession opts, plus workflow context when the id resolves. */
|
|
27
28
|
export function automationWorkflowOpts(workflowId) {
|
|
29
|
+
const approvalOpts = { approvalMode: IMPLICIT_APPROVAL_MODE };
|
|
28
30
|
const id = String(workflowId || '').trim();
|
|
29
|
-
if (!id) return
|
|
31
|
+
if (!id) return approvalOpts;
|
|
30
32
|
try {
|
|
31
33
|
const h = helpers();
|
|
32
34
|
const pack = h.loadWorkflowPack(undefined, id);
|
|
33
|
-
if (!pack) return
|
|
35
|
+
if (!pack) return approvalOpts;
|
|
34
36
|
return {
|
|
37
|
+
...approvalOpts,
|
|
35
38
|
workflow: h.workflowSummary(pack),
|
|
36
39
|
workflowContext: h.workflowContextBlock({ workflow: { active: id } }, undefined),
|
|
37
40
|
};
|
|
38
41
|
} catch {
|
|
39
|
-
return
|
|
42
|
+
return approvalOpts;
|
|
40
43
|
}
|
|
41
44
|
}
|
|
@@ -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';
|
|
@@ -328,6 +329,7 @@ export async function createMixdogSessionRuntime({
|
|
|
328
329
|
model,
|
|
329
330
|
cwd = process.cwd(),
|
|
330
331
|
toolMode = 'full',
|
|
332
|
+
approvalMode = null,
|
|
331
333
|
remote = false,
|
|
332
334
|
desktopSession: initialDesktopSession = null,
|
|
333
335
|
agentSession = null,
|
|
@@ -335,6 +337,8 @@ export async function createMixdogSessionRuntime({
|
|
|
335
337
|
// Shared mutable runtime state, promoted from closure `let`s so extracted
|
|
336
338
|
// modules can read/write live values through one reference.
|
|
337
339
|
const rt = {};
|
|
340
|
+
rt.approvalMode = approvalMode === 'implicit' ? 'implicit' : null;
|
|
341
|
+
rt.mcpScopeId = randomUUID();
|
|
338
342
|
rt.desktopSession = initialDesktopSession;
|
|
339
343
|
// Agent shard spread: a daemon-hosted worker runtime carries the resolved
|
|
340
344
|
// agent session spec; session creation routes through prepareAgentSession
|
|
@@ -656,6 +660,7 @@ export async function createMixdogSessionRuntime({
|
|
|
656
660
|
mcpClient,
|
|
657
661
|
getConfig: () => rt.config,
|
|
658
662
|
getCurrentCwd: () => rt.currentCwd,
|
|
663
|
+
getMcpScopeId: () => rt.mcpScopeId,
|
|
659
664
|
getDesktopSession: () => rt.desktopSession,
|
|
660
665
|
setDesktopSession: (v) => { rt.desktopSession = v; },
|
|
661
666
|
state: mcpState,
|
|
@@ -750,6 +755,7 @@ export async function createMixdogSessionRuntime({
|
|
|
750
755
|
mgr,
|
|
751
756
|
dataDir: cfgMod.getPluginData(),
|
|
752
757
|
cwd,
|
|
758
|
+
mcpScopeId: rt.mcpScopeId,
|
|
753
759
|
awaitKeychainPrewarm,
|
|
754
760
|
isKeychainPrewarmReady: () => rt.keychainPrewarmWaitDone,
|
|
755
761
|
// SubagentStart/SubagentStop: bridge internal worker spawn/finish to the
|
|
@@ -843,6 +849,7 @@ export async function createMixdogSessionRuntime({
|
|
|
843
849
|
getSession: () => rt.session,
|
|
844
850
|
getRoute: () => rt.route,
|
|
845
851
|
getConfig: () => rt.config,
|
|
852
|
+
getMcpScopeId: () => rt.mcpScopeId,
|
|
846
853
|
cfgMod,
|
|
847
854
|
loadWorkflowPack,
|
|
848
855
|
activeWorkflowId,
|
|
@@ -854,6 +861,7 @@ export async function createMixdogSessionRuntime({
|
|
|
854
861
|
getSession: () => rt.session,
|
|
855
862
|
getRoute: () => rt.route,
|
|
856
863
|
getCurrentCwd: () => rt.currentCwd,
|
|
864
|
+
getMcpScopeId: () => rt.mcpScopeId,
|
|
857
865
|
getMode: () => rt.mode,
|
|
858
866
|
});
|
|
859
867
|
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;
|
|
@@ -231,7 +232,9 @@ export function createSessionLifecycle({
|
|
|
231
232
|
lane: 'cli',
|
|
232
233
|
sourceType: 'lead',
|
|
233
234
|
sourceName: 'main',
|
|
235
|
+
...(rt.approvalMode ? { approvalMode: rt.approvalMode } : {}),
|
|
234
236
|
clientHostPid: process.pid,
|
|
237
|
+
mcpScopeId: rt.mcpScopeId,
|
|
235
238
|
disallowedTools: [...LEAD_DISALLOWED_TOOLS, ...featureDisallowedTools()],
|
|
236
239
|
cwd: rt.currentCwd,
|
|
237
240
|
...(rt.desktopSession && typeof rt.desktopSession === 'object' ? { desktopSession: rt.desktopSession } : {}),
|
|
@@ -258,7 +261,7 @@ export function createSessionLifecycle({
|
|
|
258
261
|
// prompt renders. This fold keeps recreate paths (cwd change with MCP
|
|
259
262
|
// already connected) seeding their manifest instead of re-announcing late.
|
|
260
263
|
let connectedMcpTools = [];
|
|
261
|
-
try { connectedMcpTools = mcpClient.getMcpTools?.() || []; }
|
|
264
|
+
try { connectedMcpTools = mcpClient.getMcpTools?.(rt.mcpScopeId) || []; }
|
|
262
265
|
catch { connectedMcpTools = []; }
|
|
263
266
|
applyDeferredToolSurface(
|
|
264
267
|
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,
|