usb-rea
usb-rea is an MCP server for reverse-engineering USB devices by capturing traffic, labeling actions, inferring protocols, and generating reports, drivers, and dissectors.
Capture and stop USB traffic on Linux (usbmon) or Windows (USBPcap).
Mark vendor-app actions and “seen” observations such as readings, modes, or on/off states.
Diff labeled actions and infer protocol byte roles, checksums, counters, values, replies, handshakes, states, and image streams.
Generate Markdown evidence reports linking conclusions to capture frame numbers.
Scaffold standalone Python/pyusb driver source, including query methods, handshakes, state handling, and image sending.
Generate Wireshark Lua dissectors for decoding the inferred protocol.
Decode HID report descriptors and optionally report payloads.
Replay a specific packet to real hardware only after explicit human confirmation.
Generates Wireshark Lua dissectors from an inferred USB protocol, so captured traffic is displayed as named commands with decoded values, counters, lookup labels, and checksum checks (filterable via tshark/Wireshark), and parses Wireshark/USBPcap pcapng captures as input for protocol inference.
usb-rea: Reverse Engineer any USB gadget
Concrete result: from captures of the NZXT Kraken X (1e71:170e) fan command, usb-rea recovers 02 4d <channel> 00 <duty> byte for byte, exactly as liquidctl's kraken2.py sends it, and the generated driver emits identical packets for duty values it never saw. Same for the SteelSeries Rival 100 (1038:1702) DPI, polling and RGB tables from rivalcfg. Proof runs in CI: docs/VALIDATION.md. Full walk-through with a CRC8 and a rolling counter: Corsair Hydro Platinum case study.
Have a Windows-only device? Open a device request with a capture and I will run it and publish the result.

