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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.105",
3
+ "version": "0.9.107",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -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 = _connectAbortGeneration;
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 servers.values()) {
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 [...servers.values()].map((server) => ({
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
- let gate = callAdmissions.get(server.name);
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
- callAdmissions.set(server.name, gate);
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 gate = callAdmissions.get(name);
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(name);
235
+ callAdmissions.delete(registryKey);
202
236
  gate.close(reason || `MCP ${name} disconnected`);
203
237
  }
204
- export function getMcpAdmissionSnapshot() {
205
- return [...callAdmissions.entries()].map(([name, gate]) => ({
206
- name,
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 servers.values()) {
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
- return reconnects.run(serverName, async () => {
223
- const current = servers.get(serverName);
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(serverName) === current) servers.delete(serverName);
269
+ if (servers.get(registryKey) === current) servers.delete(registryKey);
230
270
  }
231
- await connectServer(serverName, failedServer.cfg);
232
- const replacement = servers.get(serverName);
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 server = servers.get(serverName);
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 = servers.get(serverName);
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 memoKey = `${name}|${field}`;
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 = servers.get(serverName);
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
- _connectAbortGeneration++;
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 [name, server] of servers) {
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(name);
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 server = servers.get(name);
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(name);
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
- let _connectAbortGeneration = 0;
519
- const _pendingConnects = new Set();
520
- async function connectServer(name, cfg, genAtStart = _connectAbortGeneration) {
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 !== _connectAbortGeneration) {
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 !== _connectAbortGeneration) {
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
- servers.set(name, { name, client, transport, tools, cfg, instructions, generation: genAtStart });
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
- if (!isOnDeferredToolSurface(sessionRef, name) && !isRegisteredMcpTool(name)) {
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, { ownerIsAgentSession: ownerIsAgent });
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 || null,
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, { ownerIsAgentSession: ownerIsAgent });
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) return cached;
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
- /** { workflow, workflowContext } createSession opts for a workflow id, or {} when unset/unresolvable. */
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 = activeDesktopSession && typeof activeDesktopSession === 'object'
524
- ? { desktopSession: activeDesktopSession }
525
- : undefined;
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,