@buoy-gg/mcp 4.0.1

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.
@@ -0,0 +1,379 @@
1
+ import { ToolCapability } from '@buoy-gg/sync-broker';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+
4
+ interface SyncClientOptions {
5
+ /** Broker URL to connect to. @default http://localhost:42831 */
6
+ url?: string;
7
+ /** Broker port to probe/spawn on. @default 42831 */
8
+ port?: number;
9
+ /**
10
+ * If no broker is already running on the port, spawn one in-process so the
11
+ * MCP server works standalone (no desktop app needed). @default true
12
+ */
13
+ autoSpawnBroker?: boolean;
14
+ /** Default per-action timeout. @default 15000 */
15
+ actionTimeoutMs?: number;
16
+ /** Optional logger — writes to stderr by default (stdout is the MCP channel). */
17
+ log?: (msg: string) => void;
18
+ }
19
+ /** A device the broker knows about, merged with its advertised tools. */
20
+ interface DeviceInfo {
21
+ deviceId: string;
22
+ deviceName: string;
23
+ platform?: string;
24
+ isConnected: boolean;
25
+ /** Whether the connected app reported an active Buoy Pro license. */
26
+ isPro: boolean;
27
+ tools: ToolCapability[];
28
+ }
29
+ /** How the client obtained its broker connection. */
30
+ type BrokerMode = "connected" | "spawned";
31
+ /**
32
+ * A headless "Dashboard" client of the Buoy sync broker — the same role the
33
+ * desktop app plays, but driven programmatically by the MCP server. Implements
34
+ * connect-or-spawn: connects to a running broker (e.g. the desktop app) if one
35
+ * is up, otherwise hosts its own in-process so it works standalone.
36
+ */
37
+ declare class SyncClient {
38
+ private socket;
39
+ private broker;
40
+ private readonly url;
41
+ private readonly port;
42
+ private readonly actionTimeoutMs;
43
+ private readonly log;
44
+ /** Latest capabilities announce per deviceId. */
45
+ private capabilities;
46
+ /** Latest device list from the broker's `all-devices-update`. */
47
+ private devices;
48
+ /** Latest snapshot payload per `${deviceId}::${toolId}`. */
49
+ private snapshots;
50
+ /** Tools we've asked the broker to keep streaming, as `${deviceId}::${toolId}`. */
51
+ private watching;
52
+ /** In-flight actions awaiting a result, keyed by requestId. */
53
+ private pendingActions;
54
+ /** Callbacks waiting for the next snapshot of `${deviceId}::${toolId}`. */
55
+ private snapshotWaiters;
56
+ constructor(opts?: SyncClientOptions);
57
+ private readonly autoSpawn;
58
+ /** Connect to a running broker, or spawn one in-process, then handshake. */
59
+ start(): Promise<BrokerMode>;
60
+ private connectSocket;
61
+ /** Disconnect and tear down a spawned broker, if any. */
62
+ stop(): Promise<void>;
63
+ /** All devices the broker knows about, merged with their advertised tools. */
64
+ listDevices(): DeviceInfo[];
65
+ /** Connected devices only. */
66
+ connectedDevices(): DeviceInfo[];
67
+ /** Actions advertised for a tool, with the devices offering it. */
68
+ toolActions(toolId: string): {
69
+ deviceId: string;
70
+ deviceName: string;
71
+ actions: string[];
72
+ }[];
73
+ /**
74
+ * Resolve a concrete target deviceId. If `deviceId` is given it's used as-is.
75
+ * Otherwise, if exactly one device is connected, that one is chosen; if none
76
+ * or many, an informative error is thrown (callers can pass "All" explicitly).
77
+ */
78
+ resolveDevice(deviceId?: string): string;
79
+ /**
80
+ * Resolve a target device AND require it to be on Buoy Pro. The Buoy MCP is a
81
+ * Pro feature: data/action tools call this so a free app can't be driven
82
+ * through the MCP. The app's Keygen-validated license is the source of truth
83
+ * (the device reports it over the broker); this just reads that flag.
84
+ *
85
+ * `list_devices` deliberately does NOT call this — a free user can still see
86
+ * their device and that it's unlicensed, which explains why everything else
87
+ * is blocked.
88
+ */
89
+ requireProDevice(deviceId?: string): string;
90
+ private ensureWatching;
91
+ /** Stop streaming a tool (releases the device-side backpressure). */
92
+ unwatch(toolId: string, targetDeviceId: string): void;
93
+ /** Last snapshot received for a tool/device, if any. */
94
+ cachedSnapshot(toolId: string, deviceId: string): unknown;
95
+ /**
96
+ * Get a tool's current state. Starts watching (so the device begins emitting),
97
+ * explicitly requests state, and resolves with the next snapshot. Falls back
98
+ * to the last cached snapshot if the request times out.
99
+ */
100
+ getSnapshot(toolId: string, deviceId?: string, opts?: {
101
+ timeoutMs?: number;
102
+ }): Promise<{
103
+ deviceId: string;
104
+ payload: unknown;
105
+ }>;
106
+ private waitForSnapshot;
107
+ /**
108
+ * Invoke a remote action on a device and await its result. Pass deviceId to
109
+ * target one device, or "All" to broadcast (resolves with the first responder).
110
+ */
111
+ callAction(toolId: string, action: string, params?: unknown, deviceId?: string, opts?: {
112
+ timeoutMs?: number;
113
+ }): Promise<unknown>;
114
+ }
115
+
116
+ /**
117
+ * Build the Buoy MCP server and register all tools against a connected
118
+ * {@link SyncClient}. Tool packs (perf-monitor, etc.) are layered on top of the
119
+ * generic core here.
120
+ */
121
+ declare function createMcpServer(sync: SyncClient): McpServer;
122
+
123
+ /**
124
+ * The generic core: tools that work for ANY Buoy tool exposing a sync adapter,
125
+ * with zero tool-specific knowledge. Tool packs add ergonomic wrappers on top.
126
+ */
127
+ declare function registerGenericTools(server: McpServer, sync: SyncClient): void;
128
+
129
+ /**
130
+ * The perf-monitor tool pack: ergonomic, workflow-level tools that wrap the
131
+ * generic call_action/get_snapshot for benchmarking. Headlined by
132
+ * run_benchmark_batch — feed a case matrix, get back a ranked winner.
133
+ */
134
+ declare function registerPerfMonitorTools(server: McpServer, sync: SyncClient): void;
135
+
136
+ /** One row in an automation batch (MCP-friendly input; `id` added internally). */
137
+ interface PerfCaseInput {
138
+ /** Display name; becomes the saved report's metadata.name. */
139
+ name: string;
140
+ /** Query params applied to the route for this case (e.g. { renderer: "v2" }). */
141
+ params?: Record<string, string>;
142
+ /** Optional per-case route override; falls back to the batch targetRoute. */
143
+ route?: string;
144
+ }
145
+ /** Full config sent to the device's `startAutomation` action. */
146
+ interface AutomationConfig {
147
+ targetRoute: string;
148
+ bounceRoute: string;
149
+ perCaseDurationMs: number;
150
+ settleMs: number;
151
+ navTimeoutMs: number;
152
+ cases: {
153
+ id: string;
154
+ name: string;
155
+ params: Record<string, string>;
156
+ route?: string;
157
+ }[];
158
+ reloadBetweenCases?: boolean;
159
+ reloadStrategy?: "auto" | "dev-settings" | "expo-updates";
160
+ postReloadSettleMs?: number;
161
+ runsPerCase?: number;
162
+ coolDownMs?: number;
163
+ discardWarmupRuns?: number;
164
+ shuffleCases?: boolean;
165
+ }
166
+ /** A lightweight saved-recording summary (the device's index entry). */
167
+ interface IndexEntry {
168
+ id: string;
169
+ createdAt: number;
170
+ name: string;
171
+ route?: string;
172
+ durationMs?: number;
173
+ sampleCount?: number;
174
+ jsFpsAvg?: number;
175
+ uiFpsAvg?: number;
176
+ cpuAvg?: number;
177
+ memMaxMb?: number;
178
+ jsJankFrames?: number;
179
+ uiJankFrames?: number;
180
+ deviceMaxRefreshRate?: number;
181
+ source?: "manual" | "batch";
182
+ batchId?: string;
183
+ batchIndex?: number;
184
+ batchFailureReason?: string;
185
+ params?: Record<string, string>;
186
+ caseId?: string;
187
+ runIndex?: number;
188
+ isMedianRun?: boolean;
189
+ }
190
+ /** Per-metric min/max/avg/p95 channel. */
191
+ interface AggregateChannel {
192
+ min: number;
193
+ max: number;
194
+ avg: number;
195
+ p95: number;
196
+ }
197
+ /** Full report stats (from loadReport) — used by compare_reports. */
198
+ interface BenchmarkReportStats {
199
+ sampleCount: number;
200
+ durationMs: number;
201
+ jsFps: AggregateChannel;
202
+ uiFps: AggregateChannel;
203
+ cpuUsage: AggregateChannel;
204
+ memoryUsage: AggregateChannel;
205
+ jsJankFrames: number;
206
+ uiJankFrames: number;
207
+ deviceMaxRefreshRate: number;
208
+ }
209
+
210
+ /**
211
+ * Pure helpers for the perf-monitor pack: build an AutomationConfig from
212
+ * MCP-friendly input, estimate batch duration, and rank a finished batch's
213
+ * results into a winner + comparison table. No network/sync — unit-testable.
214
+ */
215
+
216
+ interface BuildConfigInput {
217
+ targetRoute: string;
218
+ bounceRoute?: string;
219
+ cases: PerfCaseInput[];
220
+ runsPerCase?: number;
221
+ perCaseDurationMs?: number;
222
+ settleMs?: number;
223
+ coolDownMs?: number;
224
+ navTimeoutMs?: number;
225
+ discardWarmupRuns?: number;
226
+ shuffleCases?: boolean;
227
+ reloadBetweenCases?: boolean;
228
+ }
229
+ /** The settings subset a driver inherits from the device's saved config. */
230
+ type BenchmarkSettings = Partial<Pick<AutomationConfig, "perCaseDurationMs" | "settleMs" | "navTimeoutMs" | "runsPerCase" | "coolDownMs" | "discardWarmupRuns" | "shuffleCases" | "reloadBetweenCases">>;
231
+ /**
232
+ * Build + validate the AutomationConfig the device expects. Throws on bad input.
233
+ *
234
+ * `base` is the device's saved benchmark settings — caller args override it,
235
+ * and it overrides the hardcoded fallback. So a run respects the profile the
236
+ * user tuned in the app unless the caller explicitly overrides a field.
237
+ */
238
+ declare function buildAutomationConfig(input: BuildConfigInput, base?: BenchmarkSettings): {
239
+ config: AutomationConfig;
240
+ warnings: string[];
241
+ };
242
+ /** Rough wall-clock estimate for a batch, in ms. */
243
+ declare function estimateBatchMs(config: AutomationConfig): number;
244
+ interface RankRow {
245
+ batchIndex: number;
246
+ caseName: string;
247
+ params: Record<string, string>;
248
+ runs: number;
249
+ failed: boolean;
250
+ failureReason?: string;
251
+ uiFps: number | null;
252
+ jsFps: number | null;
253
+ cpu: number | null;
254
+ memMb: number | null;
255
+ jsJank: number | null;
256
+ uiJank: number | null;
257
+ deviceMaxRefreshRate: number | null;
258
+ /** Delta vs the baseline (first case) — positive = better/more fps. */
259
+ deltaUiFps: number | null;
260
+ deltaJsFps: number | null;
261
+ /** At-risk axes for this case (e.g. "UI thread drops frames", "CPU near cap"). */
262
+ flags: string[];
263
+ }
264
+ interface BatchRanking {
265
+ batchId: string;
266
+ baselineName: string | null;
267
+ deviceMaxRefreshRate: number | null;
268
+ rows: RankRow[];
269
+ /** The single best case per metric — so no one number masks the others. */
270
+ leaders: {
271
+ uiFps?: string;
272
+ jsFps?: string;
273
+ lowestCpu?: string;
274
+ lowestMem?: string;
275
+ fewestJank?: string;
276
+ };
277
+ /** Cases with an at-risk axis (esp. UI thread) — never a silent winner. */
278
+ flagged: {
279
+ caseName: string;
280
+ flags: string[];
281
+ }[];
282
+ /** Cases that cap nothing, best-first — the safe candidates to favor. */
283
+ balanced: string[];
284
+ }
285
+ /**
286
+ * Rank a finished batch from its index entries. Picks the median run per case,
287
+ * computes per-metric leaders + at-risk flags, and surfaces the "balanced"
288
+ * cases (those that cap no axis). Deliberately does NOT crown a single winner:
289
+ * a cheap run that drops UI frames is worse than a pricier one that holds them,
290
+ * so the model/user must weigh the trade-offs from the full picture.
291
+ */
292
+ declare function rankBatchFromIndex(index: IndexEntry[], batchId: string, casesOrder?: {
293
+ name: string;
294
+ }[], failures?: {
295
+ index: number;
296
+ reason: string;
297
+ }[]): BatchRanking;
298
+ /** Render a batch ranking as a trade-off-aware report (no single false winner). */
299
+ declare function formatRankingMarkdown(ranking: BatchRanking): string;
300
+ /** Side-by-side comparison of two full report stats. */
301
+ declare function compareReportStats(a: {
302
+ name: string;
303
+ stats: BenchmarkReportStats;
304
+ }, b: {
305
+ name: string;
306
+ stats: BenchmarkReportStats;
307
+ }): {
308
+ a: string;
309
+ b: string;
310
+ uiFps: {
311
+ a: {
312
+ avg: number;
313
+ p95: number;
314
+ };
315
+ b: {
316
+ avg: number;
317
+ p95: number;
318
+ };
319
+ deltaAvg: number;
320
+ };
321
+ jsFps: {
322
+ a: {
323
+ avg: number;
324
+ p95: number;
325
+ };
326
+ b: {
327
+ avg: number;
328
+ p95: number;
329
+ };
330
+ deltaAvg: number;
331
+ };
332
+ cpuUsage: {
333
+ a: {
334
+ avg: number;
335
+ p95: number;
336
+ };
337
+ b: {
338
+ avg: number;
339
+ p95: number;
340
+ };
341
+ deltaAvg: number;
342
+ };
343
+ memoryUsage: {
344
+ a: {
345
+ avg: number;
346
+ p95: number;
347
+ };
348
+ b: {
349
+ avg: number;
350
+ p95: number;
351
+ };
352
+ deltaAvg: number;
353
+ };
354
+ jsJankFrames: {
355
+ a: number;
356
+ b: number;
357
+ delta: number;
358
+ };
359
+ uiJankFrames: {
360
+ a: number;
361
+ b: number;
362
+ delta: number;
363
+ };
364
+ };
365
+
366
+ /** Default broker port (shared with the desktop app + sync-broker). */
367
+ declare const BROKER_PORT = 42831;
368
+ /** Default broker URL the MCP server connects to as a "Dashboard" client. */
369
+ declare const BROKER_URL = "http://localhost:42831";
370
+ /** How long to wait for a device's action result before giving up. */
371
+ declare const DEFAULT_ACTION_TIMEOUT_MS = 15000;
372
+ /** How long to wait for a fresh snapshot after requesting tool state. */
373
+ declare const DEFAULT_SNAPSHOT_TIMEOUT_MS = 5000;
374
+ /** The exact handshake name the broker uses to recognize a dashboard client. */
375
+ declare const DASHBOARD_NAME = "Dashboard";
376
+ /** Target token meaning "every connected device". */
377
+ declare const ALL_DEVICES = "All";
378
+
379
+ export { ALL_DEVICES, BROKER_PORT, BROKER_URL, type BrokerMode, DASHBOARD_NAME, DEFAULT_ACTION_TIMEOUT_MS, DEFAULT_SNAPSHOT_TIMEOUT_MS, type DeviceInfo, SyncClient, type SyncClientOptions, buildAutomationConfig, compareReportStats, createMcpServer, estimateBatchMs, formatRankingMarkdown, rankBatchFromIndex, registerGenericTools, registerPerfMonitorTools };