@buoy-gg/mcp 7.0.12 → 7.0.14

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.
@@ -301,7 +301,7 @@ async function waitForBroker(url, attempts = 20) {
301
301
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
302
302
 
303
303
  // src/version.ts
304
- var VERSION = "7.0.12" ? "7.0.12" : "0.0.0-dev";
304
+ var VERSION = "7.0.14" ? "7.0.14" : "0.0.0-dev";
305
305
  var PACKAGE_NAME = "@buoy-gg/mcp";
306
306
 
307
307
  // src/update/checkForUpdate.ts
@@ -2033,6 +2033,10 @@ var NETWORK_TOOL_ID = "network";
2033
2033
  var text10 = (s) => ({ content: [{ type: "text", text: s }] });
2034
2034
  var fail12 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2035
2035
  var errMsg12 = (e) => e instanceof Error ? e.message : String(e);
2036
+ var masterNote = (state) => {
2037
+ if (state?.enabled !== false) return "";
2038
+ return state.autoPaused ? ' \u2014 AUTO-PAUSED after the app failed to settle on launch with overrides active; nothing is firing. Re-arm with network_override({action:"enable"}).' : " \u2014 MASTER SWITCH IS OFF, nothing is firing";
2039
+ };
2036
2040
  function parseSnapshot(payload) {
2037
2041
  if (Array.isArray(payload)) return { events: payload, saved: [] };
2038
2042
  if (payload && typeof payload === "object") {
@@ -2246,6 +2250,222 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
2246
2250
  }
2247
2251
  }
2248
2252
  );
