@buoy-gg/mcp 7.0.1 → 7.0.3

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.
@@ -296,7 +296,7 @@ async function waitForBroker(url, attempts = 20) {
296
296
  var delay = (ms) => new Promise((r) => setTimeout(r, ms));
297
297
 
298
298
  // src/version.ts
299
- var VERSION = "7.0.1" ? "7.0.1" : "0.0.0-dev";
299
+ var VERSION = "7.0.3" ? "7.0.3" : "0.0.0-dev";
300
300
  var PACKAGE_NAME = "@buoy-gg/mcp";
301
301
 
302
302
  // src/update/checkForUpdate.ts
@@ -2006,7 +2006,7 @@ function registerScreenshotTools(server, sync) {
2006
2006
 
2007
2007
  // src/server.ts
2008
2008
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2009
- import { z as z13 } from "zod";
2009
+ import { z as z15 } from "zod";
2010
2010
 
2011
2011
  // src/tools/packs/interact.ts
2012
2012
  import { z as z12 } from "zod";
@@ -2112,6 +2112,439 @@ ${elements.map(formatElement).join("\n")}`);
2112
2112
  );
2113
2113
  }
2114
2114
 
2115
+ // src/tools/packs/jsThread.ts
2116
+ import { z as z13 } from "zod";
2117
+ var JS_TOP_TOOL_ID = "js-top";
2118
+ var text11 = (s) => ({ content: [{ type: "text", text: s }] });
2119
+ var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2120
+ var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
2121
+ var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
2122
+ function sparkline(values) {
2123
+ const max = Math.max(1, ...values);
2124
+ return values.map((v) => SPARK_CHARS[Math.min(7, Math.floor(v / max * 7.999))]).join("");
2125
+ }
2126
+ function formatTopMarkdown(snap, durationMs) {
2127
+ const lines = [];
2128
+ lines.push(`## JS thread top (${(durationMs / 1e3).toFixed(1)}s sample)`);
2129
+ lines.push("");
2130
+ lines.push(`Thread busy: **${snap.busyPct.toFixed(1)}%** of the trailing 5s window.`);
2131
+ if (snap.freezeSummary.count > 0) {
2132
+ lines.push(
2133
+ `Freezes (\u226550ms single-task blocks, last 60s): **${snap.freezeSummary.count}**, worst **${snap.freezeSummary.worstMs}ms** \u2014 during these, taps and JS animations stall.`
2134
+ );
2135
+ }
2136
+ if (!snap.tiers.bridge) {
2137
+ lines.push(
2138
+ "_New Architecture device: touch/React commit work appears as \u201Cunattributed\u201D; timers, rAF and Promise chains are fully attributed._"
2139
+ );
2140
+ }
2141
+ lines.push("");
2142
+ lines.push("| # | task origin | api | 5s ms | % busy | calls | avg ms | max ms | freezes |");
2143
+ lines.push("|---|---|---|---|---|---|---|---|---|");
2144
+ snap.rows.slice(0, 15).forEach((r, i) => {
2145
+ const freezeCell = r.freezes > 0 ? `\u26A0 ${r.freezes} (worst ${r.worstFreezeMs}ms)` : "\u2013";
2146
+ lines.push(
2147
+ `| ${i + 1} | ${r.label} | ${r.api} | ${r.windowMs.toFixed(1)} | ${r.pctOfBusy.toFixed(1)} | ${r.calls} | ${r.avgMs.toFixed(2)} | ${r.maxMs.toFixed(1)} | ${freezeCell} |`
2148
+ );
2149
+ });
2150
+ lines.push("");
2151
+ lines.push(
2152
+ "_Drill into any row with `get_js_thread_origin_detail` using its `key` from the JSON below._"
2153
+ );
2154
+ return lines.join("\n");
2155
+ }
2156
+ function formatDetailMarkdown(d) {
2157
+ const lines = [];
2158
+ lines.push(`## Origin detail \u2014 ${d.label}`);
2159
+ lines.push("");
2160
+ lines.push(`API: \`${d.api}\` \xB7 key: \`${d.key}\``);
2161
+ lines.push("");
2162
+ lines.push(`Activity last 5s (250ms buckets): \`${sparkline(d.buckets)}\` \u2014 ${d.windowMs.toFixed(1)}ms in window, ${d.pctOfBusy.toFixed(1)}% of busy time.`);
2163
+ lines.push("");
2164
+ lines.push("| runs | scheduled | avg/run | max/run | total | last run | freezes (60s) | worst freeze |");
2165
+ lines.push("|---|---|---|---|---|---|---|---|");
2166
+ lines.push(
2167
+ `| ${d.calls} | ${d.scheduleCount} | ${d.avgMs.toFixed(2)}ms | ${d.maxMs.toFixed(1)}ms | ${d.totalMs.toFixed(1)}ms | ${d.lastSeenAgoMs > 0 ? `${Math.round(d.lastSeenAgoMs / 1e3)}s ago` : "\u2014"} | ${d.freezes} | ${d.worstFreezeMs > 0 ? `${d.worstFreezeMs}ms` : "\u2014"} |`
2168
+ );
2169
+ if (d.caller || d.file) {
2170
+ lines.push("");
2171
+ lines.push(`Scheduled from: **${d.caller ? `${d.caller}()` : "unknown caller"}**${d.file ? ` \u2014 ${d.file}${d.line != null ? `:${d.line}` : ""}` : ""}`);
2172
+ if (d.frames?.length) {
2173
+ lines.push(
2174
+ `Raw frames (Hermes bundle offsets): ${d.frames.map((f) => `${f.file}:${f.line}:${f.col}`).join(" \xB7 ")}`
2175
+ );
2176
+ }
2177
+ } else if (d.api !== "unattributed" && d.api !== "other") {
2178
+ lines.push("");
2179
+ lines.push(
2180
+ d.promoted ? "_Scheduling stack not captured yet \u2014 the next schedule from this site records it._" : "_Not hot enough for stack capture yet (threshold: 25 schedules or 50ms total)._"
2181
+ );
2182
+ }
2183
+ return lines.join("\n");
2184
+ }
2185
+ function registerJsThreadTools(server, sync) {
2186
+ server.registerTool(
2187
+ "get_js_thread_top",
2188
+ {
2189
+ description: "Task Manager for the React Native JS thread: sample for a few seconds and return a ranked table of which task origins (timers, Promise chains, rAF callbacks, bridge call-ins) consumed JS-thread time \u2014 plus thread busy% and freeze (50ms+ block) attribution. Use when JS FPS is low and you need to know WHAT is eating the thread. To profile one specific interaction, pass clearFirst:true, then drive the UI with tap_element while the sample runs. Drill into a row with get_js_thread_origin_detail. Requires @buoy-gg/js-top on the device.",
2190
+ inputSchema: {
2191
+ deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected)."),
2192
+ durationMs: z13.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
2193
+ clearFirst: z13.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
2194
+ }
2195
+ },
2196
+ async ({ deviceId, durationMs, clearFirst }) => {
2197
+ try {
2198
+ const target = sync.requireProDevice(deviceId);
2199
+ const duration = Math.min(Math.max(durationMs ?? 3e3, 250), 3e4);
2200
+ const snap = await sync.callAction(
2201
+ JS_TOP_TOOL_ID,
2202
+ "sample",
2203
+ { durationMs: duration, clearFirst: clearFirst === true },
2204
+ target
2205
+ );
2206
+ if (!snap) return fail13("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
2207
+ const md = formatTopMarkdown(snap, duration);
2208
+ const slim = {
2209
+ busyPct: Math.round(snap.busyPct * 10) / 10,
2210
+ freezeSummary: snap.freezeSummary,
2211
+ tiers: snap.tiers,
2212
+ rows: snap.rows.slice(0, 15).map((r) => ({
2213
+ key: r.key,
2214
+ api: r.api,
2215
+ windowMs: Math.round(r.windowMs * 10) / 10,
2216
+ pctOfBusy: Math.round(r.pctOfBusy * 10) / 10,
2217
+ calls: r.calls,
2218
+ avgMs: Math.round(r.avgMs * 100) / 100,
2219
+ maxMs: Math.round(r.maxMs * 10) / 10,
2220
+ totalMs: Math.round(r.totalMs),
2221
+ freezes: r.freezes,
2222
+ worstFreezeMs: r.worstFreezeMs
2223
+ }))
2224
+ };
2225
+ return text11(`${md}
2226
+
2227
+ \`\`\`json
2228
+ ${JSON.stringify(slim)}
2229
+ \`\`\``);
2230
+ } catch (e) {
2231
+ return fail13(errMsg13(e));
2232
+ }
2233
+ }
2234
+ );
2235
+ server.registerTool(
2236
+ "get_js_thread_origin_detail",
2237
+ {
2238
+ description: 'Inspect one js-top task origin in depth: schedules vs runs, avg/max/total exclusive ms, a 5s activity sparkline, freeze attribution, and the captured scheduling site (caller function + stack frames). Pass a row `key` from get_js_thread_top\'s JSON (e.g. "interval|pollFeed" or "unattributed" for the system row).',
2239
+ inputSchema: {
2240
+ deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected)."),
2241
+ key: z13.string().describe("Origin key from get_js_thread_top's JSON rows.")
2242
+ }
2243
+ },
2244
+ async ({ deviceId, key }) => {
2245
+ try {
2246
+ const target = sync.requireProDevice(deviceId);
2247
+ const detail = await sync.callAction(
2248
+ JS_TOP_TOOL_ID,
2249
+ "getOriginDetail",
2250
+ { key },
2251
+ target
2252
+ );
2253
+ if (!detail) {
2254
+ return fail13(
2255
+ `Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
2256
+ );
2257
+ }
2258
+ const md = formatDetailMarkdown(detail);
2259
+ return text11(`${md}
2260
+
2261
+ \`\`\`json
2262
+ ${JSON.stringify(detail)}
2263
+ \`\`\``);
2264
+ } catch (e) {
2265
+ return fail13(errMsg13(e));
2266
+ }
2267
+ }
2268
+ );
2269
+ server.registerTool(
2270
+ "clear_js_thread_stats",
2271
+ {
2272
+ description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
2273
+ inputSchema: {
2274
+ deviceId: z13.string().optional().describe("Target device id (auto-selected if one is connected).")
2275
+ }
2276
+ },
2277
+ async ({ deviceId }) => {
2278
+ try {
2279
+ const target = sync.requireProDevice(deviceId);
2280
+ await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
2281
+ return text11("Cleared js-top stats.");
2282
+ } catch (e) {
2283
+ return fail13(errMsg13(e));
2284
+ }
2285
+ }
2286
+ );
2287
+ }
2288
+
2289
+ // src/tools/packs/images.ts
2290
+ import { z as z14 } from "zod";
2291
+ var IMAGES_TOOL_ID = "images";
2292
+ var text12 = (s) => ({ content: [{ type: "text", text: s }] });
2293
+ var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
2294
+ var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
2295
+ function uriTail(uri) {
2296
+ if (!uri) return "(no source)";
2297
+ const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
2298
+ const effective = unstable ? decodeURIComponent(unstable[1]) : uri;
2299
+ const noQuery = effective.split("?")[0] ?? effective;
2300
+ const segs = noQuery.split("/").filter(Boolean);
2301
+ return segs[segs.length - 1] ?? effective;
2302
+ }
2303
+ function verdictLabel(r) {
2304
+ if (r.oversizeFactor == null) return "\u2013";
2305
+ if (r.oversizeFactor < 0.9) return `${r.oversizeFactor.toFixed(1)}\xD7 upscaled`;
2306
+ if (r.oversizeFactor <= 1.1) return "right-sized";
2307
+ return `${r.oversizeFactor.toFixed(1)}\xD7 oversized (${r.wastedKB}KB wasted)`;
2308
+ }
2309
+ function cacheLabel(r) {
2310
+ if (r.cache) return r.cache;
2311
+ if (r.progressSeen) return "network";
2312
+ return "?";
2313
+ }
2314
+ function mb(bytes) {
2315
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
2316
+ }
2317
+ function formatListMarkdown(list, limit) {
2318
+ const lines = [];
2319
+ const s = list.stats;
2320
+ lines.push(`## Images registry (${list.total} records, showing ${Math.min(limit, list.records.length)})`);
2321
+ lines.push("");
2322
+ lines.push(
2323
+ `${s.loaded} loaded \xB7 ${s.errors} failed \xB7 ${s.loading} in flight \xB7 \u2248${mb(s.estDecodedBytes)} decoded` + (s.estWastedBytes > 0 ? ` \xB7 **${mb(s.estWastedBytes)} wasted on oversized sources**` : "")
2324
+ );
2325
+ const ins = list.insights;
2326
+ if (ins) {
2327
+ const chips = [];
2328
+ if (ins.duplicates.length > 0)
2329
+ chips.push(`${ins.duplicates.length} duplicate URL(s): ${ins.duplicates.slice(0, 3).map((d) => `${uriTail(d.uri)}\xD7${d.count}`).join(", ")}`);
2330
+ if (ins.retryStorms.length > 0)
2331
+ chips.push(`${ins.retryStorms.length} retry storm(s): ${ins.retryStorms.slice(0, 3).map((r) => `#${r.id}(${r.loadCount} loads)`).join(", ")}`);
2332
+ if (ins.queueSaturated) chips.push("iOS image queue saturated (>4 concurrent loads)");
2333
+ if (ins.missingAlt > 0) chips.push(`${ins.missingAlt} image(s) missing alt/accessibilityLabel`);
2334
+ if (ins.layoutShifters.length > 0)
2335
+ chips.push(`${ins.layoutShifters.length} layout-shifting image(s): ${ins.layoutShifters.slice(0, 3).map((l) => `#${l.id}`).join(", ")} \u2014 reserve explicit dimensions`);
2336
+ if (chips.length > 0) {
2337
+ lines.push("");
2338
+ lines.push(`**Insights:** ${chips.join(" \xB7 ")}`);
2339
+ }
2340
+ }
2341
+ const cs = list.captureStatus;
2342
+ if (cs.rnDecoratorTooLate) {
2343
+ lines.push("");
2344
+ lines.push('\u26A0 RN <Image> capture missed the module-load race \u2014 add `import "@buoy-gg/images/register"` as the FIRST import of the app entry file.');
2345
+ }
2346
+ if (list.globalModes.network !== "normal" || list.globalModes.blank) {
2347
+ lines.push("");
2348
+ lines.push(`\u26A0 Simulation active: network=${list.globalModes.network}${list.globalModes.blank ? ", blank images ON" : ""} (set_image_simulation to reset).`);
2349
+ }
2350
+ lines.push("");
2351
+ lines.push("| id | image | lib | status | cache | ms | decoded px | shown dp | verdict | sim |");
2352
+ lines.push("|---|---|---|---|---|---|---|---|---|---|");
2353
+ for (const r of list.records) {
2354
+ const dims = r.intrinsic ? `${r.intrinsic.width}\xD7${r.intrinsic.height}` : "\u2013";
2355
+ const layout = r.layout ? `${r.layout.width}\xD7${r.layout.height}` : "\u2013";
2356
+ const status = r.status === "error" ? `**error**${r.errorCode ? ` (${r.errorCode})` : ""}` : r.status;
2357
+ lines.push(
2358
+ `| ${r.id} | ${uriTail(r.uri)} | ${r.lib} | ${status}${r.mounted ? "" : " (unmounted)"} | ${cacheLabel(r)} | ${r.ms ?? "\u2013"} | ${dims} | ${layout} | ${verdictLabel(r)} | ${r.overrideLabel ?? "\u2013"} |`
2359
+ );
2360
+ }
2361
+ lines.push("");
2362
+ lines.push("_Act on a row with `image_action` using its `id` (hard_reload / retry / force_error / force_loading / force_blank / swap_url / flash / restore / evict_disk)._");
2363
+ return lines.join("\n");
2364
+ }
2365
+ var ACTION_MAP = {
2366
+ hard_reload: { action: "hardReload" },
2367
+ retry: { action: "retry" },
2368
+ evict_disk: { action: "evictDisk" },
2369
+ flash: { action: "flash" },
2370
+ restore: { action: "clearOverride" },
2371
+ force_error: { action: "setOverride", params: () => ({ kind: "error" }) },
2372
+ force_loading: { action: "setOverride", params: () => ({ kind: "hang" }) },
2373
+ force_blank: { action: "setOverride", params: () => ({ kind: "blank" }) },
2374
+ swap_url: { action: "setOverride", params: (url) => ({ kind: "url", uri: url }) }
2375
+ };
2376
+ function registerImagesTools(server, sync) {
2377
+ server.registerTool(
2378
+ "get_images",
2379
+ {
2380
+ description: "Live image registry from the app: every RN <Image> and expo-image load with cache verdict (memory/disk/network), load ms, decoded vs displayed size with an oversize/upscale verdict and wasted-KB estimate, error details, and any active simulation overrides. Image HTTP never hits the JS network stack, so this is the only visibility into image loading. Requires @buoy-gg/images on the device.",
2381
+ inputSchema: {
2382
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
2383
+ limit: z14.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
2384
+ status: z14.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
2385
+ }
2386
+ },
2387
+ async ({ deviceId, limit, status }) => {
2388
+ try {
2389
+ const target = sync.requireProDevice(deviceId);
2390
+ const capped = Math.min(Math.max(limit ?? 30, 1), 200);
2391
+ const list = await sync.callAction(
2392
+ IMAGES_TOOL_ID,
2393
+ "list",
2394
+ { limit: capped, status },
2395
+ target
2396
+ );
2397
+ if (!list) return fail14("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
2398
+ const md = formatListMarkdown(list, capped);
2399
+ const slim = {
2400
+ stats: list.stats,
2401
+ insights: list.insights,
2402
+ captureStatus: list.captureStatus,
2403
+ globalModes: list.globalModes,
2404
+ records: list.records.map((r) => ({
2405
+ id: r.id,
2406
+ uri: r.uri,
2407
+ lib: r.lib,
2408
+ status: r.status,
2409
+ mounted: r.mounted,
2410
+ cache: r.cache,
2411
+ ms: r.ms,
2412
+ intrinsic: r.intrinsic,
2413
+ layout: r.layout,
2414
+ oversizeFactor: r.oversizeFactor,
2415
+ wastedKB: r.wastedKB,
2416
+ error: r.error,
2417
+ errorCode: r.errorCode,
2418
+ overrideLabel: r.overrideLabel,
2419
+ loadCount: r.loadCount
2420
+ }))
2421
+ };
2422
+ return text12(`${md}
2423
+
2424
+ \`\`\`json
2425
+ ${JSON.stringify(slim)}
2426
+ \`\`\``);
2427
+ } catch (e) {
2428
+ return fail14(errMsg14(e));
2429
+ }
2430
+ }
2431
+ );
2432
+ server.registerTool(
2433
+ "image_action",
2434
+ {
2435
+ description: "Act on one image from get_images (by id): hard_reload (bypass caches + refetch), retry, evict_disk (expo disk entry), flash (red border to locate it on screen), prove_savings (re-encode at displayed size as WebP ON DEVICE and return real byte savings + a previewable file:// uri), or simulation overrides \u2014 force_error (instant offline-safe failure), force_loading (request hangs forever), force_blank, swap_url (requires url), restore (remove override). Returns the record's state after the action so the effect is visible.",
2436
+ inputSchema: {
2437
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
2438
+ id: z14.number().describe("Record id from get_images."),
2439
+ action: z14.enum([
2440
+ "hard_reload",
2441
+ "retry",
2442
+ "evict_disk",
2443
+ "flash",
2444
+ "prove_savings",
2445
+ "force_error",
2446
+ "force_loading",
2447
+ "force_blank",
2448
+ "swap_url",
2449
+ "restore"
2450
+ ]).describe("What to do."),
2451
+ url: z14.string().optional().describe("Replacement URL (swap_url only).")
2452
+ }
2453
+ },
2454
+ async ({ deviceId, id, action, url }) => {
2455
+ try {
2456
+ const target = sync.requireProDevice(deviceId);
2457
+ if (action === "prove_savings") {
2458
+ const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
2459
+ if (!savings) return fail14("Device returned nothing for proveSavings.");
2460
+ if (!savings.ok) return fail14(savings.message);
2461
+ const parts = [`**prove_savings** on #${id}: ${savings.message}`];
2462
+ if (savings.originalBytes != null)
2463
+ parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
2464
+ if (savings.optimizedBytes != null)
2465
+ parts.push(
2466
+ `Optimized: ${Math.round(savings.optimizedBytes / 1024)}KB \u2014 ${savings.optimizedDims?.width}\xD7${savings.optimizedDims?.height} WebP`
2467
+ );
2468
+ if (savings.savedBytes != null)
2469
+ parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
2470
+ if (savings.optimizedUri)
2471
+ parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
2472
+ return text12(parts.join("\n"));
2473
+ }
2474
+ const entry = ACTION_MAP[action];
2475
+ if (!entry) return fail14(`Unknown action: ${action}`);
2476
+ if (action === "swap_url" && !url) return fail14("swap_url requires the `url` param.");
2477
+ const result = await sync.callAction(
2478
+ IMAGES_TOOL_ID,
2479
+ entry.action,
2480
+ { id, ...entry.params?.(url) ?? {} },
2481
+ target
2482
+ );
2483
+ const settleMs = action === "flash" ? 0 : 1500;
2484
+ if (settleMs > 0) await new Promise((r) => setTimeout(r, settleMs));
2485
+ const detail = await sync.callAction(IMAGES_TOOL_ID, "getDetail", { id }, target).catch(() => null);
2486
+ const lines = [
2487
+ `**${action}** on #${id}: ${result?.message ?? (result?.ok === false ? "failed" : "done")}`
2488
+ ];
2489
+ if (detail) {
2490
+ lines.push("");
2491
+ lines.push(
2492
+ `Now: status=${detail.status}${detail.errorCode ? ` (${detail.errorCode})` : ""} \xB7 cache=${cacheLabel(detail)} \xB7 ms=${detail.ms ?? "\u2013"} \xB7 loads=${detail.loadCount}${detail.overrideLabel ? ` \xB7 sim="${detail.overrideLabel}"` : ""}${detail.mounted ? "" : " \xB7 UNMOUNTED"}`
2493
+ );
2494
+ if (detail.error) lines.push(`Error: ${detail.error}`);
2495
+ }
2496
+ return text12(lines.join("\n"));
2497
+ } catch (e) {
2498
+ return fail14(errMsg14(e));
2499
+ }
2500
+ }
2501
+ );
2502
+ server.registerTool(
2503
+ "set_image_simulation",
2504
+ {
2505
+ description: "App-wide image simulation modes: network 'offline' (every NETWORK image load fails immediately, both libs \u2014 bundled assets still load; reload/remount on-screen images to see it), 'cold' (every image bypasses caches \u2014 first-launch behavior, nothing cleared), 'normal' to reset. blankImages renders every image without a source. massAction applies per-image simulations to everything mounted. Also clears the registry or expo caches when asked.",
2506
+ inputSchema: {
2507
+ deviceId: z14.string().optional().describe("Target device id (auto-selected if one is connected)."),
2508
+ network: z14.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
2509
+ blankImages: z14.boolean().optional().describe("Render all images without a source."),
2510
+ massAction: z14.enum(["error", "loading", "blank", "reload", "flash", "restore"]).optional().describe("Apply to EVERY mounted image at once: force error/loading/blank overrides, hard-reload all, flash all, or restore all overrides."),
2511
+ clearRegistry: z14.boolean().optional().describe("Clear the captured image log."),
2512
+ clearExpoCaches: z14.boolean().optional().describe("Clear expo-image memory + disk caches.")
2513
+ }
2514
+ },
2515
+ async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
2516
+ try {
2517
+ const target = sync.requireProDevice(deviceId);
2518
+ const done = [];
2519
+ if (massAction) {
2520
+ const r = await sync.callAction(IMAGES_TOOL_ID, "massAction", { kind: massAction }, target);
2521
+ done.push(r?.message ?? `mass ${massAction} applied`);
2522
+ }
2523
+ if (network) {
2524
+ await sync.callAction(IMAGES_TOOL_ID, "setNetworkMode", { mode: network }, target);
2525
+ done.push(`network=${network}`);
2526
+ }
2527
+ if (blankImages != null) {
2528
+ await sync.callAction(IMAGES_TOOL_ID, "setBlankImages", { enabled: blankImages }, target);
2529
+ done.push(`blankImages=${blankImages}`);
2530
+ }
2531
+ if (clearRegistry) {
2532
+ await sync.callAction(IMAGES_TOOL_ID, "clearRecords", {}, target);
2533
+ done.push("registry cleared");
2534
+ }
2535
+ if (clearExpoCaches) {
2536
+ const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
2537
+ done.push(r?.message ?? "expo caches cleared");
2538
+ }
2539
+ if (done.length === 0) return fail14("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
2540
+ return text12(`Applied: ${done.join(" \xB7 ")}`);
2541
+ } catch (e) {
2542
+ return fail14(errMsg14(e));
2543
+ }
2544
+ }
2545
+ );
2546
+ }
2547
+
2115
2548
  // src/server.ts
2116
2549
  var SERVER_INSTRUCTIONS = `Buoy MCP \u2014 drive Buoy dev tools running in a React Native app from your editor.
2117
2550
 
@@ -2134,6 +2567,15 @@ elements (accessibility-style tree with tap points + testIDs, no screenshot), th
2134
2567
  ({ testID | query | nativeTag }) to tap one \u2014 it invokes the element's onPress directly in JS, so
2135
2568
  it's fast and works on physical devices too. Re-run \`describe_screen\` after the screen changes.
2136
2569
 
2570
+ JS FPS low / app hitching and you need to know WHAT is eating the JS thread? Use
2571
+ \`get_js_thread_top\` (a live "Task Manager" of timers, Promise chains, rAF callbacks and
2572
+ freezes, ranked by thread time), then \`get_js_thread_origin_detail\` to drill into a row.
2573
+ Pass clearFirst:true and drive the UI with \`tap_element\` mid-sample to profile one interaction.
2574
+
2575
+ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists every RN <Image> /
2576
+ expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
2577
+ simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
2578
+
2137
2579
  For everything else:
2138
2580
  - \`list_tool_actions\` to see what a tool can do,
2139
2581
  - \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
@@ -2162,10 +2604,12 @@ function createMcpServer(sync) {
2162
2604
  registerConsoleTools(server, sync);
2163
2605
  registerScreenshotTools(server, sync);
2164
2606
  registerInteractTools(server, sync);
2607
+ registerJsThreadTools(server, sync);
2608
+ registerImagesTools(server, sync);
2165
2609
  return server;
2166
2610
  }
2167
2611
  var deviceIdField = {
2168
- deviceId: z13.string().optional().describe(
2612
+ deviceId: z15.string().optional().describe(
2169
2613
  'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
2170
2614
  )
2171
2615
  };