teamshare-bridge 0.28.0 → 0.28.2

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.
@@ -30,6 +30,7 @@ const line_assembler_1 = require("./line-assembler");
30
30
  const opencode_outcome_1 = require("./opencode-outcome");
31
31
  const opencode_control_1 = require("./opencode-control");
32
32
  const activity_1 = require("./activity");
33
+ const turn_watch_1 = require("./turn-watch");
33
34
  const agy_pty_1 = require("./agy-pty");
34
35
  const prompt_file_1 = require("./prompt-file");
35
36
  const diagnostics_1 = require("../diagnostics");
@@ -38,12 +39,6 @@ const relay_key_1 = require("./relay-key");
38
39
  const resume_1 = require("./resume");
39
40
  const opencode_session_1 = require("../llm/opencode-session");
40
41
  const session_store_1 = require("./session-store");
41
- /**
42
- * Maximum prompt length before we switch to temp-file delivery.
43
- * Windows argv limit is 32,767 chars; we leave headroom for binary path,
44
- * MCP config, and other flags.
45
- */
46
- const PROMPT_FILE_THRESHOLD = 28_000;
47
42
  /** Real task ids are UUIDs; synthetic keys (chat-<projectId>) are not reported. */
48
43
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
49
44
  let warnedNoPty = false;
@@ -376,45 +371,56 @@ function spawnHarness(harness, prompt, log, settings = {}) {
376
371
  log(`[harness] ${adapter.binary} not found - install it first.`, 'stderr');
377
372
  return Promise.resolve(1);
378
373
  }
