@buoy-gg/mcp 7.0.22 → 7.0.24
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-5C6BLMHY.mjs → chunk-DIXFOY4A.mjs} +48 -12
- package/dist/cli.js +48 -12
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +48 -12
- package/dist/index.mjs +1 -1
- package/package.json +2 -2
|
@@ -196,6 +196,7 @@ var SyncClient = class {
|
|
|
196
196
|
platform: d.platform,
|
|
197
197
|
isConnected: d.isConnected !== false,
|
|
198
198
|
isPro: d.isPro === true,
|
|
199
|
+
tier: d.tier ?? (d.isPro === true ? "pro" : "anonymous"),
|
|
199
200
|
tools: caps?.tools ?? []
|
|
200
201
|
};
|
|
201
202
|
});
|
|
@@ -245,14 +246,20 @@ var SyncClient = class {
|
|
|
245
246
|
* is blocked.
|
|
246
247
|
*/
|
|
247
248
|
requireProDevice(deviceId) {
|
|
248
|
-
const
|
|
249
|
+
const tierNote = (dev2) => {
|
|
250
|
+
if (!dev2) return "";
|
|
251
|
+
return dev2.tier === "anonymous" ? ` The connected app "${dev2.deviceName}" has no license key at all.` : ` The connected app "${dev2.deviceName}" is on the ${dev2.tier} tier.`;
|
|
252
|
+
};
|
|
253
|
+
const proMsg = (dev2) => `Buoy Pro is required to use the MCP.${tierNote(
|
|
254
|
+
dev2
|
|
255
|
+
)} Upgrade to Buoy Pro in the app to enable MCP access (run \`list_devices\` to check status).`;
|
|
249
256
|
if (deviceId === ALL_DEVICES) {
|
|
250
257
|
if (!this.connectedDevices().some((d) => d.isPro)) throw new Error(proMsg());
|
|
251
258
|
return ALL_DEVICES;
|
|
252
259
|
}
|
|
253
260
|
const target = this.resolveDevice(deviceId);
|
|
254
261
|
const dev = this.connectedDevices().find((d) => d.deviceId === target);
|
|
255
|
-
if (!dev?.isPro) throw new Error(proMsg(dev
|
|
262
|
+
if (!dev?.isPro) throw new Error(proMsg(dev));
|
|
256
263
|
return target;
|
|
257
264
|
}
|
|
258
265
|
// ----------------------------------------------------------------
|
|
@@ -565,7 +572,7 @@ async function waitForBroker(url, attempts = 20) {
|
|
|
565
572
|
var delay = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
566
573
|
|
|
567
574
|
// src/version.ts
|
|
568
|
-
var VERSION = "7.0.
|
|
575
|
+
var VERSION = "7.0.24" ? "7.0.24" : "0.0.0-dev";
|
|
569
576
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
570
577
|
|
|
571
578
|
// src/update/checkForUpdate.ts
|
|
@@ -660,11 +667,13 @@ function registerGenericTools(server, sync) {
|
|
|
660
667
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
661
668
|
});
|
|
662
669
|
}
|
|
663
|
-
const
|
|
670
|
+
const connected = devices.filter((d) => d.isConnected);
|
|
671
|
+
const anyPro = connected.some((d) => d.isPro);
|
|
672
|
+
const anySignedIn = connected.some((d) => d.tier !== "anonymous");
|
|
664
673
|
return json({
|
|
665
674
|
devices,
|
|
666
675
|
...anyPro ? {} : {
|
|
667
|
-
hint: "The Buoy MCP is a Pro feature \u2014 no connected device has an active Buoy Pro license, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them."
|
|
676
|
+
hint: anySignedIn ? "The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them." : "The Buoy MCP is a Pro feature \u2014 no connected device has an active Buoy Pro license, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them."
|
|
668
677
|
},
|
|
669
678
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
670
679
|
});
|
|
@@ -737,6 +746,14 @@ function errMsg(e) {
|
|
|
737
746
|
|
|
738
747
|
// src/tools/packs/perfRanking.ts
|
|
739
748
|
var clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
749
|
+
var WARMUP_CASE_NAME = "__batch_warmup__";
|
|
750
|
+
var THERMAL_MARK = {
|
|
751
|
+
nominal: "\u{1F7E2} nominal",
|
|
752
|
+
fair: "\u{1F7E1} fair",
|
|
753
|
+
serious: "\u{1F7E0} serious",
|
|
754
|
+
critical: "\u{1F534} critical",
|
|
755
|
+
unknown: "\u2014"
|
|
756
|
+
};
|
|
740
757
|
var FALLBACK = {
|
|
741
758
|
perCaseDurationMs: 5e3,
|
|
742
759
|
settleMs: 600,
|
|
@@ -796,6 +813,7 @@ function buildAutomationConfig(input, base = {}) {
|
|
|
796
813
|
runsPerCase,
|
|
797
814
|
coolDownMs,
|
|
798
815
|
discardWarmupRuns,
|
|
816
|
+
discardWarmupCase: input.discardWarmupCase ?? base.discardWarmupCase ?? true,
|
|
799
817
|
shuffleCases,
|
|
800
818
|
reloadBetweenCases: input.reloadBetweenCases ?? base.reloadBetweenCases ?? false,
|
|
801
819
|
captureRenders: input.captureRenders ?? base.captureRenders ?? true
|
|
@@ -840,7 +858,9 @@ function computeFlags(r) {
|
|
|
840
858
|
function rankBatchFromIndex(index, batchId, casesOrder, failures = []) {
|
|
841
859
|
const entries = index.filter((e) => e.batchId === batchId);
|
|
842
860
|
const failureByIndex = new Map(failures.map((f) => [f.index, f.reason]));
|
|
843
|
-
const indexes =
|
|
861
|
+
const indexes = Array.from(new Set(entries.map((e) => e.batchIndex ?? 0))).sort(
|
|
862
|
+
(a, b) => a - b
|
|
863
|
+
);
|
|
844
864
|
const medianForCase = (batchIndex) => {
|
|
845
865
|
const runs = entries.filter((e) => (e.batchIndex ?? 0) === batchIndex);
|
|
846
866
|
if (runs.length === 0) return void 0;
|
|
@@ -850,7 +870,7 @@ function rankBatchFromIndex(index, batchId, casesOrder, failures = []) {
|
|
|
850
870
|
const rows = indexes.map((batchIndex) => {
|
|
851
871
|
const median = medianForCase(batchIndex);
|
|
852
872
|
const runs = entries.filter((e) => (e.batchIndex ?? 0) === batchIndex).length;
|
|
853
|
-
const caseName = casesOrder?.[batchIndex]?.name ??
|
|
873
|
+
const caseName = median?.name ?? casesOrder?.[batchIndex]?.name ?? `case ${batchIndex + 1}`;
|
|
854
874
|
const failureReason = failureByIndex.get(batchIndex) ?? median?.batchFailureReason;
|
|
855
875
|
const failed = Boolean(failureReason) || !median;
|
|
856
876
|
const base = {
|
|
@@ -869,12 +889,13 @@ function rankBatchFromIndex(index, batchId, casesOrder, failures = []) {
|
|
|
869
889
|
deviceMaxRefreshRate: num(median?.deviceMaxRefreshRate),
|
|
870
890
|
deltaUiFps: null,
|
|
871
891
|
deltaJsFps: null,
|
|
892
|
+
...median?.thermalState ? { thermalState: median.thermalState } : {},
|
|
872
893
|
renderCommits: num(median?.renderCommits),
|
|
873
894
|
renderWasted: median?.renderWasted,
|
|
874
895
|
topRenderers: median?.topRenderers ?? []
|
|
875
896
|
};
|
|
876
897
|
return { ...base, flags: failed ? [] : computeFlags(base) };
|
|
877
|
-
});
|
|
898
|
+
}).filter((r) => r.caseName !== WARMUP_CASE_NAME).map((r, i) => ({ ...r, batchIndex: i }));
|
|
878
899
|
const baseline = rows[0];
|
|
879
900
|
if (baseline) {
|
|
880
901
|
for (const r of rows) {
|
|
@@ -928,18 +949,33 @@ function formatRankingMarkdown(ranking) {
|
|
|
928
949
|
`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`
|
|
929
950
|
);
|
|
930
951
|
lines.push("");
|
|
931
|
-
|
|
932
|
-
|
|
952
|
+
const anyThermal = ranking.rows.some((r) => r.thermalState);
|
|
953
|
+
const thermalHead = anyThermal ? " Thermal |" : "";
|
|
954
|
+
const thermalRule = anyThermal ? "-------|" : "";
|
|
955
|
+
lines.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${thermalHead} Flags |`);
|
|
956
|
+
lines.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${thermalRule}-------|`);
|
|
933
957
|
for (const r of ranking.rows) {
|
|
958
|
+
const thermalCell = anyThermal ? ` ${THERMAL_MARK[r.thermalState ?? "unknown"] ?? "\u2014"} |` : "";
|
|
934
959
|
if (r.failed) {
|
|
935
|
-
lines.push(
|
|
960
|
+
lines.push(
|
|
961
|
+
`| ${r.batchIndex} | ${r.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${thermalCell} \u274C FAILED (${r.failureReason}) |`
|
|
962
|
+
);
|
|
936
963
|
continue;
|
|
937
964
|
}
|
|
938
965
|
const flagCell = r.flags.length ? `\u26A0\uFE0F ${r.flags.join("; ")}` : "\u2713 clean";
|
|
939
966
|
lines.push(
|
|
940
967
|
`| ${r.batchIndex} | ${r.caseName} | ${f(r.uiFps)}${delta(r.deltaUiFps)} | ${f(r.jsFps)}${delta(
|
|
941
968
|
r.deltaJsFps
|
|
942
|
-
)} | ${f(r.cpu)} | ${f(r.memMb)} | ${f(r.jsJank, 0)} | ${f(r.uiJank, 0)}
|
|
969
|
+
)} | ${f(r.cpu)} | ${f(r.memMb)} | ${f(r.jsJank, 0)} | ${f(r.uiJank, 0)} |${thermalCell} ${flagCell} |`
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
const throttled = ranking.rows.filter(
|
|
973
|
+
(r) => r.thermalState === "serious" || r.thermalState === "critical"
|
|
974
|
+
);
|
|
975
|
+
if (throttled.length) {
|
|
976
|
+
lines.push("");
|
|
977
|
+
lines.push(
|
|
978
|
+
`**\u{1F525} Thermally throttled: ${throttled.map((r) => r.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`
|
|
943
979
|
);
|
|
944
980
|
}
|
|
945
981
|
lines.push("");
|
package/dist/cli.js
CHANGED
|
@@ -224,6 +224,7 @@ var SyncClient = class {
|
|
|
224
224
|
platform: d.platform,
|
|
225
225
|
isConnected: d.isConnected !== false,
|
|
226
226
|
isPro: d.isPro === true,
|
|
227
|
+
tier: d.tier ?? (d.isPro === true ? "pro" : "anonymous"),
|
|
227
228
|
tools: caps?.tools ?? []
|
|
228
229
|
};
|
|
229
230
|
});
|
|
@@ -273,14 +274,20 @@ var SyncClient = class {
|
|
|
273
274
|
* is blocked.
|
|
274
275
|
*/
|
|
275
276
|
requireProDevice(deviceId) {
|
|
276
|
-
const
|
|
277
|
+
const tierNote = (dev2) => {
|
|
278
|
+
if (!dev2) return "";
|
|
279
|
+
return dev2.tier === "anonymous" ? ` The connected app "${dev2.deviceName}" has no license key at all.` : ` The connected app "${dev2.deviceName}" is on the ${dev2.tier} tier.`;
|
|
280
|
+
};
|
|
281
|
+
const proMsg = (dev2) => `Buoy Pro is required to use the MCP.${tierNote(
|
|
282
|
+
dev2
|
|
283
|
+
)} Upgrade to Buoy Pro in the app to enable MCP access (run \`list_devices\` to check status).`;
|
|
277
284
|
if (deviceId === ALL_DEVICES) {
|
|
278
285
|
if (!this.connectedDevices().some((d) => d.isPro)) throw new Error(proMsg());
|
|
279
286
|
return ALL_DEVICES;
|
|
280
287
|
}
|
|
281
288
|
const target = this.resolveDevice(deviceId);
|
|
282
289
|
const dev = this.connectedDevices().find((d) => d.deviceId === target);
|
|
283
|
-
if (!dev?.isPro) throw new Error(proMsg(dev
|
|
290
|
+
if (!dev?.isPro) throw new Error(proMsg(dev));
|
|
284
291
|
return target;
|
|
285
292
|
}
|
|
286
293
|
// ----------------------------------------------------------------
|
|
@@ -605,7 +612,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
605
612
|
var import_node_path = __toESM(require("path"));
|
|
606
613
|
|
|
607
614
|
// src/version.ts
|
|
608
|
-
var VERSION = "7.0.
|
|
615
|
+
var VERSION = "7.0.24" ? "7.0.24" : "0.0.0-dev";
|
|
609
616
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
610
617
|
|
|
611
618
|
// src/update/checkForUpdate.ts
|
|
@@ -696,11 +703,13 @@ function registerGenericTools(server, sync) {
|
|
|
696
703
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
697
704
|
});
|
|
698
705
|
}
|
|
699
|
-
const
|
|
706
|
+
const connected = devices.filter((d) => d.isConnected);
|
|
707
|
+
const anyPro = connected.some((d) => d.isPro);
|
|
708
|
+
const anySignedIn = connected.some((d) => d.tier !== "anonymous");
|
|
700
709
|
return json({
|
|
701
710
|
devices,
|
|
702
711
|
...anyPro ? {} : {
|
|
703
|
-
hint: "The Buoy MCP is a Pro feature \u2014 no connected device has an active Buoy Pro license, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them."
|
|
712
|
+
hint: anySignedIn ? "The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them." : "The Buoy MCP is a Pro feature \u2014 no connected device has an active Buoy Pro license, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them."
|
|
704
713
|
},
|
|
705
714
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
706
715
|
});
|
|
@@ -779,6 +788,14 @@ var PERF_TOOL_ID = "perf-monitor";
|
|
|
779
788
|
|
|
780
789
|
// src/tools/packs/perfRanking.ts
|
|
781
790
|
var clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
791
|
+
var WARMUP_CASE_NAME = "__batch_warmup__";
|
|
792
|
+
var THERMAL_MARK = {
|
|
793
|
+
nominal: "\u{1F7E2} nominal",
|
|
794
|
+
fair: "\u{1F7E1} fair",
|
|
795
|
+
serious: "\u{1F7E0} serious",
|
|
796
|
+
critical: "\u{1F534} critical",
|
|
797
|
+
unknown: "\u2014"
|
|
798
|
+
};
|
|
782
799
|
var FALLBACK = {
|
|
783
800
|
perCaseDurationMs: 5e3,
|
|
784
801
|
settleMs: 600,
|
|
@@ -838,6 +855,7 @@ function buildAutomationConfig(input, base = {}) {
|
|
|
838
855
|
runsPerCase,
|
|
839
856
|
coolDownMs,
|
|
840
857
|
discardWarmupRuns,
|
|
858
|
+
discardWarmupCase: input.discardWarmupCase ?? base.discardWarmupCase ?? true,
|
|
841
859
|
shuffleCases,
|
|
842
860
|
reloadBetweenCases: input.reloadBetweenCases ?? base.reloadBetweenCases ?? false,
|
|
843
861
|
captureRenders: input.captureRenders ?? base.captureRenders ?? true
|
|
@@ -882,7 +900,9 @@ function computeFlags(r) {
|
|
|
882
900
|
function rankBatchFromIndex(index, batchId, casesOrder, failures = []) {
|
|
883
901
|
const entries = index.filter((e) => e.batchId === batchId);
|
|
884
902
|
const failureByIndex = new Map(failures.map((f) => [f.index, f.reason]));
|
|
885
|
-
const indexes =
|
|
903
|
+
const indexes = Array.from(new Set(entries.map((e) => e.batchIndex ?? 0))).sort(
|
|
904
|
+
(a, b) => a - b
|
|
905
|
+
);
|
|
886
906
|
const medianForCase = (batchIndex) => {
|
|
887
907
|
const runs = entries.filter((e) => (e.batchIndex ?? 0) === batchIndex);
|
|
888
908
|
if (runs.length === 0) return void 0;
|
|
@@ -892,7 +912,7 @@ function rankBatchFromIndex(index, batchId, casesOrder, failures = []) {
|
|
|
892
912
|
const rows = indexes.map((batchIndex) => {
|
|
893
913
|
const median = medianForCase(batchIndex);
|
|
894
914
|
const runs = entries.filter((e) => (e.batchIndex ?? 0) === batchIndex).length;
|
|
895
|
-
const caseName = casesOrder?.[batchIndex]?.name ??
|
|
915
|
+
const caseName = median?.name ?? casesOrder?.[batchIndex]?.name ?? `case ${batchIndex + 1}`;
|
|
896
916
|
const failureReason = failureByIndex.get(batchIndex) ?? median?.batchFailureReason;
|
|
897
917
|
const failed = Boolean(failureReason) || !median;
|
|
898
918
|
const base = {
|
|
@@ -911,12 +931,13 @@ function rankBatchFromIndex(index, batchId, casesOrder, failures = []) {
|
|
|
911
931
|
deviceMaxRefreshRate: num(median?.deviceMaxRefreshRate),
|
|
912
932
|
deltaUiFps: null,
|
|
913
933
|
deltaJsFps: null,
|
|
934
|
+
...median?.thermalState ? { thermalState: median.thermalState } : {},
|
|
914
935
|
renderCommits: num(median?.renderCommits),
|
|
915
936
|
renderWasted: median?.renderWasted,
|
|
916
937
|
topRenderers: median?.topRenderers ?? []
|
|
917
938
|
};
|
|
918
939
|
return { ...base, flags: failed ? [] : computeFlags(base) };
|
|
919
|
-
});
|
|
940
|
+
}).filter((r) => r.caseName !== WARMUP_CASE_NAME).map((r, i) => ({ ...r, batchIndex: i }));
|
|
920
941
|
const baseline = rows[0];
|
|
921
942
|
if (baseline) {
|
|
922
943
|
for (const r of rows) {
|
|
@@ -970,18 +991,33 @@ function formatRankingMarkdown(ranking) {
|
|
|
970
991
|
`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`
|
|
971
992
|
);
|
|
972
993
|
lines.push("");
|
|
973
|
-
|
|
974
|
-
|
|
994
|
+
const anyThermal = ranking.rows.some((r) => r.thermalState);
|
|
995
|
+
const thermalHead = anyThermal ? " Thermal |" : "";
|
|
996
|
+
const thermalRule = anyThermal ? "-------|" : "";
|
|
997
|
+
lines.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${thermalHead} Flags |`);
|
|
998
|
+
lines.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${thermalRule}-------|`);
|
|
975
999
|
for (const r of ranking.rows) {
|
|
1000
|
+
const thermalCell = anyThermal ? ` ${THERMAL_MARK[r.thermalState ?? "unknown"] ?? "\u2014"} |` : "";
|
|
976
1001
|
if (r.failed) {
|
|
977
|
-
lines.push(
|
|
1002
|
+
lines.push(
|
|
1003
|
+
`| ${r.batchIndex} | ${r.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${thermalCell} \u274C FAILED (${r.failureReason}) |`
|
|
1004
|
+
);
|
|
978
1005
|
continue;
|
|
979
1006
|
}
|
|
980
1007
|
const flagCell = r.flags.length ? `\u26A0\uFE0F ${r.flags.join("; ")}` : "\u2713 clean";
|
|
981
1008
|
lines.push(
|
|
982
1009
|
`| ${r.batchIndex} | ${r.caseName} | ${f(r.uiFps)}${delta(r.deltaUiFps)} | ${f(r.jsFps)}${delta(
|
|
983
1010
|
r.deltaJsFps
|
|
984
|
-
)} | ${f(r.cpu)} | ${f(r.memMb)} | ${f(r.jsJank, 0)} | ${f(r.uiJank, 0)}
|
|
1011
|
+
)} | ${f(r.cpu)} | ${f(r.memMb)} | ${f(r.jsJank, 0)} | ${f(r.uiJank, 0)} |${thermalCell} ${flagCell} |`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
const throttled = ranking.rows.filter(
|
|
1015
|
+
(r) => r.thermalState === "serious" || r.thermalState === "critical"
|
|
1016
|
+
);
|
|
1017
|
+
if (throttled.length) {
|
|
1018
|
+
lines.push("");
|
|
1019
|
+
lines.push(
|
|
1020
|
+
`**\u{1F525} Thermally throttled: ${throttled.map((r) => r.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`
|
|
985
1021
|
);
|
|
986
1022
|
}
|
|
987
1023
|
lines.push("");
|
package/dist/cli.mjs
CHANGED
package/dist/index.d.mts
CHANGED
|
@@ -24,6 +24,14 @@ interface DeviceInfo {
|
|
|
24
24
|
isConnected: boolean;
|
|
25
25
|
/** Whether the connected app reported an active Buoy Pro license. */
|
|
26
26
|
isPro: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The app's access tier. Apps older than the tier split report only `isPro`,
|
|
29
|
+
* which maps exactly — a key could only ever grant Pro back then.
|
|
30
|
+
*
|
|
31
|
+
* Surfaced so `list_devices` can say WHY the MCP is blocked: "signed in on
|
|
32
|
+
* the free tier" and "no account at all" are different problems.
|
|
33
|
+
*/
|
|
34
|
+
tier: "anonymous" | "free" | "pro";
|
|
27
35
|
tools: ToolCapability[];
|
|
28
36
|
}
|
|
29
37
|
/** How the client obtained its broker connection. */
|
|
@@ -426,6 +434,12 @@ interface AutomationConfig {
|
|
|
426
434
|
runsPerCase?: number;
|
|
427
435
|
coolDownMs?: number;
|
|
428
436
|
discardWarmupRuns?: number;
|
|
437
|
+
/**
|
|
438
|
+
* Run a throwaway case at the front of the batch and drop it from the
|
|
439
|
+
* results, so the batch's cold start doesn't land on real data. Default true.
|
|
440
|
+
* See the perf-monitor AutomationConfig for the reproduction.
|
|
441
|
+
*/
|
|
442
|
+
discardWarmupCase?: boolean;
|
|
429
443
|
shuffleCases?: boolean;
|
|
430
444
|
/** Capture per-component render counts/durations each run. Default true. */
|
|
431
445
|
captureRenders?: boolean;
|
|
@@ -453,6 +467,8 @@ interface IndexEntry {
|
|
|
453
467
|
caseId?: string;
|
|
454
468
|
runIndex?: number;
|
|
455
469
|
isMedianRun?: boolean;
|
|
470
|
+
/** Thermal state at run start; a throttled run reads cheap AND janky. */
|
|
471
|
+
thermalState?: "nominal" | "fair" | "serious" | "critical";
|
|
456
472
|
/** Render-capture mirror (absent when capture was unavailable/disabled). */
|
|
457
473
|
renderCommits?: number;
|
|
458
474
|
renderTotalMs?: number;
|
|
@@ -528,12 +544,13 @@ interface BuildConfigInput {
|
|
|
528
544
|
coolDownMs?: number;
|
|
529
545
|
navTimeoutMs?: number;
|
|
530
546
|
discardWarmupRuns?: number;
|
|
547
|
+
discardWarmupCase?: boolean;
|
|
531
548
|
shuffleCases?: boolean;
|
|
532
549
|
reloadBetweenCases?: boolean;
|
|
533
550
|
captureRenders?: boolean;
|
|
534
551
|
}
|
|
535
552
|
/** The settings subset a driver inherits from the device's saved config. */
|
|
536
|
-
type BenchmarkSettings = Partial<Pick<AutomationConfig, "perCaseDurationMs" | "settleMs" | "navTimeoutMs" | "runsPerCase" | "coolDownMs" | "discardWarmupRuns" | "shuffleCases" | "reloadBetweenCases" | "captureRenders">>;
|
|
553
|
+
type BenchmarkSettings = Partial<Pick<AutomationConfig, "perCaseDurationMs" | "settleMs" | "navTimeoutMs" | "runsPerCase" | "coolDownMs" | "discardWarmupRuns" | "discardWarmupCase" | "shuffleCases" | "reloadBetweenCases" | "captureRenders">>;
|
|
537
554
|
/**
|
|
538
555
|
* Build + validate the AutomationConfig the device expects. Throws on bad input.
|
|
539
556
|
*
|
|
@@ -566,6 +583,8 @@ interface RankRow {
|
|
|
566
583
|
deltaJsFps: number | null;
|
|
567
584
|
/** At-risk axes for this case (e.g. "UI thread drops frames", "CPU near cap"). */
|
|
568
585
|
flags: string[];
|
|
586
|
+
/** Thermal state at the median run's start, when the device reported one. */
|
|
587
|
+
thermalState?: "nominal" | "fair" | "serious" | "critical";
|
|
569
588
|
/** React commits during the median run (null when capture was off/unavailable). */
|
|
570
589
|
renderCommits: number | null;
|
|
571
590
|
/** Top components by total render self-time during the median run. */
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,14 @@ interface DeviceInfo {
|
|
|
24
24
|
isConnected: boolean;
|
|
25
25
|
/** Whether the connected app reported an active Buoy Pro license. */
|
|
26
26
|
isPro: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The app's access tier. Apps older than the tier split report only `isPro`,
|
|
29
|
+
* which maps exactly — a key could only ever grant Pro back then.
|
|
30
|
+
*
|
|
31
|
+
* Surfaced so `list_devices` can say WHY the MCP is blocked: "signed in on
|
|
32
|
+
* the free tier" and "no account at all" are different problems.
|
|
33
|
+
*/
|
|
34
|
+
tier: "anonymous" | "free" | "pro";
|
|
27
35
|
tools: ToolCapability[];
|
|
28
36
|
}
|
|
29
37
|
/** How the client obtained its broker connection. */
|
|
@@ -426,6 +434,12 @@ interface AutomationConfig {
|
|
|
426
434
|
runsPerCase?: number;
|
|
427
435
|
coolDownMs?: number;
|
|
428
436
|
discardWarmupRuns?: number;
|
|
437
|
+
/**
|
|
438
|
+
* Run a throwaway case at the front of the batch and drop it from the
|
|
439
|
+
* results, so the batch's cold start doesn't land on real data. Default true.
|
|
440
|
+
* See the perf-monitor AutomationConfig for the reproduction.
|
|
441
|
+
*/
|
|
442
|
+
discardWarmupCase?: boolean;
|
|
429
443
|
shuffleCases?: boolean;
|
|
430
444
|
/** Capture per-component render counts/durations each run. Default true. */
|
|
431
445
|
captureRenders?: boolean;
|
|
@@ -453,6 +467,8 @@ interface IndexEntry {
|
|
|
453
467
|
caseId?: string;
|
|
454
468
|
runIndex?: number;
|
|
455
469
|
isMedianRun?: boolean;
|
|
470
|
+
/** Thermal state at run start; a throttled run reads cheap AND janky. */
|
|
471
|
+
thermalState?: "nominal" | "fair" | "serious" | "critical";
|
|
456
472
|
/** Render-capture mirror (absent when capture was unavailable/disabled). */
|
|
457
473
|
renderCommits?: number;
|
|
458
474
|
renderTotalMs?: number;
|
|
@@ -528,12 +544,13 @@ interface BuildConfigInput {
|
|
|
528
544
|
coolDownMs?: number;
|
|
529
545
|
navTimeoutMs?: number;
|
|
530
546
|
discardWarmupRuns?: number;
|
|
547
|
+
discardWarmupCase?: boolean;
|
|
531
548
|
shuffleCases?: boolean;
|
|
532
549
|
reloadBetweenCases?: boolean;
|
|
533
550
|
captureRenders?: boolean;
|
|
534
551
|
}
|
|
535
552
|
/** The settings subset a driver inherits from the device's saved config. */
|
|
536
|
-
type BenchmarkSettings = Partial<Pick<AutomationConfig, "perCaseDurationMs" | "settleMs" | "navTimeoutMs" | "runsPerCase" | "coolDownMs" | "discardWarmupRuns" | "shuffleCases" | "reloadBetweenCases" | "captureRenders">>;
|
|
553
|
+
type BenchmarkSettings = Partial<Pick<AutomationConfig, "perCaseDurationMs" | "settleMs" | "navTimeoutMs" | "runsPerCase" | "coolDownMs" | "discardWarmupRuns" | "discardWarmupCase" | "shuffleCases" | "reloadBetweenCases" | "captureRenders">>;
|
|
537
554
|
/**
|
|
538
555
|
* Build + validate the AutomationConfig the device expects. Throws on bad input.
|
|
539
556
|
*
|
|
@@ -566,6 +583,8 @@ interface RankRow {
|
|
|
566
583
|
deltaJsFps: number | null;
|
|
567
584
|
/** At-risk axes for this case (e.g. "UI thread drops frames", "CPU near cap"). */
|
|
568
585
|
flags: string[];
|
|
586
|
+
/** Thermal state at the median run's start, when the device reported one. */
|
|
587
|
+
thermalState?: "nominal" | "fair" | "serious" | "critical";
|
|
569
588
|
/** React commits during the median run (null when capture was off/unavailable). */
|
|
570
589
|
renderCommits: number | null;
|
|
571
590
|
/** Top components by total render self-time during the median run. */
|
package/dist/index.js
CHANGED
|
@@ -259,6 +259,7 @@ var SyncClient = class {
|
|
|
259
259
|
platform: d.platform,
|
|
260
260
|
isConnected: d.isConnected !== false,
|
|
261
261
|
isPro: d.isPro === true,
|
|
262
|
+
tier: d.tier ?? (d.isPro === true ? "pro" : "anonymous"),
|
|
262
263
|
tools: caps?.tools ?? []
|
|
263
264
|
};
|
|
264
265
|
});
|
|
@@ -308,14 +309,20 @@ var SyncClient = class {
|
|
|
308
309
|
* is blocked.
|
|
309
310
|
*/
|
|
310
311
|
requireProDevice(deviceId) {
|
|
311
|
-
const
|
|
312
|
+
const tierNote = (dev2) => {
|
|
313
|
+
if (!dev2) return "";
|
|
314
|
+
return dev2.tier === "anonymous" ? ` The connected app "${dev2.deviceName}" has no license key at all.` : ` The connected app "${dev2.deviceName}" is on the ${dev2.tier} tier.`;
|
|
315
|
+
};
|
|
316
|
+
const proMsg = (dev2) => `Buoy Pro is required to use the MCP.${tierNote(
|
|
317
|
+
dev2
|
|
318
|
+
)} Upgrade to Buoy Pro in the app to enable MCP access (run \`list_devices\` to check status).`;
|
|
312
319
|
if (deviceId === ALL_DEVICES) {
|
|
313
320
|
if (!this.connectedDevices().some((d) => d.isPro)) throw new Error(proMsg());
|
|
314
321
|
return ALL_DEVICES;
|
|
315
322
|
}
|
|
316
323
|
const target = this.resolveDevice(deviceId);
|
|
317
324
|
const dev = this.connectedDevices().find((d) => d.deviceId === target);
|
|
318
|
-
if (!dev?.isPro) throw new Error(proMsg(dev
|
|
325
|
+
if (!dev?.isPro) throw new Error(proMsg(dev));
|
|
319
326
|
return target;
|
|
320
327
|
}
|
|
321
328
|
// ----------------------------------------------------------------
|
|
@@ -640,7 +647,7 @@ var import_node_os = __toESM(require("os"));
|
|
|
640
647
|
var import_node_path = __toESM(require("path"));
|
|
641
648
|
|
|
642
649
|
// src/version.ts
|
|
643
|
-
var VERSION = "7.0.
|
|
650
|
+
var VERSION = "7.0.24" ? "7.0.24" : "0.0.0-dev";
|
|
644
651
|
var PACKAGE_NAME = "@buoy-gg/mcp";
|
|
645
652
|
|
|
646
653
|
// src/update/checkForUpdate.ts
|
|
@@ -731,11 +738,13 @@ function registerGenericTools(server, sync) {
|
|
|
731
738
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
732
739
|
});
|
|
733
740
|
}
|
|
734
|
-
const
|
|
741
|
+
const connected = devices.filter((d) => d.isConnected);
|
|
742
|
+
const anyPro = connected.some((d) => d.isPro);
|
|
743
|
+
const anySignedIn = connected.some((d) => d.tier !== "anonymous");
|
|
735
744
|
return json({
|
|
736
745
|
devices,
|
|
737
746
|
...anyPro ? {} : {
|
|
738
|
-
hint: "The Buoy MCP is a Pro feature \u2014 no connected device has an active Buoy Pro license, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them."
|
|
747
|
+
hint: anySignedIn ? "The Buoy MCP is a Pro feature \u2014 the connected device is signed in on the free tier, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them." : "The Buoy MCP is a Pro feature \u2014 no connected device has an active Buoy Pro license, so data/action tools are blocked. Upgrade to Buoy Pro in the app to enable them."
|
|
739
748
|
},
|
|
740
749
|
...updateNotice ? { updateAvailable: updateNotice } : {}
|
|
741
750
|
});
|
|
@@ -814,6 +823,14 @@ var PERF_TOOL_ID = "perf-monitor";
|
|
|
814
823
|
|
|
815
824
|
// src/tools/packs/perfRanking.ts
|
|
816
825
|
var clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));
|
|
826
|
+
var WARMUP_CASE_NAME = "__batch_warmup__";
|
|
827
|
+
var THERMAL_MARK = {
|
|
828
|
+
nominal: "\u{1F7E2} nominal",
|
|
829
|
+
fair: "\u{1F7E1} fair",
|
|
830
|
+
serious: "\u{1F7E0} serious",
|
|
831
|
+
critical: "\u{1F534} critical",
|
|
832
|
+
unknown: "\u2014"
|
|
833
|
+
};
|
|
817
834
|
var FALLBACK = {
|
|
818
835
|
perCaseDurationMs: 5e3,
|
|
819
836
|
settleMs: 600,
|
|
@@ -873,6 +890,7 @@ function buildAutomationConfig(input, base = {}) {
|
|
|
873
890
|
runsPerCase,
|
|
874
891
|
coolDownMs,
|
|
875
892
|
discardWarmupRuns,
|
|
893
|
+
discardWarmupCase: input.discardWarmupCase ?? base.discardWarmupCase ?? true,
|
|
876
894
|
shuffleCases,
|
|
877
895
|
reloadBetweenCases: input.reloadBetweenCases ?? base.reloadBetweenCases ?? false,
|
|
878
896
|
captureRenders: input.captureRenders ?? base.captureRenders ?? true
|
|
@@ -917,7 +935,9 @@ function computeFlags(r) {
|
|
|
917
935
|
function rankBatchFromIndex(index, batchId, casesOrder, failures = []) {
|
|
918
936
|
const entries = index.filter((e) => e.batchId === batchId);
|
|
919
937
|
const failureByIndex = new Map(failures.map((f) => [f.index, f.reason]));
|
|
920
|
-
const indexes =
|
|
938
|
+
const indexes = Array.from(new Set(entries.map((e) => e.batchIndex ?? 0))).sort(
|
|
939
|
+
(a, b) => a - b
|
|
940
|
+
);
|
|
921
941
|
const medianForCase = (batchIndex) => {
|
|
922
942
|
const runs = entries.filter((e) => (e.batchIndex ?? 0) === batchIndex);
|
|
923
943
|
if (runs.length === 0) return void 0;
|
|
@@ -927,7 +947,7 @@ function rankBatchFromIndex(index, batchId, casesOrder, failures = []) {
|
|
|
927
947
|
const rows = indexes.map((batchIndex) => {
|
|
928
948
|
const median = medianForCase(batchIndex);
|
|
929
949
|
const runs = entries.filter((e) => (e.batchIndex ?? 0) === batchIndex).length;
|
|
930
|
-
const caseName = casesOrder?.[batchIndex]?.name ??
|
|
950
|
+
const caseName = median?.name ?? casesOrder?.[batchIndex]?.name ?? `case ${batchIndex + 1}`;
|
|
931
951
|
const failureReason = failureByIndex.get(batchIndex) ?? median?.batchFailureReason;
|
|
932
952
|
const failed = Boolean(failureReason) || !median;
|
|
933
953
|
const base = {
|
|
@@ -946,12 +966,13 @@ function rankBatchFromIndex(index, batchId, casesOrder, failures = []) {
|
|
|
946
966
|
deviceMaxRefreshRate: num(median?.deviceMaxRefreshRate),
|
|
947
967
|
deltaUiFps: null,
|
|
948
968
|
deltaJsFps: null,
|
|
969
|
+
...median?.thermalState ? { thermalState: median.thermalState } : {},
|
|
949
970
|
renderCommits: num(median?.renderCommits),
|
|
950
971
|
renderWasted: median?.renderWasted,
|
|
951
972
|
topRenderers: median?.topRenderers ?? []
|
|
952
973
|
};
|
|
953
974
|
return { ...base, flags: failed ? [] : computeFlags(base) };
|
|
954
|
-
});
|
|
975
|
+
}).filter((r) => r.caseName !== WARMUP_CASE_NAME).map((r, i) => ({ ...r, batchIndex: i }));
|
|
955
976
|
const baseline = rows[0];
|
|
956
977
|
if (baseline) {
|
|
957
978
|
for (const r of rows) {
|
|
@@ -1005,18 +1026,33 @@ function formatRankingMarkdown(ranking) {
|
|
|
1005
1026
|
`**No single winner \u2014 weigh the trade-offs.** UI-thread health matters more than raw averages: a cheap run that drops UI frames is worse than a pricier one that holds them.`
|
|
1006
1027
|
);
|
|
1007
1028
|
lines.push("");
|
|
1008
|
-
|
|
1009
|
-
|
|
1029
|
+
const anyThermal = ranking.rows.some((r) => r.thermalState);
|
|
1030
|
+
const thermalHead = anyThermal ? " Thermal |" : "";
|
|
1031
|
+
const thermalRule = anyThermal ? "-------|" : "";
|
|
1032
|
+
lines.push(`| # | Case | UI FPS | JS FPS | CPU % | Mem MB | JS jank | UI jank |${thermalHead} Flags |`);
|
|
1033
|
+
lines.push(`|---|------|-------:|-------:|------:|-------:|--------:|--------:|${thermalRule}-------|`);
|
|
1010
1034
|
for (const r of ranking.rows) {
|
|
1035
|
+
const thermalCell = anyThermal ? ` ${THERMAL_MARK[r.thermalState ?? "unknown"] ?? "\u2014"} |` : "";
|
|
1011
1036
|
if (r.failed) {
|
|
1012
|
-
lines.push(
|
|
1037
|
+
lines.push(
|
|
1038
|
+
`| ${r.batchIndex} | ${r.caseName} | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 | \u2014 |${thermalCell} \u274C FAILED (${r.failureReason}) |`
|
|
1039
|
+
);
|
|
1013
1040
|
continue;
|
|
1014
1041
|
}
|
|
1015
1042
|
const flagCell = r.flags.length ? `\u26A0\uFE0F ${r.flags.join("; ")}` : "\u2713 clean";
|
|
1016
1043
|
lines.push(
|
|
1017
1044
|
`| ${r.batchIndex} | ${r.caseName} | ${f(r.uiFps)}${delta(r.deltaUiFps)} | ${f(r.jsFps)}${delta(
|
|
1018
1045
|
r.deltaJsFps
|
|
1019
|
-
)} | ${f(r.cpu)} | ${f(r.memMb)} | ${f(r.jsJank, 0)} | ${f(r.uiJank, 0)}
|
|
1046
|
+
)} | ${f(r.cpu)} | ${f(r.memMb)} | ${f(r.jsJank, 0)} | ${f(r.uiJank, 0)} |${thermalCell} ${flagCell} |`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
const throttled = ranking.rows.filter(
|
|
1050
|
+
(r) => r.thermalState === "serious" || r.thermalState === "critical"
|
|
1051
|
+
);
|
|
1052
|
+
if (throttled.length) {
|
|
1053
|
+
lines.push("");
|
|
1054
|
+
lines.push(
|
|
1055
|
+
`**\u{1F525} Thermally throttled: ${throttled.map((r) => r.caseName).join(", ")}.** A throttled device runs SLOWER at a LOWER cpu%, so these cases can look like the cheapest in the batch while actually delivering fewer frames \u2014 check their UI FPS and jank, and re-run them on a cool device before drawing any conclusion.`
|
|
1020
1056
|
);
|
|
1021
1057
|
}
|
|
1022
1058
|
lines.push("");
|
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.24",
|
|
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",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"socket.io-client": "^4.8.1",
|
|
33
33
|
"zod": "^3.25.0",
|
|
34
34
|
"ws": "^8.18.0",
|
|
35
|
-
"@buoy-gg/sync-broker": "^7.0.
|
|
35
|
+
"@buoy-gg/sync-broker": "^7.0.24"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@types/node": "^22.0.0",
|