2253
+ server.registerTool(
2254
+ "network_override",
2255
+ {
2256
+ description: "Force matching HTTP requests to return a chosen status/body, to fail outright, or to arrive late \u2014 without touching the backend or the app's code. This is how you drive an app into a state it won't reach on its own: force a 500 and see whether the error UI appears, return [] and check the empty state, simulate offline or a slow connection. Rules run INSIDE the app and survive an app reload, so they also cover the boot path. After adding one, trigger the request (navigate/tap_element) and read the result with get_network_requests or describe_screen. ALWAYS remove your rules when done \u2014 a forgotten rule looks exactly like a real bug. Requires Buoy Pro.",
2257
+ inputSchema: {
2258
+ action: z12.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
2259
+ "add creates a rule (or replaces one when `id` is given). remove drops one by `id`. clear drops all. enable/disable flip ONE rule when `id` is given, otherwise the master switch \u2014 rules are kept either way. list shows every rule with its hit counts; pass `id` to dump one rule in full, body included. debug reports why a rule is or isn't firing."
2260
+ ),
2261
+ urlPattern: z12.string().optional().describe(
2262
+ "For `add`: glob matched against the WHOLE request URL, query string included. `*` is the only wildcard, e.g. '*pokeapi.co*' or 'https://api.example.com/v1/users*'. Careful with an exact URL copied from get_network_requests \u2014 if the client cache-busts or pages, the next call won't match it and the rule sits at 0 hits. Prefer `fromRequestId`, which handles that for you."
2263
+ ),
2264
+ fromRequestId: z12.string().optional().describe(
2265
+ "For `add`: build the rule from a captured request id (from get_network_requests) \u2014 the same 'Override this request' the developer gets in the UI. Seeds the pattern (query string replaced with `*` so it keeps matching), the method, the status, and the REAL response body, so you can change two fields of a payload you just saw instead of authoring one from scratch. Anything you pass explicitly overrides the prefill."
2266
+ ),
2267
+ methods: z12.array(z12.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
2268
+ kind: z12.enum(["respond", "fail", "delay"]).optional().describe(
2269
+ "respond = return your status/body, never hitting the network (default). fail = a transport failure, as if the device were offline. delay = let the real request run, just late."
2270
+ ),
2271
+ status: z12.number().optional().describe(
2272
+ "For kind 'respond': HTTP status, 200\u2013599 (default 200). Outside that range fetch itself throws, so use kind 'fail' for connection errors rather than status 0."
2273
+ ),
2274
+ body: z12.union([z12.string(), z12.record(z12.unknown()), z12.array(z12.unknown())]).optional().describe(
2275
+ "For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
2276
+ ),
2277
+ statusText: z12.string().optional().describe(
2278
+ "For kind 'respond': the status line's text, e.g. 'Service Unavailable'. Cosmetic \u2014 React Native's XMLHttpRequest has no statusText field, so the app never sees it; it only makes the developer's rule list read correctly."
2279
+ ),
2280
+ headers: z12.record(z12.string()).optional().describe("For kind 'respond': response headers to send."),
2281
+ failKind: z12.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
2282
+ delayMs: z12.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
2283
+ times: z12.number().optional().describe(
2284
+ "Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."
2285
+ ),
2286
+ alternate: z12.boolean().optional().describe(
2287
+ "Apply to every OTHER matching request, forever: the first goes through, the second is overridden, and so on. This is how you test retry logic and flaky endpoints \u2014 a rule that is always on or always off can't reach those paths."
2288
+ ),
2289
+ name: z12.string().optional().describe("Label shown in the developer's UI."),
2290
+ enabled: z12.boolean().optional().describe(
2291
+ "For `add`: create the rule switched off (default true). Useful for staging a rule you'll arm later with enable({id})."
2292
+ ),
2293
+ id: z12.string().optional().describe(
2294
+ "Rule id. Required for `remove`. For `add` it replaces that rule. For `enable`/`disable` it targets ONE rule instead of the master switch. For `list` it dumps that rule in full."
2295
+ ),
2296
+ url: z12.string().optional().describe(
2297
+ "For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."
2298
+ ),
2299
+ deviceId: z12.string().optional().describe("Target device id (auto-selected if one is connected).")
2300
+ }
2301
+ },
2302
+ async (args) => {
2303
+ try {
2304
+ const target = sync.requireProDevice(args.deviceId);
2305
+ switch (args.action) {
2306
+ case "add": {
2307
+ if (!args.urlPattern && !args.fromRequestId) {
2308
+ return fail12(
2309
+ "`add` needs a `urlPattern` or a `fromRequestId`. Take an id from get_network_requests to override a request you just saw, or pass a glob like '*pokeapi.co*'."
2310
+ );
2311
+ }
2312
+ const res = await sync.callAction(
2313
+ NETWORK_TOOL_ID,
2314
+ "upsertOverrideRule",
2315
+ {
2316
+ fromRequestId: args.fromRequestId,
2317
+ rule: {
2318
+ id: args.id,
2319
+ name: args.name,
2320
+ urlPattern: args.urlPattern,
2321
+ methods: args.methods,
2322
+ // Not defaulted here — the device defaults to "respond" too,
2323
+ // and leaving it absent lets a `fromRequestId` prefill win.
2324
+ kind: args.kind,
2325
+ status: args.status,
2326
+ statusText: args.statusText,
2327
+ body: args.body,
2328
+ headers: args.headers,
2329
+ failKind: args.failKind,
2330
+ delayMs: args.delayMs,
2331
+ times: args.times,
2332
+ alternate: args.alternate,
2333
+ enabled: args.enabled !== false
2334
+ }
2335
+ },
2336
+ target
2337
+ );
2338
+ const shaped = res;
2339
+ if (!shaped?.ok) {
2340
+ return fail12(shaped?.error ?? `Could not add the rule on ${target}.`);
2341
+ }
2342
+ const pattern = shaped.rule?.urlPattern ?? args.urlPattern;
2343
+ const armed = shaped.rule?.enabled === false ? `
2344
+
2345
+ It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${shaped.rule?.id}"}).` : "";
2346
+ return text10(
2347
+ `Override added on ${target} \u2014 \`${shaped.rule?.id}\` matching \`${pattern}\`.${armed}
2348
+
2349
+ Trigger the request now (navigate / tap_element), then check it with get_network_requests or describe_screen. Remove it with network_override({action:"remove", id:"${shaped.rule?.id}"}) when you're done.`
2350
+ );
2351
+ }
2352
+ case "remove": {
2353
+ if (!args.id) {
2354
+ return fail12('`remove` needs an `id`. List them with network_override({action:"list"}).');
2355
+ }
2356
+ await sync.callAction(NETWORK_TOOL_ID, "deleteOverrideRule", { id: args.id }, target);
2357
+ return text10(`Removed override \`${args.id}\` on ${target}.`);
2358
+ }
2359
+ case "clear":
2360
+ await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
2361
+ return text10(`Removed all override rules on ${target}. Traffic is real again.`);
2362
+ case "enable":
2363
+ case "disable": {
2364
+ const enabled = args.action === "enable";
2365
+ if (args.id) {
2366
+ await sync.callAction(
2367
+ NETWORK_TOOL_ID,
2368
+ "setOverrideRuleEnabled",
2369
+ { id: args.id, enabled },
2370
+ target
2371
+ );
2372
+ return text10(
2373
+ `Rule \`${args.id}\` is now ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
2374
+ );
2375
+ }
2376
+ await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
2377
+ return text10(
2378
+ enabled ? `Overrides switched ON for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
2379
+ );
2380
+ }
2381
+ case "list": {
2382
+ const res = await sync.callAction(
2383
+ NETWORK_TOOL_ID,
2384
+ "listOverrideRules",
2385
+ {},
2386
+ target
2387
+ );
2388
+ const rules = res?.rules ?? [];
2389
+ if (args.id) {
2390
+ const rule = rules.find((r) => r.id === args.id);
2391
+ if (!rule) {
2392
+ return fail12(
2393
+ `No override rule \`${args.id}\` on ${target}. List them with network_override({action:"list"}).`
2394
+ );
2395
+ }
2396
+ return text10(
2397
+ `**Override rule \`${rule.id}\`** on ${target}${masterNote(res)}
2398
+
2399
+ \`\`\`json
2400
+ ${JSON.stringify(rule, null, 2)}
2401
+ \`\`\``
2402
+ );
2403
+ }
2404
+ if (rules.length === 0) {
2405
+ return text10(
2406
+ `No override rules on ${target}.
2407
+
2408
+ Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
2409
+ );
2410
+ }
2411
+ const rows = rules.map((rule, i) => {
2412
+ const what = rule.kind === "respond" ? `\u2192 ${rule.status ?? 200}${rule.statusText ? ` ${rule.statusText}` : ""}` : rule.kind === "fail" ? `\u2192 ${rule.failKind === "timeout" ? "timeout" : "network error"}` : "\u2192 real response";
2413
+ const bits = [
2414
+ rule.methods?.length ? rule.methods.join("/") : "any method",
2415
+ what,
2416
+ // Enough to tell WHICH body this is without printing it.
2417
+ rule.kind === "respond" && rule.body ? `body ${rule.body.length}B` : "",
2418
+ rule.headers && Object.keys(rule.headers).length ? `${Object.keys(rule.headers).length} header(s)` : "",
2419
+ rule.delayMs ? `${rule.delayMs}ms delay` : "",
2420
+ rule.alternate ? "every other request" : "",
2421
+ typeof rule.times === "number" ? `${rule.times}\xD7 then off` : "",
2422
+ `${rule.hits} hit${rule.hits === 1 ? "" : "s"}`,
2423
+ // Only interesting when the two can disagree: `alternate` and
2424
+ // spent `times` rules match without applying, and a gap between
2425
+ // the numbers is the explanation for "it matched but nothing
2426
+ // happened".
2427
+ rule.seen != null && rule.seen !== rule.hits ? `${rule.seen} matched` : "",
2428
+ rule.enabled ? "" : "DISABLED"
2429
+ ].filter(Boolean);
2430
+ return `${i + 1}. \`${rule.id}\`${rule.name ? ` ${rule.name}` : ""}
2431
+ \u2514\u2500 \`${rule.urlPattern}\` \xB7 ${bits.join(" \xB7 ")}`;
2432
+ });
2433
+ return text10(
2434
+ `**Override rules on ${target}**${masterNote(res)}
2435
+
2436
+ ${rows.join("\n")}
2437
+
2438
+ Full detail incl. body: network_override({action:"list", id:"${rules[0]?.id}"}).`
2439
+ );
2440
+ }
2441
+ case "debug": {
2442
+ const res = await sync.callAction(
2443
+ NETWORK_TOOL_ID,
2444
+ "debugOverrides",
2445
+ { url: args.url },
2446
+ target
2447
+ );
2448
+ if (!res) {
2449
+ return fail12(
2450
+ `${target} did not answer the diagnostics call \u2014 the network tool may not be mounted.`
2451
+ );
2452
+ }
2453
+ return text10(
2454
+ `**Override diagnostics on ${target}**${args.url ? ` for \`${args.url}\`` : ""}
2455
+
2456
+ \`\`\`json
2457
+ ${JSON.stringify(res, null, 2)}
2458
+ \`\`\`
2459
+
2460
+ A rule that never fires is usually one of: the master switch is off, the interceptor isn't installed (nothing is subscribed to the network tool), or the pattern doesn't match the FULL url including its query string.`
2461
+ );
2462
+ }
2463
+ }
2464
+ } catch (e) {
2465
+ return fail12(errMsg12(e));
2466
+ }
2467
+ }
2468
+ );
2249
2469
  }
