MCP server giving Claude Code direct BLE control of Battle Company BRX taggers
brx-mcp demonstrates solid tool design with clear, domain-specific naming and comprehensive descriptions. All 15 tools have explicit descriptions (avg ~150 chars) and input schemas with typed parameters. Naming follows verb_noun convention (scan, identify, connect, send). However, output schemas are not documented in the visible code, and error handling lacks recovery guidance. Tool composition is excellent, each tool has a single responsibility and outputs chain well (e.g., scan → identify → connect → send). No security issues detected (no credentials in params). The server targets a specialized hardware domain (BLE taggers) with appropriate abstractions.
Open a persistent BLE session to a tagger and start buffering its messages. All other tools refer to the session by this alias (e.g. 'tagger1'). Multiple simultaneous connections are supported.
One-shot BLE health sweep of one tagger: firmware version ($VERSION), ping latency, and battery ($VOLTS). Connects, reads, disconnects. Never raises for an unreachable tagger — reports it in the record.
Token-level diff of two session captures (paths or bare labels). Use to crack field meanings: capture the official app setting damage=24, then damage=38, and diff to see which $WEAP token changed.
Close the BLE session for an alias.
Armory dashboard: scan for BRX taggers (or use the given addresses), then run the diagnostics sweep on each (serially — one radio). Returns the scan count plus a per-tagger record (firmware, battery, latency, RSSI).
Drain buffered tagger→host messages newer than since_seq. Each event: {seq, t_ms, direction, raw, parsed} — parsed decodes $HIR (shooter id/team), $HP (health/death), $BUT (button).
Output schemas not documented. Tools return structured data (e.g., scan returns device records with has_uart_service, diagnostics returns firmware/battery/latency) but no schema definitions visible in code. LLMs cannot plan downstream calls without knowing response structure.
Error handling lacks recovery guidance. Tools like send() and send_batch() can fail (e.g., unreachable device, invalid command) but descriptions do not explain what the LLM should do next (retry, check connection, validate command syntax).
send() and send_batch() accept free-form command strings without validation hints. Descriptions mention 'known-safe list' and 'denied commands' but do not enumerate valid commands or provide format constraints. LLMs may hallucinate invalid commands.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 63 | 2026-07-28+ | v2 |
Connect briefly to a BLE address, send $PING,*, and report: reachable?, PONG latency, inferred generation. Saves the result to the device registry.
List open sessions: alias, address, connected, buffer depth, last-seen.
Return a tagger to a sane state: sends $CLEAR,* then $SP,99,*. (Power-cycling the tagger always restores it fully — stock firmware is never modified.)
Parse a Teensy USB `QUERY` console dump (paste the text) into a device record: serial/head PIN, versions, voltages, NRF radio flags, PCB rev, tested-by. The richest per-tagger diagnostic (bench, USB not BLE).
BLE scan for nearby devices. BRX Gen2/3 taggers advertise the Nordic UART service and are flagged with has_uart_service=true and sorted first. A tagger that never appears here is likely Gen1 (Bluetooth Classic).
Send one raw framed command (e.g. '$GLED,1,1,1,0,10,,*') and return any replies arriving within reply_window_ms. Commands outside the known-safe list from brx-protocol.md require confirm=true. Denied commands (persistent state, pairing, DFU, factory tests) are refused whatever you pass; a hang-prone one ($DPLAY) goes only with confirm=true AND allow_hang=true, for a supervised bench run that wants a lock-up on demand.
Send an ordered command sequence with pacing between frames (config sequences like CLEAR→START→GSET→PSET→SIR×n→BMAP need pacing so the tagger isn't overrun). Same safety rules as send().
Timestamped capture of ALL traffic both directions to a JSONL file. action: 'start' (begin logging to captures/<label>.jsonl), 'stop', or 'dump' (return the file path + line count).
Block until a tagger message starting with prefix (e.g. '$HIR', '$BUT') arrives, or time out. The workhorse for human-in-the-loop experiments: send a config, then wait_for '$BUT' while the user pulls the trigger.
wait_for() timeout behavior not specified. Description says 'time out' but does not clarify: does it return null, an error, or a timeout exception? How should the LLM interpret a timeout vs. no matching message?
session_log() action parameter lacks enum constraint. Description lists 'start', 'stop', 'dump' but parameter definition does not enforce enum. LLMs may pass invalid actions.