@buoy-gg/mcp 7.0.65 → 7.0.67

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/cli.mjs CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import{createRequire as d}from"module";const R=d(import.meta.url);import{runInit as m}from"./chunk-WVVRNHPL.mjs";import{SyncClient as u,createMcpServer as w,stopAllHidStreamsNow as f,stopAllRecordingsNow as g}from"./chunk-SY6CJRCL.mjs";import"./chunk-QW2BPKIP.mjs";import"./chunk-6EOB4FHJ.mjs";import"./chunk-SWS5NTYM.mjs";import"./chunk-OPHPYPJD.mjs";import"./chunk-MTTWR4T7.mjs";import{StdioServerTransport as b}from"@modelcontextprotocol/sdk/server/stdio.js";console.log=(...e)=>console.error(...e);console.info=(...e)=>console.error(...e);console.debug=(...e)=>console.error(...e);async function v(){const e=process.env.BUOY_BROKER_PORT;const s=new u({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const r=w(s);const o=new b;let n=false;const c=t=>{if(n)return;n=true;process.stderr.write(`[buoy-mcp] shutting down (${t})
2
+ import{createRequire as d}from"module";const R=d(import.meta.url);import{runInit as m}from"./chunk-7XCEVM3W.mjs";import{SyncClient as u,createMcpServer as w,stopAllHidStreamsNow as f,stopAllRecordingsNow as g}from"./chunk-LDULNQCZ.mjs";import"./chunk-2MSKDRYW.mjs";import"./chunk-VKWX4XAY.mjs";import"./chunk-AMU2EOGE.mjs";import"./chunk-OPHPYPJD.mjs";import"./chunk-MTTWR4T7.mjs";import{StdioServerTransport as b}from"@modelcontextprotocol/sdk/server/stdio.js";console.log=(...e)=>console.error(...e);console.info=(...e)=>console.error(...e);console.debug=(...e)=>console.error(...e);async function v(){const e=process.env.BUOY_BROKER_PORT;const s=new u({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const r=w(s);const o=new b;let n=false;const c=t=>{if(n)return;n=true;process.stderr.write(`[buoy-mcp] shutting down (${t})
3
3
  `);g();f();setTimeout(()=>process.exit(0),2e3).unref();void(async()=>{await s.stop().catch(()=>{});await r.close().catch(()=>{});process.exit(0)})()};process.stdin.once("end",()=>c("stdin closed"));process.stdin.once("close",()=>c("stdin closed"));r.server.onclose=()=>c("transport closed");process.on("SIGINT",()=>c("SIGINT"));process.on("SIGTERM",()=>c("SIGTERM"));process.on("SIGHUP",()=>c("SIGHUP"));await r.connect(o);process.stderr.write(`[buoy-mcp] serving MCP over stdio; connecting to broker\u2026
4
4
  `);s.start().then(t=>process.stderr.write(`[buoy-mcp] broker ${t}
5
5
  `)).catch(t=>process.stderr.write(`[buoy-mcp] broker connection failed: ${t?.message??t} \u2014 tools will report no devices until it recovers
6
- `))}async function y(){const e=process.env.BUOY_BROKER_PORT;const s=new u({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:false});try{const r=await Promise.race([s.start().then(()=>true,()=>false),new Promise(o=>setTimeout(()=>o(false),4e3))]);if(!r)return false;for(let o=0;o<10;o++){if(s.connectedDevices().some(n=>n.tier==="pro"))return true;await new Promise(n=>setTimeout(n,300))}return false}finally{await Promise.race([s.stop().catch(()=>{}),new Promise(r=>setTimeout(r,2e3))])}}if(process.argv[2]==="web-bench"){void(async()=>{try{const e=process.argv.slice(3);const s=e.find(a=>!a.startsWith("--"));if(!s)throw new Error("usage: buoy-mcp web-bench <config.json> [--json]");const{readFileSync:r}=await import("fs");const{runWebBench:o,formatWebBenchMarkdown:n}=await import("./webBench-QCHYXYCA.mjs");const{readEntitlement:c}=await import("./camera-JI2FMAKN.mjs");if(!c().isPro&&!await y()){throw new Error("web-bench is a Buoy Pro feature. Sign in to Buoy Desktop with a Pro account, or open an app signed in to Pro. See https://buoy.gg/pricing.")}const t=JSON.parse(r(s,"utf8"));const i=await o({...t,durationMs:t.durationMs??t.perCaseDurationMs},a=>process.stderr.write(`[web-bench] ${a}
6
+ `))}async function y(){const e=process.env.BUOY_BROKER_PORT;const s=new u({url:process.env.BUOY_BROKER_URL,port:e?Number(e):void 0,autoSpawnBroker:false});try{const r=await Promise.race([s.start().then(()=>true,()=>false),new Promise(o=>setTimeout(()=>o(false),4e3))]);if(!r)return false;for(let o=0;o<10;o++){if(s.connectedDevices().some(n=>n.tier==="pro"))return true;await new Promise(n=>setTimeout(n,300))}return false}finally{await Promise.race([s.stop().catch(()=>{}),new Promise(r=>setTimeout(r,2e3))])}}if(process.argv[2]==="web-bench"){void(async()=>{try{const e=process.argv.slice(3);const s=e.find(a=>!a.startsWith("--"));if(!s)throw new Error("usage: buoy-mcp web-bench <config.json> [--json]");const{readFileSync:r}=await import("fs");const{runWebBench:o,formatWebBenchMarkdown:n}=await import("./webBench-QCHYXYCA.mjs");const{readEntitlement:c}=await import("./camera-F5Q7WMHF.mjs");if(!c().isPro&&!await y()){throw new Error("web-bench is a Buoy Pro feature. Sign in to Buoy Desktop with a Pro account, or open an app signed in to Pro. See https://buoy.gg/pricing.")}const t=JSON.parse(r(s,"utf8"));const i=await o({...t,durationMs:t.durationMs??t.perCaseDurationMs},a=>process.stderr.write(`[web-bench] ${a}
7
7
  `));const p=i.status==="failed"||i.browserLeftRunning?1:0;const l=(e.includes("--json")?JSON.stringify(i,null,2):n(i))+"\n";setTimeout(()=>process.exit(p),5e3).unref();process.stdout.write(l,()=>process.exit(p))}catch(e){process.stderr.write(`[buoy-mcp] web-bench failed: ${e?.message??e}
8
- `,()=>process.exit(1));setTimeout(()=>process.exit(1),5e3).unref()}})()}else if(process.argv[2]==="flow"&&process.argv[3]==="run"){void(async()=>{try{const{parseFlowRunArgs:e,runFlows:s,formatFlowResults:r}=await import("./flowRun-NGLENLWW.mjs");const o=e(process.argv.slice(4));const n=await s(o);const c=o.json?JSON.stringify(n,null,2):r(n);const t=n.every(i=>i.passed)?0:1;setTimeout(()=>process.exit(t),5e3).unref();process.stdout.write(`${c}
8
+ `,()=>process.exit(1));setTimeout(()=>process.exit(1),5e3).unref()}})()}else if(process.argv[2]==="flow"&&process.argv[3]==="run"){void(async()=>{try{const{parseFlowRunArgs:e,runFlows:s,formatFlowResults:r}=await import("./flowRun-WBOCHITQ.mjs");const o=e(process.argv.slice(4));const n=await s(o);const c=o.json?JSON.stringify(n,null,2):r(n);const t=n.every(i=>i.passed)?0:1;setTimeout(()=>process.exit(t),5e3).unref();process.stdout.write(`${c}
9
9
  `,()=>process.exit(t))}catch(e){process.stderr.write(`[buoy-mcp] flow run failed: ${e?.message??e}
10
- `,()=>process.exit(1));setTimeout(()=>process.exit(1),5e3).unref()}})()}else if(process.argv[2]==="farmd"){void import("./simFarm-HLOW7LIW.mjs").then(e=>e.runFarmd()).catch(e=>{process.stderr.write(`[buoy-mcp] farmd failed: ${e?.message??e}
10
+ `,()=>process.exit(1));setTimeout(()=>process.exit(1),5e3).unref()}})()}else if(process.argv[2]==="farmd"){void import("./simFarm-OFT42BGC.mjs").then(e=>e.runFarmd()).catch(e=>{process.stderr.write(`[buoy-mcp] farmd failed: ${e?.message??e}
11
11
  `);process.exit(1)})}else if(process.argv[2]==="init"){try{const e=process.argv.slice(3);let s="auto";if(e.includes("--local"))s="local";else if(e.includes("--npx"))s="npx";const r=e.indexOf("--registry");const o=r!==-1?e[r+1]:void 0;m({mode:s,registry:o});process.exit(0)}catch(e){process.stderr.write(`[buoy-mcp] init failed: ${e?.message??e}
12
12
  `);process.exit(1)}}else{v().catch(e=>{process.stderr.write(`[buoy-mcp] fatal: ${e?.message??e}
13
13
  `);process.exit(1)})}
@@ -0,0 +1,2 @@
1
+ import{createRequire as v}from"module";const E=v(import.meta.url);import{SyncClient as y,createMcpServer as R}from"./chunk-LDULNQCZ.mjs";import"./chunk-2MSKDRYW.mjs";import"./chunk-VKWX4XAY.mjs";import"./chunk-AMU2EOGE.mjs";import"./chunk-OPHPYPJD.mjs";import"./chunk-MTTWR4T7.mjs";import{promises as S}from"fs";import $ from"path";import{Client as _}from"@modelcontextprotocol/sdk/client/index.js";import{InMemoryTransport as B}from"@modelcontextprotocol/sdk/inMemory.js";function I(e){const o={names:[]};for(let n=0;n<e.length;n++){const t=e[n];if(t==="--device")o.deviceId=e[++n];else if(t==="--update-snapshots")o.updateSnapshots=true;else if(t==="--json")o.json=true;else if(t==="--wait")o.waitMs=Number(e[++n])*1e3;else if(t==="--all")continue;else if(!t.startsWith("--"))o.names.push(t);else throw new Error(`Unknown option ${t}.`)}return o}async function P(){const e=$.join(process.cwd(),".buoy","flows");return(await S.readdir(e).catch(()=>[])).filter(o=>o.endsWith(".json")).map(o=>o.replace(/\.json$/,"")).sort()}async function g(e,o=n=>process.stderr.write(`${n}
2
+ `)){const n=e.names.length?e.names:await P();if(n.length===0)throw new Error('No saved flows. Save one with run_flow and save:"name", or pass a flow name.');const t=process.env.BUOY_BROKER_PORT;const s=new y({url:process.env.BUOY_BROKER_URL,port:t?Number(t):void 0,autoSpawnBroker:process.env.BUOY_NO_SPAWN!=="1"});const w=R(s);const[d,f]=B.createLinkedPair();const i=new _({name:"buoy-flow-run",version:"1"});try{await w.connect(f);await i.connect(d);await s.start();const a=Date.now()+(e.waitMs??6e4);while(Date.now()<a&&!s.connectedDevices().some(r=>(r.isPro||r.tier==="free")&&(!e.deviceId||r.deviceId===e.deviceId))){await new Promise(r=>setTimeout(r,500))}if(!s.connectedDevices().some(r=>r.isPro||r.tier==="free")){throw new Error("No app on a Buoy account is connected. Start the app (with Buoy) on a simulator or device, then run again.")}const u=[];for(const r of n){const m=Date.now();o(`\u25B6 ${r}`);const l=await i.callTool({name:"run_flow",arguments:{flow:r,include:"none",...e.deviceId?{deviceId:e.deviceId}:{},...e.updateSnapshots?{updateSnapshots:true}:{}}},void 0,{timeout:10*6e4});const h=(l.content??[]).filter(c=>c.type==="text").map(c=>c.text).join("\n");const p=l.isError!==true;u.push({name:r,passed:p,ms:Date.now()-m,report:h});o(`${p?"\u2713":"\u2717"} ${r} (${((Date.now()-m)/1e3).toFixed(1)}s)`)}return u}finally{await i.close().catch(()=>void 0);await Promise.race([s.stop().catch(()=>void 0),new Promise(a=>setTimeout(a,2e3))])}}function j(e){const o=e.filter(t=>!t.passed);const n=[`${e.length-o.length} of ${e.length} flows passed.`];for(const t of o){n.push("",`\u2717 ${t.name}`,t.report.trim(),` Rerun: npx @buoy-gg/mcp flow run ${t.name}`)}return n.join("\n")}export{j as formatFlowResults,I as parseFlowRunArgs,g as runFlows};
package/dist/index.d.mts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { ToolCapability } from '@buoy-gg/sync-broker';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
 
4
+ type AccessLevel = "free" | "pro";
5
+
4
6
  interface SyncClientOptions {
5
7
  /** Broker URL to connect to. @default http://localhost:42831 */
6
8
  url?: string;
@@ -24,14 +26,14 @@ interface DeviceInfo {
24
26
  /** Whether the connected app reported an active Buoy Pro license. */
25
27
  isPro: boolean;
26
28
  /**
27
- * The app's tier once the broker has admitted its account, or "locked"
28
- * until then. Apps older than the tier split report only `isPro`, which
29
- * maps exactly — a key could only ever grant Pro back then.
29
+ * The app's tier after admission. New sockets are "checking"; an account
30
+ * lock is "locked". Older apps report only `isPro`, which maps to Pro
31
+ * or locked because keys could only grant Pro before the tier split.
30
32
  *
31
33
  * Surfaced so `list_devices` can say WHY the MCP is blocked: "signed in on
32
34
  * the free tier" and "no admitted account" are different problems.
33
35
  */
34
- tier: "locked" | "free" | "pro";
36
+ tier: "checking" | "locked" | "free" | "pro";
35
37
  /**
36
38
  * More than one app is registered under this id right now (a pinned
37
39
  * `externalSync.deviceId` copied onto two devices). Actions sent to it
@@ -116,7 +118,10 @@ declare class SyncClient {
116
118
  * with every simulator erase and fresh dev client, and each one can hold
117
119
  * multi-MB snapshots, so a long-lived server has to forget eventually.
118
120
  */
121
+ /** Whether the broker admitted this host's Buoy account. */
122
+ isAdmitted(): boolean;
119
123
  private requireAdmission;
124
+ private transportLost;
120
125
  private setAdmission;
121
126
  private requireDeviceAccount;
122
127
  private lockDevice;
@@ -145,22 +150,53 @@ declare class SyncClient {
145
150
  */
146
151
  resolveDevice(deviceId?: string): string;
147
152
  /**
148
- * Resolve a target device AND require it to be on Buoy Pro. The Buoy MCP is a
149
- * Pro feature: data/action tools call this so a free app can't be driven
150
- * through the MCP. The app's Keygen-validated license is the source of truth
151
- * (the device reports it over the broker); this just reads that flag.
153
+ * Resolve a target device AND require the tier the current tool call needs.
154
+ * Most tools need Buoy Pro; the free ones (access/policy.ts) also take an app
155
+ * on the free tier. The level comes from the call's scope, set centrally in
156
+ * server.ts before any handler runs, so a handler can't pick its own. With no
157
+ * scope (work outside any tool call) the level is Pro. The app's own license
158
+ * check is the source of truth (the device reports it over the broker); this
159
+ * just reads it.
152
160
  *
153
- * `list_devices` deliberately does NOT call this — a free user can still see
154
- * their device and that it's unlicensed, which explains why everything else
155
- * is blocked.
161
+ * `list_devices` deliberately does NOT call this — a user without access can
162
+ * still see their device and its tier, which explains what is blocked.
156
163
  */
157
164
  requireProDevice(deviceId?: string): string;
158
165
  /**
159
- * Where a device stands on access: connected and Pro ("ready"), not
160
- * connected ("offline"), connected but its account not announced yet
161
- * ("pending"), or announced on a tier without MCP access ("denied").
166
+ * The check under every app call (callAction, getSnapshot, the snapshot
167
+ * cache, lastSync), on top of requireProDevice. A call to a Pro app always
168
+ * passes. A call to any other app passes only inside a free tool call and
169
+ * only for the app calls listed in access/policy.ts, so a Pro tool that
170
+ * forgot requireProDevice still can't act on a free app, and a free tool
171
+ * can't reach Pro data by any path. Work outside any tool call (reconnects,
172
+ * health pings) is plumbing and passes.
173
+ */
174
+ private checkAppCall;
175
+ /**
176
+ * A free app's tap must land on the app, not on Buoy's own panels: their
177
+ * controls do what Pro tools do. The app side leaves Buoy out of fuzzy
178
+ * matches but lets an exact testID or nativeTag through, so on a free app an
179
+ * exact tap goes through only when the app-only screen list shows that
180
+ * target. Anything this can't prove (a Buoy control, an off-screen target, a
181
+ * failed read, a broadcast to "All") is refused. Resolves to replacement
182
+ * params when it narrows the tap (never edits the caller's object, which a
183
+ * flow reuses on retries). Returns null, without an await, for every call it
184
+ * doesn't apply to.
185
+ */
186
+ private refuseBuoyPanelTap;
187
+ /**
188
+ * Whether this device was seen on a tier good enough for the current call,
189
+ * connected or not. For host controls that keep working after the app
190
+ * quits or crashes (see hostKit), and for the last-sync fallback.
191
+ */
192
+ deviceWasAllowed(deviceId: string): boolean;
193
+ /**
194
+ * Where a device stands for `level` (the current call's by default):
195
+ * connected and allowed ("ready"), not connected ("offline"), connected but
196
+ * its account not announced yet ("pending"), or announced on a tier without
197
+ * this access ("denied").
162
198
  */
163
- accessState(deviceId: string): "ready" | "offline" | "pending" | "denied";
199
+ accessState(deviceId: string, level?: AccessLevel): "ready" | "offline" | "pending" | "denied";
164
200
  private healthOf;
165
201
  /** The device answered — it's alive. */
166
202
  private noteOk;
@@ -408,94 +444,6 @@ declare function runInit(opts?: InitOptions): void;
408
444
  */
409
445
  declare function registerGenericTools(server: McpServer, sync: SyncClient): void;
410
446
 
411
- /**
412
- * The perf-monitor tool pack: ergonomic, workflow-level tools that wrap the
413
- * generic call_action/get_snapshot for benchmarking. Headlined by
414
- * run_benchmark_batch — feed a case matrix, get back a ranked winner.
415
- */
416
- declare function registerPerfMonitorTools(server: McpServer, sync: SyncClient): void;
417
-
418
- /**
419
- * The events tool pack: a compact, token-bounded view of the app's captured
420
- * events. Drives the device's existing "Copy Settings" formatter (the heavy raw
421
- * payloads stay on the device unless asked for), so the model gets a compact
422
- * log by default and opts into detail when needed.
423
- */
424
- declare function registerEventsTools(server: McpServer, sync: SyncClient): void;
425
-
426
- /**
427
- * The react-query pack: a compact current-CACHE reader (the activity log is
428
- * already covered by get_events) plus the cache-mutating actions, which reuse
429
- * the adapter's existing handlers verbatim.
430
- */
431
- declare function registerReactQueryTools(server: McpServer, sync: SyncClient): void;
432
-
433
- /**
434
- * The zustand pack: a compact current-STATE reader (the change log is already
435
- * covered by get_events sources:['zustand']) plus a setState action that reuses
436
- * the adapter's existing handler.
437
- */
438
- declare function registerZustandTools(server: McpServer, sync: SyncClient): void;
439
-
440
- /**
441
- * The jotai pack: a compact current-VALUE reader (the change log is already
442
- * covered by get_events sources:['jotai']) plus a setAtom action for writable
443
- * atoms.
444
- */
445
- declare function registerJotaiTools(server: McpServer, sync: SyncClient): void;
446
-
447
- /**
448
- * The redux pack: a compact current-STATE reader (redux is action-based, so the
449
- * current state isn't in the action-log snapshot — get_events sources:['redux']
450
- * covers the action history) plus a dispatch action, both driving the live
451
- * instrumented store.
452
- */
453
- declare function registerReduxTools(server: McpServer, sync: SyncClient): void;
454
-
455
- /**
456
- * The storage pack: a friendly, compact reader + writer over the app's
457
- * storage backends — AsyncStorage, MMKV (all registered instances), and Expo
458
- * SecureStore (registered keys). Reuses the storage adapter's async.*, mmkv.*
459
- * and secure.* actions; the device already strips Buoy's own devtool keys at
460
- * the source, so only real app keys are returned.
461
- */
462
- declare function registerStorageTools(server: McpServer, sync: SyncClient): void;
463
-
464
- /**
465
- * The routes pack: current route + navigation stack (compact) with an optional
466
- * sitemap, plus a navigate action — all reusing the route-events adapter's
467
- * existing snapshot and `navigate` action.
468
- */
469
- declare function registerRoutesTools(server: McpServer, sync: SyncClient): void;
470
-
471
- /**
472
- * The console pack: a compact, token-bounded tail of the app's captured
473
- * console.* logs, reusing the console adapter's snapshot (each entry carries a
474
- * pre-rendered single-line `message`, so no heavy args cross into context).
475
- */
476
- declare function registerConsoleTools(server: McpServer, sync: SyncClient): void;
477
-
478
- /**
479
- * The screenshot pack: capture a tight, focused PNG of a single RN component,
480
- * reusing the same flow the desktop app's Screenshot tool uses — the device's
481
- * highlight-updates `locateComponent` action resolves a testID/name to a live
482
- * on-screen rect (scrolling it into view first), and the MCP server itself
483
- * captures + crops the iOS Simulator's framebuffer via `xcrun simctl` + `sips`
484
- * (no desktop app required, as long as the MCP server runs on the same Mac as
485
- * the booted Simulator).
486
- */
487
- declare function registerScreenshotTools(server: McpServer, sync: SyncClient): void;
488
-
489
- /**
490
- * A one-line update notice if a newer version is published, else null. Safe to
491
- * await inline — bounded by the fetch timeout and short-circuited by the cache.
492
- */
493
- declare function getUpdateNotice(now?: number): Promise<string | null>;
494
-
495
- declare const VERSION: string;
496
- /** The published npm package name — used by init + the update check. */
497
- declare const PACKAGE_NAME = "@buoy-gg/mcp";
498
-
499
447
  /** One row in an automation batch (MCP-friendly input; `id` added internally). */
500
448
  interface PerfCaseInput {
501
449
  /** Display name; becomes the saved report's metadata.name. */
@@ -691,6 +639,94 @@ interface RenderCapture {
691
639
  }[];
692
640
  }
693
641
 
642
+ /**
643
+ * The perf-monitor tool pack: ergonomic, workflow-level tools that wrap the
644
+ * generic call_action/get_snapshot for benchmarking. Headlined by
645
+ * run_benchmark_batch — feed a case matrix, get back a ranked winner.
646
+ */
647
+ declare function registerPerfMonitorTools(server: McpServer, sync: SyncClient): void;
648
+
649
+ /**
650
+ * The events tool pack: a compact, token-bounded view of the app's captured
651
+ * events. Drives the device's existing "Copy Settings" formatter (the heavy raw
652
+ * payloads stay on the device unless asked for), so the model gets a compact
653
+ * log by default and opts into detail when needed.
654
+ */
655
+ declare function registerEventsTools(server: McpServer, sync: SyncClient): void;
656
+
657
+ /**
658
+ * The react-query pack: a compact current-CACHE reader (the activity log is
659
+ * already covered by get_events) plus the cache-mutating actions, which reuse
660
+ * the adapter's existing handlers verbatim.
661
+ */
662
+ declare function registerReactQueryTools(server: McpServer, sync: SyncClient): void;
663
+
664
+ /**
665
+ * The zustand pack: a compact current-STATE reader (the change log is already
666
+ * covered by get_events sources:['zustand']) plus a setState action that reuses
667
+ * the adapter's existing handler.
668
+ */
669
+ declare function registerZustandTools(server: McpServer, sync: SyncClient): void;
670
+
671
+ /**
672
+ * The jotai pack: a compact current-VALUE reader (the change log is already
673
+ * covered by get_events sources:['jotai']) plus a setAtom action for writable
674
+ * atoms.
675
+ */
676
+ declare function registerJotaiTools(server: McpServer, sync: SyncClient): void;
677
+
678
+ /**
679
+ * The redux pack: a compact current-STATE reader (redux is action-based, so the
680
+ * current state isn't in the action-log snapshot — get_events sources:['redux']
681
+ * covers the action history) plus a dispatch action, both driving the live
682
+ * instrumented store.
683
+ */
684
+ declare function registerReduxTools(server: McpServer, sync: SyncClient): void;
685
+
686
+ /**
687
+ * The storage pack: a friendly, compact reader + writer over the app's
688
+ * storage backends — AsyncStorage, MMKV (all registered instances), and Expo
689
+ * SecureStore (registered keys). Reuses the storage adapter's async.*, mmkv.*
690
+ * and secure.* actions; the device already strips Buoy's own devtool keys at
691
+ * the source, so only real app keys are returned.
692
+ */
693
+ declare function registerStorageTools(server: McpServer, sync: SyncClient): void;
694
+
695
+ /**
696
+ * The routes pack: current route + navigation stack (compact) with an optional
697
+ * sitemap, plus a navigate action — all reusing the route-events adapter's
698
+ * existing snapshot and `navigate` action.
699
+ */
700
+ declare function registerRoutesTools(server: McpServer, sync: SyncClient): void;
701
+
702
+ /**
703
+ * The console pack: a compact, token-bounded tail of the app's captured
704
+ * console.* logs, reusing the console adapter's snapshot (each entry carries a
705
+ * pre-rendered single-line `message`, so no heavy args cross into context).
706
+ */
707
+ declare function registerConsoleTools(server: McpServer, sync: SyncClient): void;
708
+
709
+ /**
710
+ * The screenshot pack: capture a tight, focused PNG of a single RN component,
711
+ * reusing the same flow the desktop app's Screenshot tool uses — the device's
712
+ * highlight-updates `locateComponent` action resolves a testID/name to a live
713
+ * on-screen rect (scrolling it into view first), and the MCP server itself
714
+ * captures + crops the iOS Simulator's framebuffer via `xcrun simctl` + `sips`
715
+ * (no desktop app required, as long as the MCP server runs on the same Mac as
716
+ * the booted Simulator).
717
+ */
718
+ declare function registerScreenshotTools(server: McpServer, sync: SyncClient): void;
719
+
720
+ /**
721
+ * A one-line update notice if a newer version is published, else null. Safe to
722
+ * await inline — bounded by the fetch timeout and short-circuited by the cache.
723
+ */
724
+ declare function getUpdateNotice(now?: number): Promise<string | null>;
725
+
726
+ declare const VERSION: string;
727
+ /** The published npm package name — used by init + the update check. */
728
+ declare const PACKAGE_NAME = "@buoy-gg/mcp";
729
+
694
730
  interface BuildConfigInput {
695
731
  targetRoute: string;
696
732
  bounceRoute?: string;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { ToolCapability } from '@buoy-gg/sync-broker';
2
2
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
3
 
4
+ type AccessLevel = "free" | "pro";
5
+
4
6
  interface SyncClientOptions {
5
7
  /** Broker URL to connect to. @default http://localhost:42831 */
6
8
  url?: string;
@@ -24,14 +26,14 @@ interface DeviceInfo {
24
26
  /** Whether the connected app reported an active Buoy Pro license. */
25
27
  isPro: boolean;
26
28
  /**
27
- * The app's tier once the broker has admitted its account, or "locked"
28
- * until then. Apps older than the tier split report only `isPro`, which
29
- * maps exactly — a key could only ever grant Pro back then.
29
+ * The app's tier after admission. New sockets are "checking"; an account
30
+ * lock is "locked". Older apps report only `isPro`, which maps to Pro
31
+ * or locked because keys could only grant Pro before the tier split.
30
32
  *
31
33
  * Surfaced so `list_devices` can say WHY the MCP is blocked: "signed in on
32
34
  * the free tier" and "no admitted account" are different problems.
33
35
  */
34
- tier: "locked" | "free" | "pro";
36
+ tier: "checking" | "locked" | "free" | "pro";
35
37
  /**
36
38
  * More than one app is registered under this id right now (a pinned
37
39
  * `externalSync.deviceId` copied onto two devices). Actions sent to it
@@ -116,7 +118,10 @@ declare class SyncClient {
116
118
  * with every simulator erase and fresh dev client, and each one can hold
117
119
  * multi-MB snapshots, so a long-lived server has to forget eventually.
118
120
  */
121
+ /** Whether the broker admitted this host's Buoy account. */
122
+ isAdmitted(): boolean;
119
123
  private requireAdmission;
124
+ private transportLost;
120
125
  private setAdmission;
121
126
  private requireDeviceAccount;
122
127
  private lockDevice;
@@ -145,22 +150,53 @@ declare class SyncClient {
145
150
  */
146
151
  resolveDevice(deviceId?: string): string;
147
152
  /**
148
- * Resolve a target device AND require it to be on Buoy Pro. The Buoy MCP is a
149
- * Pro feature: data/action tools call this so a free app can't be driven
150
- * through the MCP. The app's Keygen-validated license is the source of truth
151
- * (the device reports it over the broker); this just reads that flag.
153
+ * Resolve a target device AND require the tier the current tool call needs.
154
+ * Most tools need Buoy Pro; the free ones (access/policy.ts) also take an app
155
+ * on the free tier. The level comes from the call's scope, set centrally in
156
+ * server.ts before any handler runs, so a handler can't pick its own. With no
157
+ * scope (work outside any tool call) the level is Pro. The app's own license
158
+ * check is the source of truth (the device reports it over the broker); this
159
+ * just reads it.
152
160
  *
153
- * `list_devices` deliberately does NOT call this — a free user can still see
154
- * their device and that it's unlicensed, which explains why everything else
155
- * is blocked.
161
+ * `list_devices` deliberately does NOT call this — a user without access can
162
+ * still see their device and its tier, which explains what is blocked.
156
163
  */
157
164
  requireProDevice(deviceId?: string): string;
158
165
  /**
159
- * Where a device stands on access: connected and Pro ("ready"), not
160
- * connected ("offline"), connected but its account not announced yet
161
- * ("pending"), or announced on a tier without MCP access ("denied").
166
+ * The check under every app call (callAction, getSnapshot, the snapshot
167
+ * cache, lastSync), on top of requireProDevice. A call to a Pro app always
168
+ * passes. A call to any other app passes only inside a free tool call and
169
+ * only for the app calls listed in access/policy.ts, so a Pro tool that
170
+ * forgot requireProDevice still can't act on a free app, and a free tool
171
+ * can't reach Pro data by any path. Work outside any tool call (reconnects,
172
+ * health pings) is plumbing and passes.
173
+ */
174
+ private checkAppCall;
175
+ /**
176
+ * A free app's tap must land on the app, not on Buoy's own panels: their
177
+ * controls do what Pro tools do. The app side leaves Buoy out of fuzzy
178
+ * matches but lets an exact testID or nativeTag through, so on a free app an
179
+ * exact tap goes through only when the app-only screen list shows that
180
+ * target. Anything this can't prove (a Buoy control, an off-screen target, a
181
+ * failed read, a broadcast to "All") is refused. Resolves to replacement
182
+ * params when it narrows the tap (never edits the caller's object, which a
183
+ * flow reuses on retries). Returns null, without an await, for every call it
184
+ * doesn't apply to.
185
+ */
186
+ private refuseBuoyPanelTap;
187
+ /**
188
+ * Whether this device was seen on a tier good enough for the current call,
189
+ * connected or not. For host controls that keep working after the app
190
+ * quits or crashes (see hostKit), and for the last-sync fallback.
191
+ */
192
+ deviceWasAllowed(deviceId: string): boolean;
193
+ /**
194
+ * Where a device stands for `level` (the current call's by default):
195
+ * connected and allowed ("ready"), not connected ("offline"), connected but
196
+ * its account not announced yet ("pending"), or announced on a tier without
197
+ * this access ("denied").
162
198
  */
163
- accessState(deviceId: string): "ready" | "offline" | "pending" | "denied";
199
+ accessState(deviceId: string, level?: AccessLevel): "ready" | "offline" | "pending" | "denied";
164
200
  private healthOf;
165
201
  /** The device answered — it's alive. */
166
202
  private noteOk;
@@ -408,94 +444,6 @@ declare function runInit(opts?: InitOptions): void;
408
444
  */
409
445
  declare function registerGenericTools(server: McpServer, sync: SyncClient): void;
410
446
 
411
- /**
412
- * The perf-monitor tool pack: ergonomic, workflow-level tools that wrap the
413
- * generic call_action/get_snapshot for benchmarking. Headlined by
414
- * run_benchmark_batch — feed a case matrix, get back a ranked winner.
415
- */
416
- declare function registerPerfMonitorTools(server: McpServer, sync: SyncClient): void;
417
-
418
- /**
419
- * The events tool pack: a compact, token-bounded view of the app's captured
420
- * events. Drives the device's existing "Copy Settings" formatter (the heavy raw
421
- * payloads stay on the device unless asked for), so the model gets a compact
422
- * log by default and opts into detail when needed.
423
- */
424
- declare function registerEventsTools(server: McpServer, sync: SyncClient): void;
425
-
426
- /**
427
- * The react-query pack: a compact current-CACHE reader (the activity log is
428
- * already covered by get_events) plus the cache-mutating actions, which reuse
429
- * the adapter's existing handlers verbatim.
430
- */
431
- declare function registerReactQueryTools(server: McpServer, sync: SyncClient): void;
432
-
433
- /**
434
- * The zustand pack: a compact current-STATE reader (the change log is already
435
- * covered by get_events sources:['zustand']) plus a setState action that reuses
436
- * the adapter's existing handler.
437
- */
438
- declare function registerZustandTools(server: McpServer, sync: SyncClient): void;
439
-
440
- /**
441
- * The jotai pack: a compact current-VALUE reader (the change log is already
442
- * covered by get_events sources:['jotai']) plus a setAtom action for writable
443
- * atoms.
444
- */
445
- declare function registerJotaiTools(server: McpServer, sync: SyncClient): void;
446
-
447
- /**
448
- * The redux pack: a compact current-STATE reader (redux is action-based, so the
449
- * current state isn't in the action-log snapshot — get_events sources:['redux']
450
- * covers the action history) plus a dispatch action, both driving the live
451
- * instrumented store.
452
- */
453
- declare function registerReduxTools(server: McpServer, sync: SyncClient): void;
454
-
455
- /**
456
- * The storage pack: a friendly, compact reader + writer over the app's
457
- * storage backends — AsyncStorage, MMKV (all registered instances), and Expo
458
- * SecureStore (registered keys). Reuses the storage adapter's async.*, mmkv.*
459
- * and secure.* actions; the device already strips Buoy's own devtool keys at
460
- * the source, so only real app keys are returned.
461
- */
462
- declare function registerStorageTools(server: McpServer, sync: SyncClient): void;
463
-
464
- /**
465
- * The routes pack: current route + navigation stack (compact) with an optional
466
- * sitemap, plus a navigate action — all reusing the route-events adapter's
467
- * existing snapshot and `navigate` action.
468
- */
469
- declare function registerRoutesTools(server: McpServer, sync: SyncClient): void;
470
-
471
- /**
472
- * The console pack: a compact, token-bounded tail of the app's captured
473
- * console.* logs, reusing the console adapter's snapshot (each entry carries a
474
- * pre-rendered single-line `message`, so no heavy args cross into context).
475
- */
476
- declare function registerConsoleTools(server: McpServer, sync: SyncClient): void;
477
-
478
- /**
479
- * The screenshot pack: capture a tight, focused PNG of a single RN component,
480
- * reusing the same flow the desktop app's Screenshot tool uses — the device's
481
- * highlight-updates `locateComponent` action resolves a testID/name to a live
482
- * on-screen rect (scrolling it into view first), and the MCP server itself
483
- * captures + crops the iOS Simulator's framebuffer via `xcrun simctl` + `sips`
484
- * (no desktop app required, as long as the MCP server runs on the same Mac as
485
- * the booted Simulator).
486
- */
487
- declare function registerScreenshotTools(server: McpServer, sync: SyncClient): void;
488
-
489
- /**
490
- * A one-line update notice if a newer version is published, else null. Safe to
491
- * await inline — bounded by the fetch timeout and short-circuited by the cache.
492
- */
493
- declare function getUpdateNotice(now?: number): Promise<string | null>;
494
-
495
- declare const VERSION: string;
496
- /** The published npm package name — used by init + the update check. */
497
- declare const PACKAGE_NAME = "@buoy-gg/mcp";
498
-
499
447
  /** One row in an automation batch (MCP-friendly input; `id` added internally). */
500
448
  interface PerfCaseInput {
501
449
  /** Display name; becomes the saved report's metadata.name. */
@@ -691,6 +639,94 @@ interface RenderCapture {
691
639
  }[];
692
640
  }
693
641
 
642
+ /**
643
+ * The perf-monitor tool pack: ergonomic, workflow-level tools that wrap the
644
+ * generic call_action/get_snapshot for benchmarking. Headlined by
645
+ * run_benchmark_batch — feed a case matrix, get back a ranked winner.
646
+ */
647
+ declare function registerPerfMonitorTools(server: McpServer, sync: SyncClient): void;
648
+
649
+ /**
650
+ * The events tool pack: a compact, token-bounded view of the app's captured
651
+ * events. Drives the device's existing "Copy Settings" formatter (the heavy raw
652
+ * payloads stay on the device unless asked for), so the model gets a compact
653
+ * log by default and opts into detail when needed.
654
+ */
655
+ declare function registerEventsTools(server: McpServer, sync: SyncClient): void;
656
+
657
+ /**
658
+ * The react-query pack: a compact current-CACHE reader (the activity log is
659
+ * already covered by get_events) plus the cache-mutating actions, which reuse
660
+ * the adapter's existing handlers verbatim.
661
+ */
662
+ declare function registerReactQueryTools(server: McpServer, sync: SyncClient): void;
663
+
664
+ /**
665
+ * The zustand pack: a compact current-STATE reader (the change log is already
666
+ * covered by get_events sources:['zustand']) plus a setState action that reuses
667
+ * the adapter's existing handler.
668
+ */
669
+ declare function registerZustandTools(server: McpServer, sync: SyncClient): void;
670
+
671
+ /**
672
+ * The jotai pack: a compact current-VALUE reader (the change log is already
673
+ * covered by get_events sources:['jotai']) plus a setAtom action for writable
674
+ * atoms.
675
+ */
676
+ declare function registerJotaiTools(server: McpServer, sync: SyncClient): void;
677
+
678
+ /**
679
+ * The redux pack: a compact current-STATE reader (redux is action-based, so the
680
+ * current state isn't in the action-log snapshot — get_events sources:['redux']
681
+ * covers the action history) plus a dispatch action, both driving the live
682
+ * instrumented store.
683
+ */
684
+ declare function registerReduxTools(server: McpServer, sync: SyncClient): void;
685
+
686
+ /**
687
+ * The storage pack: a friendly, compact reader + writer over the app's
688
+ * storage backends — AsyncStorage, MMKV (all registered instances), and Expo
689
+ * SecureStore (registered keys). Reuses the storage adapter's async.*, mmkv.*
690
+ * and secure.* actions; the device already strips Buoy's own devtool keys at
691
+ * the source, so only real app keys are returned.
692
+ */
693
+ declare function registerStorageTools(server: McpServer, sync: SyncClient): void;
694
+
695
+ /**
696
+ * The routes pack: current route + navigation stack (compact) with an optional
697
+ * sitemap, plus a navigate action — all reusing the route-events adapter's
698
+ * existing snapshot and `navigate` action.
699
+ */
700
+ declare function registerRoutesTools(server: McpServer, sync: SyncClient): void;
701
+
702
+ /**
703
+ * The console pack: a compact, token-bounded tail of the app's captured
704
+ * console.* logs, reusing the console adapter's snapshot (each entry carries a
705
+ * pre-rendered single-line `message`, so no heavy args cross into context).
706
+ */
707
+ declare function registerConsoleTools(server: McpServer, sync: SyncClient): void;
708
+
709
+ /**
710
+ * The screenshot pack: capture a tight, focused PNG of a single RN component,
711
+ * reusing the same flow the desktop app's Screenshot tool uses — the device's
712
+ * highlight-updates `locateComponent` action resolves a testID/name to a live
713
+ * on-screen rect (scrolling it into view first), and the MCP server itself
714
+ * captures + crops the iOS Simulator's framebuffer via `xcrun simctl` + `sips`
715
+ * (no desktop app required, as long as the MCP server runs on the same Mac as
716
+ * the booted Simulator).
717
+ */
718
+ declare function registerScreenshotTools(server: McpServer, sync: SyncClient): void;
719
+
720
+ /**
721
+ * A one-line update notice if a newer version is published, else null. Safe to
722
+ * await inline — bounded by the fetch timeout and short-circuited by the cache.
723
+ */
724
+ declare function getUpdateNotice(now?: number): Promise<string | null>;
725
+
726
+ declare const VERSION: string;
727
+ /** The published npm package name — used by init + the update check. */
728
+ declare const PACKAGE_NAME = "@buoy-gg/mcp";
729
+
694
730
  interface BuildConfigInput {
695
731
  targetRoute: string;
696
732
  bounceRoute?: string;