2250
2470
 
2251
2471
  // src/tools/packs/interact.ts
package/dist/cli.js CHANGED
@@ -341,7 +341,7 @@ var import_node_os = __toESM(require("os"));
341
341
  var import_node_path = __toESM(require("path"));
342
342
 
343
343
  // src/version.ts
344
- var VERSION = "7.0.12" ? "7.0.12" : "0.0.0-dev";
344
+ var VERSION = "7.0.14" ? "7.0.14" : "0.0.0-dev";
345
345
  var PACKAGE_NAME = "@buoy-gg/mcp";
346
346
 
347
347
  // src/update/checkForUpdate.ts
@@ -1296,6 +1296,10 @@ var NETWORK_TOOL_ID = "network";
1296
1296
  var text3 = (s) => ({ content: [{ type: "text", text: s }] });
1297
1297
  var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1298
1298
  var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
1299
+ var masterNote = (state) => {
1300
+ if (state?.enabled !== false) return "";
1301
+ return state.autoPaused ? ' \u2014 AUTO-PAUSED after the app failed to settle on launch with overrides active; nothing is firing. Re-arm with network_override({action:"enable"}).' : " \u2014 MASTER SWITCH IS OFF, nothing is firing";
1302
+ };
1299
1303
  function parseSnapshot(payload) {
1300
1304
  if (Array.isArray(payload)) return { events: payload, saved: [] };
1301
1305
  if (payload && typeof payload === "object") {
@@ -1509,6 +1513,222 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
1509
1513
  }
1510
1514
  }
1511
1515
  );
