Skip to main content
Glama

OpenBaud

CI Release License: MIT

Turn serial hardware into reusable, auditable tools for coding agents.

OpenBaud gives Codex structured access to local serial devices. Inside an agent task, it makes hardware actions visible and auditable, returns structured results instead of byte dumps, and turns verified exploration into durable tools. It combines an MCP server with device profiles, typed commands, workflows, lossless captures, byte-exact replay, and a hardware-exploration skill.

A coding agent uses OpenBaud to identify a real ESP32, capture and validate its protocol, create a reusable command, and replay it without hardware

Watch the 46-second PV · Try without hardware · Install for Codex

Why OpenBaud

A serial terminal can send bytes. An ad-hoc script can make one device work. OpenBaud keeps the evidence that makes the result repeatable:

  • Explore safely: identify ports by USB metadata, frame responses, audit writes, and require explicit acknowledgement for dangerous commands.

  • Sediment knowledge: turn verified behavior into typed, reviewable YAML commands and fixed workflows with provenance.

  • Parse real protocols: validate checksums and decode scalars, bit fields, split lists, and record arrays into structured results — including data a device pushes on its own, checked frame by frame.

  • Refuse what would be wrong: a command declares the device state it depends on (requires), and is refused before a byte is sent when the session has not established that state — instead of returning a valid frame that means something else.

  • Replay without hardware: record every RX/TX byte in .obcap, then rerun the same command and parser against the capture.

serial device → lossless capture → checksum → typed parse → command → replay

Related MCP server: sbl-probe

Real ESP32 demo

The product video uses a physical ESP32-S3 development board running a new binary protocol, OBP/1. OpenBaud recorded eight hardware exchanges and decoded every 196-byte response into 36 polar points with CRC-16 verification.

The USB transport, board timestamps, frames, captures, and parsing are real. The radar scene is generated by the test firmware, and that fact is declared on the wire as simulated_scene: 1.

Run the exact parser against the included real-device capture:

cargo run --locked -p openbaud -- run openbaud-pv-board/obp1_radar_scan \
  --workspace examples/pv-demo \
  --port replay:captures/obp1-radar-seq42.obcap \
  --set seq=42

Expected result: outcome: normal, seq: 42, point_count: 36, and 36 decoded {angle_deg, distance_mm, intensity} records. The example workspace includes the command, protocol layout, and lossless capture.

Install the Codex plugin

Register this Git repository as the openbaud-marketplace marketplace and install its plugin:

codex plugin marketplace add Leonezz/openbaud --ref stable
codex plugin add openbaud@openbaud-marketplace

Start a new Codex task, connect a serial device, then ask:

Use OpenBaud to identify this serial device. Start read-only, preserve a
capture, and turn verified behavior into reusable commands.

The stable marketplace plugin bundles all five native MCP runtimes and selects the matching one locally at startup. Users need Node.js 18 or newer for the portable launcher, but do not need Rust, Cargo, Homebrew, an install hook, or a runtime download. The supported hosts are macOS ARM64/Intel, Linux x64/ARM64, and Windows x64. macOS binaries are ad-hoc signed for preview distribution but are not yet Apple-notarized.

The stable branch advances only after every native package and the aggregated plugin pass their smoke tests. To refresh it immediately, run:

codex plugin marketplace upgrade openbaud-marketplace

Start a new task after an update. For an immutable installation, download the versioned universal or host-specific marketplace archive from GitHub Releases and register its extracted local path.

See plugins/openbaud/README.md for platform archives, upgrade instructions, packaging details, and smoke tests.

Knowledge format

An OpenBaud workspace keeps device knowledge next to the project:

