MohibShaikh

hwcontract

Community MohibShaikh
Updated

hwcontract

A zero-dependency MCP server that judges hardware against a contract. Codingagents (Claude Code, Codex, opencode) write firmware that's correct on paper butwrong on the wire — a WS2812 pulse 180ns short, an ESC bit out of spec, a boot logthat silently panics. This closes the loop: it captures what the hardware actuallydid and returns pass / marginal / fail the agent can iterate on.

marginal is the valuable verdict — in-spec but low-headroom, the bug that works onyour bench and fails on a cold board in the field.

Demo — a real capture, no hardware needed

python3 demo/ws2812b_neopixel.py downloads a real 24-LED NeoPixel capture(recorded off hardware by the sigrok project, 24 MHz), extracts the data line, andjudges it against two contracts:

measured on the real WS2812B signal (300000 samples @24MHz):
  T0H 333 ns   T1H 833 ns   T1L 417 ns   T0L 917 ns   RESET 992250 ns

=== generic WS2812 contract -> FAIL ===
  T1L   600   417   FAIL   183ns short (typ 600)      # real WS2812B low times are shorter
  T0L   800   917   marginal
  T1H   700   833   marginal

=== matching WS2812B contract -> PASS ===
  (all pass)

Same real signal: it fails the generic WS2812 contract (correctly — a WS2812Bisn't a WS2812) and passes the matching WS2812B one. That's the tool doing itsjob: measure the real signal, hold it to a spec, and make contracts chip-specific.

How it fits together

  observers (capture)                judge (this repo)
  ─────────────────────              ─────────────────
  logic analyzer  ─ pulse widths ─┐
  serial port     ─ log text ─────┼─►  contract × observation  ─►  pass/marginal/fail
                                  ┘         (judge.py)
  • judge.py — the pure judge (timing + serial). No hardware, no framework, cached.
  • sigrok_adapter.py — logic-analyzer capture → pulse-width observations (WS2812/DShot).
  • serial_adapter.py — serial log capture (or replay a saved log).
  • server.py — the MCP server (stdio JSON-RPC, stdlib only).
  • *.contract.yaml — what "correct" looks like. Human-editable. Also serve as regression tests.

Install

pip install hwcontract              # judge + logic-analyzer adapter
pip install "hwcontract[serial]"    # + live serial capture (pyserial)
pip install "hwcontract[untrusted]" # + google-re2 (ReDoS-immune, for untrusted contracts)
pip install "hwcontract[all]"       # everything

Also needs sigrok-cli on PATH for live logic-analyzer capture (check_ws2812 /check_dshot). Judge-only tools (judge_contract, judge_serial) need nothing extra.

Wire it into an agent

One stanza per client (not auto-discovered — add it once). After pip install, thehwcontract command is on your PATH.

Claude Code

claude mcp add hwcontract -- hwcontract

Codex CLI~/.codex/config.toml

[mcp_servers.hwcontract]
command = "hwcontract"

opencode / Cursor / Gemini / any stdio MCP client

{ "mcpServers": { "hwcontract": { "command": "hwcontract" } } }

Transport is stdio by default (local, no auth surface). For remote-only clients(e.g. ChatGPT connectors), run hwcontract --http 8791 and expose it via a tunnelwith HWCONTRACT_TOKEN set for bearer auth.

If the client can't find hwcontract (PATH issues)

GUI apps and some agents don't inherit your shell PATH, so a bare hwcontractcan fail with "command not found". Two robust fixes:

  • Use the absolute path: which hwcontract → put that full path in command.
  • Or invoke via Python (no PATH lookup for the script): command: "python3",args: ["-m", "hwcontract.server"] — works from any directory once installed.

Contract paths: pass an absolute contract_path, or set HWCONTRACT_ROOTto your contracts folder — relative paths resolve against it (default: the process'sworking directory, which the client controls and may not be your project). Pathsoutside the root are rejected. Bundled examples install with the package underhwcontract/examples/.

Tools

Tool Hardware? What it does
judge_contract no Judge given observations against a timing contract. Replay / testing.
judge_serial no Judge a given log string against a serial contract's expect/forbid.
check_ws2812 yes Capture a live WS2812 line and judge it, one call.
check_dshot yes Same, for a DShot600 ESC signal.
capture_ws2812 yes Just capture → observations (no judging).
check_serial yes Read a serial port for N seconds and judge the log.

Contracts

Timing (ws2812.contract.yaml, dshot.contract.yaml) — pulse widths in ns:

contract: ws2812
headroom_pct: 20         # in-spec but within 20% of a rail => "marginal"
edges:
  - {name: T0H, min: 200, typ: 350, max: 500}   # '0' bit high time

Serial (boot.contract.yaml) — Python regex:

contract: boot
kind: serial
expect: ["IMU init OK", "boot v\\d+"]
forbid: ["panic", "Guru Meditation", "\\bnan\\b"]

Add a protocol = drop a new YAML. No code change for another timing signal.

Kill switch

Instantly disable every hardware-touching tool (captures) while leaving the purejudge tools working:

export HWCONTRACT_SAFE=1          # env, or:
touch /home/tsd/projects/hardware/KILLSWITCH   # file next to server.py

Security

Every tool argument is treated as hostile (the caller is an LLM that can be prompt-injected): contract paths are confined to the server dir (override HWCONTRACT_ROOT),driver/channel/port are charset-validated, samples/seconds/samplerate areclamped, sigrok-cli runs with a timeout, YAML is safe_load. Do not expose thisserver over the network without adding authentication.

Self-tests (no hardware, run from anywhere)

hwcontract --selftest                       # full MCP round-trip
python3 -m hwcontract.judge --demo
python3 -m hwcontract.sigrok_adapter --demo
python3 -m hwcontract.serial_adapter --demo

MCP Server · Populars

MCP Server · New