1516
+ server.registerTool(
1517
+ "network_override",
1518
+ {
1519
+ description: "Force matching HTTP requests to return a chosen status/body, to fail outright, or to arrive late \u2014 without touching the backend or the app's code. This is how you drive an app into a state it won't reach on its own: force a 500 and see whether the error UI appears, return [] and check the empty state, simulate offline or a slow connection. Rules run INSIDE the app and survive an app reload, so they also cover the boot path. After adding one, trigger the request (navigate/tap_element) and read the result with get_network_requests or describe_screen. ALWAYS remove your rules when done \u2014 a forgotten rule looks exactly like a real bug. Requires Buoy Pro.",
1520
+ inputSchema: {
1521
+ action: import_zod4.z.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
1522
+ "add creates a rule (or replaces one when `id` is given). remove drops one by `id`. clear drops all. enable/disable flip ONE rule when `id` is given, otherwise the master switch \u2014 rules are kept either way. list shows every rule with its hit counts; pass `id` to dump one rule in full, body included. debug reports why a rule is or isn't firing."
1523
+ ),
1524
+ urlPattern: import_zod4.z.string().optional().describe(
1525
+ "For `add`: glob matched against the WHOLE request URL, query string included. `*` is the only wildcard, e.g. '*pokeapi.co*' or 'https://api.example.com/v1/users*'. Careful with an exact URL copied from get_network_requests \u2014 if the client cache-busts or pages, the next call won't match it and the rule sits at 0 hits. Prefer `fromRequestId`, which handles that for you."
1526
+ ),
1527
+ fromRequestId: import_zod4.z.string().optional().describe(
1528
+ "For `add`: build the rule from a captured request id (from get_network_requests) \u2014 the same 'Override this request' the developer gets in the UI. Seeds the pattern (query string replaced with `*` so it keeps matching), the method, the status, and the REAL response body, so you can change two fields of a payload you just saw instead of authoring one from scratch. Anything you pass explicitly overrides the prefill."
1529
+ ),
1530
+ methods: import_zod4.z.array(import_zod4.z.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
1531
+ kind: import_zod4.z.enum(["respond", "fail", "delay"]).optional().describe(
1532
+ "respond = return your status/body, never hitting the network (default). fail = a transport failure, as if the device were offline. delay = let the real request run, just late."
1533
+ ),
1534
+ status: import_zod4.z.number().optional().describe(
1535
+ "For kind 'respond': HTTP status, 200\u2013599 (default 200). Outside that range fetch itself throws, so use kind 'fail' for connection errors rather than status 0."
1536
+ ),
1537
+ body: import_zod4.z.union([import_zod4.z.string(), import_zod4.z.record(import_zod4.z.unknown()), import_zod4.z.array(import_zod4.z.unknown())]).optional().describe(
1538
+ "For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
1539
+ ),
1540
+ statusText: import_zod4.z.string().optional().describe(
1541
+ "For kind 'respond': the status line's text, e.g. 'Service Unavailable'. Cosmetic \u2014 React Native's XMLHttpRequest has no statusText field, so the app never sees it; it only makes the developer's rule list read correctly."
1542
+ ),
1543
+ headers: import_zod4.z.record(import_zod4.z.string()).optional().describe("For kind 'respond': response headers to send."),
1544
+ failKind: import_zod4.z.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
1545
+ delayMs: import_zod4.z.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
1546
+ times: import_zod4.z.number().optional().describe(
1547
+ "Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."
1548
+ ),
1549
+ alternate: import_zod4.z.boolean().optional().describe(
1550
+ "Apply to every OTHER matching request, forever: the first goes through, the second is overridden, and so on. This is how you test retry logic and flaky endpoints \u2014 a rule that is always on or always off can't reach those paths."
1551
+ ),
1552
+ name: import_zod4.z.string().optional().describe("Label shown in the developer's UI."),
1553
+ enabled: import_zod4.z.boolean().optional().describe(
1554
+ "For `add`: create the rule switched off (default true). Useful for staging a rule you'll arm later with enable({id})."
1555
+ ),
1556
+ id: import_zod4.z.string().optional().describe(
1557
+ "Rule id. Required for `remove`. For `add` it replaces that rule. For `enable`/`disable` it targets ONE rule instead of the master switch. For `list` it dumps that rule in full."
1558
+ ),
1559
+ url: import_zod4.z.string().optional().describe(
1560
+ "For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."
1561
+ ),
1562
+ deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1563
+ }
1564
+ },
1565
+ async (args) => {
1566
+ try {
1567
+ const target = sync.requireProDevice(args.deviceId);
1568
+ switch (args.action) {
1569
+ case "add": {
1570
+ if (!args.urlPattern && !args.fromRequestId) {
1571
+ return fail4(
1572
+ "`add` needs a `urlPattern` or a `fromRequestId`. Take an id from get_network_requests to override a request you just saw, or pass a glob like '*pokeapi.co*'."
1573
+ );
1574
+ }
1575
+ const res = await sync.callAction(
1576
+ NETWORK_TOOL_ID,
1577
+ "upsertOverrideRule",
1578
+ {
1579
+ fromRequestId: args.fromRequestId,
1580
+ rule: {
1581
+ id: args.id,
1582
+ name: args.name,
1583
+ urlPattern: args.urlPattern,
1584
+ methods: args.methods,
1585
+ // Not defaulted here — the device defaults to "respond" too,
1586
+ // and leaving it absent lets a `fromRequestId` prefill win.
1587
+ kind: args.kind,
1588
+ status: args.status,
1589
+ statusText: args.statusText,
1590
+ body: args.body,
1591
+ headers: args.headers,
1592
+ failKind: args.failKind,
1593
+ delayMs: args.delayMs,
1594
+ times: args.times,
1595
+ alternate: args.alternate,
1596
+ enabled: args.enabled !== false
1597
+ }
1598
+ },
1599
+ target
1600
+ );
1601
+ const shaped = res;
1602
+ if (!shaped?.ok) {
1603
+ return fail4(shaped?.error ?? `Could not add the rule on ${target}.`);
1604
+ }
1605
+ const pattern = shaped.rule?.urlPattern ?? args.urlPattern;
1606
+ const armed = shaped.rule?.enabled === false ? `
1607
+
1608
+ It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${shaped.rule?.id}"}).` : "";
1609
+ return text3(
1610
+ `Override added on ${target} \u2014 \`${shaped.rule?.id}\` matching \`${pattern}\`.${armed}
1611
+
1612
+ Trigger the request now (navigate / tap_element), then check it with get_network_requests or describe_screen. Remove it with network_override({action:"remove", id:"${shaped.rule?.id}"}) when you're done.`
1613
+ );
1614
+ }
1615
+ case "remove": {
1616
+ if (!args.id) {
1617
+ return fail4('`remove` needs an `id`. List them with network_override({action:"list"}).');
1618
+ }
1619
+ await sync.callAction(NETWORK_TOOL_ID, "deleteOverrideRule", { id: args.id }, target);
1620
+ return text3(`Removed override \`${args.id}\` on ${target}.`);
1621
+ }
1622
+ case "clear":
1623
+ await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
1624
+ return text3(`Removed all override rules on ${target}. Traffic is real again.`);
1625
+ case "enable":
1626
+ case "disable": {
1627
+ const enabled = args.action === "enable";
1628
+ if (args.id) {
1629
+ await sync.callAction(
1630
+ NETWORK_TOOL_ID,
1631
+ "setOverrideRuleEnabled",
1632
+ { id: args.id, enabled },
1633
+ target
1634
+ );
1635
+ return text3(
1636
+ `Rule \`${args.id}\` is now ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
1637
+ );
1638
+ }
1639
+ await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
1640
+ return text3(
1641
+ enabled ? `Overrides switched ON for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
1642
+ );
1643
+ }
1644
+ case "list": {
1645
+ const res = await sync.callAction(
1646
+ NETWORK_TOOL_ID,
1647
+ "listOverrideRules",
1648
+ {},
1649
+ target
1650
+ );
1651
+ const rules = res?.rules ?? [];
1652
+ if (args.id) {
1653
+ const rule = rules.find((r) => r.id === args.id);
1654
+ if (!rule) {
1655
+ return fail4(
1656
+ `No override rule \`${args.id}\` on ${target}. List them with network_override({action:"list"}).`
1657
+ );
1658
+ }
1659
+ return text3(
1660
+ `**Override rule \`${rule.id}\`** on ${target}${masterNote(res)}
1661
+
1662
+ \`\`\`json
1663
+ ${JSON.stringify(rule, null, 2)}
1664
+ \`\`\``
1665
+ );
1666
+ }
1667
+ if (rules.length === 0) {
1668
+ return text3(
1669
+ `No override rules on ${target}.
1670
+
1671
+ Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
1672
+ );
1673
+ }
1674
+ const rows = rules.map((rule, i) => {
1675
+ const what = rule.kind === "respond" ? `\u2192 ${rule.status ?? 200}${rule.statusText ? ` ${rule.statusText}` : ""}` : rule.kind === "fail" ? `\u2192 ${rule.failKind === "timeout" ? "timeout" : "network error"}` : "\u2192 real response";
1676
+ const bits = [
1677
+ rule.methods?.length ? rule.methods.join("/") : "any method",
1678
+ what,
1679
+ // Enough to tell WHICH body this is without printing it.
1680
+ rule.kind === "respond" && rule.body ? `body ${rule.body.length}B` : "",
1681
+ rule.headers && Object.keys(rule.headers).length ? `${Object.keys(rule.headers).length} header(s)` : "",
1682
+ rule.delayMs ? `${rule.delayMs}ms delay` : "",
1683
+ rule.alternate ? "every other request" : "",
1684
+ typeof rule.times === "number" ? `${rule.times}\xD7 then off` : "",
1685
+ `${rule.hits} hit${rule.hits === 1 ? "" : "s"}`,
1686
+ // Only interesting when the two can disagree: `alternate` and
1687
+ // spent `times` rules match without applying, and a gap between
1688
+ // the numbers is the explanation for "it matched but nothing
1689
+ // happened".
1690
+ rule.seen != null && rule.seen !== rule.hits ? `${rule.seen} matched` : "",
1691
+ rule.enabled ? "" : "DISABLED"
1692
+ ].filter(Boolean);
1693
+ return `${i + 1}. \`${rule.id}\`${rule.name ? ` ${rule.name}` : ""}
1694
+ \u2514\u2500 \`${rule.urlPattern}\` \xB7 ${bits.join(" \xB7 ")}`;
1695
+ });
1696
+ return text3(
1697
+ `**Override rules on ${target}**${masterNote(res)}
1698
+
1699
+ ${rows.join("\n")}
1700
+
1701
+ Full detail incl. body: network_override({action:"list", id:"${rules[0]?.id}"}).`
1702
+ );
1703
+ }
1704
+ case "debug": {
1705
+ const res = await sync.callAction(
1706
+ NETWORK_TOOL_ID,
1707
+ "debugOverrides",
1708
+ { url: args.url },
1709
+ target
1710
+ );
1711
+ if (!res) {
1712
+ return fail4(
1713
+ `${target} did not answer the diagnostics call \u2014 the network tool may not be mounted.`
1714
+ );
1715
+ }
1716
+ return text3(
1717
+ `**Override diagnostics on ${target}**${args.url ? ` for \`${args.url}\`` : ""}
1718
+
1719
+ \`\`\`json
1720
+ ${JSON.stringify(res, null, 2)}
1721
+ \`\`\`
1722
+
1723
+ A rule that never fires is usually one of: the master switch is off, the interceptor isn't installed (nothing is subscribed to the network tool), or the pattern doesn't match the FULL url including its query string.`
1724
+ );
1725
+ }
1726
+ }
1727
+ } catch (e) {
1728
+ return fail4(errMsg4(e));
1729
+ }
1730
+ }
1731
+ );
1512
1732
  }
