@buoy-gg/mcp 7.0.14 → 7.0.16
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-ZEHIU6OG.mjs → chunk-P2OALRPK.mjs} +242 -16
- package/dist/cli.js +242 -16
- package/dist/cli.mjs +1 -1
- package/dist/index.js +242 -16
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -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.
|
|
304
|
+
var VERSION = "7.0.16" ? "7.0.16" : "0.0.0-dev";
|
|
305
305
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
306
306
|
|
|
307
307
|
// src/update/checkForUpdate.ts
|
|
@@ -2025,7 +2025,7 @@ function registerScreenshotTools(server, sync) {
|
|
|
2025
2025
|
|
|
2026
2026
|
// src/server.ts
|
|
2027
2027
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2028
|
-
import { z as
|
|
2028
|
+
import { z as z18 } from "zod";
|
|
2029
2029
|
|
|
2030
2030
|
// src/tools/packs/network.ts
|
|
2031
2031
|
import { z as z12 } from "zod";
|
|
@@ -2879,7 +2879,9 @@ function registerImagesTools(server, sync) {
|
|
|
2879
2879
|
error: r.error,
|
|
2880
2880
|
errorCode: r.errorCode,
|
|
2881
2881
|
overrideLabel: r.overrideLabel,
|
|
2882
|
-
loadCount: r.loadCount
|
|
2882
|
+
loadCount: r.loadCount,
|
|
2883
|
+
hasAltText: r.hasAltText,
|
|
2884
|
+
layoutShifts: r.layoutShifts
|
|
2883
2885
|
}))
|
|
2884
2886
|
};
|
|
2885
2887
|
return text13(`${md}
|
|
@@ -3008,17 +3010,234 @@ ${JSON.stringify(slim)}
|
|
|
3008
3010
|
);
|
|
3009
3011
|
}
|
|
3010
3012
|
|
|
3011
|
-
// src/tools/packs/
|
|
3013
|
+
// src/tools/packs/assets.ts
|
|
3012
3014
|
import { z as z16 } from "zod";
|
|
3015
|
+
var ASSETS_TOOL_ID = "assets";
|
|
3016
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3017
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3018
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3019
|
+
function kb(bytes) {
|
|
3020
|
+
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3021
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
3022
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3023
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
3024
|
+
}
|
|
3025
|
+
function sizeLabel(r) {
|
|
3026
|
+
if (r.sizeBytes == null) return "\u2013";
|
|
3027
|
+
return r.sizeSource === "measured" ? kb(r.sizeBytes) : `~${kb(r.sizeBytes)}`;
|
|
3028
|
+
}
|
|
3029
|
+
function scalesLabel(scales) {
|
|
3030
|
+
if (!scales.length) return "\u2013";
|
|
3031
|
+
return scales.map((s) => `@${s}x`).join(" ");
|
|
3032
|
+
}
|
|
3033
|
+
function flagsLabel(r, ins) {
|
|
3034
|
+
const flags = [];
|
|
3035
|
+
if (!r.loaded) flags.push("UNUSED");
|
|
3036
|
+
if (ins.overDecode.includes(r.id)) flags.push("OVERSIZE");
|
|
3037
|
+
if (ins.duplicates.some((d) => d.ids.includes(r.id))) flags.push("DUP");
|
|
3038
|
+
if (ins.heavyGifs.includes(r.id) || ins.heavyFonts.includes(r.id)) flags.push("HEAVY");
|
|
3039
|
+
if (ins.scaleAnomalies.includes(r.id)) flags.push("BAD-SCALE");
|
|
3040
|
+
else if (ins.scaleGaps.includes(r.id)) flags.push("LOW-RES");
|
|
3041
|
+
if (ins.webpCandidates.includes(r.id)) flags.push("WEBP");
|
|
3042
|
+
return flags.length ? flags.join(" ") : "\u2013";
|
|
3043
|
+
}
|
|
3044
|
+
function formatListMarkdown2(list, limit) {
|
|
3045
|
+
const lines = [];
|
|
3046
|
+
const s = list.stats;
|
|
3047
|
+
const ss = list.scanStatus;
|
|
3048
|
+
lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
|
|
3049
|
+
lines.push("");
|
|
3050
|
+
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb(v.bytes)})` : ""}`);
|
|
3051
|
+
lines.push(
|
|
3052
|
+
`${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${kb(s.totalBytes)} measured (${s.measuredCount}/${s.total})` : "sizes not measured") + ` \xB7 ${kindBits.join(" \xB7 ")}`
|
|
3053
|
+
);
|
|
3054
|
+
if (ss.fontFamilies.length > 0) {
|
|
3055
|
+
lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
|
|
3056
|
+
}
|
|
3057
|
+
if (!ss.graphSupported || ss.graphCount == null) {
|
|
3058
|
+
lines.push(
|
|
3059
|
+
"\u26A0 No Metro bundle graph (release build or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable." + (ss.graphError ? ` (${ss.graphError})` : "")
|
|
3060
|
+
);
|
|
3061
|
+
}
|
|
3062
|
+
if (ss.measuring) lines.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");
|
|
3063
|
+
const ins = list.insights;
|
|
3064
|
+
const chips = [];
|
|
3065
|
+
if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
|
|
3066
|
+
if (ins.duplicates.length) {
|
|
3067
|
+
const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
|
|
3068
|
+
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb(wasted)} wasted)` : ""}`);
|
|
3069
|
+
}
|
|
3070
|
+
if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
|
|
3071
|
+
if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
|
|
3072
|
+
if (ins.heavyGifs.length) chips.push(`${ins.heavyGifs.length} heavy GIF(s)`);
|
|
3073
|
+
if (ins.heavyFonts.length) chips.push(`${ins.heavyFonts.length} heavy font(s)`);
|
|
3074
|
+
if (ins.scaleGaps.length) chips.push(`${ins.scaleGaps.length} scale gap(s)`);
|
|
3075
|
+
if (ins.scaleAnomalies.length) chips.push(`${ins.scaleAnomalies.length} scale anomaly(ies)`);
|
|
3076
|
+
if (chips.length) {
|
|
3077
|
+
lines.push("");
|
|
3078
|
+
lines.push(`**Findings:** ${chips.join(" \xB7 ")}`);
|
|
3079
|
+
}
|
|
3080
|
+
const diff = list.diff;
|
|
3081
|
+
if (diff) {
|
|
3082
|
+
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb(Math.abs(diff.netBytes))} net`;
|
|
3083
|
+
lines.push("");
|
|
3084
|
+
lines.push(
|
|
3085
|
+
`**Since baseline** (${new Date(diff.baselineSavedAt).toISOString()}): ${diff.added.length} added \xB7 ${diff.removed.length} removed \xB7 ${diff.grown.length} grew \xB7 ${net}` + (diff.removed.length ? ` \u2014 removed: ${diff.removed.slice(0, 5).map((r) => `${r.name}.${r.type}`).join(", ")}${diff.removed.length > 5 ? "\u2026" : ""}` : "")
|
|
3086
|
+
);
|
|
3087
|
+
}
|
|
3088
|
+
lines.push("");
|
|
3089
|
+
lines.push("| id | asset | kind | dims | scales | size | flags |");
|
|
3090
|
+
lines.push("|---|---|---|---|---|---|---|");
|
|
3091
|
+
for (const r of list.records) {
|
|
3092
|
+
const dims = r.width != null && r.height != null ? `${Math.round(r.width)}\xD7${Math.round(r.height)}` : "\u2013";
|
|
3093
|
+
lines.push(
|
|
3094
|
+
`| ${r.id} | ${r.name}.${r.type} | ${r.kind} | ${dims} | ${scalesLabel(r.scales)} | ${sizeLabel(r)} | ${flagsLabel(r, ins)} |`
|
|
3095
|
+
);
|
|
3096
|
+
}
|
|
3097
|
+
lines.push("");
|
|
3098
|
+
lines.push(
|
|
3099
|
+
"_Sizes marked ~ are decoded-memory estimates (unmeasurable in this environment). Use `asset_action` for rescan / measure_sizes / detail / baseline ops._"
|
|
3100
|
+
);
|
|
3101
|
+
return lines.join("\n");
|
|
3102
|
+
}
|
|
3103
|
+
function registerAssetsTools(server, sync) {
|
|
3104
|
+
server.registerTool(
|
|
3105
|
+
"get_assets",
|
|
3106
|
+
{
|
|
3107
|
+
description: "Bundled-asset inventory from the app: every asset (images, fonts, video, audio) with dimensions, @1x/@2x/@3x scale coverage, byte sizes (measured from the Metro server in dev), loaded vs never-loaded state, and audit findings \u2014 duplicates (by content hash), shipped-but-never-loaded, WebP candidates, images decoding larger than the screen, heavy GIFs/fonts, scale gaps \u2014 plus the change vs a saved baseline. This is what the app SHIPS; use get_images for what it RENDERS. Requires @buoy-gg/assets on the device.",
|
|
3108
|
+
inputSchema: {
|
|
3109
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3110
|
+
limit: z16.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
3111
|
+
kind: z16.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
3112
|
+
unusedOnly: z16.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3113
|
+
}
|
|
3114
|
+
},
|
|
3115
|
+
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
3116
|
+
try {
|
|
3117
|
+
const target = sync.requireProDevice(deviceId);
|
|
3118
|
+
const capped = Math.min(Math.max(limit ?? 40, 1), 500);
|
|
3119
|
+
const list = await sync.callAction(
|
|
3120
|
+
ASSETS_TOOL_ID,
|
|
3121
|
+
"list",
|
|
3122
|
+
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3123
|
+
target
|
|
3124
|
+
);
|
|
3125
|
+
if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3126
|
+
if (unusedOnly) {
|
|
3127
|
+
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3128
|
+
}
|
|
3129
|
+
const md = formatListMarkdown2(list, capped);
|
|
3130
|
+
const slim = {
|
|
3131
|
+
stats: list.stats,
|
|
3132
|
+
scanStatus: list.scanStatus,
|
|
3133
|
+
insights: list.insights,
|
|
3134
|
+
diff: list.diff,
|
|
3135
|
+
records: list.records.map((r) => ({
|
|
3136
|
+
id: r.id,
|
|
3137
|
+
name: r.name,
|
|
3138
|
+
type: r.type,
|
|
3139
|
+
kind: r.kind,
|
|
3140
|
+
hash: r.hash,
|
|
3141
|
+
width: r.width,
|
|
3142
|
+
height: r.height,
|
|
3143
|
+
scales: r.scales,
|
|
3144
|
+
loaded: r.loaded,
|
|
3145
|
+
sizeBytes: r.sizeBytes,
|
|
3146
|
+
sizeSource: r.sizeSource
|
|
3147
|
+
}))
|
|
3148
|
+
};
|
|
3149
|
+
return text14(`${md}
|
|
3150
|
+
|
|
3151
|
+
\`\`\`json
|
|
3152
|
+
${JSON.stringify(slim)}
|
|
3153
|
+
\`\`\``);
|
|
3154
|
+
} catch (e) {
|
|
3155
|
+
return fail16(errMsg16(e));
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
);
|
|
3159
|
+
server.registerTool(
|
|
3160
|
+
"asset_action",
|
|
3161
|
+
{
|
|
3162
|
+
description: "Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",
|
|
3163
|
+
inputSchema: {
|
|
3164
|
+
deviceId: z16.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3165
|
+
action: z16.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
3166
|
+
id: z16.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3167
|
+
}
|
|
3168
|
+
},
|
|
3169
|
+
async ({ deviceId, action, id }) => {
|
|
3170
|
+
try {
|
|
3171
|
+
const target = sync.requireProDevice(deviceId);
|
|
3172
|
+
switch (action) {
|
|
3173
|
+
case "rescan": {
|
|
3174
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3175
|
+
const ss = result?.scanStatus;
|
|
3176
|
+
if (!ss) return fail16("Device returned nothing for rescan.");
|
|
3177
|
+
return text14(
|
|
3178
|
+
`Rescan done: ${ss.registryCount} registered at runtime` + (ss.graphCount != null ? ` \xB7 ${ss.graphCount} in the bundle graph` : " \xB7 no bundle graph") + (ss.graphError ? ` (graph: ${ss.graphError})` : "") + ` \xB7 ${ss.fontFamilies.length} font families` + (ss.measuring ? " \xB7 size measurement running \u2014 get_assets in a moment" : "")
|
|
3179
|
+
);
|
|
3180
|
+
}
|
|
3181
|
+
case "measure_sizes": {
|
|
3182
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3183
|
+
return text14(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3184
|
+
}
|
|
3185
|
+
case "detail": {
|
|
3186
|
+
if (id == null) return fail16("detail requires the `id` param (from get_assets).");
|
|
3187
|
+
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3188
|
+
if (!detail) return fail16(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3189
|
+
const lines = [
|
|
3190
|
+
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3191
|
+
"",
|
|
3192
|
+
`- Kind: ${detail.kind} \xB7 ${detail.loaded ? "loaded at runtime" : "in bundle, NEVER loaded"} (${detail.origin})`
|
|
3193
|
+
];
|
|
3194
|
+
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
3195
|
+
if (detail.sizesByScale) {
|
|
3196
|
+
for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
|
|
3197
|
+
lines.push(`- Size @${scale}x: ${kb(bytes)}`);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
3201
|
+
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb(detail.estDecodedBytes)} (RGBA)`);
|
|
3202
|
+
if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
|
|
3203
|
+
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3204
|
+
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3205
|
+
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3206
|
+
return text14(lines.join("\n"));
|
|
3207
|
+
}
|
|
3208
|
+
case "save_baseline": {
|
|
3209
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3210
|
+
return text14(
|
|
3211
|
+
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
case "clear_baseline": {
|
|
3215
|
+
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3216
|
+
return text14("Baseline cleared.");
|
|
3217
|
+
}
|
|
3218
|
+
case "clear_records": {
|
|
3219
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3220
|
+
return text14(result?.message ?? "Inventory cleared.");
|
|
3221
|
+
}
|
|
3222
|
+
}
|
|
3223
|
+
} catch (e) {
|
|
3224
|
+
return fail16(errMsg16(e));
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
);
|
|
3228
|
+
}
|
|
3229
|
+
|
|
3230
|
+
// src/tools/packs/reload.ts
|
|
3231
|
+
import { z as z17 } from "zod";
|
|
3013
3232
|
var APP_TOOL_ID = "app";
|
|
3014
3233
|
var RELOAD_ACTION = "reloadApp";
|
|
3015
3234
|
var DEFAULT_WAIT_MS = 3e4;
|
|
3016
3235
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3017
3236
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3018
3237
|
var PROBE_INTERVAL_MS = 400;
|
|
3019
|
-
var
|
|
3020
|
-
var
|
|
3021
|
-
var
|
|
3238
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3239
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3240
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3022
3241
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3023
3242
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3024
3243
|
try {
|
|
@@ -3053,12 +3272,12 @@ function registerReloadTools(server, sync) {
|
|
|
3053
3272
|
{
|
|
3054
3273
|
description: "Reload the running app's JS bundle (Expo Go, dev client, or RN CLI) \u2014 the equivalent of shaking the device and hitting Reload, but from here. Use it to pick up a change Fast Refresh didn't apply, to clear leaked in-memory state before a measurement, or to re-run app startup. In dev builds it uses React Native's DevSettings.reload(); otherwise it falls back to expo-updates (if the app installs it). All in-memory state is lost. By default this waits for the app to come back up and reports how long the reload took. Works on any app running @buoy-gg/core \u2014 no particular tool package needed. Requires Buoy Pro.",
|
|
3055
3274
|
inputSchema: {
|
|
3056
|
-
strategy:
|
|
3275
|
+
strategy: z17.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
3057
3276
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
3058
3277
|
),
|
|
3059
|
-
wait:
|
|
3060
|
-
timeoutMs:
|
|
3061
|
-
deviceId:
|
|
3278
|
+
wait: z17.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
3279
|
+
timeoutMs: z17.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
3280
|
+
deviceId: z17.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
3062
3281
|
}
|
|
3063
3282
|
},
|
|
3064
3283
|
async (args) => {
|
|
@@ -3074,19 +3293,19 @@ function registerReloadTools(server, sync) {
|
|
|
3074
3293
|
);
|
|
3075
3294
|
const via = ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
3076
3295
|
if (args.wait === false) {
|
|
3077
|
-
return
|
|
3296
|
+
return text15(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
3078
3297
|
}
|
|
3079
3298
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
3080
3299
|
if (elapsed == null) {
|
|
3081
|
-
return
|
|
3300
|
+
return fail17(
|
|
3082
3301
|
`Reload was accepted${via}, but the app hasn't reconnected yet. It may still be building the bundle (a cold Metro rebuild can take a while) \u2014 check the device, then run list_devices.`
|
|
3083
3302
|
);
|
|
3084
3303
|
}
|
|
3085
|
-
return
|
|
3304
|
+
return text15(
|
|
3086
3305
|
`Reloaded the app${via} \u2014 back and connected after ${(elapsed / 1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`
|
|
3087
3306
|
);
|
|
3088
3307
|
} catch (e) {
|
|
3089
|
-
return
|
|
3308
|
+
return fail17(errMsg17(e));
|
|
3090
3309
|
}
|
|
3091
3310
|
}
|
|
3092
3311
|
);
|
|
@@ -3130,6 +3349,12 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
|
|
|
3130
3349
|
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
3131
3350
|
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
3132
3351
|
|
|
3352
|
+
Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
|
|
3353
|
+
fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
|
|
3354
|
+
WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
|
|
3355
|
+
the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
|
|
3356
|
+
get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
|
|
3357
|
+
|
|
3133
3358
|
Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
|
|
3134
3359
|
measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
|
|
3135
3360
|
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
@@ -3166,11 +3391,12 @@ function createMcpServer(sync) {
|
|
|
3166
3391
|
registerInteractTools(server, sync);
|
|
3167
3392
|
registerJsThreadTools(server, sync);
|
|
3168
3393
|
registerImagesTools(server, sync);
|
|
3394
|
+
registerAssetsTools(server, sync);
|
|
3169
3395
|
registerReloadTools(server, sync);
|
|
3170
3396
|
return server;
|
|
3171
3397
|
}
|
|
3172
3398
|
var deviceIdField = {
|
|
3173
|
-
deviceId:
|
|
3399
|
+
deviceId: z18.string().optional().describe(
|
|
3174
3400
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
3175
3401
|
)
|
|
3176
3402
|
};
|
package/dist/cli.js
CHANGED
|
@@ -330,7 +330,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
330
330
|
|
|
331
331
|
// src/server.ts
|
|
332
332
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
333
|
-
var
|
|
333
|
+
var import_zod18 = require("zod");
|
|
334
334
|
|
|
335
335
|
// src/tools/generic.ts
|
|
336
336
|
var import_zod = require("zod");
|
|
@@ -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.
|
|
344
|
+
var VERSION = "7.0.16" ? "7.0.16" : "0.0.0-dev";
|
|
345
345
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
346
346
|
|
|
347
347
|
// src/update/checkForUpdate.ts
|
|
@@ -2911,7 +2911,9 @@ function registerImagesTools(server, sync) {
|
|
|
2911
2911
|
error: r.error,
|
|
2912
2912
|
errorCode: r.errorCode,
|
|
2913
2913
|
overrideLabel: r.overrideLabel,
|
|
2914
|
-
loadCount: r.loadCount
|
|
2914
|
+
loadCount: r.loadCount,
|
|
2915
|
+
hasAltText: r.hasAltText,
|
|
2916
|
+
layoutShifts: r.layoutShifts
|
|
2915
2917
|
}))
|
|
2916
2918
|
};
|
|
2917
2919
|
return text13(`${md}
|
|
@@ -3040,17 +3042,234 @@ ${JSON.stringify(slim)}
|
|
|
3040
3042
|
);
|
|
3041
3043
|
}
|
|
3042
3044
|
|
|
3043
|
-
// src/tools/packs/
|
|
3045
|
+
// src/tools/packs/assets.ts
|
|
3044
3046
|
var import_zod16 = require("zod");
|
|
3047
|
+
var ASSETS_TOOL_ID = "assets";
|
|
3048
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3049
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3050
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3051
|
+
function kb(bytes) {
|
|
3052
|
+
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3053
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
3054
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3055
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
3056
|
+
}
|
|
3057
|
+
function sizeLabel(r) {
|
|
3058
|
+
if (r.sizeBytes == null) return "\u2013";
|
|
3059
|
+
return r.sizeSource === "measured" ? kb(r.sizeBytes) : `~${kb(r.sizeBytes)}`;
|
|
3060
|
+
}
|
|
3061
|
+
function scalesLabel(scales) {
|
|
3062
|
+
if (!scales.length) return "\u2013";
|
|
3063
|
+
return scales.map((s) => `@${s}x`).join(" ");
|
|
3064
|
+
}
|
|
3065
|
+
function flagsLabel(r, ins) {
|
|
3066
|
+
const flags = [];
|
|
3067
|
+
if (!r.loaded) flags.push("UNUSED");
|
|
3068
|
+
if (ins.overDecode.includes(r.id)) flags.push("OVERSIZE");
|
|
3069
|
+
if (ins.duplicates.some((d) => d.ids.includes(r.id))) flags.push("DUP");
|
|
3070
|
+
if (ins.heavyGifs.includes(r.id) || ins.heavyFonts.includes(r.id)) flags.push("HEAVY");
|
|
3071
|
+
if (ins.scaleAnomalies.includes(r.id)) flags.push("BAD-SCALE");
|
|
3072
|
+
else if (ins.scaleGaps.includes(r.id)) flags.push("LOW-RES");
|
|
3073
|
+
if (ins.webpCandidates.includes(r.id)) flags.push("WEBP");
|
|
3074
|
+
return flags.length ? flags.join(" ") : "\u2013";
|
|
3075
|
+
}
|
|
3076
|
+
function formatListMarkdown2(list, limit) {
|
|
3077
|
+
const lines = [];
|
|
3078
|
+
const s = list.stats;
|
|
3079
|
+
const ss = list.scanStatus;
|
|
3080
|
+
lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
|
|
3081
|
+
lines.push("");
|
|
3082
|
+
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb(v.bytes)})` : ""}`);
|
|
3083
|
+
lines.push(
|
|
3084
|
+
`${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${kb(s.totalBytes)} measured (${s.measuredCount}/${s.total})` : "sizes not measured") + ` \xB7 ${kindBits.join(" \xB7 ")}`
|
|
3085
|
+
);
|
|
3086
|
+
if (ss.fontFamilies.length > 0) {
|
|
3087
|
+
lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
|
|
3088
|
+
}
|
|
3089
|
+
if (!ss.graphSupported || ss.graphCount == null) {
|
|
3090
|
+
lines.push(
|
|
3091
|
+
"\u26A0 No Metro bundle graph (release build or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable." + (ss.graphError ? ` (${ss.graphError})` : "")
|
|
3092
|
+
);
|
|
3093
|
+
}
|
|
3094
|
+
if (ss.measuring) lines.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");
|
|
3095
|
+
const ins = list.insights;
|
|
3096
|
+
const chips = [];
|
|
3097
|
+
if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
|
|
3098
|
+
if (ins.duplicates.length) {
|
|
3099
|
+
const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
|
|
3100
|
+
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb(wasted)} wasted)` : ""}`);
|
|
3101
|
+
}
|
|
3102
|
+
if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
|
|
3103
|
+
if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
|
|
3104
|
+
if (ins.heavyGifs.length) chips.push(`${ins.heavyGifs.length} heavy GIF(s)`);
|
|
3105
|
+
if (ins.heavyFonts.length) chips.push(`${ins.heavyFonts.length} heavy font(s)`);
|
|
3106
|
+
if (ins.scaleGaps.length) chips.push(`${ins.scaleGaps.length} scale gap(s)`);
|
|
3107
|
+
if (ins.scaleAnomalies.length) chips.push(`${ins.scaleAnomalies.length} scale anomaly(ies)`);
|
|
3108
|
+
if (chips.length) {
|
|
3109
|
+
lines.push("");
|
|
3110
|
+
lines.push(`**Findings:** ${chips.join(" \xB7 ")}`);
|
|
3111
|
+
}
|
|
3112
|
+
const diff = list.diff;
|
|
3113
|
+
if (diff) {
|
|
3114
|
+
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb(Math.abs(diff.netBytes))} net`;
|
|
3115
|
+
lines.push("");
|
|
3116
|
+
lines.push(
|
|
3117
|
+
`**Since baseline** (${new Date(diff.baselineSavedAt).toISOString()}): ${diff.added.length} added \xB7 ${diff.removed.length} removed \xB7 ${diff.grown.length} grew \xB7 ${net}` + (diff.removed.length ? ` \u2014 removed: ${diff.removed.slice(0, 5).map((r) => `${r.name}.${r.type}`).join(", ")}${diff.removed.length > 5 ? "\u2026" : ""}` : "")
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
lines.push("");
|
|
3121
|
+
lines.push("| id | asset | kind | dims | scales | size | flags |");
|
|
3122
|
+
lines.push("|---|---|---|---|---|---|---|");
|
|
3123
|
+
for (const r of list.records) {
|
|
3124
|
+
const dims = r.width != null && r.height != null ? `${Math.round(r.width)}\xD7${Math.round(r.height)}` : "\u2013";
|
|
3125
|
+
lines.push(
|
|
3126
|
+
`| ${r.id} | ${r.name}.${r.type} | ${r.kind} | ${dims} | ${scalesLabel(r.scales)} | ${sizeLabel(r)} | ${flagsLabel(r, ins)} |`
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
lines.push("");
|
|
3130
|
+
lines.push(
|
|
3131
|
+
"_Sizes marked ~ are decoded-memory estimates (unmeasurable in this environment). Use `asset_action` for rescan / measure_sizes / detail / baseline ops._"
|
|
3132
|
+
);
|
|
3133
|
+
return lines.join("\n");
|
|
3134
|
+
}
|
|
3135
|
+
function registerAssetsTools(server, sync) {
|
|
3136
|
+
server.registerTool(
|
|
3137
|
+
"get_assets",
|
|
3138
|
+
{
|
|
3139
|
+
description: "Bundled-asset inventory from the app: every asset (images, fonts, video, audio) with dimensions, @1x/@2x/@3x scale coverage, byte sizes (measured from the Metro server in dev), loaded vs never-loaded state, and audit findings \u2014 duplicates (by content hash), shipped-but-never-loaded, WebP candidates, images decoding larger than the screen, heavy GIFs/fonts, scale gaps \u2014 plus the change vs a saved baseline. This is what the app SHIPS; use get_images for what it RENDERS. Requires @buoy-gg/assets on the device.",
|
|
3140
|
+
inputSchema: {
|
|
3141
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3142
|
+
limit: import_zod16.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
3143
|
+
kind: import_zod16.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
3144
|
+
unusedOnly: import_zod16.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3145
|
+
}
|
|
3146
|
+
},
|
|
3147
|
+
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
3148
|
+
try {
|
|
3149
|
+
const target = sync.requireProDevice(deviceId);
|
|
3150
|
+
const capped = Math.min(Math.max(limit ?? 40, 1), 500);
|
|
3151
|
+
const list = await sync.callAction(
|
|
3152
|
+
ASSETS_TOOL_ID,
|
|
3153
|
+
"list",
|
|
3154
|
+
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3155
|
+
target
|
|
3156
|
+
);
|
|
3157
|
+
if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3158
|
+
if (unusedOnly) {
|
|
3159
|
+
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3160
|
+
}
|
|
3161
|
+
const md = formatListMarkdown2(list, capped);
|
|
3162
|
+
const slim = {
|
|
3163
|
+
stats: list.stats,
|
|
3164
|
+
scanStatus: list.scanStatus,
|
|
3165
|
+
insights: list.insights,
|
|
3166
|
+
diff: list.diff,
|
|
3167
|
+
records: list.records.map((r) => ({
|
|
3168
|
+
id: r.id,
|
|
3169
|
+
name: r.name,
|
|
3170
|
+
type: r.type,
|
|
3171
|
+
kind: r.kind,
|
|
3172
|
+
hash: r.hash,
|
|
3173
|
+
width: r.width,
|
|
3174
|
+
height: r.height,
|
|
3175
|
+
scales: r.scales,
|
|
3176
|
+
loaded: r.loaded,
|
|
3177
|
+
sizeBytes: r.sizeBytes,
|
|
3178
|
+
sizeSource: r.sizeSource
|
|
3179
|
+
}))
|
|
3180
|
+
};
|
|
3181
|
+
return text14(`${md}
|
|
3182
|
+
|
|
3183
|
+
\`\`\`json
|
|
3184
|
+
${JSON.stringify(slim)}
|
|
3185
|
+
\`\`\``);
|
|
3186
|
+
} catch (e) {
|
|
3187
|
+
return fail16(errMsg16(e));
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
);
|
|
3191
|
+
server.registerTool(
|
|
3192
|
+
"asset_action",
|
|
3193
|
+
{
|
|
3194
|
+
description: "Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",
|
|
3195
|
+
inputSchema: {
|
|
3196
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3197
|
+
action: import_zod16.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
3198
|
+
id: import_zod16.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3199
|
+
}
|
|
3200
|
+
},
|
|
3201
|
+
async ({ deviceId, action, id }) => {
|
|
3202
|
+
try {
|
|
3203
|
+
const target = sync.requireProDevice(deviceId);
|
|
3204
|
+
switch (action) {
|
|
3205
|
+
case "rescan": {
|
|
3206
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3207
|
+
const ss = result?.scanStatus;
|
|
3208
|
+
if (!ss) return fail16("Device returned nothing for rescan.");
|
|
3209
|
+
return text14(
|
|
3210
|
+
`Rescan done: ${ss.registryCount} registered at runtime` + (ss.graphCount != null ? ` \xB7 ${ss.graphCount} in the bundle graph` : " \xB7 no bundle graph") + (ss.graphError ? ` (graph: ${ss.graphError})` : "") + ` \xB7 ${ss.fontFamilies.length} font families` + (ss.measuring ? " \xB7 size measurement running \u2014 get_assets in a moment" : "")
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
case "measure_sizes": {
|
|
3214
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3215
|
+
return text14(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3216
|
+
}
|
|
3217
|
+
case "detail": {
|
|
3218
|
+
if (id == null) return fail16("detail requires the `id` param (from get_assets).");
|
|
3219
|
+
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3220
|
+
if (!detail) return fail16(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3221
|
+
const lines = [
|
|
3222
|
+
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3223
|
+
"",
|
|
3224
|
+
`- Kind: ${detail.kind} \xB7 ${detail.loaded ? "loaded at runtime" : "in bundle, NEVER loaded"} (${detail.origin})`
|
|
3225
|
+
];
|
|
3226
|
+
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
3227
|
+
if (detail.sizesByScale) {
|
|
3228
|
+
for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
|
|
3229
|
+
lines.push(`- Size @${scale}x: ${kb(bytes)}`);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
3233
|
+
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb(detail.estDecodedBytes)} (RGBA)`);
|
|
3234
|
+
if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
|
|
3235
|
+
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3236
|
+
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3237
|
+
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3238
|
+
return text14(lines.join("\n"));
|
|
3239
|
+
}
|
|
3240
|
+
case "save_baseline": {
|
|
3241
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3242
|
+
return text14(
|
|
3243
|
+
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3244
|
+
);
|
|
3245
|
+
}
|
|
3246
|
+
case "clear_baseline": {
|
|
3247
|
+
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3248
|
+
return text14("Baseline cleared.");
|
|
3249
|
+
}
|
|
3250
|
+
case "clear_records": {
|
|
3251
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3252
|
+
return text14(result?.message ?? "Inventory cleared.");
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
} catch (e) {
|
|
3256
|
+
return fail16(errMsg16(e));
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
// src/tools/packs/reload.ts
|
|
3263
|
+
var import_zod17 = require("zod");
|
|
3045
3264
|
var APP_TOOL_ID = "app";
|
|
3046
3265
|
var RELOAD_ACTION = "reloadApp";
|
|
3047
3266
|
var DEFAULT_WAIT_MS = 3e4;
|
|
3048
3267
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3049
3268
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3050
3269
|
var PROBE_INTERVAL_MS = 400;
|
|
3051
|
-
var
|
|
3052
|
-
var
|
|
3053
|
-
var
|
|
3270
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3271
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3272
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3054
3273
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3055
3274
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3056
3275
|
try {
|
|
@@ -3085,12 +3304,12 @@ function registerReloadTools(server, sync) {
|
|
|
3085
3304
|
{
|
|
3086
3305
|
description: "Reload the running app's JS bundle (Expo Go, dev client, or RN CLI) \u2014 the equivalent of shaking the device and hitting Reload, but from here. Use it to pick up a change Fast Refresh didn't apply, to clear leaked in-memory state before a measurement, or to re-run app startup. In dev builds it uses React Native's DevSettings.reload(); otherwise it falls back to expo-updates (if the app installs it). All in-memory state is lost. By default this waits for the app to come back up and reports how long the reload took. Works on any app running @buoy-gg/core \u2014 no particular tool package needed. Requires Buoy Pro.",
|
|
3087
3306
|
inputSchema: {
|
|
3088
|
-
strategy:
|
|
3307
|
+
strategy: import_zod17.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
3089
3308
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
3090
3309
|
),
|
|
3091
|
-
wait:
|
|
3092
|
-
timeoutMs:
|
|
3093
|
-
deviceId:
|
|
3310
|
+
wait: import_zod17.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
3311
|
+
timeoutMs: import_zod17.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
3312
|
+
deviceId: import_zod17.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
3094
3313
|
}
|
|
3095
3314
|
},
|
|
3096
3315
|
async (args) => {
|
|
@@ -3106,19 +3325,19 @@ function registerReloadTools(server, sync) {
|
|
|
3106
3325
|
);
|
|
3107
3326
|
const via = ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
3108
3327
|
if (args.wait === false) {
|
|
3109
|
-
return
|
|
3328
|
+
return text15(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
3110
3329
|
}
|
|
3111
3330
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
3112
3331
|
if (elapsed == null) {
|
|
3113
|
-
return
|
|
3332
|
+
return fail17(
|
|
3114
3333
|
`Reload was accepted${via}, but the app hasn't reconnected yet. It may still be building the bundle (a cold Metro rebuild can take a while) \u2014 check the device, then run list_devices.`
|
|
3115
3334
|
);
|
|
3116
3335
|
}
|
|
3117
|
-
return
|
|
3336
|
+
return text15(
|
|
3118
3337
|
`Reloaded the app${via} \u2014 back and connected after ${(elapsed / 1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`
|
|
3119
3338
|
);
|
|
3120
3339
|
} catch (e) {
|
|
3121
|
-
return
|
|
3340
|
+
return fail17(errMsg17(e));
|
|
3122
3341
|
}
|
|
3123
3342
|
}
|
|
3124
3343
|
);
|
|
@@ -3162,6 +3381,12 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
|
|
|
3162
3381
|
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
3163
3382
|
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
3164
3383
|
|
|
3384
|
+
Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
|
|
3385
|
+
fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
|
|
3386
|
+
WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
|
|
3387
|
+
the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
|
|
3388
|
+
get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
|
|
3389
|
+
|
|
3165
3390
|
Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
|
|
3166
3391
|
measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
|
|
3167
3392
|
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
@@ -3198,11 +3423,12 @@ function createMcpServer(sync) {
|
|
|
3198
3423
|
registerInteractTools(server, sync);
|
|
3199
3424
|
registerJsThreadTools(server, sync);
|
|
3200
3425
|
registerImagesTools(server, sync);
|
|
3426
|
+
registerAssetsTools(server, sync);
|
|
3201
3427
|
registerReloadTools(server, sync);
|
|
3202
3428
|
return server;
|
|
3203
3429
|
}
|
|
3204
3430
|
var deviceIdField = {
|
|
3205
|
-
deviceId:
|
|
3431
|
+
deviceId: import_zod18.z.string().optional().describe(
|
|
3206
3432
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
3207
3433
|
)
|
|
3208
3434
|
};
|
package/dist/cli.mjs
CHANGED
package/dist/index.js
CHANGED
|
@@ -365,7 +365,7 @@ var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
365
365
|
|
|
366
366
|
// src/server.ts
|
|
367
367
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
368
|
-
var
|
|
368
|
+
var import_zod18 = require("zod");
|
|
369
369
|
|
|
370
370
|
// src/tools/generic.ts
|
|
371
371
|
var import_zod = require("zod");
|
|
@@ -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.
|
|
379
|
+
var VERSION = "7.0.16" ? "7.0.16" : "0.0.0-dev";
|
|
380
380
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
381
381
|
|
|
382
382
|
// src/update/checkForUpdate.ts
|
|
@@ -2946,7 +2946,9 @@ function registerImagesTools(server, sync) {
|
|
|
2946
2946
|
error: r.error,
|
|
2947
2947
|
errorCode: r.errorCode,
|
|
2948
2948
|
overrideLabel: r.overrideLabel,
|
|
2949
|
-
loadCount: r.loadCount
|
|
2949
|
+
loadCount: r.loadCount,
|
|
2950
|
+
hasAltText: r.hasAltText,
|
|
2951
|
+
layoutShifts: r.layoutShifts
|
|
2950
2952
|
}))
|
|
2951
2953
|
};
|
|
2952
2954
|
return text13(`${md}
|
|
@@ -3075,17 +3077,234 @@ ${JSON.stringify(slim)}
|
|
|
3075
3077
|
);
|
|
3076
3078
|
}
|
|
3077
3079
|
|
|
3078
|
-
// src/tools/packs/
|
|
3080
|
+
// src/tools/packs/assets.ts
|
|
3079
3081
|
var import_zod16 = require("zod");
|
|
3082
|
+
var ASSETS_TOOL_ID = "assets";
|
|
3083
|
+
var text14 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3084
|
+
var fail16 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3085
|
+
var errMsg16 = (e) => e instanceof Error ? e.message : String(e);
|
|
3086
|
+
function kb(bytes) {
|
|
3087
|
+
if (bytes == null || bytes <= 0) return "\u2013";
|
|
3088
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
3089
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
3090
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
|
|
3091
|
+
}
|
|
3092
|
+
function sizeLabel(r) {
|
|
3093
|
+
if (r.sizeBytes == null) return "\u2013";
|
|
3094
|
+
return r.sizeSource === "measured" ? kb(r.sizeBytes) : `~${kb(r.sizeBytes)}`;
|
|
3095
|
+
}
|
|
3096
|
+
function scalesLabel(scales) {
|
|
3097
|
+
if (!scales.length) return "\u2013";
|
|
3098
|
+
return scales.map((s) => `@${s}x`).join(" ");
|
|
3099
|
+
}
|
|
3100
|
+
function flagsLabel(r, ins) {
|
|
3101
|
+
const flags = [];
|
|
3102
|
+
if (!r.loaded) flags.push("UNUSED");
|
|
3103
|
+
if (ins.overDecode.includes(r.id)) flags.push("OVERSIZE");
|
|
3104
|
+
if (ins.duplicates.some((d) => d.ids.includes(r.id))) flags.push("DUP");
|
|
3105
|
+
if (ins.heavyGifs.includes(r.id) || ins.heavyFonts.includes(r.id)) flags.push("HEAVY");
|
|
3106
|
+
if (ins.scaleAnomalies.includes(r.id)) flags.push("BAD-SCALE");
|
|
3107
|
+
else if (ins.scaleGaps.includes(r.id)) flags.push("LOW-RES");
|
|
3108
|
+
if (ins.webpCandidates.includes(r.id)) flags.push("WEBP");
|
|
3109
|
+
return flags.length ? flags.join(" ") : "\u2013";
|
|
3110
|
+
}
|
|
3111
|
+
function formatListMarkdown2(list, limit) {
|
|
3112
|
+
const lines = [];
|
|
3113
|
+
const s = list.stats;
|
|
3114
|
+
const ss = list.scanStatus;
|
|
3115
|
+
lines.push(`## Asset inventory (${list.total} assets, showing ${Math.min(limit, list.records.length)}, largest first)`);
|
|
3116
|
+
lines.push("");
|
|
3117
|
+
const kindBits = Object.entries(s.byKind).filter(([, v]) => v.count > 0).map(([k, v]) => `${v.count} ${k}${v.bytes > 0 ? ` (${kb(v.bytes)})` : ""}`);
|
|
3118
|
+
lines.push(
|
|
3119
|
+
`${s.loaded} loaded \xB7 ${s.neverLoaded} never loaded \xB7 ` + (s.totalBytes > 0 ? `${kb(s.totalBytes)} measured (${s.measuredCount}/${s.total})` : "sizes not measured") + ` \xB7 ${kindBits.join(" \xB7 ")}`
|
|
3120
|
+
);
|
|
3121
|
+
if (ss.fontFamilies.length > 0) {
|
|
3122
|
+
lines.push(`Loaded font families: ${ss.fontFamilies.join(", ")}`);
|
|
3123
|
+
}
|
|
3124
|
+
if (!ss.graphSupported || ss.graphCount == null) {
|
|
3125
|
+
lines.push(
|
|
3126
|
+
"\u26A0 No Metro bundle graph (release build or graph fetch failed) \u2014 showing runtime-registered assets only; never-loaded detection unavailable." + (ss.graphError ? ` (${ss.graphError})` : "")
|
|
3127
|
+
);
|
|
3128
|
+
}
|
|
3129
|
+
if (ss.measuring) lines.push("\u26A0 Size measurement in progress \u2014 byte totals are partial; re-run get_assets shortly.");
|
|
3130
|
+
const ins = list.insights;
|
|
3131
|
+
const chips = [];
|
|
3132
|
+
if (ins.neverLoaded.length) chips.push(`${ins.neverLoaded.length} never loaded`);
|
|
3133
|
+
if (ins.duplicates.length) {
|
|
3134
|
+
const wasted = ins.duplicates.reduce((sum, d) => sum + (d.wastedBytes ?? 0), 0);
|
|
3135
|
+
chips.push(`${ins.duplicates.length} duplicate group(s)${wasted > 0 ? ` (${kb(wasted)} wasted)` : ""}`);
|
|
3136
|
+
}
|
|
3137
|
+
if (ins.overDecode.length) chips.push(`${ins.overDecode.length} decode larger than 2\xD7 the screen`);
|
|
3138
|
+
if (ins.webpCandidates.length) chips.push(`${ins.webpCandidates.length} WebP candidate(s)`);
|
|
3139
|
+
if (ins.heavyGifs.length) chips.push(`${ins.heavyGifs.length} heavy GIF(s)`);
|
|
3140
|
+
if (ins.heavyFonts.length) chips.push(`${ins.heavyFonts.length} heavy font(s)`);
|
|
3141
|
+
if (ins.scaleGaps.length) chips.push(`${ins.scaleGaps.length} scale gap(s)`);
|
|
3142
|
+
if (ins.scaleAnomalies.length) chips.push(`${ins.scaleAnomalies.length} scale anomaly(ies)`);
|
|
3143
|
+
if (chips.length) {
|
|
3144
|
+
lines.push("");
|
|
3145
|
+
lines.push(`**Findings:** ${chips.join(" \xB7 ")}`);
|
|
3146
|
+
}
|
|
3147
|
+
const diff = list.diff;
|
|
3148
|
+
if (diff) {
|
|
3149
|
+
const net = diff.netBytes == null ? "net unknown" : Math.abs(diff.netBytes) < 1024 ? "\xB10 net" : `${diff.netBytes >= 0 ? "+" : "\u2212"}${kb(Math.abs(diff.netBytes))} net`;
|
|
3150
|
+
lines.push("");
|
|
3151
|
+
lines.push(
|
|
3152
|
+
`**Since baseline** (${new Date(diff.baselineSavedAt).toISOString()}): ${diff.added.length} added \xB7 ${diff.removed.length} removed \xB7 ${diff.grown.length} grew \xB7 ${net}` + (diff.removed.length ? ` \u2014 removed: ${diff.removed.slice(0, 5).map((r) => `${r.name}.${r.type}`).join(", ")}${diff.removed.length > 5 ? "\u2026" : ""}` : "")
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
lines.push("");
|
|
3156
|
+
lines.push("| id | asset | kind | dims | scales | size | flags |");
|
|
3157
|
+
lines.push("|---|---|---|---|---|---|---|");
|
|
3158
|
+
for (const r of list.records) {
|
|
3159
|
+
const dims = r.width != null && r.height != null ? `${Math.round(r.width)}\xD7${Math.round(r.height)}` : "\u2013";
|
|
3160
|
+
lines.push(
|
|
3161
|
+
`| ${r.id} | ${r.name}.${r.type} | ${r.kind} | ${dims} | ${scalesLabel(r.scales)} | ${sizeLabel(r)} | ${flagsLabel(r, ins)} |`
|
|
3162
|
+
);
|
|
3163
|
+
}
|
|
3164
|
+
lines.push("");
|
|
3165
|
+
lines.push(
|
|
3166
|
+
"_Sizes marked ~ are decoded-memory estimates (unmeasurable in this environment). Use `asset_action` for rescan / measure_sizes / detail / baseline ops._"
|
|
3167
|
+
);
|
|
3168
|
+
return lines.join("\n");
|
|
3169
|
+
}
|
|
3170
|
+
function registerAssetsTools(server, sync) {
|
|
3171
|
+
server.registerTool(
|
|
3172
|
+
"get_assets",
|
|
3173
|
+
{
|
|
3174
|
+
description: "Bundled-asset inventory from the app: every asset (images, fonts, video, audio) with dimensions, @1x/@2x/@3x scale coverage, byte sizes (measured from the Metro server in dev), loaded vs never-loaded state, and audit findings \u2014 duplicates (by content hash), shipped-but-never-loaded, WebP candidates, images decoding larger than the screen, heavy GIFs/fonts, scale gaps \u2014 plus the change vs a saved baseline. This is what the app SHIPS; use get_images for what it RENDERS. Requires @buoy-gg/assets on the device.",
|
|
3175
|
+
inputSchema: {
|
|
3176
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3177
|
+
limit: import_zod16.z.number().optional().describe("Max records to return (largest first). Default 40, max 500."),
|
|
3178
|
+
kind: import_zod16.z.enum(["image", "font", "video", "audio", "data", "other"]).optional().describe("Only assets of this kind."),
|
|
3179
|
+
unusedOnly: import_zod16.z.boolean().optional().describe("Only assets shipped in the bundle but never loaded at runtime.")
|
|
3180
|
+
}
|
|
3181
|
+
},
|
|
3182
|
+
async ({ deviceId, limit, kind, unusedOnly }) => {
|
|
3183
|
+
try {
|
|
3184
|
+
const target = sync.requireProDevice(deviceId);
|
|
3185
|
+
const capped = Math.min(Math.max(limit ?? 40, 1), 500);
|
|
3186
|
+
const list = await sync.callAction(
|
|
3187
|
+
ASSETS_TOOL_ID,
|
|
3188
|
+
"list",
|
|
3189
|
+
{ limit: unusedOnly ? 500 : capped, kind, loadedOnly: false },
|
|
3190
|
+
target
|
|
3191
|
+
);
|
|
3192
|
+
if (!list) return fail16("Device returned nothing \u2014 is @buoy-gg/assets installed in the app?");
|
|
3193
|
+
if (unusedOnly) {
|
|
3194
|
+
list.records = list.records.filter((r) => !r.loaded).slice(0, capped);
|
|
3195
|
+
}
|
|
3196
|
+
const md = formatListMarkdown2(list, capped);
|
|
3197
|
+
const slim = {
|
|
3198
|
+
stats: list.stats,
|
|
3199
|
+
scanStatus: list.scanStatus,
|
|
3200
|
+
insights: list.insights,
|
|
3201
|
+
diff: list.diff,
|
|
3202
|
+
records: list.records.map((r) => ({
|
|
3203
|
+
id: r.id,
|
|
3204
|
+
name: r.name,
|
|
3205
|
+
type: r.type,
|
|
3206
|
+
kind: r.kind,
|
|
3207
|
+
hash: r.hash,
|
|
3208
|
+
width: r.width,
|
|
3209
|
+
height: r.height,
|
|
3210
|
+
scales: r.scales,
|
|
3211
|
+
loaded: r.loaded,
|
|
3212
|
+
sizeBytes: r.sizeBytes,
|
|
3213
|
+
sizeSource: r.sizeSource
|
|
3214
|
+
}))
|
|
3215
|
+
};
|
|
3216
|
+
return text14(`${md}
|
|
3217
|
+
|
|
3218
|
+
\`\`\`json
|
|
3219
|
+
${JSON.stringify(slim)}
|
|
3220
|
+
\`\`\``);
|
|
3221
|
+
} catch (e) {
|
|
3222
|
+
return fail16(errMsg16(e));
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
);
|
|
3226
|
+
server.registerTool(
|
|
3227
|
+
"asset_action",
|
|
3228
|
+
{
|
|
3229
|
+
description: "Drive the assets tool: rescan (re-walk the registry + merge the Metro bundle graph + refresh fonts + measure sizes \u2014 run this first if the inventory looks stale or empty), measure_sizes (re-measure byte sizes only), detail (full metadata for one asset by id: source dir, per-scale sizes, resolved URI, per-file graph paths), save_baseline (persist the current inventory for before/after comparison \u2014 save, make your change, then get_assets shows added/removed/grown/net bytes), clear_baseline, clear_records (wipe + rescan rebuilds).",
|
|
3230
|
+
inputSchema: {
|
|
3231
|
+
deviceId: import_zod16.z.string().optional().describe("Target device id (auto-selected if one is connected)."),
|
|
3232
|
+
action: import_zod16.z.enum(["rescan", "measure_sizes", "detail", "save_baseline", "clear_baseline", "clear_records"]).describe("What to do."),
|
|
3233
|
+
id: import_zod16.z.number().optional().describe("Asset id from get_assets (detail only).")
|
|
3234
|
+
}
|
|
3235
|
+
},
|
|
3236
|
+
async ({ deviceId, action, id }) => {
|
|
3237
|
+
try {
|
|
3238
|
+
const target = sync.requireProDevice(deviceId);
|
|
3239
|
+
switch (action) {
|
|
3240
|
+
case "rescan": {
|
|
3241
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "rescan", {}, target);
|
|
3242
|
+
const ss = result?.scanStatus;
|
|
3243
|
+
if (!ss) return fail16("Device returned nothing for rescan.");
|
|
3244
|
+
return text14(
|
|
3245
|
+
`Rescan done: ${ss.registryCount} registered at runtime` + (ss.graphCount != null ? ` \xB7 ${ss.graphCount} in the bundle graph` : " \xB7 no bundle graph") + (ss.graphError ? ` (graph: ${ss.graphError})` : "") + ` \xB7 ${ss.fontFamilies.length} font families` + (ss.measuring ? " \xB7 size measurement running \u2014 get_assets in a moment" : "")
|
|
3246
|
+
);
|
|
3247
|
+
}
|
|
3248
|
+
case "measure_sizes": {
|
|
3249
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "measureSizes", {}, target);
|
|
3250
|
+
return text14(`Measured ${result?.measured ?? 0} of ${result?.total ?? 0} assets.`);
|
|
3251
|
+
}
|
|
3252
|
+
case "detail": {
|
|
3253
|
+
if (id == null) return fail16("detail requires the `id` param (from get_assets).");
|
|
3254
|
+
const detail = await sync.callAction(ASSETS_TOOL_ID, "getDetail", { id }, target);
|
|
3255
|
+
if (!detail) return fail16(`Asset ${id} not found \u2014 re-run get_assets for current ids.`);
|
|
3256
|
+
const lines = [
|
|
3257
|
+
`## ${detail.name}.${detail.type} (#${detail.id})`,
|
|
3258
|
+
"",
|
|
3259
|
+
`- Kind: ${detail.kind} \xB7 ${detail.loaded ? "loaded at runtime" : "in bundle, NEVER loaded"} (${detail.origin})`
|
|
3260
|
+
];
|
|
3261
|
+
if (detail.width != null) lines.push(`- Dimensions: ${detail.width}\xD7${detail.height} logical \xB7 scales ${scalesLabel(detail.scales)}`);
|
|
3262
|
+
if (detail.sizesByScale) {
|
|
3263
|
+
for (const [scale, bytes] of Object.entries(detail.sizesByScale)) {
|
|
3264
|
+
lines.push(`- Size @${scale}x: ${kb(bytes)}`);
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
if (detail.sizeBytes != null) lines.push(`- Total: ${sizeLabel(detail)}`);
|
|
3268
|
+
if (detail.estDecodedBytes) lines.push(`- Decoded memory: ~${kb(detail.estDecodedBytes)} (RGBA)`);
|
|
3269
|
+
if (detail.hash) lines.push(`- Hash: ${detail.hash}`);
|
|
3270
|
+
if (detail.fileSystemLocation) lines.push(`- Source dir: ${detail.fileSystemLocation}`);
|
|
3271
|
+
if (detail.files?.length) lines.push(`- Files: ${detail.files.join(", ")}`);
|
|
3272
|
+
if (detail.uri) lines.push(`- Resolved URI: ${detail.uri}`);
|
|
3273
|
+
return text14(lines.join("\n"));
|
|
3274
|
+
}
|
|
3275
|
+
case "save_baseline": {
|
|
3276
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "saveBaseline", {}, target);
|
|
3277
|
+
return text14(
|
|
3278
|
+
`Baseline saved (${result?.assets ?? 0} assets). Make your change, then get_assets reports added/removed/grown/net bytes against it.`
|
|
3279
|
+
);
|
|
3280
|
+
}
|
|
3281
|
+
case "clear_baseline": {
|
|
3282
|
+
await sync.callAction(ASSETS_TOOL_ID, "clearBaseline", {}, target);
|
|
3283
|
+
return text14("Baseline cleared.");
|
|
3284
|
+
}
|
|
3285
|
+
case "clear_records": {
|
|
3286
|
+
const result = await sync.callAction(ASSETS_TOOL_ID, "clearRecords", {}, target);
|
|
3287
|
+
return text14(result?.message ?? "Inventory cleared.");
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
} catch (e) {
|
|
3291
|
+
return fail16(errMsg16(e));
|
|
3292
|
+
}
|
|
3293
|
+
}
|
|
3294
|
+
);
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
// src/tools/packs/reload.ts
|
|
3298
|
+
var import_zod17 = require("zod");
|
|
3080
3299
|
var APP_TOOL_ID = "app";
|
|
3081
3300
|
var RELOAD_ACTION = "reloadApp";
|
|
3082
3301
|
var DEFAULT_WAIT_MS = 3e4;
|
|
3083
3302
|
var TEARDOWN_GRACE_MS = 1200;
|
|
3084
3303
|
var PROBE_TIMEOUT_MS = 1500;
|
|
3085
3304
|
var PROBE_INTERVAL_MS = 400;
|
|
3086
|
-
var
|
|
3087
|
-
var
|
|
3088
|
-
var
|
|
3305
|
+
var text15 = (s) => ({ content: [{ type: "text", text: s }] });
|
|
3306
|
+
var fail17 = (message) => ({ isError: true, content: [{ type: "text", text: message }] });
|
|
3307
|
+
var errMsg17 = (e) => e instanceof Error ? e.message : String(e);
|
|
3089
3308
|
var delay3 = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3090
3309
|
async function ping(sync, deviceId, timeoutMs) {
|
|
3091
3310
|
try {
|
|
@@ -3120,12 +3339,12 @@ function registerReloadTools(server, sync) {
|
|
|
3120
3339
|
{
|
|
3121
3340
|
description: "Reload the running app's JS bundle (Expo Go, dev client, or RN CLI) \u2014 the equivalent of shaking the device and hitting Reload, but from here. Use it to pick up a change Fast Refresh didn't apply, to clear leaked in-memory state before a measurement, or to re-run app startup. In dev builds it uses React Native's DevSettings.reload(); otherwise it falls back to expo-updates (if the app installs it). All in-memory state is lost. By default this waits for the app to come back up and reports how long the reload took. Works on any app running @buoy-gg/core \u2014 no particular tool package needed. Requires Buoy Pro.",
|
|
3122
3341
|
inputSchema: {
|
|
3123
|
-
strategy:
|
|
3342
|
+
strategy: import_zod17.z.enum(["auto", "dev-settings", "expo-updates"]).optional().describe(
|
|
3124
3343
|
"Reload mechanism. 'auto' (default) uses DevSettings in dev builds and expo-updates otherwise. Force one only when debugging the reload itself."
|
|
3125
3344
|
),
|
|
3126
|
-
wait:
|
|
3127
|
-
timeoutMs:
|
|
3128
|
-
deviceId:
|
|
3345
|
+
wait: import_zod17.z.boolean().optional().describe("Wait for the app to come back before returning. Default true \u2014 turn off for fire-and-forget."),
|
|
3346
|
+
timeoutMs: import_zod17.z.number().optional().describe("How long to wait for the app to reconnect, in ms. Default 30000."),
|
|
3347
|
+
deviceId: import_zod17.z.string().optional().describe('Target device id (auto-selected if one is connected). Use "All" to reload every connected app.')
|
|
3129
3348
|
}
|
|
3130
3349
|
},
|
|
3131
3350
|
async (args) => {
|
|
@@ -3141,19 +3360,19 @@ function registerReloadTools(server, sync) {
|
|
|
3141
3360
|
);
|
|
3142
3361
|
const via = ack?.strategy ? ` via ${ack.strategy}` : "";
|
|
3143
3362
|
if (args.wait === false) {
|
|
3144
|
-
return
|
|
3363
|
+
return text15(`Reload requested${via}. The app is restarting \u2014 its in-memory state is gone; re-read anything you cached.`);
|
|
3145
3364
|
}
|
|
3146
3365
|
const elapsed = await waitForAppBack(sync, target, args.timeoutMs ?? DEFAULT_WAIT_MS, previousRealm);
|
|
3147
3366
|
if (elapsed == null) {
|
|
3148
|
-
return
|
|
3367
|
+
return fail17(
|
|
3149
3368
|
`Reload was accepted${via}, but the app hasn't reconnected yet. It may still be building the bundle (a cold Metro rebuild can take a while) \u2014 check the device, then run list_devices.`
|
|
3150
3369
|
);
|
|
3151
3370
|
}
|
|
3152
|
-
return
|
|
3371
|
+
return text15(
|
|
3153
3372
|
`Reloaded the app${via} \u2014 back and connected after ${(elapsed / 1e3).toFixed(1)}s. All in-memory state was reset, so re-read any state you cached before this.`
|
|
3154
3373
|
);
|
|
3155
3374
|
} catch (e) {
|
|
3156
|
-
return
|
|
3375
|
+
return fail17(errMsg17(e));
|
|
3157
3376
|
}
|
|
3158
3377
|
}
|
|
3159
3378
|
);
|
|
@@ -3197,6 +3416,12 @@ Debugging images (not loading, slow, stale cache, memory)? \`get_images\` lists
|
|
|
3197
3416
|
expo-image load with cache verdicts and oversize audits; \`image_action\` reloads, retries, or
|
|
3198
3417
|
simulates failures per image; \`set_image_simulation\` flips app-wide offline/cold/blank modes.
|
|
3199
3418
|
|
|
3419
|
+
Auditing app size / bundled assets? \`get_assets\` inventories everything the app SHIPS \u2014 images,
|
|
3420
|
+
fonts, video, audio \u2014 with byte sizes, scale coverage, and findings (duplicates, never-loaded,
|
|
3421
|
+
WebP candidates, heavy GIFs/fonts). \`asset_action\` rescans, fetches per-asset detail, and runs
|
|
3422
|
+
the baseline diff workflow (save_baseline \u2192 change something \u2192 get_assets shows what it added).
|
|
3423
|
+
get_images = what RENDERS at runtime; get_assets = what SHIPS in the bundle.
|
|
3424
|
+
|
|
3200
3425
|
Need the app RESTARTED (a change Fast Refresh didn't pick up, leaked in-memory state before a
|
|
3201
3426
|
measurement, re-running startup)? \`reload_app\` reloads the JS bundle \u2014 DevSettings.reload() in dev
|
|
3202
3427
|
builds, expo-updates otherwise \u2014 and waits for the app to reconnect. It wipes in-memory state, so
|
|
@@ -3233,11 +3458,12 @@ function createMcpServer(sync) {
|
|
|
3233
3458
|
registerInteractTools(server, sync);
|
|
3234
3459
|
registerJsThreadTools(server, sync);
|
|
3235
3460
|
registerImagesTools(server, sync);
|
|
3461
|
+
registerAssetsTools(server, sync);
|
|
3236
3462
|
registerReloadTools(server, sync);
|
|
3237
3463
|
return server;
|
|
3238
3464
|
}
|
|
3239
3465
|
var deviceIdField = {
|
|
3240
|
-
deviceId:
|
|
3466
|
+
deviceId: import_zod18.z.string().optional().describe(
|
|
3241
3467
|
'Target device id (from list_devices). Omit to auto-select when exactly one device is connected. Use "All" to broadcast.'
|
|
3242
3468
|
)
|
|
3243
3469
|
};
|
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.16",
|
|
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.16"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@types/node": "^22.0.0",
|