379
- // ── Long prompt → temp file delivery (decided once) ────────────────────
380
- let promptFile = null;
381
- const longPrompt = prompt.length > PROMPT_FILE_THRESHOLD && adapter.supportsPromptFile;
382
- if (longPrompt) {
383
- promptFile = (0, prompt_file_1.writePromptFile)(prompt);
384
- log(`[harness] prompt too long for argv (${prompt.length} chars) — wrote to temp file`, 'stderr');
385
- }
386
- const buildArgs = (req, promptOverride) => {
374
+ // ── Long prompt → file delivery (decided PER INVOCATION) ───────────────
375
+ //
376
+ // The decision is taken inside buildArgs, from the prompt ACTUALLY being
377
+ // sent and from the whole assembled command line. It used to be taken once,
378
+ // above, and then skipped whenever `promptOverride` was passed - and runOnce
379
+ // ALWAYS passes it (`buildArgs(req, effectivePrompt)`), so the file was
380
+ // written, logged, and never used: the 62 KB brief still went on argv and
381
+ // `spawn` died with `ENAMETOOLONG` before the harness started.
382
+ const promptFiles = [];
383
+ /**
384
+ * Write one prompt file. Never throws: a full/read-only disk must produce a
385
+ * log line, not an exception on a path that is already a last resort.
386
+ */
387
+ const openPromptFile = (text) => {
388
+ try {
389
+ const file = (0, prompt_file_1.writePromptFile)(text);
390
+ promptFiles.push(file);
391
+ return file;
392
+ }
393
+ catch (err) {
394
+ log(`[harness] could not write the prompt file (${msg(err)}) - ` +
395
+ 'falling back to argv, which may exceed the OS command-line limit', 'stderr');
396
+ return null;
397
+ }
398
+ };
399
+ const buildArgs = (req, promptForRun) => {
387
400
  const opts = req
388
401
  ? { ...settings, resume: req }
389
402
  : { ...settings, resume: undefined };
390
- const effectivePrompt = promptOverride ?? prompt;
391
- // A continuation prompt is a SHORT follow-up ("your shell finished, carry
392
- // on"), so it is always passed on argv - never re-written to the prompt
393
- // file, which still holds the original task brief.
394
- if (promptFile && !promptOverride) {
395
- if (adapter.promptFileFlag) {
396
- const a = adapter.headlessArgs(effectivePrompt, opts, true);
397
- a.push(adapter.promptFileFlag, promptFile);
398
- return a;
399
- }
400
- const a = adapter.headlessArgs(effectivePrompt, opts, true);
401
- const shortInstruction = `Read the file "${promptFile}" and follow its instructions exactly. ` +
402
- 'Do not respond until you have read the file. ' +
403
- 'The file contains your complete instructions including project context, tools, and the user message.';
404
- if (['claude', 'gemini', 'antigravity', 'copilot', 'cursor'].includes(resolved)) {
405
- a.unshift('-p', shortInstruction);
406
- }
407
- else {
408
- a.push(shortInstruction);
409
- }
410
- return a;
403
+ const effectivePrompt = promptForRun ?? prompt;
404
+ const inline = adapter.headlessArgs(effectivePrompt, opts);
405
+ const commandLineChars = (0, prompt_file_1.argvChars)([
406
+ bin.file,
407
+ ...(bin.args ?? []),
408
+ ...inline,
409
+ ]);
410
+ if (!(0, prompt_file_1.shouldUsePromptFile)(adapter, effectivePrompt.length, commandLineChars)) {
411
+ return inline;
411
412
  }
412
- return adapter.headlessArgs(effectivePrompt, opts);
413
+ const file = openPromptFile(effectivePrompt);
414
+ if (!file)
415
+ return inline;
416
+ log(`[harness] prompt too long for argv (${effectivePrompt.length} chars, ` +
417
+ `~${commandLineChars} char command line) — wrote it to ${file} and ` +
418
+ 'passed the path instead', 'stderr');
419
+ return (0, prompt_file_1.promptFileArgs)(adapter, adapter.headlessArgs(effectivePrompt, opts, true), file);
413
420
  };
414
421
  const cleanupPrompt = () => {
415
- if (promptFile)
416
- (0, prompt_file_1.cleanupPromptFile)(promptFile);
417
- promptFile = null;
422
+ for (const file of promptFiles.splice(0))
423
+ (0, prompt_file_1.cleanupPromptFile)(file);
418
424
  };
419
425
  // Shell/subagent lifecycle (2026-10): OpenCode V2 executes `shell` and
420
426
  // subagent work on the SHARED SERVICE. Both can outlive the CLI client and
@@ -675,6 +681,26 @@ function spawnHarness(harness, prompt, log, settings = {}) {
675
681
  * kill fires only when ALL of them have been quiet for the budget.
676
682
  */
677
683
  const activity = new activity_1.ActivityClock();
684
+ /**
685
+ * TURN WATCHDOG (2026-11) - see `turn-watch.ts`.
686
+ *
687
+ * The activity clock answers "is anything happening ANYWHERE"; this one
688
+ * answers "is a model turn OPEN with no output", which is the exact shape
689
+ * of a hung provider call (a `step_start` and then nothing for 19 minutes
690
+ * was the Aga Autos chat incident). Ladder: warn -> interrupt -> kill.
691
+ * The kill reuses exit 124 so the conversation is kept for a retry.
692
+ */
693
+ const turn = new turn_watch_1.TurnWatch();
694
+ const turnBudget = (0, turn_watch_1.turnBudgets)(settings.sessionMode ?? store?.mode, settings.timeoutMin ?? 60);
695
+ const turnWatchOff = /^(0|off|false|no)$/i.test(process.env.TEAMSHARE_TURN_WATCH ?? '');
696
+ /** One warning / interrupt per step id, so a recovered run is not re-hit. */
697
+ let turnWarnedStep = null;
698
+ let turnInterruptedStep = null;
699
+ let turnInterruptedAt = null;
700
+ /** Hard guard inputs - real work in flight is never a stall. */
701
+ let shellRunningNow = false;
702
+ let relayInFlight = 0;
703
+ let lastSubagentAt = 0;
678
704
  const STALL_WARN_MS = Math.max(1, Number(process.env.TEAMSHARE_STALL_WARN_MIN) || 5) * 60_000;
679
705
  const HEARTBEAT_MS = 5 * 60_000;
680
706
  /**
@@ -811,6 +837,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
811
837
  // A NEW subagent message is real work even though the parent
812
838
  // CLI printed nothing for it (the line below is OUR log).
813
839
  activity.note('subagent', `ses ${shortId(child.id)}`);
840
+ // The turn watchdog's "active subagent" guard: a child that
841
+ // is producing output must never be killed as a stalled turn.
842
+ lastSubagentAt = Date.now();
814
843
  startIdleTimer();
815
844
  if (Date.now() - lastHeartbeatAt >= HEARTBEAT_MS) {
816
845
  lastHeartbeatAt = Date.now();
@@ -860,6 +889,14 @@ function spawnHarness(harness, prompt, log, settings = {}) {
860
889
  // reach the console or the diagnostic sidecar (2026-10-25).
861
890
  const safeLine = (0, diagnostics_1.scrubSecrets)(line);
862
891
  rawCapture(safeLine);
892
+ // TURN WATCHDOG (2026-11): feed the open-step clock. A `step_start`
893
+ // that never reaches `step_finish` is a hung model turn, not work -
894
+ // see `turn-watch.ts` for the incident this exists for.
895
+ if (!turnWatchOff && resolved === 'opencode') {
896
+ const turnEvent = (0, turn_watch_1.extractTurnEvent)(line);
897
+ if (turnEvent)
898
+ turn.note(turnEvent);
899
+ }
863
900
  // SCOPE GUARD (2026-10-25): see `tripScopeGuard`.
864
901
  tripScopeGuard(safeLine);
865
902
  // Track background shells the service owns (they outlive this CLI).
@@ -999,6 +1036,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
999
1036
  if (settled)
1000
1037
  return;
1001
1038
  const now = Date.now();
1039
+ // Turn-watchdog guards are re-derived every tick: a shell that
1040
+ // exited must stop shielding a stalled turn, and vice versa.
1041
+ shellRunningNow = false;
1002
1042
  // 1) Relayed MCP/tool activity. Give up after 3 misses (no daemon,
1003
1043
  // or a run wired straight to the API) instead of polling forever.
1004
1044
  if (runKey && relayProbeMisses < 3) {
@@ -1008,6 +1048,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
1008
1048
  }
1009
1049
  else {
1010
1050
  relayProbeMisses = 0;
1051
+ // A call that has not answered yet is work in flight, so the
1052
+ // turn watchdog must not treat the silence as a hung model.
1053
+ relayInFlight = signal.inFlight;
1011
1054
  if (signal.lastActivityAt > lastRelayActivityAt) {
1012
1055
  lastRelayActivityAt = signal.lastActivityAt;
1013
1056
  activity.note('mcp', signal.lastTool ?? 'tool call');
@@ -1053,6 +1096,9 @@ function spawnHarness(harness, prompt, log, settings = {}) {
1053
1096
  continue;
1054
1097
  }
1055
1098
  activity.note('shell', shell.command ?? shell.id);
1099
+ // HARD GUARD: a running command is real work. The turn
1100
+ // watchdog must never kill a run the agent is waiting on.
1101
+ shellRunningNow = true;
1056
1102
  startIdleTimer();
1057
1103
  heartbeat(`background shell ${shell.command ?? shell.id} is still running`);
1058
1104
  break;
@@ -1075,6 +1121,110 @@ function spawnHarness(harness, prompt, log, settings = {}) {
1075
1121
  ? 'no output at all'
1076
1122
  : `no stdout for ${Math.max(1, stdoutMin)} min`} - last activity: ${(0, activity_1.activityActor)(latest)}. Not dead unless the ${Math.max(1, Math.round(timeoutMs / 60_000))} min idle timeout is reached.`, 'stdout');
1077
1123
  }
1124
+ // 4) TURN WATCHDOG (2026-11) - see `turn-watch.ts`.
1125
+ //
1126
+ // The activity clock above is deliberately patient: it cannot
1127
+ // tell a hung provider call from a long tool call, so it waits the
1128
+ // full idle budget. A model turn that OPENED and never produced a
1129
+ // token is not ambiguous, though - it is the Aga Autos incident
1130
+ // (a `step_start`, then 19 silent minutes). Ladder: warn ->
1131
+ // interrupt -> kill, and the kill reuses exit 124 so the
1132
+ // conversation is kept for a retry instead of starting over.
1133
+ if (!turnWatchOff && !settled) {
1134
+ const turnSnap = turn.snapshot();
1135
+ // A DIFFERENT step means the session recovered (or moved on):
1136
+ // clear the interrupt latch so the next stall is judged fresh.
1137
+ if (turnInterruptedStep !== null &&
1138
+ turnSnap.stepId !== turnInterruptedStep) {
1139
+ turnInterruptedStep = null;
1140
+ turnInterruptedAt = null;
1141
+ }
1142
+ const verdict = (0, turn_watch_1.decideTurnAction)({
1143
+ snapshot: turnSnap,
1144
+ now,
1145
+ budgets: turnBudget,
1146
+ inFlightMcp: relayInFlight,
1147
+ shellRunning: shellRunningNow,
1148
+ subagentActive: lastSubagentAt > 0 &&
1149
+ now - lastSubagentAt < turnBudget.subagentGraceMs,
1150
+ interruptedAt: turnInterruptedAt,
1151
+ });
1152
+ const stepKey = turnSnap.stepId ?? `step-${turnSnap.stepOpenedAt ?? 0}`;
1153
+ if (verdict.action === 'warn' && turnWarnedStep !== stepKey) {
1154
+ turnWarnedStep = stepKey;
1155
+ log(`[harness] ${adapter.name} model turn open for ` +
1156
+ `${Math.max(1, Math.round(verdict.openForMs / 60_000))} min, ` +
1157
+ `silent for ${Math.max(1, Math.round(verdict.silentMs / 60_000))} min ` +
1158
+ `(${verdict.reason}) - interrupting at ` +
1159
+ `${Math.round(turnBudget.interruptMs / 60_000)} min, killing at ` +
1160
+ `${Math.round(turnBudget.killMs / 60_000)} min unless it recovers`, 'stderr');
1161
+ (0, diagnostics_1.reportDiagnosticSafe)({
1162
+ kind: 'harness_failure',
1163
+ severity: 'warning',
1164
+ message: `model turn stalled (${verdict.reason})`,
1165
+ errorCode: 'HARNESS_TURN_STALL',
1166
+ agentId: store?.agentId,
1167
+ context: {
1168
+ harness: resolved,
1169
+ mode: settings.sessionMode ?? null,
1170
+ runKey: runKey ?? null,
1171
+ openForMs: verdict.openForMs,
1172
+ silentMs: verdict.silentMs,
1173
+ },
1174
+ });
1175
+ }
1176
+ if (verdict.action === 'interrupt' &&
1177
+ turnInterruptedStep !== stepKey) {
1178
+ turnInterruptedStep = stepKey;
1179
+ turnInterruptedAt = now;
1180
+ log(`[harness] ${adapter.name} model turn stalled ` +
1181
+ `(${verdict.reason}) - interrupting the service session so it ` +
1182
+ 'can continue; no work is lost', 'stderr');
1183
+ if (capturedId)
1184
+ await (0, opencode_control_1.interruptOpencodeSession)(capturedId);
1185
+ }
1186
+ if (verdict.action === 'kill') {
1187
+ // Same contract as the idle kill: OUR decision, not a broken
1188
+ // conversation, so the session is kept for a retry.
1189
+ idleKilled = true;
1190
+ forcedExitCode = 124;
1191
+ log(`[harness] ${adapter.name} model turn stalled ` +
1192
+ `(${verdict.reason}) - killing the run; the conversation is ` +
1193
+ 'kept so the next attempt continues it', 'stderr');
1194
+ (0, diagnostics_1.reportDiagnosticSafe)({
1195
+ kind: 'harness_failure',
1196
+ severity: 'error',
1197
+ message: `model turn stalled and was killed (${verdict.reason})`,
1198
+ errorCode: 'HARNESS_TURN_STALLED',
1199
+ agentId: store?.agentId,
1200
+ context: {
1201
+ harness: resolved,
1202
+ mode: settings.sessionMode ?? null,
1203
+ runKey: runKey ?? null,
1204
+ openForMs: verdict.openForMs,
1205
+ silentMs: verdict.silentMs,
1206
+ },
1207
+ });
1208
+ // Interrupt FIRST: on OpenCode V2 the shared service owns the
1209
+ // turn, so killing the CLI alone leaves a silent session.
1210
+ if (capturedId)
1211
+ await (0, opencode_control_1.interruptOpencodeSession)(capturedId);
1212
+ try {
1213
+ killTree(child.pid);
1214
+ }
1215
+ catch {
1216
+ /* ignore */
1217
+ }
1218
+ try {
1219
+ child.kill();
1220
+ }
1221
+ catch {
1222
+ /* ignore */
1223
+ }
1224
+ done(124);
1225
+ return;
1226
+ }
1227
+ }
1078
1228
  })();
