
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).
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.
screenshot_component tool (it captures the iOS Simulator). Other host-driven features, such as camera and TV input, also have platform-specific requirements; check their tool pages.Run setup from the app project. Configure the MCP process with your account key through its supported environment configuration; a device key does not sign the MCP process in. 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 /> with your account key (broker address is derived from Metro; physical devices usually need no config). Profiling a release build? Sync is off there unless you opt in — see release builds.BuoyDevTools with your account key (simulators auto-connect; physical devices pass 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; network_action pins or saves one (see below).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_benchmark_batch and the perf-monitor tools.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 locates a component by testID/name and returns a tight, cropped image.reload_app restarts the JS bundle (see below).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.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.
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. Configure account access, app integrations and the broker connection, then inspect list_devices. Configuration alone does not establish a working app connection.
For data and actions, yes. Configure a verified account for the MCP process and the connected app. Reading runtime data and running actions require Pro.
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.