1513
1733
 
1514
1734
  // src/tools/packs/reactQuery.ts
package/dist/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  SyncClient,
4
4
  createMcpServer,
5
5
  runInit
6
- } from "./chunk-IGB5AS45.mjs";
6
+ } from "./chunk-ZEHIU6OG.mjs";
7
7
 
8
8
  // src/cli.ts
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
package/dist/index.js CHANGED
@@ -376,7 +376,7 @@ var import_node_os = __toESM(require("os"));
376
376
  var import_node_path = __toESM(require("path"));
377
377
 
378
378
  // src/version.ts
379
- var VERSION = "7.0.12" ? "7.0.12" : "0.0.0-dev";
379
+ var VERSION = "7.0.14" ? "7.0.14" : "0.0.0-dev";
380
380
  var PACKAGE_NAME = "@buoy-gg/mcp";
381
381
 
382
382
  // src/update/checkForUpdate.ts
@@ -1331,6 +1331,10 @@ var NETWORK_TOOL_ID = "network";
1331
1331
  var text3 = (s) => ({ content: [{ type: "text", text: s }] });
1332
1332
  var fail4 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
1333
1333
  var errMsg4 = (e) => e instanceof Error ? e.message : String(e);
1334
+ var masterNote = (state) => {
1335
+ if (state?.enabled !== false) return "";
1336
+ return state.autoPaused ? ' \u2014 AUTO-PAUSED after the app failed to settle on launch with overrides active; nothing is firing. Re-arm with network_override({action:"enable"}).' : " \u2014 MASTER SWITCH IS OFF, nothing is firing";
1337
+ };
1334
1338
  function parseSnapshot(payload) {
1335
1339
  if (Array.isArray(payload)) return { events: payload, saved: [] };
1336
1340
  if (payload && typeof payload === "object") {
@@ -1544,6 +1548,222 @@ _\u{1F4CC} pinned \xB7 \u{1F516} saved. Act on one with its id, e.g. network_act
1544
1548
  }
1545
1549
  }