1079
1229
  }, 30_000);
1080
1230
  activityTimer.unref?.();
@@ -1130,11 +1280,33 @@ function spawnHarness(harness, prompt, log, settings = {}) {
1130
1280
  done(forcedExitCode ?? code ?? 1);
1131
1281
  });
1132
1282
  }).catch((err) => {
1133
- // `spawn()` can throw SYNCHRONOUSLY (bad argv, ENOENT before the `error`
1134
- // event), which means the `close` handler above never runs. Clean the MCP
1135
- // config here too - it holds the agent API key - and let the rejection
1136
- // reach the caller.
1283
+ // `spawn()` can throw SYNCHRONOUSLY (bad argv, ENAMETOOLONG, ENOENT
1284
+ // before the `error` event), which means the `close` handler above never
1285
+ // runs. Clean the MCP config here too - it holds the agent API key - and
1286
+ // let the rejection reach the caller.
1137
1287
  cleanMcp();
1288
+ // A raw `spawn ENAMETOOLONG` reaches the task as if the binary were
1289
+ // broken. Name the real cause and the fix instead.
1290
+ if (err?.code === 'ENAMETOOLONG') {
1291
+ const detail = `the ${adapter.name} command line was too long to start ` +
1292
+ `(prompt ${prompt.length} chars, OS limit ~32k). Shorten the task ` +
1293
+ 'brief / project context, or use a harness that supports prompt files.';
1294
+ log(`[harness] ${detail}`, 'stderr');
1295
+ (0, diagnostics_1.reportDiagnosticSafe)({
1296
+ kind: 'harness_failure',
1297
+ severity: 'error',
1298
+ message: detail,
1299
+ errorCode: 'HARNESS_ENAMETOOLONG',
1300
+ agentId: settings.sessionStore?.agentId,
1301
+ context: {
1302
+ harness: adapter.name,
1303
+ promptChars: prompt.length,
1304
+ mode: settings.sessionMode ?? null,
1305
+ taskId: settings.sessionStore?.taskId ?? null,
1306
+ },
1307
+ });
1308
+ throw new Error(`spawn ENAMETOOLONG: ${detail}`);
1309
+ }
1138
1310
  throw err;
1139
1311
  });
