AI / MCP Server

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).

What your agent can do1 / 8

Races your implementations. Keeps the winner.

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.

connected · iPhone 16 Pro
buoy optimize — find the fastest way to render this 12,000-cell LED grid
run_benchmark_batch8 variants × 3 runs · reload between cases
#1 skia-atlas — 60 UI FPS · 41% CPU · caps nothing
#4 reanimated-cells — ⚠ UI jank: drops frames under load
Skia atlas batching wins on every axis — applied, re-benchmarked, still #1.
Eight of the things an agent does through the Buoy MCP server — against your real running app, not a mock. Pick one, or let them play.

Requirements

  • Buoy Pro — the MCP is a Pro feature. list_devices always works, but data/action tools require a connected app on an active Buoy Pro license.
  • Node.js 18+ on the machine running your editor.
  • A running app with Buoy devtools open on a device or simulator (React Native or Flutter).
  • macOS + Xcode — only for the screenshot_component tool (it captures the iOS Simulator). Everything else is platform-agnostic.

Install

One command wires the server into your editor and installs the Buoy skill:

bash
npx -y @buoy-gg/mcp@latest init
npx -y @buoy-gg/mcp@latest init

This merges a buoy server into your MCP config (.mcp.json for Claude Code, plus .cursor/mcp.json and .vscode/mcp.json when those folders exist) and drops the buoy-optimize skill into .claude/skills/. It's non-destructive — existing servers are preserved, and re-running just refreshes the Buoy entry.

Then restart your editor (or reconnect the MCP server) and open your app with Buoy devtools running:

  • React Native — mount <FloatingDevTools /> (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.
  • Flutter — mount BuoyDevTools (simulators auto-connect; physical devices pass socketUrl: 'http://<lan-ip>:42831').

The config it writes launches the server via npx -y @buoy-gg/mcp@latest, so every editor restart re-resolves the newest published version — you don't get pinned to a stale copy.

Corporate / private npm registries

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 — taking the network off the startup path entirely. You can also force the behavior:

bash
npx -y @buoy-gg/mcp@latest init --local             # always install locally, no network on startup
npx -y @buoy-gg/mcp@latest init --npx               # always use the auto-updating npx entry
npx -y @buoy-gg/mcp@latest init --registry <url>    # registry the local install pulls from
npx -y @buoy-gg/mcp@latest init --local             # always install locally, no network on startup
npx -y @buoy-gg/mcp@latest init --npx               # always use the auto-updating npx entry
npx -y @buoy-gg/mcp@latest init --registry <url>    # registry the local install pulls from

To update a local install later, re-run init.

Updating

Because the default config uses @latest, you're normally always current. To force a refresh (or update the installed skill), re-run:

bash
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.

Manual configuration

If you'd rather wire it by hand, add this to your MCP config:

json
{
  "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.

Usage

Ask your assistant to start with list_devices to see connected devices and the tools each exposes. From there it can:

  • Inspect runtimeget_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).
  • Work a single HTTP requestget_network_requests lists requests with their ids and marks which are pinned/saved; network_action pins or saves one (see below).
  • Take actions — navigate routes, edit storage, and more via call_action or the tool-specific wrappers. React Native also exposes dispatch Redux / set Zustand/Jotai / invalidate React Query when those tools are installed.
  • Drive the UI (React Native)describe_screen and tap_element let the agent read what's on screen and interact with it, no screenshots (see below).
  • Benchmark performance (React Native Bench)run_benchmark_batch and the perf-monitor tools.
  • Profile the JS thread (React Native)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 a component (React Native / iOS Simulator)screenshot_component locates a component by testID/name and returns a tight, cropped image.
  • Reload the app (React Native)reload_app restarts the JS bundle (see below).

A good starting prompt on React Native: "buoy optimize" kicks off a guided performance pass using the bundled skill.

Confirming a fix actually worked

The hard part of letting an agent fix things isn't the fixing — it's that "done" is a claim you can't check without re-testing the app yourself.

Buoy closes that loop. After an edit, the agent re-runs the exact interaction that was broken and reports what changed:

measure_renders — tap "favorite-deal-2" ×5, compareToPrevious: true

vs "before fix"
- total renders: 2414 → 194 (-2220)
- wasted: 1379 → 81 (-1298)
- per component: DealRow 192 → 5 (-187)

✅ Fewer renders than before on this interaction.
measure_renders — tap "favorite-deal-2" ×5, compareToPrevious: true

vs "before fix"
- total renders: 2414 → 194 (-2220)
- wasted: 1379 → 81 (-1298)
- per component: DealRow 192 → 5 (-187)

✅ Fewer renders than before on this interaction.

That catches the two ways a fix normally fails: being incomplete (the value was wrong in three places and one got corrected), and moving the cost instead of removing it — the usual outcome of re-render work, where a screen gets faster to type in and slower to tap.

If the project has changed since anything was last checked on the device, Buoy says so in its tool results, naming the changed files and the call to make. Set BUOY_VERIFY in your MCP config to choose how insistent that is:

ValueBehaviour
auto (default)Mentions it once per burst of edits, then gets out of the way. The agent decides whether a given change is worth a device check.
alwaysRepeats until something is actually checked.
neverSilent.

The reminder never fires when nothing has changed, and never when no device is connected — a doc edit doesn't need a device check, a state-management change does.

Driving the UI (React Native)

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.

Pinned requests as a handoff

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. Because pinned and saved requests (RN · Flutter) keep a full snapshot — surviving Clear, the 500-request cap, and app restarts — this works as a handoff in both directions:

  • You → your agent. Pin the request that's broken, then ask the assistant to look at "the pinned request". get_network_requests({ flagged: "pinned" }) reads exactly what you flagged, even if it happened before the last reload.
  • Your agent → you. An assistant that finds a failing call can pin it, so it's waiting at the top of your Network list when you next open the tool.
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.

Reloading the app (React Native)

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 knows when it's safe to keep going — 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, so it works on any React Native app running Buoy — no particular tool package, and no extra native dependencies.

The buoy-optimize wizard (React Native)

init also installs a buoy-optimize skill — a guided wizard that automates mobile performance work end to end. Whether you're shipping a new feature or fixing a screen that's janky on device, your assistant benchmarks implementation variants on the real device with Bench, reads the ranked results, applies the winning change, and repeats until the metrics plateau.

Measuring on-device instead of guessing is what makes it fast: optimization passes that used to take days or weeks of AI back-and-forth finish in minutes. One real run took a Skia LED display from 28 lights stuttering to over 12,000 lights with no lag.

It's close to fully automated — the parts that stay manual are the ones only a human can judge, like confirming the UI still renders correctly. When you're working with Skia or other GPU-drawn views, plan to glance at the screen each pass: the wizard drives the metrics, you confirm it still looks right. Kick it off with "buoy optimize".

How it works

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.

React Native and Flutter devices appear together in list_devices.

What's Next


FAQ

How do I let Claude Code or Cursor debug my React Native app?

Run npx -y @buoy-gg/mcp@latest init. It merges a buoy server into your MCP config (.mcp.json for Claude Code, plus .cursor/mcp.json and .vscode/mcp.json when those folders exist) and installs the buoy-optimize skill. Restart your editor with the app running and Buoy devtools open, and the agent can read live network, storage, console, routes, and state — and act on them.

Do I need Buoy Pro for the MCP server?

For data and actions, yes. list_devices always works, but the tools that read your app's runtime or drive it require a connected app on an active Buoy Pro license.

Does the MCP server work with Flutter?

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.