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.
Setup
Install the plugin
claude plugin marketplace add bvandevliet/betaflight-mcp
claude plugin install betaflight-mcp@betaflight-mcpThis 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-readsFound 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.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.
MSP_STATUS. The fault
is this server’s MSP implementation, nothing else. See below.| Call | MSP code | Result |
|---|---|---|
get_version (identity) | — | works |
All CLI text commands (cli_exec, cli_dump, cli_diff, get, set) | — | work |
MSP_STATUS | 101 | times out |
MSP_RAW_IMU | 102 | times out |
| Battery state | 110 | times out |
| Dataflash summary | 72 | times out |
MSP_STATUS_EX | 150 | times 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.
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 channelsA ~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
CLIin 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 adjrange — OSD 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.