1140
1312
  };
@@ -1384,11 +1556,17 @@ function spawnHarness(harness, prompt, log, settings = {}) {
1384
1556
  // session is consumed) exists because a retry must not resume a poisoned
1385
1557
  // planning session; a conversation is different, so it gets one retry and
1386
1558
  // is replaced only after the SECOND consecutive failure.
1559
+ //
1560
+ // `failedRun` is REQUIRED here (2026-11): without it a SUCCESSFUL turn
1561
+ // took this branch too, and logged "run failed (attempt 0) - KEEPING the
1562
+ // conversation" on every single reply. Live case: a chat mention answered
1563
+ // the question, posted the message through MCP, exited 0 - and the
1564
+ // console still said the run failed.
1387
1565
  const conversational = session_store_1.CONVERSATIONAL_MODES.has(store.mode);
1388
1566
  const failureCount = failedRun
1389
1567
  ? Math.max(1, (0, session_store_1.noteHarnessSessionFailure)(store.agentId, store.taskId, store.mode, `${adapter.name} exited with code ${finalCode}`))
1390
1568
  : 0;
1391
- const keepForConversation = conversational && failureCount < 2;
1569
+ const keepForConversation = failedRun && conversational && failureCount < 2;
1392
1570
  if (failedRun &&
1393
1571
  !questionStillOpen &&
1394
1572
  !keepForIdleResume &&
@@ -1398,13 +1576,8 @@ function spawnHarness(harness, prompt, log, settings = {}) {
1398
1576
  }
1399
1577
  else if (spec.capture !== 'none') {
1400
1578
  if (keepForConversation) {
1401
- log(`[resume] run failed (attempt ${failureCount}) - KEEPING the ` +
1402
- `conversation so the next message continues instead of starting over`);
1403
- if (failureCount >= 2) {
1404
- // Unreachable by construction (`keepForConversation` is false at
1405
- // 2), kept as a guard if the threshold ever changes.
1406
- log('[resume] session failed twice - replacing it', 'stderr');
1407
- }
1579
+ log(`[resume] run failed (attempt ${failureCount} of 2) - keeping the ` +
1580
+ `conversation so the next message continues instead of starting over`, 'stderr');
1408
1581
  }
1409
1582
  if (questionStillOpen) {
1410
1583
  log(`[resume] run failed but a question is still open - keeping the ` +
@@ -1502,9 +1675,11 @@ async function readRelayActivity(runKey) {
1502
1675
  const at = Number(body.lastActivityAt);
1503
1676
  if (!Number.isFinite(at) || at <= 0)
1504
1677
  return null;
1678
+ const inFlight = Number(body.inFlight);
1505
1679
  return {
1506
1680
  lastActivityAt: at,
1507
1681
  lastTool: typeof body.lastTool === 'string' ? body.lastTool : null,
1682
+ inFlight: Number.isFinite(inFlight) && inFlight > 0 ? inFlight : 0,
1508
1683
  };
1509
1684
  }
1510
1685
  catch {
@@ -1568,28 +1743,33 @@ function spawnThroughPty(binary, prompt, log, settings, name) {
1568
1743
  const adapterLike = (0, harness_adapter_1.getAdapter)(name);
1569
1744
  let promptFile = null;
1570
1745
  let args;
1571
- if (prompt.length > PROMPT_FILE_THRESHOLD && adapterLike.supportsPromptFile) {
1572
- promptFile = (0, prompt_file_1.writePromptFile)(prompt);
1573
- log(`[harness] prompt too long for argv (${prompt.length} chars) — wrote to temp file`, 'stderr');
1574
- if (adapterLike.promptFileFlag) {
1575
- args = adapterLike.headlessArgs(prompt, settings, true);
1576
- args.push(adapterLike.promptFileFlag, promptFile);
1746
+ const inlineArgs = adapterLike.headlessArgs(prompt, settings);
1747
+ const commandLineChars = (0, prompt_file_1.argvChars)([
1748
+ bin.file,
1749
+ ...(bin.args ?? []),
1750
+ ...inlineArgs,
1751
+ ]);
1752
+ if ((0, prompt_file_1.shouldUsePromptFile)(adapterLike, prompt.length, commandLineChars)) {
1753
+ try {
1754
+ promptFile = (0, prompt_file_1.writePromptFile)(prompt);
1755
+ }
1756
+ catch (err) {
1757
+ promptFile = null;
1758
+ log(`[harness] could not write the prompt file (${msg(err)}) - ` +
1759
+ 'falling back to argv, which may exceed the OS command-line limit', 'stderr');
1760
+ }
1761
+ if (promptFile) {
1762
+ log(`[harness] prompt too long for argv (${prompt.length} chars, ` +
1763
+ `~${commandLineChars} char command line) — wrote it to ` +
1764
+ `${promptFile} and passed the path instead`, 'stderr');
1765
+ args = (0, prompt_file_1.promptFileArgs)(adapterLike, adapterLike.headlessArgs(prompt, settings, true), promptFile);
1577
1766
  }
1578
1767
  else {
1579
- args = adapterLike.headlessArgs(prompt, settings, true);
1580
- const shortInstruction = `Read the file "${promptFile}" and follow its instructions exactly. ` +
1581
- 'Do not respond until you have read the file. ' +
1582
- 'The file contains your complete instructions including project context, tools, and the user message.';
1583
- if (['claude', 'gemini', 'antigravity', 'copilot', 'cursor'].includes(name)) {
1584
- args.unshift('-p', shortInstruction);
1585
- }
1586
- else {
1587
- args.push(shortInstruction);
1588
- }
1768
+ args = inlineArgs;
1589
1769
  }
1590
1770
  }
1591
1771
  else {
1592
- args = adapterLike.headlessArgs(prompt, settings);
1772
+ args = inlineArgs;
1593
1773
  }
1594
1774
  let mcpOverride = null;
1595
1775
  if (adapterLike.supportsMcp &&