1546
1550
  );
1551
+ server.registerTool(
1552
+ "network_override",
1553
+ {
1554
+ description: "Force matching HTTP requests to return a chosen status/body, to fail outright, or to arrive late \u2014 without touching the backend or the app's code. This is how you drive an app into a state it won't reach on its own: force a 500 and see whether the error UI appears, return [] and check the empty state, simulate offline or a slow connection. Rules run INSIDE the app and survive an app reload, so they also cover the boot path. After adding one, trigger the request (navigate/tap_element) and read the result with get_network_requests or describe_screen. ALWAYS remove your rules when done \u2014 a forgotten rule looks exactly like a real bug. Requires Buoy Pro.",
1555
+ inputSchema: {
1556
+ action: import_zod4.z.enum(["list", "add", "remove", "clear", "enable", "disable", "debug"]).describe(
1557
+ "add creates a rule (or replaces one when `id` is given). remove drops one by `id`. clear drops all. enable/disable flip ONE rule when `id` is given, otherwise the master switch \u2014 rules are kept either way. list shows every rule with its hit counts; pass `id` to dump one rule in full, body included. debug reports why a rule is or isn't firing."
1558
+ ),
1559
+ urlPattern: import_zod4.z.string().optional().describe(
1560
+ "For `add`: glob matched against the WHOLE request URL, query string included. `*` is the only wildcard, e.g. '*pokeapi.co*' or 'https://api.example.com/v1/users*'. Careful with an exact URL copied from get_network_requests \u2014 if the client cache-busts or pages, the next call won't match it and the rule sits at 0 hits. Prefer `fromRequestId`, which handles that for you."
1561
+ ),
1562
+ fromRequestId: import_zod4.z.string().optional().describe(
1563
+ "For `add`: build the rule from a captured request id (from get_network_requests) \u2014 the same 'Override this request' the developer gets in the UI. Seeds the pattern (query string replaced with `*` so it keeps matching), the method, the status, and the REAL response body, so you can change two fields of a payload you just saw instead of authoring one from scratch. Anything you pass explicitly overrides the prefill."
1564
+ ),
1565
+ methods: import_zod4.z.array(import_zod4.z.string()).optional().describe("For `add`: restrict to these HTTP methods. Omit for any."),
1566
+ kind: import_zod4.z.enum(["respond", "fail", "delay"]).optional().describe(
1567
+ "respond = return your status/body, never hitting the network (default). fail = a transport failure, as if the device were offline. delay = let the real request run, just late."
1568
+ ),
1569
+ status: import_zod4.z.number().optional().describe(
1570
+ "For kind 'respond': HTTP status, 200\u2013599 (default 200). Outside that range fetch itself throws, so use kind 'fail' for connection errors rather than status 0."
1571
+ ),
1572
+ body: import_zod4.z.union([import_zod4.z.string(), import_zod4.z.record(import_zod4.z.unknown()), import_zod4.z.array(import_zod4.z.unknown())]).optional().describe(
1573
+ "For kind 'respond': the response body. Pass JSON directly (an object or array) or a raw string; content-type is inferred."
1574
+ ),
1575
+ statusText: import_zod4.z.string().optional().describe(
1576
+ "For kind 'respond': the status line's text, e.g. 'Service Unavailable'. Cosmetic \u2014 React Native's XMLHttpRequest has no statusText field, so the app never sees it; it only makes the developer's rule list read correctly."
1577
+ ),
1578
+ headers: import_zod4.z.record(import_zod4.z.string()).optional().describe("For kind 'respond': response headers to send."),
1579
+ failKind: import_zod4.z.enum(["network", "timeout"]).optional().describe("For kind 'fail': 'network' (default) or 'timeout'."),
1580
+ delayMs: import_zod4.z.number().optional().describe("Latency before the outcome, in ms. Works with every kind."),
1581
+ times: import_zod4.z.number().optional().describe(
1582
+ "Auto-disable the rule after N matches. Use 1 for 'make the next call fail' so it can't linger."
1583
+ ),
1584
+ alternate: import_zod4.z.boolean().optional().describe(
1585
+ "Apply to every OTHER matching request, forever: the first goes through, the second is overridden, and so on. This is how you test retry logic and flaky endpoints \u2014 a rule that is always on or always off can't reach those paths."
1586
+ ),
1587
+ name: import_zod4.z.string().optional().describe("Label shown in the developer's UI."),
1588
+ enabled: import_zod4.z.boolean().optional().describe(
1589
+ "For `add`: create the rule switched off (default true). Useful for staging a rule you'll arm later with enable({id})."
1590
+ ),
1591
+ id: import_zod4.z.string().optional().describe(
1592
+ "Rule id. Required for `remove`. For `add` it replaces that rule. For `enable`/`disable` it targets ONE rule instead of the master switch. For `list` it dumps that rule in full."
1593
+ ),
1594
+ url: import_zod4.z.string().optional().describe(
1595
+ "For `debug`: a concrete URL to test the rules against, so the report says which rule would match it and why."
1596
+ ),
1597
+ deviceId: import_zod4.z.string().optional().describe("Target device id (auto-selected if one is connected).")
1598
+ }
1599
+ },
1600
+ async (args) => {
1601
+ try {
1602
+ const target = sync.requireProDevice(args.deviceId);
1603
+ switch (args.action) {
1604
+ case "add": {
1605
+ if (!args.urlPattern && !args.fromRequestId) {
1606
+ return fail4(
1607
+ "`add` needs a `urlPattern` or a `fromRequestId`. Take an id from get_network_requests to override a request you just saw, or pass a glob like '*pokeapi.co*'."
1608
+ );
1609
+ }
1610
+ const res = await sync.callAction(
1611
+ NETWORK_TOOL_ID,
1612
+ "upsertOverrideRule",
1613
+ {
1614
+ fromRequestId: args.fromRequestId,
1615
+ rule: {
1616
+ id: args.id,
1617
+ name: args.name,
1618
+ urlPattern: args.urlPattern,
1619
+ methods: args.methods,
1620
+ // Not defaulted here — the device defaults to "respond" too,
1621
+ // and leaving it absent lets a `fromRequestId` prefill win.
1622
+ kind: args.kind,
1623
+ status: args.status,
1624
+ statusText: args.statusText,
1625
+ body: args.body,
1626
+ headers: args.headers,
1627
+ failKind: args.failKind,
1628
+ delayMs: args.delayMs,
1629
+ times: args.times,
1630
+ alternate: args.alternate,
1631
+ enabled: args.enabled !== false
1632
+ }
1633
+ },
1634
+ target
1635
+ );
1636
+ const shaped = res;
1637
+ if (!shaped?.ok) {
1638
+ return fail4(shaped?.error ?? `Could not add the rule on ${target}.`);
1639
+ }
1640
+ const pattern = shaped.rule?.urlPattern ?? args.urlPattern;
1641
+ const armed = shaped.rule?.enabled === false ? `
1642
+
1643
+ It is switched OFF \u2014 arm it with network_override({action:"enable", id:"${shaped.rule?.id}"}).` : "";
1644
+ return text3(
1645
+ `Override added on ${target} \u2014 \`${shaped.rule?.id}\` matching \`${pattern}\`.${armed}
1646
+
1647
+ Trigger the request now (navigate / tap_element), then check it with get_network_requests or describe_screen. Remove it with network_override({action:"remove", id:"${shaped.rule?.id}"}) when you're done.`
1648
+ );
1649
+ }
1650
+ case "remove": {
1651
+ if (!args.id) {
1652
+ return fail4('`remove` needs an `id`. List them with network_override({action:"list"}).');
1653
+ }
1654
+ await sync.callAction(NETWORK_TOOL_ID, "deleteOverrideRule", { id: args.id }, target);
1655
+ return text3(`Removed override \`${args.id}\` on ${target}.`);
1656
+ }
1657
+ case "clear":
1658
+ await sync.callAction(NETWORK_TOOL_ID, "clearOverrideRules", {}, target);
1659
+ return text3(`Removed all override rules on ${target}. Traffic is real again.`);
1660
+ case "enable":
1661
+ case "disable": {
1662
+ const enabled = args.action === "enable";
1663
+ if (args.id) {
1664
+ await sync.callAction(
1665
+ NETWORK_TOOL_ID,
1666
+ "setOverrideRuleEnabled",
1667
+ { id: args.id, enabled },
1668
+ target
1669
+ );
1670
+ return text3(
1671
+ `Rule \`${args.id}\` is now ${enabled ? "ON" : "OFF"} on ${target}. The other rules and the master switch are unchanged.`
1672
+ );
1673
+ }
1674
+ await sync.callAction(NETWORK_TOOL_ID, "setOverridesEnabled", { enabled }, target);
1675
+ return text3(
1676
+ enabled ? `Overrides switched ON for ${target}.` : `Overrides switched OFF for ${target}. Rules are kept \u2014 re-enable with network_override({action:"enable"}).`
1677
+ );
1678
+ }
1679
+ case "list": {
1680
+ const res = await sync.callAction(
1681
+ NETWORK_TOOL_ID,
1682
+ "listOverrideRules",
1683
+ {},
1684
+ target
1685
+ );
1686
+ const rules = res?.rules ?? [];
1687
+ if (args.id) {
1688
+ const rule = rules.find((r) => r.id === args.id);
1689
+ if (!rule) {
1690
+ return fail4(
1691
+ `No override rule \`${args.id}\` on ${target}. List them with network_override({action:"list"}).`
1692
+ );
1693
+ }
1694
+ return text3(
1695
+ `**Override rule \`${rule.id}\`** on ${target}${masterNote(res)}
1696
+
1697
+ \`\`\`json
1698
+ ${JSON.stringify(rule, null, 2)}
1699
+ \`\`\``
1700
+ );
1701
+ }
1702
+ if (rules.length === 0) {
1703
+ return text3(
1704
+ `No override rules on ${target}.
1705
+
1706
+ Add one with network_override({action:"add", urlPattern:"*", status:500}) to force an error state.`
1707
+ );
1708
+ }
1709
+ const rows = rules.map((rule, i) => {
1710
+ const what = rule.kind === "respond" ? `\u2192 ${rule.status ?? 200}${rule.statusText ? ` ${rule.statusText}` : ""}` : rule.kind === "fail" ? `\u2192 ${rule.failKind === "timeout" ? "timeout" : "network error"}` : "\u2192 real response";
1711
+ const bits = [
1712
+ rule.methods?.length ? rule.methods.join("/") : "any method",
1713
+ what,
1714
+ // Enough to tell WHICH body this is without printing it.
1715
+ rule.kind === "respond" && rule.body ? `body ${rule.body.length}B` : "",
1716
+ rule.headers && Object.keys(rule.headers).length ? `${Object.keys(rule.headers).length} header(s)` : "",
1717
+ rule.delayMs ? `${rule.delayMs}ms delay` : "",
1718
+ rule.alternate ? "every other request" : "",
1719
+ typeof rule.times === "number" ? `${rule.times}\xD7 then off` : "",
1720
+ `${rule.hits} hit${rule.hits === 1 ? "" : "s"}`,
1721
+ // Only interesting when the two can disagree: `alternate` and
1722
+ // spent `times` rules match without applying, and a gap between
1723
+ // the numbers is the explanation for "it matched but nothing
1724
+ // happened".
1725
+ rule.seen != null && rule.seen !== rule.hits ? `${rule.seen} matched` : "",
1726
+ rule.enabled ? "" : "DISABLED"
1727
+ ].filter(Boolean);
1728
+ return `${i + 1}. \`${rule.id}\`${rule.name ? ` ${rule.name}` : ""}
1729
+ \u2514\u2500 \`${rule.urlPattern}\` \xB7 ${bits.join(" \xB7 ")}`;
1730
+ });
1731
+ return text3(
1732
+ `**Override rules on ${target}**${masterNote(res)}
1733
+
1734
+ ${rows.join("\n")}
1735
+
1736
+ Full detail incl. body: network_override({action:"list", id:"${rules[0]?.id}"}).`
1737
+ );
1738
+ }
1739
+ case "debug": {
1740
+ const res = await sync.callAction(
1741
+ NETWORK_TOOL_ID,
1742
+ "debugOverrides",
1743
+ { url: args.url },
1744
+ target
1745
+ );
1746
+ if (!res) {
1747
+ return fail4(
1748
+ `${target} did not answer the diagnostics call \u2014 the network tool may not be mounted.`
1749
+ );
1750
+ }
1751
+ return text3(
1752
+ `**Override diagnostics on ${target}**${args.url ? ` for \`${args.url}\`` : ""}
1753
+
1754
+ \`\`\`json
1755
+ ${JSON.stringify(res, null, 2)}
1756
+ \`\`\`
1757
+
1758
+ A rule that never fires is usually one of: the master switch is off, the interceptor isn't installed (nothing is subscribed to the network tool), or the pattern doesn't match the FULL url including its query string.`
1759
+ );
1760
+ }
1761
+ }
1762
+ } catch (e) {
1763
+ return fail4(errMsg4(e));
1764
+ }
1765
+ }
1766
+ );
1547
1767
  }
1548
1768
 
1549
1769
  // src/tools/packs/reactQuery.ts
package/dist/index.mjs CHANGED
@@ -27,7 +27,7 @@ import {
27
27
  registerStorageTools,
28
28
  registerZustandTools,
29
29
  runInit
30
- } from "./chunk-IGB5AS45.mjs";
30
+ } from "./chunk-ZEHIU6OG.mjs";
31
31
  export {
32
32
  ALL_DEVICES,
33
33
  BROKER_PORT,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@buoy-gg/mcp",
3
3
  "mcpName": "io.github.buoy-gg/mcp",
4
- "version": "7.0.12",
4
+ "version": "7.0.14",
5
5
  "description": "MCP server for Buoy devtools — lets any LLM copilot drive Buoy tools (perf-monitor benchmarks, etc.) over the devtool sync protocol",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
@@ -31,7 +31,7 @@
31
31
  "@modelcontextprotocol/sdk": "^1.29.0",
32
32
  "socket.io-client": "^4.8.1",
33
33
  "zod": "^3.25.0",
34
- "@buoy-gg/sync-broker": "^7.0.12"
34
+ "@buoy-gg/sync-broker": "^7.0.14"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/node": "^22.0.0",