devices/<device>/profile.yaml
devices/<device>/commands/*.yaml
devices/<device>/workflows/*.yaml
devices/<device>/notes.md
captures/*.obcap
.openbaud/audit.jsonl
reports/    # derived analysis for people, citing the captures it came from
scripts/    # one-off analysis helpers; device behavior belongs in commands

The schema MCP tool and openbaud schema CLI command are the authority for the current profile, command, and workflow formats.

Build and verify

cargo test --workspace
python3 scripts/validate-package.py
python3 -m unittest scripts/test_package_plugin.py
node --test scripts/test-plugin-launcher.mjs
plugins/openbaud/scripts/build-runtime
plugins/openbaud/scripts/smoke-test

GitHub Actions runs Clippy, package validation, native builds, tests, archive verification, and real MCP smoke tests on macOS ARM64/Intel, Linux x64/ARM64, and Windows x64. Stable vMAJOR.MINOR.PATCH tags publish all five platform archives, one universal marketplace archive, and SHA-256 files. The release workflow then advances the stable marketplace branch to that verified universal bundle.

The Remotion source and real capture used to produce the public video live in media/pv. Regenerate its data with pnpm extract, preview with pnpm studio, and render with pnpm render.

Safety

Serial writes can reconfigure or actuate physical hardware. OpenBaud marks raw writes and device commands as potentially destructive MCP operations, keeps an append-only audit, and requires explicit acknowledgement for commands declared with risk: danger. Commands that declare requires are refused until the session has established that device state; overriding with assume_preconditions is recorded in the result and the audit. Start unknown devices read-only and preserve a recovery path before changing state.

License

MIT

Available Tools

18 tools
ask_portA
Read-only

Ask the user which serial port to use, on hosts that can render it (elsewhere the candidate list comes back as text for you to relay). Use it when several ports could plausibly be the device and the choice is the user's to make — not for your own discovery, which is what list_ports is for. This does not touch hardware: the user's choice arrives in your context, and you then call open yourself.

Inputs: device, reason.

capture_framesA
Read-only

Re-frame a recorded capture's byte stream: tx and rx each run through a deframer and come back as timestamped frames, paginated by cursor/max_frames. Framing comes from the explicit framing object or the named device's profile — one of the two is required, nothing is assumed. Idle-gap framing is driven by the recorded timestamps, so boundaries match what a live session would have produced. Reads captures/.obcap — no hardware. The result declares view:{kind:"capture"} for show_result.

Inputs: path (required), to_ms, cursor, device, +4 more.

capture_startA

Start lossless timestamped capture of all rx/tx on a session into captures/*.obcap (JSONL). Returns the workspace-relative path (captures/ — pass it verbatim to capture_frames, session_timeline, or replay:) plus abs_path.

Inputs: session_id (required), note.

capture_stopA

Stop the active capture on a session; returns stats plus the workspace-relative path (captures/ — directly usable with capture_frames, session_timeline, and replay:) and abs_path.

Inputs: session_id (required).

closeB

Close a session and release the port.

Inputs: session_id (required).

diagnose_frameA
Read-only

Diagnose one raw frame, pure computation — no hardware, no audit entry. Every checksum algorithm x value encoding is verified at the default tail position (checksum_matrix); each row's at is the byte offset where the stored checksum starts (frame_len - stored_len; stored_len is the algorithm's byte count for raw, twice that for ascii_hex), and a row whose algorithm cannot apply to this frame carries only kind/encoding/error, no at. With expected:{device,command} the command's response.parse is also attempted at byte offsets -2..=+2 (parse_attempts) to expose off-by-N framing — parsed: true only means the bytes are structurally decodable at that offset; the offsets are mutually exclusive hypotheses, so judge which (if any) is the real alignment yourself. The result declares view:{kind:"diagnostics"} for show_result.

Inputs: hex (required), expected.

list_portsA
Read-only

List serial ports (USB metadata included) plus the always-available mock:echo loopback.

Inputs: no named parameters.

openA

Open a serial port session. If device names a workspace device, its profile supplies transport defaults and framing; explicit arguments override. port may be omitted when the device profile declares a selector (vid/pid/serial_number/product) — exactly one live match is required. replay:<capture path> (relative to the workspace) replays a recorded .obcap. Returns a session_id.

Inputs: baud, port, device, parity, +3 more.

readA
Read-only

Cursor-based framed read: returns frames received since your previous read (waits up to timeout_ms for the first frame). Reports dropped_bytes if the buffer overflowed.

Inputs: session_id (required), max_frames, timeout_ms, max_inline_bytes.

requestB
Destructive

Send bytes and await the response (audited). match decides when the response is complete: {length:N} | {delimiter:"..."} | {idle_ms:N}; default is a 50 ms idle gap.

Inputs: session_id (required), hex, text, match, +2 more.

run_commandA
Destructive

Execute a named command from devices//commands/ with typed params: builds the frame, awaits the response and classifies the outcome (normal | exception | silence | timeout | checksum_error | malformed) against the command's declared expect (audited; an unmet expectation is an error carrying the full result JSON). Give session_id for an open session, or port to open an ephemeral one with the device's profile transport — port may be omitted when the profile declares a selector, and accepts replay:. Commands with risk=danger additionally require acknowledge_risk=true.

Inputs: device (required), command (required), port, params, +3 more.

run_workflowA
Destructive

Execute a workflow from devices//workflows/: its steps run in order on one session (step params override command defaults); the first failing step skips the rest, then every finally step is attempted regardless. Returns {ok, steps, finally, skipped}; ok=false is an error carrying the full result JSON. Workflow risk is the maximum of its commands' risk — danger requires acknowledge_risk=true. Session resolution is the same as run_command (session_id, port, or profile selector).

Inputs: device (required), workflow (required), port, session_id, +2 more.

schemaA
Read-only

The authoritative JSON Schema (or, with example=true, an annotated YAML example) of the openbaud knowledge formats — profile, command, workflow. Call this before writing or modifying any YAML under devices/: it is generated from the exact types that parse those files and includes the semantic rules the schema grammar cannot express.

Inputs: kind (required), example.

sendB
Destructive

Write raw bytes (audited). Provide exactly one of hex ('01 A0 FF') or text (control chars as-is, e.g. "AT\r\n").

Inputs: session_id (required), hex, text.

session_statsA
Read-only

Live counters for open sessions: port, framing, opened_ms, buffered/dropped bytes, chunks seen, rx/tx byte totals, last_rx_ms, the transport settings the port was opened with, the active capture (path and live counters), and the session error when the port died. Pass session_id for one session; default is all.

Inputs: session_id.

session_timelineA
Read-only

Lay a recorded capture out as a session timeline: rx/tx byte density folded into buckets, plus the capture session's audited operations (commands, raw writes, workflow steps, denials) as events. Reads captures/.obcap and .openbaud/audit.jsonl — no hardware. The result declares view:{kind:"timeline"}, so its full_result path (or the result itself) renders via show_result.

Inputs: path (required), to_ms, buckets, from_ms, +1 more.

show_resultA
Read-only

Show a saved tool result to the user, rendered (a polar plot for angular scans, a field table otherwise) on hosts that support it. Pass path from an earlier result's full_result — the viewer then reads the complete payload directly, so the full data never enters your context. A result is charted only when it declares how (the command's view, or an encoding you pass here); otherwise it renders as a field table. Use it when the user would rather see the data than read it.

Inputs: data, path, encoding.

stream_pollA
Read-only

Per-consumer incremental frame poll on an open session — a private cursor with its own deframer, fully independent of read (the two never steal frames from each other). Call with session_id to create a subscription: the result carries its subscription_id plus any frames already buffered. Call with that subscription_id to fetch what arrived since; frames carry a monotonic per-subscription seq, and next_seq is the seq the next frame will get. since_seq acknowledges delivery: frames with seq < since_seq are released — ack with the last delivered frame's seq + 1, not next_seq (next_seq can run ahead of a max_frames-capped page, and acknowledging frames never delivered is a loud error). Each page also stops at max_inline_bytes of rendered payload (default 4096, metered as each frame's hex length + text length; page_bytes reports the page's actual total) — whole frames only, in seq order; a first frame alone beyond the budget is still delivered, alone, with oversized_frame: true, so delivery always makes progress. Omit since_seq and the unacknowledged frames are redelivered unchanged (idempotent re-read, so a lost response costs nothing). A consumer that lags loses the oldest retained frames beyond 1024, counted cumulatively in dropped_frames; a subscription the session's byte buffer overflowed past counts the gaps in dropped_chunks — never silently. close: true releases the subscription (result carries closed: true). At most 8 subscriptions per session; ones idle beyond 120 s are swept by later stream_poll calls, and closing the session releases them all. Every result folds in the session's live counters as stats. Returns immediately with whatever is available — never waits; a poll with nothing left to deliver on a dead port errors loudly instead of looking healthy. Optionally pass parse: {device, command} when CREATING a subscription (never on a follow-up poll — that is a loud error) to parse every frame server-side with that workspace command's response.parse: each frame then carries parsed (the field values) or parse_error (the per-frame reason; one bad frame never stops the stream), parsed once at arrival so redelivery repeats the identical outcome, and every result echoes parse: {device, command} plus the command's units (same semantics as run_command). A device or command that does not exist, or a command without a response.parse block, fails subscription creation loudly.

Inputs: close, parse, since_seq, max_frames, +3 more.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.9/5.0

    Scored across 18 tools

    Disambiguation4/5

    Descriptions go out of their way to delineate boundaries (send=raw fire-and-forget, request=send+await, run_command=named typed command; read=cursor read, stream_poll=independent per-consumer subscription; ask_port 'not for discovery, that's list_ports'). This is unusually explicit, though three byte-sending tools and two frame-reading tools still require careful reading to pick correctly.

    Naming Consistency4/5

    Most names are a clean verb_noun or noun_noun pattern (list_ports, run_command, run_workflow, capture_start, session_timeline, diagnose_frame, show_result, stream_poll), and the capture_*/session_* prefixes group cleanly. A handful of bare verbs (open, close, read, send, request) deviate slightly, but the overall scheme stays predictable.

    Tool Count4/5

    18 tools is slightly heavy but justified by the breadth of a serial-toolkit domain: discovery, session lifecycle, three send modes, capture, re-framing, diagnostics, stats, streaming, plus schema and viewer helpers. Each tool maps to a distinct capability rather than being redundant.

    Completeness5/5

    The surface covers the full lifecycle: discovery (list_ports), open/close, multiple send/execute modes (send, request, run_command, run_workflow), lossless capture (capture_start/stop) with offline analysis (capture_frames, session_timeline, diagnose_frame), live stats and streaming, plus schema and result rendering. No obvious dead ends for the stated purpose.

    Maintenance

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      B
      maintenance
      MCP server that lets LLMs talk to serial devices: microcontrollers, routers, modems, embedded Linux, anything with a UART.
      24
      61 PyPI
      7
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Serial communication and protocol analysis MCP server that gives AI coding assistants direct access to serial ports for reading, writing, decoding, and capturing embedded device output.
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      Enables AI agents to interact with physical embedded target boards over a dual-CDC USB bridge using MCP tools. It exposes console reading, pattern waiting, command sending, reboot/login state machines, and in-RAM MicroPython execution while a single-owner daemon safely arbitrates access to the serial devices.
      6
      4
      Apache 2.0
    • A
      license
      Not graded
      quality
      C
      maintenance
      Lets any MCP-capable AI and a human share a single serial port through one resident daemon, so the AI can read device output, send commands, and drive embedded boards alongside an engineer. It also performs atomic expect→send matching to win sub-second races such as U-Boot countdowns and login prompts, while keeping reads bounded for context hygiene.
      2
      MIT