@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.
- package/dist/{chunk-FK76APS5.mjs → chunk-MGZN6MJE.mjs} +447 -3
- package/dist/cli.js +450 -3
- package/dist/cli.mjs +4 -1
- package/dist/index.js +447 -3
- package/dist/index.mjs +1 -1
- package/package.json +5 -4
package/dist/index.js
CHANGED
|
@@ -360,7 +360,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
360
360
|
|
|
361
361
|
// src/server.ts
|
|
362
362
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
363
|
-
var
|
|
363
|
+
var import_zod15 = require("zod");
|
|
364
364
|
|
|
365
365
|
// src/tools/generic.ts
|
|
366
366
|
var import_zod = require("zod");
|
|
@@ -371,7 +371,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
371
371
|
var import_node_path = __toESM(require("path"));
|
|
372
372
|
|
|
373
373
|
// src/version.ts
|
|
374
|
-
var VERSION = "7.0.
|
|
374
|
+
var VERSION = "7.0.3" ? "7.0.3" : "0.0.0-dev";
|
|
375
375
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
376
376
|
|
|
377
377
|
// src/update/checkForUpdate.ts
|
|
@@ -2179,6 +2179,439 @@ ${elements.map(formatElement).join("\n")}`);
|
|
|
2179
2179
|
);
|
|
2180
2180
|
}
|
|
2181
2181
|
|
|
2182
|
+
// src/tools/packs/jsThread.ts
|
|
2183
|
+
var import_zod13 = require("zod");
|
|
2184
|
+
var JS_TOP_TOOL_ID = "js-top";
|
|
2185
|
+
var text11 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2186
|
+
var fail13 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2187
|
+
var errMsg13 = (e) => e instanceof Error ? e.message : String(e);
|
|
2188
|
+
var SPARK_CHARS = "\u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588";
|
|
2189
|
+
function sparkline(values) {
|
|
2190
|
+
const max = Math.max(1, ...values);
|
|
2191
|
+
return values.map((v) => SPARK_CHARS[Math.min(7, Math.floor(v / max * 7.999))]).join("");
|
|
2192
|
+
}
|
|
2193
|
+
function formatTopMarkdown(snap, durationMs) {
|
|
2194
|
+
const lines = [];
|
|
2195
|
+
lines.push(`## JS thread top (${(durationMs / 1e3).toFixed(1)}s sample)`);
|
|
2196
|
+
lines.push("");
|
|
2197
|
+
lines.push(`Thread busy: **${snap.busyPct.toFixed(1)}%** of the trailing 5s window.`);
|
|
2198
|
+
if (snap.freezeSummary.count > 0) {
|
|
2199
|
+
lines.push(
|
|
2200
|
+
`Freezes (\u226550ms single-task blocks, last 60s): **${snap.freezeSummary.count}**, worst **${snap.freezeSummary.worstMs}ms** \u2014 during these, taps and JS animations stall.`
|
|
2201
|
+
);
|
|
2202
|
+
}
|
|
2203
|
+
if (!snap.tiers.bridge) {
|
|
2204
|
+
lines.push(
|
|
2205
|
+
"_New Architecture device: touch/React commit work appears as \u201Cunattributed\u201D; timers, rAF and Promise chains are fully attributed._"
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
lines.push("");
|
|
2209
|
+
lines.push("| # | task origin | api | 5s ms | % busy | calls | avg ms | max ms | freezes |");
|
|
2210
|
+
lines.push("|---|---|---|---|---|---|---|---|---|");
|
|
2211
|
+
snap.rows.slice(0, 15).forEach((r, i) => {
|
|
2212
|
+
const freezeCell = r.freezes > 0 ? `\u26A0 ${r.freezes} (worst ${r.worstFreezeMs}ms)` : "\u2013";
|
|
2213
|
+
lines.push(
|
|
2214
|
+
`| ${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} |`
|
|
2215
|
+
);
|
|
2216
|
+
});
|
|
2217
|
+
lines.push("");
|
|
2218
|
+
lines.push(
|
|
2219
|
+
"_Drill into any row with `get_js_thread_origin_detail` using its `key` from the JSON below._"
|
|
2220
|
+
);
|
|
2221
|
+
return lines.join("\n");
|
|
2222
|
+
}
|
|
2223
|
+
function formatDetailMarkdown(d) {
|
|
2224
|
+
const lines = [];
|
|
2225
|
+
lines.push(`## Origin detail \u2014 ${d.label}`);
|
|
2226
|
+
lines.push("");
|
|
2227
|
+
lines.push(`API: \`${d.api}\` \xB7 key: \`${d.key}\``);
|
|
2228
|
+
lines.push("");
|
|
2229
|
+
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.`);
|
|
2230
|
+
lines.push("");
|
|
2231
|
+
lines.push("| runs | scheduled | avg/run | max/run | total | last run | freezes (60s) | worst freeze |");
|
|
2232
|
+
lines.push("|---|---|---|---|---|---|---|---|");
|
|
2233
|
+
lines.push(
|
|
2234
|
+
`| ${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"} |`
|
|
2235
|
+
);
|
|
2236
|
+
if (d.caller || d.file) {
|
|
2237
|
+
lines.push("");
|
|
2238
|
+
lines.push(`Scheduled from: **${d.caller ? `${d.caller}()` : "unknown caller"}**${d.file ? ` \u2014 ${d.file}${d.line != null ? `:${d.line}` : ""}` : ""}`);
|
|
2239
|
+
if (d.frames?.length) {
|
|
2240
|
+
lines.push(
|
|
2241
|
+
`Raw frames (Hermes bundle offsets): ${d.frames.map((f) => `${f.file}:${f.line}:${f.col}`).join(" \xB7 ")}`
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
} else if (d.api !== "unattributed" && d.api !== "other") {
|
|
2245
|
+
lines.push("");
|
|
2246
|
+
lines.push(
|
|
2247
|
+
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)._"
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
return lines.join("\n");
|
|
2251
|
+
}
|
|
2252
|
+
function registerJsThreadTools(server, sync) {
|
|
2253
|
+
server.registerTool(
|
|
2254
|
+
"get_js_thread_top",
|
|
2255
|
+
{
|
|
2256
|
+
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.",
|
|
2257
|
+
inputSchema: {
|
|
2258
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2259
|
+
durationMs: import_zod13.z.number().optional().describe("How long to sample before reading the table. Default 3000, max 30000."),
|
|
2260
|
+
clearFirst: import_zod13.z.boolean().optional().describe("Reset all stats before sampling, so the result attributes only what happens during this sample.")
|
|
2261
|
+
}
|
|
2262
|
+
},
|
|
2263
|
+
async ({ deviceId, durationMs, clearFirst }) => {
|
|
2264
|
+
try {
|
|
2265
|
+
const target = sync.requireProDevice(deviceId);
|
|
2266
|
+
const duration = Math.min(Math.max(durationMs ?? 3e3, 250), 3e4);
|
|
2267
|
+
const snap = await sync.callAction(
|
|
2268
|
+
JS_TOP_TOOL_ID,
|
|
2269
|
+
"sample",
|
|
2270
|
+
{ durationMs: duration, clearFirst: clearFirst === true },
|
|
2271
|
+
target
|
|
2272
|
+
);
|
|
2273
|
+
if (!snap) return fail13("Device returned no snapshot \u2014 is @buoy-gg/js-top installed in the app?");
|
|
2274
|
+
const md = formatTopMarkdown(snap, duration);
|
|
2275
|
+
const slim = {
|
|
2276
|
+
busyPct: Math.round(snap.busyPct * 10) / 10,
|
|
2277
|
+
freezeSummary: snap.freezeSummary,
|
|
2278
|
+
tiers: snap.tiers,
|
|
2279
|
+
rows: snap.rows.slice(0, 15).map((r) => ({
|
|
2280
|
+
key: r.key,
|
|
2281
|
+
api: r.api,
|
|
2282
|
+
windowMs: Math.round(r.windowMs * 10) / 10,
|
|
2283
|
+
pctOfBusy: Math.round(r.pctOfBusy * 10) / 10,
|
|
2284
|
+
calls: r.calls,
|
|
2285
|
+
avgMs: Math.round(r.avgMs * 100) / 100,
|
|
2286
|
+
maxMs: Math.round(r.maxMs * 10) / 10,
|
|
2287
|
+
totalMs: Math.round(r.totalMs),
|
|
2288
|
+
freezes: r.freezes,
|
|
2289
|
+
worstFreezeMs: r.worstFreezeMs
|
|
2290
|
+
}))
|
|
2291
|
+
};
|
|
2292
|
+
return text11(`${md}
|
|
2293
|
+
|
|
2294
|
+
\`\`\`json
|
|
2295
|
+
${JSON.stringify(slim)}
|
|
2296
|
+
\`\`\``);
|
|
2297
|
+
} catch (e) {
|
|
2298
|
+
return fail13(errMsg13(e));
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2301
|
+
);
|
|
2302
|
+
server.registerTool(
|
|
2303
|
+
"get_js_thread_origin_detail",
|
|
2304
|
+
{
|
|
2305
|
+
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).',
|
|
2306
|
+
inputSchema: {
|
|
2307
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2308
|
+
key: import_zod13.z.string().describe("Origin key from get_js_thread_top's JSON rows.")
|
|
2309
|
+
}
|
|
2310
|
+
},
|
|
2311
|
+
async ({ deviceId, key }) => {
|
|
2312
|
+
try {
|
|
2313
|
+
const target = sync.requireProDevice(deviceId);
|
|
2314
|
+
const detail = await sync.callAction(
|
|
2315
|
+
JS_TOP_TOOL_ID,
|
|
2316
|
+
"getOriginDetail",
|
|
2317
|
+
{ key },
|
|
2318
|
+
target
|
|
2319
|
+
);
|
|
2320
|
+
if (!detail) {
|
|
2321
|
+
return fail13(
|
|
2322
|
+
`Origin "${key}" is not tracked (cleared, evicted, or never existed). Re-run get_js_thread_top for current keys.`
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2325
|
+
const md = formatDetailMarkdown(detail);
|
|
2326
|
+
return text11(`${md}
|
|
2327
|
+
|
|
2328
|
+
\`\`\`json
|
|
2329
|
+
${JSON.stringify(detail)}
|
|
2330
|
+
\`\`\``);
|
|
2331
|
+
} catch (e) {
|
|
2332
|
+
return fail13(errMsg13(e));
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
);
|
|
2336
|
+
server.registerTool(
|
|
2337
|
+
"clear_js_thread_stats",
|
|
2338
|
+
{
|
|
2339
|
+
description: "Reset the js-top task table, freeze history, and busy history on the device (fresh measurement window).",
|
|
2340
|
+
inputSchema: {
|
|
2341
|
+
deviceId: import_zod13.z.string().optional().describe("Target device id (auto-selected if one is connected).")
|
|
2342
|
+
}
|
|
2343
|
+
},
|
|
2344
|
+
async ({ deviceId }) => {
|
|
2345
|
+
try {
|
|
2346
|
+
const target = sync.requireProDevice(deviceId);
|
|
2347
|
+
await sync.callAction(JS_TOP_TOOL_ID, "clear", {}, target);
|
|
2348
|
+
return text11("Cleared js-top stats.");
|
|
2349
|
+
} catch (e) {
|
|
2350
|
+
return fail13(errMsg13(e));
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
);
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
// src/tools/packs/images.ts
|
|
2357
|
+
var import_zod14 = require("zod");
|
|
2358
|
+
var IMAGES_TOOL_ID = "images";
|
|
2359
|
+
var text12 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
2360
|
+
var fail14 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
2361
|
+
var errMsg14 = (e) => e instanceof Error ? e.message : String(e);
|
|
2362
|
+
function uriTail(uri) {
|
|
2363
|
+
if (!uri) return "(no source)";
|
|
2364
|
+
const unstable = uri.match(/[?&]unstable_path=([^&?]+)/);
|
|
2365
|
+
const effective = unstable ? decodeURIComponent(unstable[1]) : uri;
|
|
2366
|
+
const noQuery = effective.split("?")[0] ?? effective;
|
|
2367
|
+
const segs = noQuery.split("/").filter(Boolean);
|
|
2368
|
+
return segs[segs.length - 1] ?? effective;
|
|
2369
|
+
}
|
|
2370
|
+
function verdictLabel(r) {
|
|
2371
|
+
if (r.oversizeFactor == null) return "\u2013";
|
|
2372
|
+
if (r.oversizeFactor < 0.9) return `${r.oversizeFactor.toFixed(1)}\xD7 upscaled`;
|
|
2373
|
+
if (r.oversizeFactor <= 1.1) return "right-sized";
|
|
2374
|
+
return `${r.oversizeFactor.toFixed(1)}\xD7 oversized (${r.wastedKB}KB wasted)`;
|
|
2375
|
+
}
|
|
2376
|
+
function cacheLabel(r) {
|
|
2377
|
+
if (r.cache) return r.cache;
|
|
2378
|
+
if (r.progressSeen) return "network";
|
|
2379
|
+
return "?";
|
|
2380
|
+
}
|
|
2381
|
+
function mb(bytes) {
|
|
2382
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
2383
|
+
}
|
|
2384
|
+
function formatListMarkdown(list, limit) {
|
|
2385
|
+
const lines = [];
|
|
2386
|
+
const s = list.stats;
|
|
2387
|
+
lines.push(`## Images registry (${list.total} records, showing ${Math.min(limit, list.records.length)})`);
|
|
2388
|
+
lines.push("");
|
|
2389
|
+
lines.push(
|
|
2390
|
+
`${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**` : "")
|
|
2391
|
+
);
|
|
2392
|
+
const ins = list.insights;
|
|
2393
|
+
if (ins) {
|
|
2394
|
+
const chips = [];
|
|
2395
|
+
if (ins.duplicates.length > 0)
|
|
2396
|
+
chips.push(`${ins.duplicates.length} duplicate URL(s): ${ins.duplicates.slice(0, 3).map((d) => `${uriTail(d.uri)}\xD7${d.count}`).join(", ")}`);
|
|
2397
|
+
if (ins.retryStorms.length > 0)
|
|
2398
|
+
chips.push(`${ins.retryStorms.length} retry storm(s): ${ins.retryStorms.slice(0, 3).map((r) => `#${r.id}(${r.loadCount} loads)`).join(", ")}`);
|
|
2399
|
+
if (ins.queueSaturated) chips.push("iOS image queue saturated (>4 concurrent loads)");
|
|
2400
|
+
if (ins.missingAlt > 0) chips.push(`${ins.missingAlt} image(s) missing alt/accessibilityLabel`);
|
|
2401
|
+
if (ins.layoutShifters.length > 0)
|
|
2402
|
+
chips.push(`${ins.layoutShifters.length} layout-shifting image(s): ${ins.layoutShifters.slice(0, 3).map((l) => `#${l.id}`).join(", ")} \u2014 reserve explicit dimensions`);
|
|
2403
|
+
if (chips.length > 0) {
|
|
2404
|
+
lines.push("");
|
|
2405
|
+
lines.push(`**Insights:** ${chips.join(" \xB7 ")}`);
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
const cs = list.captureStatus;
|
|
2409
|
+
if (cs.rnDecoratorTooLate) {
|
|
2410
|
+
lines.push("");
|
|
2411
|
+
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.');
|
|
2412
|
+
}
|
|
2413
|
+
if (list.globalModes.network !== "normal" || list.globalModes.blank) {
|
|
2414
|
+
lines.push("");
|
|
2415
|
+
lines.push(`\u26A0 Simulation active: network=${list.globalModes.network}${list.globalModes.blank ? ", blank images ON" : ""} (set_image_simulation to reset).`);
|
|
2416
|
+
}
|
|
2417
|
+
lines.push("");
|
|
2418
|
+
lines.push("| id | image | lib | status | cache | ms | decoded px | shown dp | verdict | sim |");
|
|
2419
|
+
lines.push("|---|---|---|---|---|---|---|---|---|---|");
|
|
2420
|
+
for (const r of list.records) {
|
|
2421
|
+
const dims = r.intrinsic ? `${r.intrinsic.width}\xD7${r.intrinsic.height}` : "\u2013";
|
|
2422
|
+
const layout = r.layout ? `${r.layout.width}\xD7${r.layout.height}` : "\u2013";
|
|
2423
|
+
const status = r.status === "error" ? `**error**${r.errorCode ? ` (${r.errorCode})` : ""}` : r.status;
|
|
2424
|
+
lines.push(
|
|
2425
|
+
`| ${r.id} | ${uriTail(r.uri)} | ${r.lib} | ${status}${r.mounted ? "" : " (unmounted)"} | ${cacheLabel(r)} | ${r.ms ?? "\u2013"} | ${dims} | ${layout} | ${verdictLabel(r)} | ${r.overrideLabel ?? "\u2013"} |`
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
lines.push("");
|
|
2429
|
+
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)._");
|
|
2430
|
+
return lines.join("\n");
|
|
2431
|
+
}
|
|
2432
|
+
var ACTION_MAP = {
|
|
2433
|
+
hard_reload: { action: "hardReload" },
|
|
2434
|
+
retry: { action: "retry" },
|
|
2435
|
+
evict_disk: { action: "evictDisk" },
|
|
2436
|
+
flash: { action: "flash" },
|
|
2437
|
+
restore: { action: "clearOverride" },
|
|
2438
|
+
force_error: { action: "setOverride", params: () => ({ kind: "error" }) },
|
|
2439
|
+
force_loading: { action: "setOverride", params: () => ({ kind: "hang" }) },
|
|
2440
|
+
force_blank: { action: "setOverride", params: () => ({ kind: "blank" }) },
|
|
2441
|
+
swap_url: { action: "setOverride", params: (url) => ({ kind: "url", uri: url }) }
|
|
2442
|
+
};
|
|
2443
|
+
function registerImagesTools(server, sync) {
|
|
2444
|
+
server.registerTool(
|
|
2445
|
+
"get_images",
|
|
2446
|
+
{
|
|
2447
|
+
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.",
|
|
2448
|
+
inputSchema: {
|
|
2449
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2450
|
+
limit: import_zod14.z.number().optional().describe("Max records to return (newest first). Default 30, max 200."),
|
|
2451
|
+
status: import_zod14.z.enum(["pending", "loading", "loaded", "error"]).optional().describe("Only records in this state (e.g. 'error' for the failure log).")
|
|
2452
|
+
}
|
|
2453
|
+
},
|
|
2454
|
+
async ({ deviceId, limit, status }) => {
|
|
2455
|
+
try {
|
|
2456
|
+
const target = sync.requireProDevice(deviceId);
|
|
2457
|
+
const capped = Math.min(Math.max(limit ?? 30, 1), 200);
|
|
2458
|
+
const list = await sync.callAction(
|
|
2459
|
+
IMAGES_TOOL_ID,
|
|
2460
|
+
"list",
|
|
2461
|
+
{ limit: capped, status },
|
|
2462
|
+
target
|
|
2463
|
+
);
|
|
2464
|
+
if (!list) return fail14("Device returned nothing \u2014 is @buoy-gg/images installed in the app?");
|
|
2465
|
+
const md = formatListMarkdown(list, capped);
|
|
2466
|
+
const slim = {
|
|
2467
|
+
stats: list.stats,
|
|
2468
|
+
insights: list.insights,
|
|
2469
|
+
captureStatus: list.captureStatus,
|
|
2470
|
+
globalModes: list.globalModes,
|
|
2471
|
+
records: list.records.map((r) => ({
|
|
2472
|
+
id: r.id,
|
|
2473
|
+
uri: r.uri,
|
|
2474
|
+
lib: r.lib,
|
|
2475
|
+
status: r.status,
|
|
2476
|
+
mounted: r.mounted,
|
|
2477
|
+
cache: r.cache,
|
|
2478
|
+
ms: r.ms,
|
|
2479
|
+
intrinsic: r.intrinsic,
|
|
2480
|
+
layout: r.layout,
|
|
2481
|
+
oversizeFactor: r.oversizeFactor,
|
|
2482
|
+
wastedKB: r.wastedKB,
|
|
2483
|
+
error: r.error,
|
|
2484
|
+
errorCode: r.errorCode,
|
|
2485
|
+
overrideLabel: r.overrideLabel,
|
|
2486
|
+
loadCount: r.loadCount
|
|
2487
|
+
}))
|
|
2488
|
+
};
|
|
2489
|
+
return text12(`${md}
|
|
2490
|
+
|
|
2491
|
+
\`\`\`json
|
|
2492
|
+
${JSON.stringify(slim)}
|
|
2493
|
+
\`\`\``);
|
|
2494
|
+
} catch (e) {
|
|
2495
|
+
return fail14(errMsg14(e));
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
);
|
|
2499
|
+
server.registerTool(
|
|
2500
|
+
"image_action",
|
|
2501
|
+
{
|
|
2502
|
+
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.",
|
|
2503
|
+
inputSchema: {
|
|
2504
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2505
|
+
id: import_zod14.z.number().describe("Record id from get_images."),
|
|
2506
|
+
action: import_zod14.z.enum([
|
|
2507
|
+
"hard_reload",
|
|
2508
|
+
"retry",
|
|
2509
|
+
"evict_disk",
|
|
2510
|
+
"flash",
|
|
2511
|
+
"prove_savings",
|
|
2512
|
+
"force_error",
|
|
2513
|
+
"force_loading",
|
|
2514
|
+
"force_blank",
|
|
2515
|
+
"swap_url",
|
|
2516
|
+
"restore"
|
|
2517
|
+
]).describe("What to do."),
|
|
2518
|
+
url: import_zod14.z.string().optional().describe("Replacement URL (swap_url only).")
|
|
2519
|
+
}
|
|
2520
|
+
},
|
|
2521
|
+
async ({ deviceId, id, action, url }) => {
|
|
2522
|
+
try {
|
|
2523
|
+
const target = sync.requireProDevice(deviceId);
|
|
2524
|
+
if (action === "prove_savings") {
|
|
2525
|
+
const savings = await sync.callAction(IMAGES_TOOL_ID, "proveSavings", { id }, target);
|
|
2526
|
+
if (!savings) return fail14("Device returned nothing for proveSavings.");
|
|
2527
|
+
if (!savings.ok) return fail14(savings.message);
|
|
2528
|
+
const parts = [`**prove_savings** on #${id}: ${savings.message}`];
|
|
2529
|
+
if (savings.originalBytes != null)
|
|
2530
|
+
parts.push(`Original: ${Math.round(savings.originalBytes / 1024)}KB (${savings.originalBytesSource})`);
|
|
2531
|
+
if (savings.optimizedBytes != null)
|
|
2532
|
+
parts.push(
|
|
2533
|
+
`Optimized: ${Math.round(savings.optimizedBytes / 1024)}KB \u2014 ${savings.optimizedDims?.width}\xD7${savings.optimizedDims?.height} WebP`
|
|
2534
|
+
);
|
|
2535
|
+
if (savings.savedBytes != null)
|
|
2536
|
+
parts.push(`**Saved: ${Math.round(savings.savedBytes / 1024)}KB (${savings.savedPct}%)**`);
|
|
2537
|
+
if (savings.optimizedUri)
|
|
2538
|
+
parts.push(`Preview in place: image_action {id:${id}, action:"swap_url", url:"${savings.optimizedUri}"}`);
|
|
2539
|
+
return text12(parts.join("\n"));
|
|
2540
|
+
}
|
|
2541
|
+
const entry = ACTION_MAP[action];
|
|
2542
|
+
if (!entry) return fail14(`Unknown action: ${action}`);
|
|
2543
|
+
if (action === "swap_url" && !url) return fail14("swap_url requires the `url` param.");
|
|
2544
|
+
const result = await sync.callAction(
|
|
2545
|
+
IMAGES_TOOL_ID,
|
|
2546
|
+
entry.action,
|
|
2547
|
+
{ id, ...entry.params?.(url) ?? {} },
|
|
2548
|
+
target
|
|
2549
|
+
);
|
|
2550
|
+
const settleMs = action === "flash" ? 0 : 1500;
|
|
2551
|
+
if (settleMs > 0) await new Promise((r) => setTimeout(r, settleMs));
|
|
2552
|
+
const detail = await sync.callAction(IMAGES_TOOL_ID, "getDetail", { id }, target).catch(() => null);
|
|
2553
|
+
const lines = [
|
|
2554
|
+
`**${action}** on #${id}: ${result?.message ?? (result?.ok === false ? "failed" : "done")}`
|
|
2555
|
+
];
|
|
2556
|
+
if (detail) {
|
|
2557
|
+
lines.push("");
|
|
2558
|
+
lines.push(
|
|
2559
|
+
`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"}`
|
|
2560
|
+
);
|
|
2561
|
+
if (detail.error) lines.push(`Error: ${detail.error}`);
|
|
2562
|
+
}
|
|
2563
|
+
return text12(lines.join("\n"));
|
|
2564
|
+
} catch (e) {
|
|
2565
|
+
return fail14(errMsg14(e));
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
);
|
|
2569
|
+
server.registerTool(
|
|
2570
|
+
"set_image_simulation",
|
|
2571
|
+
{
|
|
2572
|
+
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.",
|
|
2573
|
+
inputSchema: {
|
|
2574
|
+
deviceId: import_zod14.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
2575
|
+
network: import_zod14.z.enum(["normal", "offline", "cold"]).optional().describe("Network simulation mode."),
|
|
2576
|
+
blankImages: import_zod14.z.boolean().optional().describe("Render all images without a source."),
|
|
2577
|
+
massAction: import_zod14.z.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."),
|
|
2578
|
+
clearRegistry: import_zod14.z.boolean().optional().describe("Clear the captured image log."),
|
|
2579
|
+
clearExpoCaches: import_zod14.z.boolean().optional().describe("Clear expo-image memory + disk caches.")
|
|
2580
|
+
}
|
|
2581
|
+
},
|
|
2582
|
+
async ({ deviceId, network, blankImages, massAction, clearRegistry, clearExpoCaches }) => {
|
|
2583
|
+
try {
|
|
2584
|
+
const target = sync.requireProDevice(deviceId);
|
|
2585
|
+
const done = [];
|
|
2586
|
+
if (massAction) {
|
|
2587
|
+
const r = await sync.callAction(IMAGES_TOOL_ID, "massAction", { kind: massAction }, target);
|
|
2588
|
+
done.push(r?.message ?? `mass ${massAction} applied`);
|
|
2589
|
+
}
|
|
2590
|
+
if (network) {
|
|
2591
|
+
await sync.callAction(IMAGES_TOOL_ID, "setNetworkMode", { mode: network }, target);
|
|
2592
|
+
done.push(`network=${network}`);
|
|
2593
|
+
}
|
|
2594
|
+
if (blankImages != null) {
|
|
2595
|
+
await sync.callAction(IMAGES_TOOL_ID, "setBlankImages", { enabled: blankImages }, target);
|
|
2596
|
+
done.push(`blankImages=${blankImages}`);
|
|
2597
|
+
}
|
|
2598
|
+
if (clearRegistry) {
|
|
2599
|
+
await sync.callAction(IMAGES_TOOL_ID, "clearRecords", {}, target);
|
|
2600
|
+
done.push("registry cleared");
|
|
2601
|
+
}
|
|
2602
|
+
if (clearExpoCaches) {
|
|
2603
|
+
const r = await sync.callAction(IMAGES_TOOL_ID, "clearExpoCaches", {}, target);
|
|
2604
|
+
done.push(r?.message ?? "expo caches cleared");
|
|
2605
|
+
}
|
|
2606
|
+
if (done.length === 0) return fail14("Pass at least one of: network, blankImages, massAction, clearRegistry, clearExpoCaches.");
|
|
2607
|
+
return text12(`Applied: ${done.join(" \xB7 ")}`);
|
|
2608
|
+
} catch (e) {
|
|
2609
|
+
return fail14(errMsg14(e));
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
);
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2182
2615
|
// src/server.ts
|
|
2183
2616
|
var SERVER_INSTRUCTIONS = `Buoy MCP \u2014 drive Buoy dev tools running in a React Native app from your editor.
|
|
2184
2617
|
|
|
@@ -2201,6 +2634,15 @@ elements (accessibility-style tree with tap points + testIDs, no screenshot), th
|
|
|
2201
2634
|
({ testID | query | nativeTag }) to tap one \u2014 it invokes the element's onPress directly in JS, so
|
|
2202
2635
|
it's fast and works on physical devices too. Re-run \`describe_screen\` after the screen changes.
|
|
2203
2636
|
|
|
2637
|
+
JS FPS low / app hitching and you need to know WHAT is eating the JS thread? Use
|
|
2638
|
+
\`get_js_thread_top\` (a live "Task Manager" of timers, Promise chains, rAF callbacks and
|
|
2639
|
+
freezes, ranked by thread time), then \`get_js_thread_origin_detail\` to drill into a row.
|
|
2640
|
+
Pass clearFirst:true and drive the UI with \`tap_element\` mid-sample to profile one interaction.
|
|
2641
|
+
|
|
2642
|
+
Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists every RN <Image> /
|
|
2643
|
+
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
2644
|
+
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
2645
|
+
|
|
2204
2646
|
For everything else:
|
|
2205
2647
|
- \`list_tool_actions\` to see what a tool can do,
|
|
2206
2648
|
- \`get_snapshot\` ONLY when you need a tool's full current state (returns raw, heavy payloads),
|
|
@@ -2229,10 +2671,12 @@ function createMcpServer(sync) {
|
|
|
2229
2671
|
registerConsoleTools(server, sync);
|
|
2230
2672
|
registerScreenshotTools(server, sync);
|
|
2231
2673
|
registerInteractTools(server, sync);
|
|
2674
|
+
registerJsThreadTools(server, sync);
|
|
2675
|
+
registerImagesTools(server, sync);
|
|
2232
2676
|
return server;
|
|
2233
2677
|
}
|
|
2234
2678
|
var deviceIdField = {
|
|
2235
|
-
deviceId:
|
|
2679
|
+
deviceId: import_zod15.z.string().optional().describe(
|
|
2236
2680
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
2237
2681
|
)
|
|
2238
2682
|
};
|
package/dist/index.mjs
CHANGED
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.
|
|
4
|
+
"version": "7.0.3",
|
|
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.
|
|
34
|
+
"@buoy-gg/sync-broker": "7.0.3"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"build": "tsup",
|
|
69
69
|
"typecheck": "tsc --noEmit",
|
|
70
70
|
"clean": "rimraf dist",
|
|
71
|
-
"test": "pnpm run typecheck",
|
|
72
|
-
"dev": "node dist/cli.js"
|
|
71
|
+
"test": "pnpm run typecheck && pnpm run smoke",
|
|
72
|
+
"dev": "node dist/cli.js",
|
|
73
|
+
"smoke": "node scripts/stdio-smoke.mjs"
|
|
73
74
|
}
|
|
74
75
|
}
|