Skip to content
Driving Betaflight from Claude Code via MCP

Driving Betaflight from Claude Code via MCP

What this is

bvandevliet/betaflight-mcp is a third-party MCP server that exposes a Betaflight flight controller over USB serial — MSP binary protocol for live data, CLI text for configuration. It auto-generates a get_<name>/set_<name> tool pair for each of roughly 375 CLI variables by parsing the firmware’s own settings.c.

Not my project. It is licensed AGPL-3.0; the Betaflight firmware it talks to is GPL-3.0. Credit and licence terms belong to their respective authors.

Setup

Install the plugin

claude plugin marketplace add bvandevliet/betaflight-mcp
claude plugin install betaflight-mcp@betaflight-mcp

This registers both the MCP server (run via npx, so it always resolves the latest published version) and a bundled PID-tuning skill.

Whitelist the read-only tools

npx -y -p betaflight-mcp betaflight-mcp-whitelist-reads
Found 375 get_* variable tools in variables.js
Added 402 entries to permissions.allow.
Write tools (set_*, cli_save, cli_exec, motor_set, etc.) are NOT whitelisted.
The project README gives this as npx -y betaflight-mcp-whitelist-reads, which fails with a 404. Without -p, npx treats the bare argument as a package name and looks for a published package called betaflight-mcp-whitelist-reads, which does not exist. The -p betaflight-mcp flag names the package that provides the binary.

Writes deliberately stay gated — motor_set can spin props, so it should require explicit approval every time.

Restart and connect

Restart Claude Code so the MCP config loads, then connect by port. The board must not be in CLI or mass-storage mode — see Recovering a Stuck Serial Link.

MSP reads time out over USB

On Betaflight 2026.6.0-alpha (MSP API 1.48), nearly every MSP binary read times out through this server, while the CLI text interface works perfectly.

Corrected twice. This page originally described the timeouts as a firmware-level MSP failure. That was wrong: the Betaflight TX Lua scripts reach the same flight controller over MSP-in-CRSF and the CMS menu renders, which is impossible without working request/response. It was then narrowed to “something in the USB path”. That was also too cautious — a hand-written USB MSP client works fine, including MSP_STATUS. The fault is this server’s MSP implementation, nothing else. See below.
CallMSP codeResult
get_version (identity)works
All CLI text commands (cli_exec, cli_dump, cli_diff, get, set)work
MSP_STATUS101times out
MSP_RAW_IMU102times out
Battery state110times out
Dataflash summary72times out
MSP_STATUS_EX150times out

The failure is consistent and survives reconnection, replugging and a fresh boot, so it is not a transient connection fault. Identity-type calls answer while data calls do not, which points at the server and the firmware disagreeing about payload format for the affected messages.

Operational rule when using this server: treat CLI text as the reliable transport. Anything exposed as a get_* MSP tool should be read with cli_exec "get <name>" instead. Board status comes from status, not get_status; flash usage from flash_info, not get_dataflash_summary; log erasure from flash_erase, not erase_blackbox_logs.

Resolved: it’s the server, not the transport

Raw MSP over USB from a hand-written client works, including the exact message that times out through this server:

MSP_API_VERSION : OK 000130
MSP_STATUS      : OK, 27 bytes     # code 101 — times out via the MCP server
MSP_RC          : OK, 16 channels

A ~40-line client is enough (betamcp/tools/bf_msp.py). MSP v1 framing is $M< + length + code + payload + checksum, where the checksum is the XOR of length, code and payload; responses arrive as $M> with the same shape.

So the boundary is now definite: the firmware is fine, USB is fine, and MSP v1 over USB is fine. The timeouts are specific to this server’s MSP implementation. Reading live RC channel values turned out to be essential for debugging OSD profile switching, and no CLI command exposes them — so a working MSP client is worth having regardless.

Consequences for CLI-first working

Routing everything through CLI text makes the stuck-CLI failure mode much more likely, because every read now enters CLI mode. The discipline that follows:

  • End every interaction with exit.
  • Expect the board to report CLI in its arming disable flags mid-session — that is normal while connected, and clears on reboot.
  • A tool that wraps CLI access may itself get confused by a board already in CLI. If commands start failing with a stale # in the buffer, replug rather than retrying.

Being in CLI disables RC adjustments, so a CLI-first workflow cannot observe them:

if (!cliMode && !(IS_RC_MODE_ACTIVE(BOXPARALYZE) && !ARMING_FLAG(ARMED))) {
    processRcAdjustments(currentControlRateProfile);
}

Anything driven by an adjrangeOSD profiles, rate profiles, PID adjustments — freezes the moment you connect. Testing one by polling over the CLI therefore reports “it isn’t working” no matter how correct the configuration is, because connecting is what stopped it.

Test them by leaving CLI (exit noreboot), letting the switch act, and only then reading the result back — or read live state over MSP, which does not have this problem.

Last updated on