@buoy-gg/mcp 7.0.27 → 7.0.28

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.
@@ -19,6 +19,35 @@ import { io as ioClient } from "socket.io-client";
19
19
  import {
20
20
  createBroker
21
21
  } from "@buoy-gg/sync-broker";
22
+
23
+ // src/sync/actionTimeout.ts
24
+ function isRecoveryAction(toolId, action) {
25
+ return toolId === "app" && (action === "ping" || action === "reloadApp");
26
+ }
27
+ function isUnresponsive(consecutiveTimeouts) {
28
+ return consecutiveTimeouts >= 2;
29
+ }
30
+ function actionTimeoutMs(input) {
31
+ if (isRecoveryAction(input.toolId, input.action)) {
32
+ return { timeoutMs: input.fullMs, usedFullBudget: false };
33
+ }
34
+ if (!isUnresponsive(input.consecutiveTimeouts)) {
35
+ return { timeoutMs: input.fullMs, usedFullBudget: false };
36
+ }
37
+ if (input.now - input.lastFullBudgetAt > input.recoveryProbeMs) {
38
+ return { timeoutMs: input.fullMs, usedFullBudget: true };
39
+ }
40
+ return { timeoutMs: input.shortMs, usedFullBudget: false };
41
+ }
42
+ function diagnoseCrashed(opts) {
43
+ if (opts.consecutiveTimeouts >= 2) return true;
44
+ if (opts.lastOkAt > 0 && opts.now - opts.lastOkAt > opts.unresponsiveAfterMs) {
45
+ return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ // src/sync/SyncClient.ts
22
51
  var SyncClient = class {
23
52
  constructor(opts = {}) {
24
53
  this.socket = null;
@@ -286,27 +315,34 @@ var SyncClient = class {
286
315
  noteTimeout(deviceId) {
287
316
  this.healthOf(deviceId).consecutiveTimeouts += 1;
288
317
  }
289
- /** True once a device has missed a call and hasn't answered since. */
318
+ /** True once a device has missed TWO calls and hasn't answered since. */
290
319
  isUnresponsive(deviceId) {
291
- return this.healthOf(deviceId).consecutiveTimeouts > 0;
320
+ return isUnresponsive(this.healthOf(deviceId).consecutiveTimeouts);
292
321
  }
293
322
  /**
294
323
  * Full timeout normally; a short one for a device already not answering.
295
324
  *
296
- * With one escape hatch: every RECOVERY_PROBE_MS a flagged device gets the
297
- * full budget again. Without it the flag is self-sustaining — a device whose
298
- * work legitimately takes longer than the short budget (a big snapshot, a
299
- * slow simulator) can never answer, so it can never clear the flag, and every
300
- * later reply is a confident "most likely CRASHED" about a healthy app.
325
+ * A single missed call does not trigger the short budget — one slow
326
+ * `describeScreen` is not a crash. `app.ping` / `app.reloadApp` always get
327
+ * the full budget (they are the recovery path).
328
+ *
329
+ * Escape hatch: every RECOVERY_PROBE_MS a flagged device gets the full
330
+ * budget again so the short timeout cannot become self-sustaining.
301
331
  */
302
- timeoutFor(deviceId) {
303
- if (!this.isUnresponsive(deviceId)) return this.actionTimeoutMs;
332
+ timeoutFor(deviceId, toolId, action) {
304
333
  const h = this.healthOf(deviceId);
305
- if (Date.now() - h.lastFullBudgetAt > RECOVERY_PROBE_MS) {
306
- h.lastFullBudgetAt = Date.now();
307
- return this.actionTimeoutMs;
308
- }
309
- return UNRESPONSIVE_ACTION_TIMEOUT_MS;
334
+ const { timeoutMs, usedFullBudget } = actionTimeoutMs({
335
+ consecutiveTimeouts: h.consecutiveTimeouts,
336
+ lastFullBudgetAt: h.lastFullBudgetAt,
337
+ now: Date.now(),
338
+ fullMs: this.actionTimeoutMs,
339
+ shortMs: UNRESPONSIVE_ACTION_TIMEOUT_MS,
340
+ recoveryProbeMs: RECOVERY_PROBE_MS,
341
+ toolId,
342
+ action
343
+ });
344
+ if (usedFullBudget) h.lastFullBudgetAt = Date.now();
345
+ return timeoutMs;
310
346
  }
311
347
  /**
312
348
  * Plain-language diagnosis for a device that isn't answering, including the
@@ -319,7 +355,12 @@ var SyncClient = class {
319
355
  unresponsiveNote(deviceId) {
320
356
  const h = this.healthOf(deviceId);
321
357
  const since = h.lastOkAt ? Math.round((Date.now() - h.lastOkAt) / 1e3) : null;
322
- const crashed = h.consecutiveTimeouts >= 2 || h.lastOkAt === 0 || Date.now() - h.lastOkAt > UNRESPONSIVE_AFTER_MS;
358
+ const crashed = diagnoseCrashed({
359
+ consecutiveTimeouts: h.consecutiveTimeouts,
360
+ lastOkAt: h.lastOkAt,
361
+ now: Date.now(),
362
+ unresponsiveAfterMs: UNRESPONSIVE_AFTER_MS
363
+ });
323
364
  const lines = [
324
365
  `The app on "${deviceId}" is not responding (${h.consecutiveTimeouts} call${h.consecutiveTimeouts === 1 ? "" : "s"} unanswered${since != null ? `, last answered ${since}s ago` : ""}).`,
325
366
  crashed ? "It is most likely CRASHED \u2014 a fatal render error unmounts the React tree, and the dev tools live in that tree, so nothing on-device can answer until the app is reloaded." : "It may be busy, mid-reload, or crashing."
@@ -521,7 +562,7 @@ var SyncClient = class {
521
562
  const target = deviceId === ALL_DEVICES ? ALL_DEVICES : this.resolveDevice(deviceId);
522
563
  if (!this.socket) throw new Error("SyncClient is not connected");
523
564
  const requestId = randomUUID();
524
- const timeoutMs = opts.timeoutMs ?? this.timeoutFor(target);
565
+ const timeoutMs = opts.timeoutMs ?? this.timeoutFor(target, toolId, action);
525
566
  const result = await new Promise((resolve, reject) => {
526
567
  const timer = setTimeout(() => {
527
568
  this.pendingActions.delete(requestId);
@@ -572,7 +613,7 @@ async function waitForBroker(url, attempts = 20) {
572
613
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
573
614
 
574
615
  // src/version.ts
575
- var VERSION = "7.0.27" ? "7.0.27" : "0.0.0-dev";
616
+ var VERSION = "7.0.28" ? "7.0.28" : "0.0.0-dev";
576
617
  var PACKAGE_NAME = "@buoy-gg/mcp";
577
618
 
578
619
  // src/update/checkForUpdate.ts
@@ -1575,10 +1616,20 @@ _Showing ${res.returned} of ${res.totalAvailable} captured event(s)${more}._`;
1575
1616
 
1576
1617
  // src/tools/packs/reactQuery.ts
1577
1618
  import { z as z4 } from "zod";
1578
- var RQ_TOOL_ID = "react-query";
1619
+ var RQ_TOOL_ID = "query";
1620
+ var RQ_TOOL_ID_LEGACY = "react-query";
1579
1621
  var text3 = (s) => ({ content: [{ type: "text", text: s }] });
1580
1622
  var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1581
1623
  var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
1624
+ async function callQueryAction(sync, action, params, target) {
1625
+ try {
1626
+ return await sync.callAction(RQ_TOOL_ID, action, params, target);
1627
+ } catch (e) {
1628
+ const msg = errMsg4(e);
1629
+ if (!/unknown tool/i.test(msg)) throw e;
1630
+ return await sync.callAction(RQ_TOOL_ID_LEGACY, action, params, target);
1631
+ }
1632
+ }
1582
1633
  var STATUS_ICON = {
1583
1634
  fresh: "\u{1F7E2}",
1584
1635
  stale: "\u{1F7E1}",
@@ -1611,8 +1662,8 @@ function registerReactQueryTools(server, sync) {
1611
1662
  async (args) => {
1612
1663
  try {
1613
1664
  const target = sync.requireProDevice(args.deviceId);
1614
- const res = await sync.callAction(
1615
- RQ_TOOL_ID,
1665
+ const res = await callQueryAction(
1666
+ sync,
1616
1667
  "listQueries",
1617
1668
  { includeData: args.includeData ?? false, staleOnly: args.staleOnly ?? false, limit: args.limit ?? 25 },
1618
1669
  target
@@ -1658,13 +1709,13 @@ ${lines.join("\n")}${note}`);
1658
1709
  try {
1659
1710
  const target = sync.requireProDevice(args.deviceId);
1660
1711
  if (args.action === "clear") {
1661
- await sync.callAction(RQ_TOOL_ID, "clearQueryCache", {}, target);
1712
+ await callQueryAction(sync, "clearQueryCache", {}, target);
1662
1713
  return text3("Cleared the entire React Query cache.");
1663
1714
  }
1664
1715
  if (!args.queryHash) {
1665
1716
  return fail4(`'${args.action}' needs a queryHash. Call get_react_query first and use a row's hash (in backticks).`);
1666
1717
  }
1667
- await sync.callAction(RQ_TOOL_ID, args.action, { queryHash: args.queryHash }, target);
1718
+ await callQueryAction(sync, args.action, { queryHash: args.queryHash }, target);
1668
1719
  return text3(`Ran ${args.action} on query \`${args.queryHash}\`.`);
1669
1720
  } catch (e) {
1670
1721
  return fail4(errMsg4(e));
@@ -2091,17 +2142,23 @@ ${shown.map((r) => `- \`${r}\``).join("\n")}`
2091
2142
  server.registerTool(
2092
2143
  "navigate",
2093
2144
  {
2094
- description: "Navigate the app to a route path (e.g. '/pokemon/25' or '/settings'). Use a concrete path \u2014 resolve any dynamic segments (like [id]) to real values first. See get_routes with includeSitemap:true for valid paths.",
2145
+ description: "Navigate the app to a route path (e.g. '/pokemon/25' or '/settings'). Use a concrete path \u2014 resolve any dynamic segments (like [id]) to real values first. See get_routes with includeSitemap:true for valid paths. Pushes by default (back still works); pass replace:true to leave the current screen instead of stacking on top of it (e.g. resetting to '/').",
2095
2146
  inputSchema: {
2096
2147
  path: z9.string().describe("The concrete path to navigate to, e.g. '/pokemon/25'."),
2148
+ replace: z9.boolean().optional().describe("Replace the current screen instead of pushing (use when resetting, e.g. to '/')."),
2097
2149
  deviceId: z9.string().optional().describe("Target device id (auto-selected if one is connected).")
2098
2150
  }
2099
2151
  },
2100
2152
  async (args) => {
2101
2153
  try {
2102
2154
  const target = sync.requireProDevice(args.deviceId);
2103
- await sync.callAction(ROUTES_TOOL_ID, "navigate", { path: args.path }, target);
2104
- return text8(`Navigated to \`${args.path}\`.`);
2155
+ await sync.callAction(
2156
+ ROUTES_TOOL_ID,
2157
+ "navigate",
2158
+ { path: args.path, replace: args.replace === true },
2159
+ target
2160
+ );
2161
+ return text8(`Navigated to \`${args.path}\`${args.replace ? " (replaced current screen)" : ""}.`);
2105
2162
  } catch (e) {
2106
2163
  return fail9(errMsg9(e));
2107
2164
  }
@@ -2248,6 +2305,40 @@ async function cropToBuffer(srcPath, full, region) {
2248
2305
  }
2249
2306
  }
2250
2307
 
2308
+ // src/tools/packs/screenshotLocate.ts
2309
+ function pickDescribeScreenMatch(elements, query, nativeTag) {
2310
+ if (nativeTag != null) {
2311
+ const exact = elements.find((e) => e.nativeTag === nativeTag);
2312
+ if (exact) return exact;
2313
+ }
2314
+ const q = (query ?? "").trim().toLowerCase();
2315
+ if (!q) return void 0;
2316
+ let best;
2317
+ for (const el of elements) {
2318
+ const fields = [el.testID, el.label, el.text, el.name].filter(Boolean).map((s) => s.toLowerCase());
2319
+ let score = 0;
2320
+ for (const f of fields) {
2321
+ if (f === q) score = Math.max(score, 100);
2322
+ else if (f.startsWith(q)) score = Math.max(score, 50);
2323
+ else if (f.includes(q)) score = Math.max(score, 25);
2324
+ }
2325
+ const tap = el.tap;
2326
+ if (tap && tap.x >= 0 && tap.x <= 1 && tap.y >= 0 && tap.y <= 1) score += 20;
2327
+ else if (tap) score -= 40;
2328
+ if (score <= 0) continue;
2329
+ if (!best || score > best.score) best = { el, score };
2330
+ }
2331
+ return best?.el;
2332
+ }
2333
+ function frameToRect(frame, screen) {
2334
+ return {
2335
+ x: frame.x * screen.width,
2336
+ y: frame.y * screen.height,
2337
+ width: frame.width * screen.width,
2338
+ height: frame.height * screen.height
2339
+ };
2340
+ }
2341
+
2251
2342
  // src/tools/packs/screenshot.ts
2252
2343
  var TOOL_ID = "highlight-updates";
2253
2344
  var MARGIN_PT = 12;
@@ -2257,7 +2348,7 @@ var errMsg11 = (e) => e instanceof Error ? e.message : String(e);
2257
2348
  function reasonMessage(res, query) {
2258
2349
  switch (res.reason) {
2259
2350
  case "no-renders":
2260
- return "No components are tracked yet. Navigate to the screen with the target component in the running app (it needs to render at least once), then retry.";
2351
+ return "No components matched via highlight tracking or the live fiber walk. Pass a testID / name from describe_screen, then retry.";
2261
2352
  case "no-match":
2262
2353
  return query ? `Nothing matched "${query}".` : "No query or nativeTag given \u2014 pass one, or a testID/name to search for.";
2263
2354
  case "no-measurement":
@@ -2281,7 +2372,7 @@ function registerScreenshotTools(server, sync) {
2281
2372
  server.registerTool(
2282
2373
  "screenshot_component",
2283
2374
  {
2284
- description: "Take a tight, cropped screenshot of a single React Native component running in the iOS Simulator \u2014 no manual coordinates needed. Give a testID, nativeID, accessibilityLabel, or component name in `query` (or an exact `nativeTag` from a previous call's candidates) and it locates the live component on-device (scrolling it into view), then captures + crops the Simulator framebuffer to just that component. The component must have rendered at least once since the app opened for it to be trackable \u2014 if nothing matches, navigate to the screen that renders it and retry. iOS Simulator only, and the MCP server must be running on the same Mac as the booted Simulator (uses `xcrun simctl` + `sips`). Requires Buoy Pro.",
2375
+ description: "Take a tight, cropped screenshot of a single React Native component running in the iOS Simulator \u2014 no manual coordinates needed. Give a testID, nativeID, accessibilityLabel, or component name in `query` (or an exact `nativeTag` from a previous call's candidates) and it locates the live component on-device (scrolling it into view), then captures + crops the Simulator framebuffer to just that component. If highlight tracking hasn't seen a render yet, it falls back to a live fiber walk (same as describe_screen). iOS Simulator only, and the MCP server must run on the same Mac as the booted Simulator. Requires Buoy Pro.",
2285
2376
  inputSchema: {
2286
2377
  query: z11.string().optional().describe('Fuzzy match against testID, nativeID, accessibilityLabel, component name, or view type \u2014 e.g. "submit-button".'),
2287
2378
  nativeTag: z11.number().optional().describe("An exact nativeTag (from a previous call's candidates) to skip fuzzy matching."),
@@ -2305,15 +2396,37 @@ function registerScreenshotTools(server, sync) {
2305
2396
  },
2306
2397
  target
2307
2398
  );
2308
- if (!res.matched || !res.rect || !res.screen) {
2309
- return fail11(reasonMessage(res, args.query) + candidatesText(res.candidates));
2399
+ let located = res;
2400
+ if (!located.matched && (located.reason === "no-renders" || located.reason === "no-match")) {
2401
+ const screen = await sync.callAction(TOOL_ID, "describeScreen", {}, target).catch(() => null);
2402
+ const match = screen?.elements ? pickDescribeScreenMatch(screen.elements, args.query, args.nativeTag) : void 0;
2403
+ if (match?.frame && screen?.screen) {
2404
+ located = {
2405
+ matched: true,
2406
+ live: true,
2407
+ rect: frameToRect(match.frame, screen.screen),
2408
+ screen: screen.screen,
2409
+ nativeTag: match.nativeTag ?? void 0,
2410
+ componentName: match.name,
2411
+ testID: match.testID
2412
+ };
2413
+ } else if (located.reason === "no-renders") {
2414
+ located = {
2415
+ matched: false,
2416
+ reason: "no-match",
2417
+ candidates: located.candidates
2418
+ };
2419
+ }
2420
+ }
2421
+ if (!located.matched || !located.rect || !located.screen) {
2422
+ return fail11(reasonMessage(located, args.query) + candidatesText(located.candidates));
2310
2423
  }
2311
2424
  const shot = await captureIosFramebuffer();
2312
2425
  if (!shot.ok || !shot.srcPath || !shot.width || !shot.height) {
2313
2426
  return fail11(shot.error ?? "Screenshot capture failed.");
2314
2427
  }
2315
- const pxScale = shot.width / res.screen.width;
2316
- const r = res.rect;
2428
+ const pxScale = shot.width / located.screen.width;
2429
+ const r = located.rect;
2317
2430
  const pad = CAPTURE_PADDING_PT;
2318
2431
  const crop = await cropToBuffer(
2319
2432
  shot.srcPath,
@@ -2328,9 +2441,9 @@ function registerScreenshotTools(server, sync) {
2328
2441
  if (!crop.ok || !crop.buffer) {
2329
2442
  return fail11(crop.error ?? "Crop failed.");
2330
2443
  }
2331
- const label = res.componentName || "component";
2332
- const idPart = res.testID ? ` (testID: "${res.testID}")` : "";
2333
- const warn = res.inView === false ? "\n\nNote: the component is partly off-screen even after scrolling, so the crop is clamped to the screen edge." : "";
2444
+ const label = located.componentName || "component";
2445
+ const idPart = located.testID ? ` (testID: "${located.testID}")` : "";
2446
+ const warn = located.inView === false ? "\n\nNote: the component is partly off-screen even after scrolling, so the crop is clamped to the screen edge." : "";
2334
2447
  const summary = `Captured "${label}"${idPart} \u2014 ${crop.width}\xD7${crop.height}px.${warn}`;
2335
2448
  return {
2336
2449
  content: [
@@ -2378,7 +2491,14 @@ function statusIcon(e) {
2378
2491
  return "\u2705";
2379
2492
  }
2380
2493
  function statusLabel(e) {
2381
- if (e.error) return `ERR ${e.error}`;
2494
+ if (e.status !== void 0 && e.status > 0) return String(e.status);
2495
+ if (e.error) {
2496
+ const cleaned = e.error.replace(/^HTTP\s+\d+:\s*/i, "").trim();
2497
+ if (!cleaned || cleaned === "undefined" || cleaned === "no error") {
2498
+ return "ERR";
2499
+ }
2500
+ return `ERR ${e.error}`;
2501
+ }
2382
2502
  if (e.status === void 0) return "pending";
2383
2503
  return String(e.status);
2384
2504
  }
@@ -3538,6 +3658,19 @@ ${JSON.stringify(slim)}
3538
3658
 
3539
3659
  // src/tools/packs/assets.ts
3540
3660
  import { z as z17 } from "zod";
3661
+
3662
+ // src/tools/packs/assetsGraphNote.ts
3663
+ function graphUnavailableNote(ss) {
3664
+ if (ss.graphSupported && ss.graphCount != null) return null;
3665
+ const err = (ss.graphError ?? "").trim();
3666
+ const looksLikeDev = ss.dev === true || !!err;
3667
+ if (looksLikeDev) {
3668
+ return err ? `\u26A0 Metro bundle graph unavailable (${err}) \u2014 showing runtime-registered assets only; never-loaded detection unavailable.` : "\u26A0 Metro bundle graph unavailable \u2014 showing runtime-registered assets only; never-loaded detection unavailable.";
3669
+ }
3670
+ return "\u26A0 No Metro bundle graph (release build, or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable.";
3671
+ }
3672
+
3673
+ // src/tools/packs/assets.ts
3541
3674
  var ASSETS_TOOL_ID = "assets";
3542
3675
  var text15 = (s) => ({ content: [{ type: "text", text: s }] });
3543
3676
  var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
@@ -3580,11 +3713,8 @@ function formatListMarkdown2(list, limit) {
3580
3713
  if (ss.fontFamilies.length > 0) {
3581
3714
  lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
3582
3715
  }
3583
- if (!ss.graphSupported || ss.graphCount == null) {
3584
- lines.push(
3585
- "\u26A0 No Metro bundle graph (release build or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable." + (ss.graphError ? ` (${ss.graphError})` : "")
3586
- );
3587
- }
3716
+ const graphNote = graphUnavailableNote(ss);
3717
+ if (graphNote) lines.push(graphNote);
3588
3718
  if (ss.measuring) lines.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");
3589
3719
  const ins = list.insights;
3590
3720
  const chips = [];
@@ -3873,16 +4003,14 @@ function registerReloadTools(server, sync) {
3873
4003
  const previousRealm = args.wait === false ? null : await ping(sync, target, PROBE_TIMEOUT_MS);
3874
4004
  let ack = null;
3875
4005
  let recovered = false;
3876
- try {
3877
- ack = await sync.callAction(
3878
- APP_TOOL_ID,
3879
- RELOAD_ACTION,
3880
- { strategy: args.strategy ?? "auto" },
3881
- target
3882
- );
3883
- } catch (inAppError) {
4006
+ const tryDevServer = async (inAppError) => {
3884
4007
  const origin = sync.devServerOrigin(target);
3885
- if (!origin) throw inAppError;
4008
+ if (!origin) {
4009
+ if (inAppError) throw inAppError;
4010
+ throw new Error(
4011
+ "The app is not answering and no dev-server origin is known (it was never learned from a successful ping). Reload the app by hand."
4012
+ );
4013
+ }
3886
4014
  const res = await reloadViaDevServer(origin);
3887
4015
  if (!res.ok) {
3888
4016
  throw new Error(
@@ -3890,6 +4018,20 @@ function registerReloadTools(server, sync) {
3890
4018
  );
3891
4019
  }
3892
4020
  recovered = true;
4021
+ };
4022
+ if (sync.isUnresponsive(target) && sync.devServerOrigin(target)) {
4023
+ await tryDevServer();
4024
+ } else {
4025
+ try {
4026
+ ack = await sync.callAction(
4027
+ APP_TOOL_ID,
4028
+ RELOAD_ACTION,
4029
+ { strategy: args.strategy ?? "auto" },
4030
+ target
4031
+ );
4032
+ } catch (inAppError) {
4033
+ await tryDevServer(inAppError);
4034
+ }
3893
4035
  }
3894
4036
  const via = recovered ? ` via the dev server (the app was unresponsive \u2014 its own reload could not be reached)` : ack?.strategy ? ` via ${ack.strategy}` : "";
3895
4037
  if (args.wait === false) {
@@ -4899,9 +5041,8 @@ function runInit(opts = {}) {
4899
5041
  const entry = resolveEntry(opts, log);
4900
5042
  const done = [];
4901
5043
  upsertMcpConfig(path4.join(cwd, ".mcp.json"), "mcpServers", entry, cwd, done);
4902
- if (fs4.existsSync(path4.join(cwd, ".cursor"))) {
4903
- upsertMcpConfig(path4.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
4904
- }
5044
+ fs4.mkdirSync(path4.join(cwd, ".cursor"), { recursive: true });
5045
+ upsertMcpConfig(path4.join(cwd, ".cursor", "mcp.json"), "mcpServers", entry, cwd, done);
4905
5046
  if (fs4.existsSync(path4.join(cwd, ".vscode"))) {
4906
5047
  upsertMcpConfig(path4.join(cwd, ".vscode", "mcp.json"), "servers", entry, cwd, done);
4907
5048
  }