All but React Native are in beta.
All but React Native are in beta.

Drive your Buoy dev tools from an AI coding assistant. The Buoy MCP server lets Claude Code, Cursor, and other MCP clients read your app's live runtime — network requests, storage, routes, console, and framework state — and take actions against your running app.
Works with React Native and Flutter on the same broker. Some capabilities below are React Native–only today (called out inline).
Want your AI to use websites in Chrome instead? That's Buoy Web. It comes in the same package. Set it up with npx -y @buoy-gg/mcp@latest init --web. It's free and needs no account.
The bundled buoy-optimize skill scaffolds variants of a render-heavy feature, benchmarks each one on your real device with Bench, and distills toward the fastest version that still looks right.
buoy login does both. Free works. Pro unlocks every tool (see Free and Pro). Device discovery is still subject to broker admission.A free account gets the basic MCP. Your AI can use the app the way a person does:
describe_screen, native_inspecttap_element, native_tap, native_swipe, native_type, native_pinchrun_flowscreenshot_component, screen_recordingdevice_control, reload_app, navigate, get_routesget_consoleget_network_requests (the list, not the bodies)get_triage (storage key names, not values)buoy_doctor. It reads the Desktop log and versions, and works even before you sign in. In Buoy Desktop, Report a problem copies a prompt that asks your AI to run it.Free also gets Buoy Optimize, with no cap on speed tests or sims. Free shows what is faster. Pro shows why.
run_benchmark_batch, get_batch_report, compare_reports, get_benchmark_settings and the other Bench toolsrun_web_benchmark, find_web_ceiling, screenshot_web_variantssim_farm_* toolget_js_thread_top gives busy % and freezesFree results have fps, CPU, memory, jank, frame gaps, time totals and render totals. They leave out the names: which components re-render and why, which scripts ran in slow frames, and which code used the JS thread. A free result ends with a short note that says what Pro would show for that run.
Pro adds the rest. Your AI can see inside the app and change it:
get_js_thread_origin_detail), measure_renders and Chrome traces (traceDir)get_snapshot, call_action and get_eventsWhen a free app calls a Pro tool, the tool says it needs Pro. Nothing reaches the app.
Every Saturday and Sunday, a free account gets everything Pro does. This is the Weekend Pass.
First, sign in. Run this in your app's folder:
npx --package=@buoy-gg/core buoy login
npx --package=@buoy-gg/core buoy login
Your browser opens buoy.gg. Click Allow. This signs in your computer, and the MCP uses that sign-in. You don't need a key for the MCP. If this computer isn't signed in, the MCP looks for a key in the env files of the folder it runs in. That is the backup. See Sign in with Buoy.
Run setup from the app project. Use a trusted development network for the broker.
Installed Buoy with the agent prompt? Ask the same agent for "the Desktop and MCP step" — the install instructions it followed cover
@buoy-gg/external-syncandnpx @buoy-gg/mcp init.
One command wires the server into your editor and installs the Buoy skill:
npx -y @buoy-gg/mcp@latest init
npx -y @buoy-gg/mcp@latest init
Setup writes .mcp.json and .cursor/mcp.json, and updates .vscode/mcp.json when .vscode exists. Other server entries are preserved; rerunning replaces the Buoy entry, including customizations. It copies the bundled buoy-optimize skill, overwriting matching files on reruns. Save skill customizations before updating. Existing debugging-guide blocks are preserved and need separate review.
Then restart your editor (or reconnect the MCP server) and open your app with Buoy devtools running:
@buoy-gg/external-sync, restart Metro, and mount <FloatingDevTools />. The dev token from buoy login opens it. Buoy gets the broker address from Metro, so a real phone usually finds it on its own. Profiling a release build? Sync is off there unless you opt in. See release builds.BuoyDevTools and run a debug build with --dart-define-from-file=.env.local. That passes in the dev token from buoy login. Sims connect on their own. A real phone passes socketUrl: 'http://<lan-ip>:42831'.The generated npx entry launches @buoy-gg/mcp@latest and may need registry access. Review package updates and verify the connected device after restarting your editor.
If your machine's .npmrc points npm at a private registry that isn't reachable (common on work laptops, e.g. off-VPN), npx @latest would hang trying to download the package on every editor launch. init probes that registry first and, when it's unreachable, automatically installs a pinned local copy from public npm and writes a node <path> config instead . Package download is removed from the local server launch path; account and broker connections can still use the network. You can also force the behavior:
npx -y @buoy-gg/mcp@latest init --local # install a pinned local server
npx -y @buoy-gg/mcp@latest init --npx # launch through npx @latest
npx -y @buoy-gg/mcp@latest init --registry <url> # registry the local install pulls from
npx -y @buoy-gg/mcp@latest init --local # install a pinned local server
npx -y @buoy-gg/mcp@latest init --npx # launch through npx @latest
npx -y @buoy-gg/mcp@latest init --registry <url> # registry the local install pulls from
To update the local server, run npx -y @buoy-gg/mcp@latest init --local.
To refresh configuration, rerun setup. Matching skill files are overwritten, while existing debugging blocks are preserved. Save skill customizations and review debugging-guide updates separately:
npx -y @buoy-gg/mcp@latest init
npx -y @buoy-gg/mcp@latest init
The server also checks npm about once a day and, when a newer version is out, includes a one-line notice in the list_devices result so your assistant can prompt you to update.
If you'd rather wire it by hand, add this to your MCP config:
{
"mcpServers": {
"buoy": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@buoy-gg/mcp@latest"],
"env": { "BUOY_VERIFY": "auto" }
}
}
}
{
"mcpServers": {
"buoy": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@buoy-gg/mcp@latest"],
"env": { "BUOY_VERIFY": "auto" }
}
}
}
BUOY_VERIFY controls the reminder to check a change on the device before
calling it fixed — see Confirming a fix actually worked.
Ask your assistant to start with list_devices to see connected devices and the tools each exposes. From there it can:
get_events (network, state changes, route changes, storage writes, …), get_snapshot, and per-tool readers. Available sources depend on which packages the app installed (Flutter includes Riverpod; React Native includes Redux/Zustand/Jotai/React Query/renders when those packages are present).get_network_requests lists requests with their ids and marks which are pinned/saved. get_network_request reads one in full: status, headers and the whole body. network_action pins or saves one (see below).get_sockets lists each socket and its messages. It also lists the problems Buoy found, like a request with no reply. socket_action pushes a fake server message into the app. It can also drop, freeze or block the socket. Then the agent sees what the app does. See Sockets.network_conditions applies offline or added latency to the whole device; response overrides cover one specific URL (see below).call_action or the tool-specific wrappers. React Native also exposes dispatch Redux / set Zustand/Jotai / invalidate React Query when those tools are installed.describe_screen and tap_element let the agent read what's on screen and interact with it, no screenshots (see below). run_flow runs several steps in one call. Pass save to keep a passing flow in .buoy/flows, and flow to replay it later. A flow tap with no app handler, like a native tab, gets a real touch. Pass real: true to make every tap a real touch. A swipe step with untilEnd: true scrolls to the end of a list. For real touches, native_tap, native_type and native_swipe press native-only controls (like native tab bars), type on the keyboard and swipe. They work on the iOS Simulator, a physical iPhone and Android. On iOS, Buoy taps the way Xcode does. Old Xcode uses idb instead. Android needs adb. A real tap won't go through a Buoy panel by mistake. If Buoy's panel covers the spot, the tap stops and tells you. list_devices shows where each app runs and which of these work there.run_benchmark_batch and the perf-monitor tools.run_web_benchmark opens your running app in its own headless Chromium. Each run gets a fresh page. It can slow the CPU 4x or 6x. It counts the frames Chrome really showed and dropped. It reports frame gaps, long tasks and the scripts behind slow frames. It also reports Chrome's own script, style and layout time. It can tap, type and scroll during each run and time the response. The app needs no Buoy setup. The project needs Playwright (npm i -D playwright && npx playwright install chromium).get_js_thread_top returns a live "Task Manager" of which timers, Promise chains & callbacks eat JS-thread time (with freeze attribution); get_js_thread_origin_detail drills into one origin's scheduling site and stats.screenshot_component with no query takes the whole screen, on the iOS Simulator or Android. The picture is made small so it costs few tokens. Give it a testID or name to crop one component instead (React Native, iOS Simulator).screenshot_component a baseline name. The first call saves the screen in .buoy/baselines. Later calls compare against it. You get the share of the screen that changed, the components that changed (by name), the text that changed, appeared or went away, and a picture with the changes in red. The status bar, the Dynamic Island and Buoy's own overlay are left out. If the app has the Clock tool, its clock is frozen at the baseline's moment while the picture is taken, so countdowns don't count as changes. Pass ignore with testIDs (or ignoreAreas) for parts that change on their own. In run_flow, a snapshot step does the same check and fails the flow when the screen changed, so a saved flow guards its screens.label needs a simulator.native_inspect lists every native element on screen. That includes native tab bars, alerts and system sheets that describe_screen can't see. Give it x and y to see what a tap there would hit. You see the native view and your app's React view. It also says if a Buoy panel is in the way. It works on the iOS Simulator and Android. On a physical iPhone it shows your app's side only.native_pinch uses two real fingers. Use it to zoom a map or photo in or out, or to turn it. After a map pinch, Buoy asks the map how far it turned and zoomed. For other views, it says how much changed and shows it. It works on the iOS Simulator and a physical iPhone. Android can't send two fingers yet.native_tap takes holdMs for a long press. native_swipe takes drag, or untilEnd to scroll to the end of a list and name its last row. device_control rotates the device, shakes it (iOS Simulator and Android emulator), puts text on the clipboard so you can paste it for real, and passes or fails Face ID, Touch ID or a fingerprint.{{secret:NAME}} in text you type. Buoy types the value of BUOY_SECRET_NAME from the MCP server's environment (set it in your editor's MCP config; at least 6 characters). Buoy hides the value in what it sends back, blacks out any field that shows it in pictures, and keeps it out of saved recordings. Typing a secret waits until no recording is running, because a video can't hide it. Buoy Web's type tool works the same way (see Buoy Web).get_component_source shows where a component on screen is used and where its own code starts, with its parent components. It needs a dev build and @buoy-gg/highlight-updates.npx @buoy-gg/mcp flow run replays every saved flow in .buoy/flows (or the ones you name) against a running app and exits with code 1 when one fails. Snapshot steps fail it too. Add --update-snapshots to accept new screens.screen_recording with start, then stop. You get an MP4 and a viewer page in .buoy/recordings. The viewer plays the video next to what the app did at each moment: web calls, query fetches, screen changes, store writes and console errors. Click an event to jump the video there. Each real touch shows as a dot on the video. With ffmpeg, you also get a copy of the MP4 with the dots drawn in. Share that one where the viewer page can't go. Pass steps with start to record a set of taps in one call: Buoy records, runs them, then stops and saves. It works on the iOS Simulator and Android, for up to 3 minutes. Recordings are kept out of git, and secrets in logs are hidden.lifecycle_action can quit the app and launch it again, even while it is closed. reinstall puts a fresh build on with no app data, like a new user. On iOS it keeps your Buoy sign-in. boot_simulator starts an iOS Simulator.reload_app restarts the JS bundle (see below). If the app is not connected, it asks the dev server to reload it. If the app had closed, it opens it again.A good starting prompt on React Native: "buoy optimize" starts a guided performance pass with the bundled skill. See Buoy Optimize.
After changing app code, repeat the interaction that exposed the problem. Record the device, build, interaction, and observed result. A successful tool call is not proof that the user-visible problem is fixed.
For render work, compare the same interaction before and after with measure_renders. Check both the metrics and the UI, and repeat other affected interactions to catch regressions.
BUOY_VERIFY controls reminders based on selected source-file edits and tool calls. The watcher does not detect every kind of project change, and clearing a reminder does not prove correctness.
| Value | Behaviour |
|---|---|
auto (default) | Reminds once per observed edit burst. |
always | Repeats while observed edits remain unaccounted for by the tool-call policy. |
never | Disables reminders. |
Report actual verification separately. Documentation-only edits generally need static checks; behavior changes need suitable behavioral evidence.
Two tools let your agent operate a React Native app the way a user would — without screenshots or pixel coordinates, so it's fast and works on physical devices too:
describe_screen walks the live React fiber tree and returns the on-screen elements as a compact, accessibility-style list — each with its label/text, testID, a normalized tap point, and (for controls) its type and current value, e.g. Switch {toggle=true}, Slider {slider=75}, TextInput {text="Ada"}. Inactive/covered navigator screens are pruned, so you see what's actually in front of the user. When text on screen matches app state, the row says where it lives. For example, Text "1,337" = live state: member store, member.crowns. It works with Zustand, Redux and Jotai.tap_element interacts with an element found by testID, nativeTag, or a fuzzy query. It invokes the element's handler directly in JS: tap a button (onPress), flip a switch or move a slider (value), or type into a field (text). Off-screen targets are scrolled into view first.A typical loop is: describe_screen to see the options → tap_element({ testID }) to act → describe_screen again to confirm the result. Prefer testID or nativeTag when a screen has repeated labels.
get_events is the right tool for skimming activity, but it deliberately emits no request ids — so it can't be used to act on a specific call. get_network_requests covers that: the same compact one-line-per-request style, but each row leads with the id that every network action is keyed by, and marks which requests are 📌 pinned or 🔖 saved. Narrow it with status: "errors", a URL pattern, or includeBodies when you need payloads. List bodies are cut short, so read one request in full with get_network_request({ id }).
network_action pins, saves, or clears. Pinned and saved requests (RN · Flutter) keep a snapshot subject to storage and body-size limits. They are separate from the live list, and availability after restart depends on successful persistence. Use retained records for handoffs:
get_network_requests({ flagged: "pinned" }) reads available retained records, including earlier sessions when persistence succeeded.get_network_requests({ status: "errors" }) → 3. `fetch_1021` ❌ 500 POST /api/checkout …
network_action({ action: "pin", id: "fetch_1021" })
get_network_requests({ status: "errors" }) → 3. `fetch_1021` ❌ 500 POST /api/checkout …
network_action({ action: "pin", id: "fetch_1021" })
Requests kept from an earlier run of the app come back with ids prefixed saved: — they're snapshots, not live requests, so they can't collide with a fresh capture.
network_conditions reads or sets the condition the device applies to new requests: normal, offline, slow (+500 ms) or verySlow (+2000 ms). Offline rejects intercepted HTTP(S) calls before they are sent, so an agent can walk your app's error and retry paths without a proxy and without touching the server. Latency adds one wait before dispatch; it does not cap bandwidth or change what NetInfo reports.
network_conditions({ action: "set", profile: "offline" })
tap_element({ testID: "checkout-submit" })
describe_screen() → the error state the app actually renders
network_conditions({ action: "set", profile: "normal" })
network_conditions({ action: "set", profile: "offline" })
tap_element({ testID: "checkout-submit" })
describe_screen() → the error state the app actually renders
network_conditions({ action: "set", profile: "normal" })
The condition lives in memory on the device. It resets on a full JS reload and is never persisted, so an agent that sets one should clear it when it's done rather than leave the next session offline. Setting a condition needs a development build and an admitted Free or Pro account; a release build refuses anything except normal. This control is a development preview, and native transports that bypass global fetch and React Native XHR are not affected by it.
reload_app restarts the app's JS bundle from your editor — the same thing as shaking the device and hitting Reload, and the same primitive Bench uses between benchmark cases. Use it when Fast Refresh didn't pick a change up, to clear leaked in-memory state before a measurement, or to re-run app startup.
In dev builds (including Expo Go and RN CLI) it uses React Native's DevSettings.reload(); otherwise it falls back to expo-updates, if your app installs it. By default the tool waits for the app to come back and reports how long the reload took, so your assistant can inspect the reconnection result — pass wait: false for fire-and-forget. All in-memory state is lost, so anything the assistant read before the reload is stale.
It ships with @buoy-gg/core itself, with supported app reload mechanisms. A failed or timed-out reload requires checking the app and connection.
For a new skill installation, init adds a workflow for investigating rendering performance with Bench and render measurements. Rerunning setup overwrites matching skill files; save customizations first. Ask your assistant for "buoy optimize" to compare a baseline and selected variants on the target device. Buoy Optimize walks through a round, with a film and a worked example.
Keep device, build mode, workload and interaction comparable. Review failures, missing metrics and visual behavior before selecting a change. Simulator measurements do not establish results on physical hardware, and a ranking is not proof that the feature works correctly.
Buoy tools run inside your app and sync to a local broker over the external-sync protocol. The MCP server connects to that broker as a "Dashboard" client — the same role the Buoy desktop app plays — or spawns its own broker in-process when no desktop app is running, so it works standalone. It proves it's a dashboard with the token the broker writes to ~/.buoy/broker-token, so it must run as the same user as Buoy Desktop. An MCP version older than Desktop's broker is refused; update both together.
React Native and Flutter devices appear together in list_devices.
Run npx -y @buoy-gg/mcp@latest init from the app project, review the configuration changes described above, and restart the editor. Sign in with npx --package=@buoy-gg/core buoy login. Then set up app integrations and the broker connection, and inspect list_devices. Configuration alone does not establish a working app connection.
No. A free account gets the basic MCP: reading the screen, taps, flows, screenshots, logs and the web call list. It also gets Buoy Optimize and the Sim Farm, without the names that say why. Pro adds state, storage values, request bodies, mocks and the other tools. See Free and Pro. The MCP process and the app both need a Buoy account.
Yes — React Native and Flutter apps connect to the same broker and the same server. A few capabilities are React Native–only today, and those are called out inline in the docs.
memory_check starts or stops a route loop. get_memory_leaks reads the report. Checks need Pro and a native dev build. Start returns a runId at once. Poll for suspects and byte growth. Missing reads stay null. An empty list without GC proof does not prove clean. See Memory Leaks for fields and setup.