Got a USB device that only ships a Windows vendor app (case LCD, RGB controller, mouse, "smart" lamp)? Click around in the vendor app while usb-rea captures the USB traffic (usbmon on Linux, USBPcap on Windows), diffs the packets per action, infers the protocol, and writes an open-source Python/libusb driver, with every conclusion linked to the exact packets that prove it.
Status: alpha (v0.7.10, on PyPI). Validated on synthetic devices, on protocols documented by liquidctl and rivalcfg, on public Wireshark captures (see docs/VALIDATION.md) and on a real AIO cooler LCD capture a user shared, where it found the same brightness command and image stream format the user had worked out by hand. Your captures are welcome.
Try it in 10 seconds (no hardware needed)
pip install usb-rea
usb-rea demo --device fanmode 100% 0x5a | counter (+1 per message) | 0x30 | lookup 1→0x11, 2→0x22, 3→0x47 | 0x00 | 0x00 | u16le crc16_modbus(bytes[0:6])
fan 100% 0x5a | counter (+1 per message) | 0x10 | u16be round(value * 1 + 0) | 0x00 | u16le crc16_modbus(bytes[0:6])
color 100% 0x5a | counter (+1 per message) | 0x20 | round(value * 1 + 0) | round(value[1] * 1 + 0) | round(value[2] * 1 + 0) | u16le crc16_modbus(bytes[0:6])
color only sent while mode 3 (3x; other commands ran in mode 1, 2): the driver sets it first
handshake (3 messages sent once when the app opens the device):
1. 5a 00 01 00 00 00 0d 1d -> a5 01 ?? ?? ?? ?? ??, sent again 2x later (session reopened)
2. 5a 00 02 38 b4 e6 52 1e 1e -> a5 02 ?? ??, bytes 3-6 = token from step 1, sent again 2x later (session reopened)
reply byte 2 = 01 (2x, most common): the steps below
reply byte 2 = 02 (1x): 5a 00 04 01 00 00 5c 11 -> a5 04 01 ??, 5a 00 03 01 00 00 5d 65 -> a5 03 01 ??
3. 5a 00 03 01 00 00 5d 65 -> a5 03 01 ??, sent again 1x later (session reopened)
device replies:
mode 6/6 0xa5 | = query byte 2 (0x30) | = query byte 1 | = query byte 3 | 0x00 | sum8(bytes[0:5])
fan 6/6 0xa5 | = query byte 2 (0x10) | = query byte 1 | u16le round(value * 1 + 0) | sum8(bytes[0:5])
color 6/6 0xa5 | = query byte 2 (0x20) | = query byte 1 | 0x00 | 0x00 | sum8(bytes[0:5])
poll 1 7/7 0xa5 | = query byte 2 (0x40) | u16le rpm = raw, seen 800..4011 | u8 temp = raw, seen 35..38 | mode: 0x11 = silent, 0x22 = balanced, 0x47 = turbo | sum8(bytes[0:6])
24 of 24 'seen' observations matched a reply byte (mode, rpm, temp)It writes a synthetic capture of a fan controller (its vendor app first opens a session with a token the fan hands out, then polls the fan's status, and the markers note what the app's screen showed, seen rpm 1207 temp 37 mode silent), recovers the protocol and the replies, names the readings and the fan's mode, notices that the app only sets a color in mode 3, and writes report.md (every claim linked to Wireshark frame numbers) and a working pyusb driver that encodes values it never saw and decodes what the fan answers.
--device lamp (default) is a simple 8-bit device; --device lcd is a pump cap screen whose app streams a JPEG video while you change the brightness; --device dial shows 32-bit values and packed bitfields:
position 100% 0xa5 | 0x01 | u32le round(value * 1 + 0) | sum8(bytes[0:6])
level 100% 0xa5 | 0x02 | bits[0:4] = round(value * 1 + 0) | 0x00 | sum8(bytes[0:4])Related MCP server: cynthion-mcp
What it can infer
Role | Example |
constant header / command id |
|
linear values, 8-, 16- or 32-bit LE/BE, several per action |
|
packed bitfields (bits beside constant flag bits, incl. 12-bit spans over two bytes) |
|
lookup tables (non-linear choices) | DPI 250→ |
rolling counters | sequence byte +1 per message |
checksums | sum8, xor8, neg/inv sum8, sum16, CRC8, CRC8/MAXIM, CRC16 CCITT-FALSE/XMODEM/MODBUS/KERMIT |
device replies: echoed query bytes, values fitted to labels, readings | status poll → |
named readings, from what the vendor app shows ( | bytes 2-3 |
on/off status flags ( | byte 5 |
modes / enum values ( | byte 5 |
signed readings, when the app shows negative values ( | byte 4 |
handshake: messages sent once when the app opens the device, session tokens | step 2 bytes 3-6 = bytes 2-5 of the reply to step 1 |
image streams to a screen, kept apart from the commands | JPEG on EP 0x03, header |
raw pixel streams (RGB565/RGB888), found by size and by the region the draw command names | 320x480 RGB565 LE after |
Linear fits must survive leave-one-out cross-checks; otherwise usb-rea falls back to an honest lookup table and says so.
Real device workflow (Linux)
sudo modprobe usbmon
usb-rea capture start -o lamp.pcapng
usb-rea mark -m lamp.pcapng.actions.json "brightness 0%" # then click it in the vendor app
usb-rea mark -m lamp.pcapng.actions.json "brightness 50%" # ...repeat for 3+ values per setting
usb-rea capture stop
usb-rea report lamp.pcapng -o report.md
usb-rea scaffold lamp.pcapng -o lamp_driver.py
usb-rea dissector lamp.pcapng -o lamp.lua # Wireshark dissector for the same protocolCaptured on Windows with USBPcap/Wireshark and no markers? Click each action with a short pause in between, then just list them in order, and usb-rea splits the traffic at the pauses:
usb-rea report lamp.pcapng --labels "brightness 0%; brightness 50%; brightness 100%; power off; power on"
usb-rea scaffold lamp.pcapng --labels labels.txt -o lamp_driver.py # one label per line(Markers, if you have them, are a JSON list of {"label", "start", "end"} epoch timestamps.)
Most vendor apps also poll the device on their own (a "get status" every few hundred ms, keep-alives).
usb-rea spots identical host→device messages sent on a steady rhythm, away from your clicks, and sets
them aside before splitting and diffing; the report lists them under Background traffic (ignored)
with their period and frame numbers. A constant "apply" packet that always follows a real command is
not treated as background. Polls that carry a sequence number are caught too: same-shape messages
that differ only in a byte counting up by one (plus a trailing checksum that changes with it) show up
as e.g. aa 10 ?? 00 ??. Those need at least 8 copies, so a user stepping a value by one is not
mistaken for a poll.
Device responses
Devices answer too: an ACK after each command, a status reply to every poll (RPM, temperature,
firmware version), or the data of a GET_REPORT. usb-rea pairs each query with the IN transfer that
follows it (by endpoint and typical delay, so a poll sent in the middle of a click doesn't steal the
click's reply) and decodes the replies with the same machinery: constants, bytes that echo the query
(command id, sequence number), counters, checksums, and values fitted to your labels (fan 1200 →
u16le in the reply). Anything else that varies is reported as a reading with its observed range,
and the report says that its meaning is unknown. A 16-bit reading is only claimed when one byte
order changes smoothly and the other jumps. The generated driver gets query_<name>() methods that
send the query, read the reply and return a dict ({"reading_2": 1834, "checksum_ok": True, …}).
Try it with usb-rea demo --device fan or on a capture with polls.
Naming readings: tell it what the vendor app shows
A reading is just bytes until you say what it is. While capturing, note what the vendor app displays now and then:
usb-rea mark -m fan.pcapng.actions.json "seen rpm 1180 temp 34" # several name/value pairs are fineA seen marker is an observation, not an action: it does not end the action in progress, is never
diffed, and never becomes a command. Each observation is compared with the reply just before it
(what the app was showing) or just after it, within 1 s. A reading takes the name when it equals
every observation of that name (2+ observations, within one unit or 1%), or when a linear
raw = value * scale + bias fits them exactly and survives the same leave-one-out check as commands
(3+ observations with distinct values). Two neighbouring byte readings are also tried as one 16-bit
LE/BE number, and four as one 32-bit LE/BE number (three are enough when the high byte is still
0x00), so energy counters and uptimes work: mark "seen energy 2.968" names bytes 1-4
u32be energy = raw / 1000. The report shows the formula with the frames and marker times behind it, and lists
observations that matched nothing; the driver returns {"rpm": 1180, "temp": 34, "ok": True, …}.
On/off states work too: mark "seen pump on, alarm off" (also true/false, yes/no). A name
seen both on and off becomes a status flag, the one bit of a reply byte that is 1 every time the app
said on and 0 every time it said off; several flags can share a byte, and the driver returns
{"pump": True, "alarm": False, …}. Two bits that always flip together are left unnamed, and bits
of a flag byte that change but were never named are pointed out in the report.
Modes and other enum values are words: mark "seen mode silent", mark "seen profile gaming" (a
word value goes last, or before a comma: seen mode turbo, rpm 3900). A name seen as 2+ different
words in 3+ observations becomes a mode: the reply byte whose raw value is the same every time the app
showed one word and different between words. If other bits of that byte change on their own (or it
holds named flags), only the bits that differ between the words are taken, e.g. bits[4:6] profile.
The report lists the map (mode: 0x00 = silent, 0x02 = turbo) with its evidence, JSON/MCP fields get
it as table, and the driver returns the word, or the raw number for a value never seen:
{"mode": "turbo", …}. When the app shows a negative number (seen temp -5), 8- and 16-bit readings
whose top bit gets set are also tried as signed (i8, i16le/i16be).
A small number that no whole byte matches (seen battery 3 for battery bars) is also looked for in
part of a byte: the free bits beside named flags or modes (trimmed to the bits that change, so fixed
reserved bits are skipped) or either nibble, e.g. bits[1:4] battery; the driver returns the number.
A value shown with decimals (seen temp 41.5) is also tried as tenths, hundredths or
thousandths (raw 415, scale 10), so two such markers are enough instead of the three a free linear
fit needs. Numbers can be typed the way the app prints them, with a decimal comma or a thousands
separator: seen temp 41,5 °C, rpm 1,180 reads as 41.5 and 1180.
Limits: only replies (not commands) are named, a name that fits two places is left out, modes are
one byte (or one contiguous bit group in it) and only the words you saw get a label, 32-bit readings
are never signed, multi-bit numeric readings packed beside other bits are not tried, and display
rounding coarser than one unit (e.g. RPM shown in steps of 10) only works for the identity, not for a
scaled fit. seen labels in --labels mode have no timestamp and are skipped.
Handshakes: what the app sends when it opens the device
Many devices want a few messages before they accept commands: a hello, an unlock or auth step, a
"start" or "PC control on". usb-rea treats host→device messages (and class/vendor control reads)
captured before your first action marker as a handshake when they are not background polls and are
never sent again later in the capture, so start capturing before you open the vendor app. Each step
is paired with the reply that follows it, and the report gets a Handshake section with the frames.
A run of 3+ reply bytes that comes back in a later handshake request, or at the same offset in every
captured copy of a command, is a session token or device id; a run of 3+ request bytes that the reply
repeats is an echo. A checksum is claimed when one algorithm fits the end of every handshake request
(or every reply). The generated driver replays the handshake in Device.open() (called by
Device(); Device(handshake=False) skips it) before any command: tokens are copied from this
session's live replies into later steps and into commands, request checksums are recomputed, and
each reply is checked against the captured one (?? for token and checksum bytes, checksums
verified), raising RuntimeError on a mismatch. JSON/MCP output has it as handshake.
Retries are recognised: when the app sends the same message again straight away because the first
copy got no reply or a different one (a device that answers "busy" until it is ready), the copies
are one step with the earlier ones listed under retries. The driver then resends that step up to
as many times as the app did when the reply times out or does not match.
Reopens are recognised too: when the app sends the whole handshake again later in the capture (the
device slept or was replugged, or the app reconnected), with each message on the same channel and
length and only the checksum or bytes copied from that block's own replies (a new session token)
changed, the block is session setup, not part of an action. It is kept out of the actions, the
steps list it under reopens, and the report says so.
Branches on the device's reply are recognised as well: when a later block follows the handshake up
to some step and then sends other messages, and the reply to that step differs from the first
session's at a byte that is not a session token or checksum (a status such as "ready" or "needs
calibration", or a mode), that step gets branches: the reply byte's offset, each value seen there
with how often and the steps the app sent next (the rest of the handshake for the first session's
value, the other messages up to the next action marker for the others), and the most common value
as default. The byte becomes ?? in Expected, the branch messages are kept out of the actions,
and the report and usb-rea demo --device fan list the branches (the synthetic fan's app sends a
calibrate step when auth answers "needs calibration", make_capture(..., handshake=True, branch=True)).
The driver's open() reads that byte of the live reply and replays the matching branch, falling back
to the most common one for a value the capture never showed; Device.branches records the values taken.
Limits: every handshake message is seen once, so a byte that changes per session but is never reused
(a random nonce the device only reports) looks fixed; open(check=False) replays without checking.
Branching is learned from one capture only (the first session and its reopens, so capture the app
reconnecting in the states you care about), with one branch point per handshake decided by one reply
byte; branches that join again are stored as separate tails, and a branch block must sit between
actions (not inside a marker). Otherwise a later handshake must repeat the whole sequence to count as
a reopen (a partial one, e.g. after a mode switch, is not recognised), and a token must be copied verbatim (a token that is
transformed, such as a challenge answered with a hash, is not recognised). With --labels (no
timestamps) the handshake is part of the first action, so use markers for this.
States: commands the app only sends in one mode
Some devices only accept a command in one mode (a color only in "custom" lighting mode, a curve only
in manual fan mode), so the vendor app switches the mode first. usb-rea walks the actions in capture
order and tracks the value last chosen by each state command (a command with one argument that is a
lookup table, like mode 1/2/3, or an on/off label). A command gets requires (state, value, how
often, the other values seen, evidence frames) when it was sent at least twice, every time after the
state command chose the same value, while the capture also shows other commands used in a different
value of that state. The report adds an "Only sent while mode 3" line, the CLI demo and the MCP
diff_actions output list it under states, and the generated driver remembers what it set:
set_color() first calls set_mode(3) unless the driver already set mode 3 since open(), and
Device(auto_state=False) raises StateError instead. The synthetic fan's app only sets a color in
mode 3 (make_capture(..., device="fan", states=True), used by usb-rea demo --device fan).
Limits: this is what the app did, not proof that the device refuses the command in other modes;
capture the command in every mode you can to avoid a false precondition. Commands sent before any
state command count as unknown and give no precondition, the state the device starts in is not read
back (the driver sets the mode on the first gated call after open()), and a reopened session resets
what the driver knows.
States the device reports, and mode switches inside an action
Some apps decide from the device's own status: the fan's status reply says it is in turbo mode (set
with a button on the device or by the app) and only then does the app offer a color. When a status
reply byte has a name from seen mode turbo markers (a mode with a value -> word table, or an on/off
flag), usb-rea takes, for every action, the last status reply before it (at most 5 s old). A command
gets requires with source: "reply" (reply, byte, value, count, other values, reply and command
frames) when it was sent at least twice, each time while the reply showed the same value, and other
commands were used while it showed another one. A command whose every use was followed by a reply
showing one value per argument (mode 3 -> turbo) is the setter. The generated driver reads the
status first (query_poll_1()), sends nothing more when it already matches, otherwise calls
set_mode(3) and reads it again (StateError when it still differs, when there is no setter, or
with Device(auto_state=False)). Try it with make_capture(..., device="fan", reported=True),
where press mode N labels stand for the fan's own button.
Other apps switch the mode inside one click: mode 3, the color, then the mode it was in before.
Diffed as is, that is a 3-packet "color" transaction whose last byte nobody can explain. When the
first packet of a multi-packet action decodes as a state command (every constant byte and the
checksum match, the argument is a value that command was captured with) with the same value every
time, and the last packet (if there is one) as the same command putting back the value from before
the action or one fixed value, the action keeps only its own packets and gets switch (state,
enter, restore "previous", a value or null, evidence frames), listed under switches too. The
driver's set_color() then sends set_mode(3), the color and set_mode(<what the driver last set>), as the app did. A first packet with different values, or a last one that fits neither rule,
stays a plain transaction with a note. See make_capture(..., device="fan", switch=True).
Limits: only named reply bytes count (a raw byte that happens to line up with the commands is not used), the driver puts back only a mode it set itself, and the device's state between status replies is assumed to be the last one reported.
Image streams: pictures and video sent to a screen
Devices with a screen (AIO pump caps, keyboards and stream decks with an LCD) often get a picture or
a whole video from the vendor app the entire time you click around: tens of thousands of large
packets on one endpoint. Diffed as actions, that traffic buries the few real commands, so a
brightness change gets read as "byte 1 of the video". usb-rea first looks for host→device channels
where packets start an image (JPEG FF D8 FF, PNG, GIF) at the same offset in at least 3 frames.
That offset is the per-packet header; a frame runs until the image's end marker (JPEG FF D9, with
embedded thumbnails counted, PNG IEND), the next start or a pause. Each header byte is described
as constant, nn (packets in the frame) or ii (the packet's index), chunks cut off at the start
of the capture are recognised by that header, and a fixed device→host message sent about once per
frame is the ack. All of it is set aside before background polls, handshakes and actions are
analysed, so the commands come from the remaining traffic.
The report gets an "Image stream" section (endpoint, format, frames, packets per frame, image size
read from the JPEG SOF / PNG IHDR, frame rate, chunk size and padding, header pattern, ack), the MCP
diff_actions output has streams, and the generated driver gets send_image(jpeg_bytes), which
cuts an image into chunks behind the captured header, fills in the count and index bytes, pads the
last chunk and returns the ack. The synthetic LCD streams a 240x240 JPEG video at 20 frames per
second next to its brightness command (make_capture(..., device="lcd", image_stream=True), used by
usb-rea demo --device lcd). On a real AIO pump LCD capture (75,788 image packets, 8,670 frames of
a 320x320 JPEG, four brightness writes on another endpoint) this turns a wrong "brightness" made of
video bytes into the right one: byte 4 = percent on EP 0x01.
Raw pixel streams have no magic bytes, so they are found by size. Many cheap case and AIO screens
take plain RGB565 (2 bytes a pixel) or RGB888 (3 bytes) behind a short draw command. On one
host→device channel usb-rea looks for runs of large packets with no pause between them (all but the
last the same length) whose payload is exactly width x height x bytes per pixel for a common screen
size (80x160 up to 1920x480, both orientations), or exactly the region the short packet sent just
before the run names: x, y, width, height or x0, y0, x1, y1 as 16-bit fields (little or big endian),
or four 10-bit fields packed into 5 bytes as on the Turing Smart Screen rev A. With a region, partial
redraws (a clock, a sensor value) count as frames of the same stream. Per-packet headers are found
the same way as for JPEG, and a packet count or index above 255 is now read as a 16-bit field
(nn nn, ii ii, with its byte order). A 16- or 32-bit byte count in the draw command is marked
ll. For RGB565 the pixel byte order is the one that makes neighbouring pixels smoother. One
transfer per frame (a whole screen handed to the USB stack at once) and several frames back to back
in one run are split by the screen size. The draw command packets are set aside with the pixels;
other commands on the same endpoint (hello, brightness, orientation) stay commands.
The report then says "Raw RGB565 pixels", the screen size and frame size, other sizes with the same
byte count, the byte order, the region fields and how many frames redraw only part of the screen,
and the draw command pattern (xx/yy position, ww/hh size, ex/ey end position, rr
packed region, ll byte count). The driver's send_image() takes a Pillow image (converted to
RGB565 in the captured byte order, or RGB888; Pillow is not needed otherwise) or bytes already in
that format, writes the draw command with the region filled in and then the pixels, so
send_image(clock, x=100, y=8, size=(48, 16)) redraws just a corner. The synthetic LCD can stream
raw pixels too: make_capture(..., device="lcd", image_stream=True, raw="rgb565") (160x128, draw
command with x, y, width, height as u16 LE, every 4th frame a 48x16 clock) or raw="rgb888" (128x160,
64-byte packets with a u16 LE packet index and count), used by usb-rea demo --device lcd --raw rgb565.
The tests also check a Turing Smart Screen rev A fixture written from its public protocol
description (6-byte commands, DISPLAY_BITMAP = 197, RGB565 little-endian pixels).
Some screens don't give the draw command a packet of its own: it is the first bytes of the frame's
first pixel packet, and the pixels follow right after it in the same packet. The frame is then a few
bytes longer than the screen, so usb-rea also reads the region fields from the start of each frame
and keeps the reading where every frame is exactly that many pixels plus a command of the same
length. The report says "Command at the start of each frame's first packet", the pixels (and the
channel order test) are read after the command, and send_image() puts the command in front of the
pixels before cutting them into packets, as the app did. Try it with
usb-rea demo --device lcd --raw rgb565 --inline (make_capture(..., raw="rgb565", inline=True)).
Whether a screen takes red or blue first can't be read from ordinary pixels (a photo with red and
blue swapped is still smooth), so usb-rea doesn't guess. usb-rea testcard -o card.png --size 320x240
writes a picture that is half red, a quarter green, an eighth blue and an eighth white; put it on the
screen in the vendor app for a few seconds while capturing. Those shares survive scaling, rotation
and mirroring, and only one reading of the pixels gets them right, so the report says
"Channel order: BGR" (or RGB, and for RGB565 also the byte order) with the frames and a confidence,
and send_image() converts a Pillow image to that order. Without the card it says "unknown" and
the driver assumes RGB. Try it with usb-rea demo --device lcd --raw rgb565 --testcard bgr.
Some screens take a frame over more than one host→device endpoint: the packets alternate between
two bulk endpoints, the first part of the frame goes to one endpoint and the rest to another, or the
draw command goes to the command endpoint (next to brightness) and only the pixels to the data
endpoint. One endpoint at a time that looks like broken frames, a JPEG with no end marker or two
smaller screens. So for a device with several host→device channels usb-rea also merges them by time
and searches again, and keeps the merged stream when most of its frames really use more than one
endpoint and it explains as many whole frames and packets as the per-endpoint guesses. Two screens on
two endpoints with their own frame rates stay two streams. streams gets endpoints (in the order
the frame uses them), endpoint_layout (alternate or blocks) and header_endpoint, the report
says where each part goes, and send_image() writes the packets and the draw command to the same
endpoints. Try usb-rea demo --device lcd --endpoints alternate (or --raw rgb565 --endpoints command).
Limits: a frame is only merged over data endpoints that mostly use the same packet size, with the
same per-packet header on each, and at most 4 data endpoints of one device are tried. A raw stream needs at least 3
frames whose payload is a listed screen size or matches the region fields; a screen size not in the
list without a region command is missed, and the same byte count can fit several sizes (320x480
RGB565 is also 480x320, and 320x320 RGB888), which the report lists so you can check on the device.
Region fields are looked for in a separate draw command or at the start of the first pixel packet;
a command inside the first packet needs region fields (constant bytes alone are not found), at most
32 bytes, and no padding after the pixels. Width and height are expected in that order, the channel order is only known from test card
frames, and a frame with a pause longer than 30 ms inside it is split. send_image() does not resize or
re-encode (send an image of the captured size and format), and a stream needs at least 3 frames in
the capture.
MCP server
pip install usb-rea then add usb-rea mcp as a stdio server. Tools: capture_start, capture_stop, mark_action, diff_actions, report, scaffold_driver, hid_descriptor (decode a HID report descriptor / report payloads), wireshark_dissector, replay_packet (refuses unless the human confirmed). Analysis tools accept the same labels argument as the CLI for marker-free captures.
With uv no install step is needed:
{"mcpServers": {"usb-rea": {"command": "uvx", "args": ["--from", "usb-rea", "usb-rea", "mcp"]}}}For Claude Code: claude mcp add usb-rea -- uvx --from usb-rea usb-rea mcp. The server is also listed on the official MCP Registry as io.github.emirmrkaljevic/usb-rea and on Glama, which builds it from the repo's Dockerfile and checks that it starts and lists its tools.
How it works
usb-rea capture start: record traffic (usbmon / USBPcap)usb-rea mark "brightness 50%": label each action you take in the vendor app (mark "seen rpm 1180"notes what it displays)usb-rea diff: which bytes change per action, value correlation, checksums, countersusb-rea infer: message templates + evidence reportusb-rea scaffold: generate a pyusb driver and replay testsusb-rea dissector: write a Wireshark Lua dissector for the inferred protocolusb-rea replay: send packets to the device (only with explicit confirmation)usb-rea hid: decode a HID report descriptor from a capture (GET_DESCRIPTOR 0x22) or from hex, and decode report payloadsusb-rea mcp: the same tools as an MCP server for AI agents
Wireshark dissector
usb-rea dissector capture.pcapng -o lamp.lua turns the inferred protocol into a Wireshark Lua plugin, so you can keep
capturing and read the traffic as commands instead of hex:
wireshark -X lua_script:lamp.lua capture.pcapng
tshark -X lua_script:lamp.lua -r capture.pcapng -Y 'usbrea.command == "brightness"'Each matching packet gets a usbrea tree with the command name, decoded values (usbrea.brightness.value), counters,
lookup labels, and a checksum check (usbrea.checksum_ok, with an expert warning on mismatch); the Info column reads
brightness 50. It works for bulk, interrupt and control/HID SET_REPORT transfers, from usbmon or USBPcap, and needs no
bit library (Wireshark 3.x and 4.x). -n NAME changes the filter prefix. CI runs every generated dissector through tshark.
HID devices
Most RGB keyboards, mice, headsets and LCD gadgets are HID with a vendor-defined page. If the capture includes the report descriptor (start capturing, then replug the device), the report gets a section with every report's ID, size and fields, and each inferred command is tagged with the HID report it travels in. Without a capture:
usb-rea hid /sys/class/hidraw/hidraw0/device/report_descriptor --items
usb-rea hid capture.pcapng --decode "10ff0d1d000000" --type OutputRoadmap
pcap/pcapng parser (usbmon + USBPcap)
action markers and per-action diffing
checksum detection (sum8, xor8, neg/inv sum8, CRC8, CRC16 variants)
evidence-backed protocol report
driver scaffolding (pyusb, bulk/interrupt + control/HID SET_REPORT)
confirmed replay
MCP server
rolling counters, multi-byte (16/32-bit LE/BE) values, lookup tables, multi-value actions
packed bitfields
validation against documented protocols (liquidctl, rivalcfg)
HID report descriptor decoding (
usb-rea hid), with captured commands mapped to their HID reportsemulated-device CI (dummy_hcd + configfs HID; script in
scripts/gadget_e2e.sh)real-hardware case studies
multi-packet transactions (write + apply), including checksums over earlier packets
background traffic: periodic status polls / keep-alives are detected and ignored
device responses: replies to commands, background polls and GET_REPORT decoded,
query_*()driver methodsnamed readings:
seen rpm 1180markers name and scale reply readings (identity or linear, 8/16/32-bit)on/off status flags:
seen pump on/seen pump offname single bits of reply bytesmodes / enum values (
seen mode turbo) and signed readings (seen temp -5)small levels packed beside flags (
seen battery 3in bits 1-3 of a status byte)handshakes: messages sent once when the app opens the device, replayed by the driver with session tokens copied from live replies
handshake retries: a step the app resent after no reply or a busy reply is resent by the driver too
handshake reopens: the whole handshake sent again mid-capture is kept out of the actions
handshake branches: the next steps chosen by a byte of the device's reply, replayed by the driver from the live reply
state preconditions: commands the app only sent after a mode command chose one value, set first by the driver
states read from device replies and mode switches inside an action, both replayed by the driver
image streams: JPEG/PNG/GIF frames sent to a screen are kept apart from the commands,
send_image()in the driverraw pixel streams (RGB565/RGB888 without an image header) and 16-bit header fields
RGB or BGR channel order (and RGB565 byte order) from frames showing the
usb-rea testcardpictureimage streams split over several endpoints (alternating, in blocks, or a draw command on the command endpoint)
draw commands at the start of the first pixel packet (no packet of their own)
Legal
Reverse engineering for interoperability is permitted (EU Directive 2009/24/EC Art. 6, US DMCA §1201(f)). usb-rea only analyzes traffic of devices you own and never redistributes vendor code.
License
MIT
Available Tools
9 toolscapture_startA
Start a USB traffic capture into out (usbmon on Linux, USBPcap on Windows).
Side effects: launches a background capture process that keeps writing until capture_stop. Needs elevated privileges on Linux (usbmon; typically root or a usbmon group). Does not touch the device itself. Pair every start with capture_stop; do not start a second capture while one is running. Prefer this over analysis tools when you still need live traffic.
| Name | Required | Description | Default |
|---|---|---|---|
| bus | No | usbmon bus number N for /dev/usbmonN; ignored on Windows/USBPcap. Default 0 | |
| out | No | Output pcapng path (overwritten if it already exists) | capture.pcapng |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations exist, so the description carries the full burden, and it does so well: it discloses the side effect of launching a persistent background process, the elevation requirement on Linux, and that it does not touch the device. It stops short of covering output-file overwrite semantics or failure/cleanup behavior, keeping it from a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Four short, front-loaded sentences with zero filler; the primary purpose and output target come first, followed by side effects and usage rules. Every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need not be described. For a side-effecting, no-annotation tool with elevated-privilege requirements, the description covers purpose, side effects, permissions, lifecycle, and routing — the agent has everything needed to invoke it correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so both parameters are already documented in the schema. The description references `out` but adds no semantics (e.g., overwrite behavior) beyond what the schema states, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (start) and resource (USB traffic capture) and names the output target, with platform nuance (usbmon on Linux, USBPcap on Windows). An agent can immediately distinguish this from capture_stop, which it explicitly pairs with.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit when-to-use guidance ('prefer this over analysis tools when you still need live traffic'), a hard when-not constraint ('do not start a second capture while one is running'), and a lifecycle rule ('pair every start with capture_stop'). Nothing is left to inference.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
capture_stopA
Stop the running capture started by capture_start.
Side effects: terminates the background capture process. The pcapng at the path given to capture_start is left on disk for report/diff_actions/scaffold_driver. Returns 'no capture running' if nothing was started (not an error). Call this before analysing.
| Name | Required | Description | Default |
|---|---|---|---|
No parameters | |||
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so well: it discloses the side effect (terminating the background process), the persistence outcome (pcapng remains on disk and who consumes it), and the error semantics ('no capture running' is not an error). This is exactly the behavioral context an agent needs before invoking a lifecycle-control tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Three short sentences, front-loaded with the core action, followed by a labeled side-effect line and the no-op case. No filler; every sentence carries actionable information.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values needn't be enumerated, and the description still covers the one non-obvious return case. For a zero-parameter lifecycle tool, nothing needed to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes zero parameters, so the baseline is 4; there is no parameter surface for the description to clarify or compensate for. No semantic gap exists here.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and resource ('Stop the running capture') and explicitly ties it to its counterpart 'capture_start', making it trivially distinguishable from that sibling. An agent knows exactly what this does without opening the schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Gives clear sequencing guidance ('Call this before analysing') and names the downstream tools that depend on the leftover pcapng (report/diff_actions/scaffold_driver). It also clarifies that calling with nothing running is a benign no-op rather than an error, though it does not state any explicit when-not-to-use condition.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
diff_actionsA
Diff packets per labelled action and infer the protocol (JSON: byte roles + evidence frames).
The JSON also has background (periodic polls set aside) and responses: device replies paired
with the query they answer (command, background poll or GET_REPORT), with byte roles
(const, echo of a query byte, value fitted to the labels, counter, checksum, or reading with
its observed min/max) and query/reply frame numbers. A reading that 'seen' markers explain
gets name, scale and bias (raw = value * scale + bias, signed for i8/i16) plus the
observations in observed; a named mode has table (raw value -> word the app showed);
observations lists every 'seen' value and which reply bytes it matched. handshake lists the
messages sent once before the first action (request, reply, expect with ?? for token and
checksum bytes, uses/tokens for reply bytes reused later as a session token, checksums,
retries, reopens, and branches on a step whose reply byte decided what the app sent next).
states lists commands the app only sent after a mode command chose one value (also on the
command as requires: state, value, count, other values seen, evidence frames), and with
source "reply" those it only sent while a named status reply byte showed one mode or flag
value (reply, where, reply_frames, and setter: the command seen to make the reply show it).
switches lists actions where the app sent a state command first (enter) and maybe again
after (restore: "previous", a value or null); that command's packets are kept out of the
action, which carries the same switch.
streams lists continuous image streams to a screen (JPEG/PNG/GIF, or raw RGB565/RGB888 pixels
found by size with raw true): endpoint, endpoints (every host->device endpoint the frame bytes
go to, with endpoint_layout 'alternate' or 'blocks' when there are several) and
header_endpoint (where the draw command goes, when not with the pixels), format, frame and packet counts, chunk size, per-packet
header (nn packet count, ii index, 8 or 16 bit), image size, frame rate, the device's
per-frame ack and for raw streams header_packet (the command before each frame, or at the start of its first packet when
header_inline is true), region
(x/y/width/height fields), byteorder, size_alternatives and channel_order ('rgb' or 'bgr',
null = unknown; only settled by frames showing the usb-rea testcard picture, see
channel_evidence); those packets are kept out of the commands.
Read-only analysis: does not write files or talk to hardware. Requires either markers from
mark_action or labels for a finished capture with pauses between clicks. Prefer report
when the human wants a Markdown evidence write-up; prefer scaffold_driver /
wireshark_dissector when they want generated code.
| Name | Required | Description | Default |
|---|---|---|---|
| device | No | USB device address to analyse; default = bus address with the most host→device payloads | |
| labels | No | Marker-free mode: ';'-separated labels in click order, or a text file path (one label per line). Splits traffic at the largest pauses between host→device messages | |
| capture | Yes | Path to a usbmon or USBPcap pcap/pcapng file | |
| markers | No | Markers JSON path; default <capture>.actions.json. Ignored when labels is set |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does declare the safety profile: 'Read-only analysis: does not write files or talk to hardware.' It also discloses the two operating modes (marker-based vs marker-free with pauses) and that markers are ignored when labels is set. It omits cost/runtime expectations for large captures, but the essential behavioral facts are present.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The middle ~80% is a dense wall of output-field documentation (`streams`, `switches`, `handshake`, `byteorder`, `channel_order`), much of which duplicates the output schema. The purpose sentence is front-loaded, but the reader must wade through parenthetical-heavy prose with no headings or bullets. Size is far out of proportion to what the agent needs to select and call the tool.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given an output schema exists, return-value explanation was not required, yet nothing critical for invocation is missing: prerequisites, read-only nature, and mode selection are all covered. The problem is redundancy rather than incompleteness, so it still passes as complete for a 4-parameter analysis tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so the baseline is 3. The description adds a little meaning beyond the schema by tying `markers` to mark_action and explaining the marker-free split heuristic, but most of its parameter-related content (device default, label format) is already in the schema descriptions.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening sentence names a specific verb and resource: 'Diff packets per labelled action and infer the protocol'. That is concrete and distinguishable from capture_start/hid_descriptor. However, the differentiation from siblings only appears at the very end of a very long block, so an agent must read the whole description to place the tool.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It states prerequisites ('Requires either markers from mark_action or `labels` for a finished capture with pauses between clicks') and routes to alternatives explicitly ('Prefer report when the human wants a Markdown evidence write-up; prefer scaffold_driver / wireshark_dissector when they want generated code'). There is no explicit statement of when NOT to use diff_actions itself, which keeps it short of a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
hid_descriptorA
Decode a HID report descriptor (and optionally one report payload).
Read-only. When source is a capture path, finds GET_DESCRIPTOR type 0x22 transfers
(replug the device while capturing if none are present). When source is hex, parses that
descriptor directly (e.g. from /sys/class/hidraw/*/device/report_descriptor). Use this to
understand report IDs/sizes before or alongside report/scaffold_driver; it does not infer
vendor protocol fields from labelled actions.
| Name | Required | Description | Default |
|---|---|---|---|
| source | Yes | Capture path (searches for GET_DESCRIPTOR 0x22) or raw descriptor hex / hidraw report_descriptor bytes as hex | |
| decode_hex | No | Optional report payload hex to decode with the descriptor | |
| report_type | No | Report type for decode_hex: Input, Output, or Feature | Input |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the burden, and it does declare 'Read-only' plus the concrete lookup behavior for capture sources (searches GET_DESCRIPTOR type 0x22) and the remediation hint about replugging. It stops short of describing failure/empty-result behavior or any limits, keeping it below a 5.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded purpose line, then a short read-only/behavior paragraph and a short usage paragraph. Every sentence adds information (source handling, sibling relationship, exclusion) with no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists so return values need not be explained, and the description covers read-only status, both source modes, and sibling routing. The remaining gap is failure/edge behavior (e.g., what happens when no descriptor is found in a capture), which the replug hint only partially addresses.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3, but the description adds real meaning beyond the schema: it distinguishes capture-path vs hex sources and gives a concrete hex source example (/sys/class/hidraw/*/device/report_descriptor). That example materially helps an agent choose the right input form.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening sentence gives a specific verb (decode) and resource (HID report descriptor) plus an optional extension (one report payload). It further sharpens scope against siblings by stating it 'does not infer vendor protocol fields from labelled actions,' which distinguishes it from report/scaffold_driver.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit when-to-use ('to understand report IDs/sizes before or alongside report/scaffold_driver') and an explicit exclusion (it does not infer vendor protocol fields). It also gives the operational precondition for capture sources: replug the device while capturing if no GET_DESCRIPTOR 0x22 transfers exist.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
mark_actionA
Record that the human is about to click this action in the vendor app.
Side effects: appends {label, start, end} to .actions.json (creates the file).
Call while capture_start is running, immediately before each click; end times are closed
when the next mark arrives. Use the same label stem with different values (brightness 0%,
50%, 100%) so inference can fit scales. For an already-finished Wireshark capture without
markers, skip this and pass labels to report/diff_actions instead.
A label starting with 'seen' ('seen rpm 1180', 'seen rpm 1180 temp 34') is an observation,
not an action: what the vendor app displays right now. It does not end the current action
and is never diffed; analysis uses it to name and scale the device's reply readings
(rpm = raw, temp = (raw - 40) / 2). Mark it when the human reads a value off the screen.
On/off states ('seen pump on', 'seen pump off, alarm on') name single status-flag bits; words
('seen mode silent', 'seen mode turbo, rpm 3900': the word last or before a comma) name a mode
byte with a value -> word map; negative values ('seen temp -5') allow signed i8/i16 readings.
| Name | Required | Description | Default |
|---|---|---|---|
| label | Yes | Action label, ideally with a value, e.g. 'brightness 50%' or 'color 255 0 0'; or an observation starting with 'seen', e.g. 'seen rpm 1180 temp 34' | |
| capture | No | Capture path whose sidecar markers file (<capture>.actions.json) is updated | capture.pcapng |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so well: it names the side effect precisely (appends {label, start, end} to <capture>.actions.json, creating the file) and discloses the non-obvious lifecycle rule that end times are closed only when the next mark arrives. The 'seen' semantics — does not end the current action, never diffed, feeds naming/scaling of readings — is behavioral context an agent could not infer elsewhere.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Purpose and side effects are front-loaded in the first two lines, which is the right ordering. The remaining label-taxonomy prose is dense and would read faster as sub-bullets, but each sentence carries operational information rather than padding.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a two-parameter marker tool with an output schema (so return values need no explanation), the description covers prerequisites, file target, marker timing, and the full label vocabulary. Nothing an agent needs in order to call it correctly is missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is already 100%, yet the description adds substantial meaning beyond it: label grammar for actions vs. observations, the 'seen' prefix's effect on action boundaries, on/off status-bit naming, mode word maps, and signed value handling for negative readings. This is exactly the interpretive layer the schema's terse descriptions cannot supply.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The opening line is a specific verb plus resource with scope: record that the human is about to click an action in the vendor app. It is clearly distinguishable from siblings by naming capture_start as the prerequisite and pointing to report/diff_actions for the alternate case.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit timing is given (call while capture_start is running, immediately before each click) along with an explicit exclusion and alternative (for a finished Wireshark capture without markers, skip this and pass `labels` to report/diff_actions). It also prescribes a labeling convention (same label stem with varying values) so downstream inference can fit scales.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
replay_packetA
Send one packet to real USB hardware (destructive / irreversible on the device).
Side effects: opens the device via libusb/pyusb and writes the packet. Refuses unless user_confirmed=true after the human approved this exact vid/pid/endpoint/hex_data (and setup_hex if any). Needs OS permission to open the device. Prefer scaffold_driver to generate a driver first; never set user_confirmed from an agent guess.
| Name | Required | Description | Default |
|---|---|---|---|
| pid | Yes | USB product id (integer) | |
| vid | Yes | USB vendor id (integer, e.g. 0x1234 or 4660) | |
| endpoint | Yes | Endpoint address (e.g. 0x01 OUT); for control transfers use 0 with setup_hex | |
| hex_data | Yes | Payload bytes as hex (spaces optional), e.g. 'aa 01 ff 00 00 ab' | |
| setup_hex | No | Optional 8-byte control setup packet as hex; when set, sends a control transfer instead of bulk/interrupt | |
| user_confirmed | No | Must be true only after a human approved this exact packet; otherwise the call is refused |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does so: it discloses destructive/irreversible device effects, the underlying mechanism (libusb/pyusb open + write), the OS permission requirement, and the exact human-approval gate. This is unusually rich behavioral context for a mutation tool.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the destructive warning, then side effects, then the confirmation gate and the sibling alternative. Every sentence adds operational value with no filler.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return values need no explanation. For a 6-parameter destructive hardware tool with no annotations, the description covers safety, mechanism, permissions, and the confirmation workflow, leaving nothing an agent needs missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema coverage is 100%, so the baseline is 3; the description goes further by tying user_confirmed to approval of the specific vid/pid/endpoint/hex_data (and setup_hex if any), which clarifies the confirmation contract beyond the schema's field-level text. It doesn't add format/syntax detail for the hex fields themselves, so it falls short of a 5.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The first sentence states a specific verb (send) and resource (one packet to real USB hardware) and immediately flags the destructive scope. It also names the sibling it relates to (scaffold_driver), so an agent can distinguish it from the other capture/scaffold tools.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives explicit when-not guidance: prefer scaffold_driver first, and never set user_confirmed from an agent guess. The refusal condition (user_confirmed must be true only after a human approved this exact packet) tells the agent precisely when the call is valid versus rejected.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
reportA
Generate a Markdown evidence report: every claim links to frame numbers in the capture.
Includes a 'Device responses' section when the device answers queries (status polls, command ACKs, GET_REPORT), saying which reply bytes could not be explained, a 'Handshake' section for messages sent once before the first action (session setup, tokens), and an 'Observations' section for 'seen' markers (named readings and unmatched observations).
Read-only: returns Markdown text (does not write a file). Same inputs as diff_actions. Use after capture_stop (or on an existing pcap). Choose this over diff_actions when the human should read the protocol; choose scaffold_driver when they need a pyusb driver.
| Name | Required | Description | Default |
|---|---|---|---|
| device | No | USB device address to analyse; default = bus address with the most host→device payloads | |
| labels | No | Marker-free mode: ';'-separated labels in click order, or a text file path (one label per line) | |
| capture | Yes | Path to a usbmon or USBPcap pcap/pcapng file | |
| markers | No | Markers JSON path; default <capture>.actions.json. Ignored when labels is set |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the burden and it does the most important part: 'Read-only: returns Markdown text (does not write a file)', pre-empting the natural assumption that a report tool persists output. It also notes the inputs match diff_actions, which bounds expectations, though it says nothing about cost or behavior on malformed pcaps.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the core action and output format, then the conditional sections, then routing guidance. Dense but every clause carries information; the parenthetical section list is slightly long but is the most useful part of the description.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists and the description still names the return medium (Markdown text, not a file) and the section structure, which is the right level of detail. With 4 params at full coverage and clear routing, an agent has what it needs; only error/failure behavior is unaddressed.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, including the non-obvious default-device heuristic and the labels/markers precedence, so the schema already does the heavy lifting. The description adds only 'Same inputs as diff_actions', which is a cross-reference rather than new parameter meaning. Baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb and artifact: 'Generate a Markdown evidence report' with 'every claim links to frame numbers in the capture'. The section breakdown (Device responses, Handshake, Observations) makes the output's shape concrete and distinguishes it from diff_actions and scaffold_driver by name.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicit lifecycle placement ('Use after capture_stop (or on an existing pcap)') plus a two-way routing rule: choose this over diff_actions 'when the human should read the protocol', choose scaffold_driver 'when they need a pyusb driver'. That is a full when/when-not/alternative mapping.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
scaffold_driverA
Generate a standalone Python/pyusb driver source for the inferred protocol.
One set_() per command; when the device replies, also query_() methods that send the command/poll (or GET_REPORT), read the reply and return it decoded as a dict (readings named by 'seen' markers come back scaled and modes as words, e.g. {'rpm': 1180, 'temp': 34, 'mode': 'turbo', 'ok': True}). When the capture has a handshake, Device() replays it in open() before any command and checks the fixed reply bytes. A command the app only sent in one mode first calls the mode setter (Device(auto_state=False) raises StateError instead). A capture with an image stream to a screen adds send_image(image), which takes JPEG bytes (or, for a raw stream, a Pillow image or RGB565/RGB888 bytes, optionally a region at x, y), chunks it behind the captured header and returns the device's ack.
Read-only analysis that returns driver source as text (does not write a file or open the device). Same capture/markers/labels inputs as report. Prefer wireshark_dissector when the goal is to keep decoding traffic in Wireshark; use replay_packet only after the human approved a specific packet from this driver.
| Name | Required | Description | Default |
|---|---|---|---|
| device | No | USB device address to analyse; default = bus address with the most host→device payloads | |
| labels | No | Marker-free mode: ';'-separated labels in click order, or a text file path (one label per line) | |
| capture | Yes | Path to a usbmon or USBPcap pcap/pcapng file | |
| markers | No | Markers JSON path; default <capture>.actions.json. Ignored when labels is set |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
No annotations are provided, so the description carries the full burden. It discloses the read-only nature, returns driver source as text, does not write a file or open the device, and describes the generated driver's behavior in detail (handshake replay, mode setter, send_image, etc.), giving rich context beyond a simple statement.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with a clear purpose statement and well-organized into functional paragraphs. It is somewhat verbose in detailing the generated driver's features, but each sentence adds useful context, so it is efficient overall.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
Given the complexity of generating a driver and the presence of an output schema, the description covers purpose, usage alternatives, behavioral constraints, and output format sufficiently. It explains what the generated driver contains, which is valuable context for an agent.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all four parameters are fully documented in the schema. The description adds only a minor reference ('Same capture/markers/labels inputs as report'), adding negligible meaning beyond what the schema already provides, so baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb 'Generate' and resource 'standalone Python/pyusb driver source for the inferred protocol'. It also distinguishes from siblings by naming wireshark_dissector and replay_packet with explicit conditions, so an agent can tell it apart without opening schemas.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly says 'Prefer wireshark_dissector when the goal is to keep decoding traffic in Wireshark' and 'use replay_packet only after the human approved a specific packet from this driver', giving clear alternatives and when-to-use conditions. It also clarifies the tool is read-only analysis that does not write a file or open the device.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
wireshark_dissectorA
Generate a Wireshark Lua dissector for the inferred protocol.
Read-only: returns Lua source as text. Labels commands, decodes values/counters/lookups,
and verifies checksums (usbrea.checksum_ok + expert warning). Load with
wireshark -X lua_script:<file>.lua capture.pcapng or the same flag on tshark. Prefer
scaffold_driver for a pyusb driver; prefer hid_descriptor to inspect HID report layouts.
| Name | Required | Description | Default |
|---|---|---|---|
| name | No | Wireshark protocol/filter prefix (fields become <name>.command, <name>.checksum_ok, …) | usbrea |
| device | No | USB device address to analyse; default = bus address with the most host→device payloads | |
| labels | No | Marker-free mode: ';'-separated labels in click order, or a text file path (one label per line) | |
| capture | Yes | Path to a usbmon or USBPcap pcap/pcapng file | |
| markers | No | Markers JSON path; default <capture>.actions.json. Ignored when labels is set |
Output Schema
| Name | Required | Description |
|---|---|---|
| result | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
With no annotations, the description carries the full burden and does well: it declares 'Read-only: returns Lua source as text', describes what the generated dissector does (labels commands, decodes values/counters/lookups, verifies checksums with usbrea.checksum_ok + expert warning), and gives the exact load invocation for wireshark/tshark. It omits auth requirements, rate limits, or behavior when the capture cannot be parsed, which keeps it out of 5 territory.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loads the action in the first sentence, then layers output type, generated behavior, load command, and sibling routing in three compact sentences with no filler. Every sentence earns its place.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a code-generation tool with a full output schema and 100% parameter coverage, the description supplies the missing operational context: what the artifact is, what it verifies, and how to run it. Minor gaps remain (no mention of what happens on an unparseable capture or of the markers/labels precedence, though the schema covers the latter).
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 100%, so all five parameters are already documented in the schema (including defaults, the usbrea prefix, and marker/label precedence). The description adds essentially no parameter-level meaning beyond echoing the usbrea.checksum_ok field name, so the baseline 3 applies.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb (Generate) and resource (Wireshark Lua dissector for the inferred protocol), and immediately distinguishes itself from siblings by naming scaffold_driver and hid_descriptor with their respective purposes. An agent can route correctly without opening any schema.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly names two alternatives and the conditions that select them ('prefer scaffold_driver for a pyusb driver; prefer hid_descriptor to inspect HID report layouts'), which is strong routing guidance. It stops short of stating the positive trigger condition for this tool (e.g. 'use when you want to inspect the capture inside Wireshark'), so it is not a full when/when-not/alternatives treatment.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
1 tool update
v0.6.0- Changed
mark_action1 field changed- changed
Input schema / properties / label / descriptionPrevious value: -"Action label, ideally with a value, e.g. 'brightness 50%' or 'color 255 0 0'"New value: +"Action label, ideally with a value, e.g. 'brightness 50%' or 'color 255 0 0'; or an observation starting with 'seen', e.g. 'seen rpm 1180 temp 34'"
8 tool updates
v0.2.1- Changed
capture_start2 fields changed- added
Input schema / properties / bus / descriptionAdded value: +"usbmon bus number N for /dev/usbmonN; ignored on Windows/USBPcap. Default 0" - added
Input schema / properties / out / descriptionAdded value: +"Output pcapng path (overwritten if it already exists)"
- Changed
diff_actions4 fields changed- added
Input schema / properties / capture / descriptionAdded value: +"Path to a usbmon or USBPcap pcap/pcapng file" - added
Input schema / properties / device / descriptionAdded value: +"USB device address to analyse; default = bus address with the most host→device payloads" - added
Input schema / properties / labelsAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Marker-free mode: ';'-separated labels in click order, or a text file path (one label per line). Splits traffic at the largest pauses between host→device messages", + "title": "Labels" +} - added
Input schema / properties / markers / descriptionAdded value: +"Markers JSON path; default <capture>.actions.json. Ignored when labels is set"
- Changed
hid_descriptor3 fields changed- added
Input schema / properties / decode_hex / descriptionAdded value: +"Optional report payload hex to decode with the descriptor" - added
Input schema / properties / report_type / descriptionAdded value: +"Report type for decode_hex: Input, Output, or Feature" - added
Input schema / properties / source / descriptionAdded value: +"Capture path (searches for GET_DESCRIPTOR 0x22) or raw descriptor hex / hidraw report_descriptor bytes as hex"
- Changed
mark_action2 fields changed- added
Input schema / properties / capture / descriptionAdded value: +"Capture path whose sidecar markers file (<capture>.actions.json) is updated" - added
Input schema / properties / label / descriptionAdded value: +"Action label, ideally with a value, e.g. 'brightness 50%' or 'color 255 0 0'"
- Changed
replay_packet6 fields changed- added
Input schema / properties / endpoint / descriptionAdded value: +"Endpoint address (e.g. 0x01 OUT); for control transfers use 0 with setup_hex" - added
Input schema / properties / hex_data / descriptionAdded value: +"Payload bytes as hex (spaces optional), e.g. 'aa 01 ff 00 00 ab'" - added
Input schema / properties / pid / descriptionAdded value: +"USB product id (integer)" - added
Input schema / properties / setup_hex / descriptionAdded value: +"Optional 8-byte control setup packet as hex; when set, sends a control transfer instead of bulk/interrupt" - added
Input schema / properties / user_confirmed / descriptionAdded value: +"Must be true only after a human approved this exact packet; otherwise the call is refused" - added
Input schema / properties / vid / descriptionAdded value: +"USB vendor id (integer, e.g. 0x1234 or 4660)"
- Changed
report4 fields changed- added
Input schema / properties / capture / descriptionAdded value: +"Path to a usbmon or USBPcap pcap/pcapng file" - added
Input schema / properties / device / descriptionAdded value: +"USB device address to analyse; default = bus address with the most host→device payloads" - added
Input schema / properties / labelsAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Marker-free mode: ';'-separated labels in click order, or a text file path (one label per line)", + "title": "Labels" +} - added
Input schema / properties / markers / descriptionAdded value: +"Markers JSON path; default <capture>.actions.json. Ignored when labels is set"
- Changed
scaffold_driver4 fields changed- added
Input schema / properties / capture / descriptionAdded value: +"Path to a usbmon or USBPcap pcap/pcapng file" - added
Input schema / properties / device / descriptionAdded value: +"USB device address to analyse; default = bus address with the most host→device payloads" - added
Input schema / properties / labelsAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Marker-free mode: ';'-separated labels in click order, or a text file path (one label per line)", + "title": "Labels" +} - added
Input schema / properties / markers / descriptionAdded value: +"Markers JSON path; default <capture>.actions.json. Ignored when labels is set"
- Changed
wireshark_dissector5 fields changed- added
Input schema / properties / capture / descriptionAdded value: +"Path to a usbmon or USBPcap pcap/pcapng file" - added
Input schema / properties / device / descriptionAdded value: +"USB device address to analyse; default = bus address with the most host→device payloads" - added
Input schema / properties / labelsAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Marker-free mode: ';'-separated labels in click order, or a text file path (one label per line)", + "title": "Labels" +} - added
Input schema / properties / markers / descriptionAdded value: +"Markers JSON path; default <capture>.actions.json. Ignored when labels is set" - added
Input schema / properties / name / descriptionAdded value: +"Wireshark protocol/filter prefix (fields become <name>.command, <name>.checksum_ok, …)"
9 tool updates
v0.2.0- First observed
capture_start - First observed
capture_stop - First observed
diff_actions - First observed
hid_descriptor - First observed
mark_action - First observed
replay_packet - First observed
report - First observed
scaffold_driver - First observed
wireshark_dissector
TDQS
Scored across 9 tools
Most tools are clearly distinct: capture_start/capture_stop bracket a session, mark_action records labels, hid_descriptor decodes HID, and replay_packet writes to hardware. The one soft spot is the analysis trio diff_actions/report/scaffold_driver/wireshark_dissector, which take the same capture/markers/labels inputs and differ only in output artifact, but the descriptions explicitly cross-reference ('prefer report when...', 'choose scaffold_driver when they need a pyusb driver') so misselection is largely mitigated.
Everything is snake_case, but the convention is mixed: capture_start/capture_stop use noun_verb while mark_action/replay_packet use verb_noun, and report/scaffold_driver/wireshark_dissector/hid_descriptor are bare nouns with no action verb. Still readable and non-chaotic, but not a single predictable pattern.
Nine tools is well within the sweet spot and each earns its place: capture lifecycle (2), labelling (1), analysis (4 distinct output forms), HID inspection (1), and hardware replay (1). Nothing looks redundant or padded for the stated purpose.
The full workflow is covered end to end: start capture, mark actions/observations, stop, analyze, then export as Markdown, pyusb driver, or Lua dissector, plus a confirmed destructive replay path. Minor gaps: no device/interface enumeration or capture-file discovery tool, and no way to inspect an individual frame or raw packet beyond the frame numbers surfaced by analysis output.
Maintenance
Related MCP Connectors
Evidence-oriented REA + Ghidra static-analysis workflows, read-only PE32 and Delphi VMT tools.
Hunt zero-days by talking to binaries. 40+ tools. Hosted, OAuth + SSO, invite: hi@byteray.ai
Captured web interfaces, screenshots, flows and structured evidence for AI coding agents.
Session replay as readable transcripts: rage and dead clicks, JS errors, heatmaps. Open source.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables reverse engineering of web applications and chat interfaces through browser automation, network traffic capture, and streaming API discovery. Provides comprehensive tools for analyzing network patterns, capturing streaming responses, and automating complex web interactions.1416 npm1ISC
- AlicenseNot gradedqualityDmaintenanceDrive a Cynthion USB test instrument from an LLM via the Model Context Protocol. Sniff, decode, and emulate USB devices over chat.6BSD 3-Clause
- AlicenseNot gradedqualityCmaintenanceEnables AI coding agents to dynamically inspect running software — API hooks, syscall summaries, memory inspection, and HTTP/TLS/DNS reconstruction from live traffic or pcaps — across Linux, macOS, and Android targets. Every finding is returned as a structured Evidence Record with observation, artifacts, confidence, and explicit limitations, so agents cite proof rather than guess.1MIT
- AlicenseNot gradedqualityAmaintenanceLet an AI agent sniff and reverse-engineer a car's LIN bus with an open-source USB LIN dongle: per-id capture with changing bits, change log, auto-detect speed/checksum, LDF decode/encode; read-only by default, master/slave simulation behind --allow-transmit, --mock simulated bus.92GPL 3.0