usb-rea 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- usb_rea-0.2.1/.dockerignore +8 -0
- usb_rea-0.2.1/.github/workflows/ci.yml +18 -0
- usb_rea-0.2.1/.github/workflows/publish.yml +28 -0
- usb_rea-0.2.1/.gitignore +11 -0
- usb_rea-0.2.1/CHANGELOG.md +44 -0
- usb_rea-0.2.1/Dockerfile +21 -0
- usb_rea-0.2.1/LICENSE +21 -0
- usb_rea-0.2.1/PKG-INFO +140 -0
- usb_rea-0.2.1/README.md +124 -0
- usb_rea-0.2.1/docs/VALIDATION.md +22 -0
- usb_rea-0.2.1/docs/demo.gif +0 -0
- usb_rea-0.2.1/docs/demo.tape +18 -0
- usb_rea-0.2.1/glama.json +4 -0
- usb_rea-0.2.1/pyproject.toml +22 -0
- usb_rea-0.2.1/scripts/gadget_e2e.sh +70 -0
- usb_rea-0.2.1/server.json +27 -0
- usb_rea-0.2.1/smithery.yaml +8 -0
- usb_rea-0.2.1/src/usb_rea/__init__.py +2 -0
- usb_rea-0.2.1/src/usb_rea/actions.py +73 -0
- usb_rea-0.2.1/src/usb_rea/capture.py +38 -0
- usb_rea-0.2.1/src/usb_rea/checksums.py +46 -0
- usb_rea-0.2.1/src/usb_rea/cli.py +180 -0
- usb_rea-0.2.1/src/usb_rea/dissector.py +271 -0
- usb_rea-0.2.1/src/usb_rea/hid.py +344 -0
- usb_rea-0.2.1/src/usb_rea/infer.py +434 -0
- usb_rea-0.2.1/src/usb_rea/mcp_server.py +190 -0
- usb_rea-0.2.1/src/usb_rea/model.py +87 -0
- usb_rea-0.2.1/src/usb_rea/pcap.py +162 -0
- usb_rea-0.2.1/src/usb_rea/pipeline.py +64 -0
- usb_rea-0.2.1/src/usb_rea/replay.py +19 -0
- usb_rea-0.2.1/src/usb_rea/report.py +43 -0
- usb_rea-0.2.1/src/usb_rea/scaffold.py +136 -0
- usb_rea-0.2.1/src/usb_rea/synth.py +216 -0
- usb_rea-0.2.1/tests/data/SOURCES.md +10 -0
- usb_rea-0.2.1/tests/test_checksums.py +10 -0
- usb_rea-0.2.1/tests/test_dissector.py +96 -0
- usb_rea-0.2.1/tests/test_e2e.py +97 -0
- usb_rea-0.2.1/tests/test_hid.py +99 -0
- usb_rea-0.2.1/tests/test_infer.py +64 -0
- usb_rea-0.2.1/tests/test_infer_advanced.py +135 -0
- usb_rea-0.2.1/tests/test_pcap.py +45 -0
- usb_rea-0.2.1/tests/test_public_pcaps.py +46 -0
- usb_rea-0.2.1/tests/test_segment.py +30 -0
- usb_rea-0.2.1/tests/test_smoke.py +8 -0
- usb_rea-0.2.1/tests/test_transactions.py +66 -0
- usb_rea-0.2.1/tests/test_validation.py +63 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
test:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
strategy:
|
|
7
|
+
matrix:
|
|
8
|
+
python: ["3.10", "3.12", "3.13"]
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: actions/setup-python@v5
|
|
12
|
+
with:
|
|
13
|
+
python-version: ${{ matrix.python }}
|
|
14
|
+
- name: tshark + lua for the generated Wireshark dissector tests
|
|
15
|
+
if: matrix.python == '3.12'
|
|
16
|
+
run: sudo apt-get update -q && sudo DEBIAN_FRONTEND=noninteractive apt-get install -y -q tshark lua5.4
|
|
17
|
+
- run: pip install -e ".[dev]"
|
|
18
|
+
- run: pytest -q
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
# Publishes to PyPI via Trusted Publishing (no token stored) when a GitHub release is published.
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
- uses: actions/setup-python@v5
|
|
13
|
+
with: {python-version: "3.12"}
|
|
14
|
+
- run: pip install build twine && python -m build && twine check dist/*
|
|
15
|
+
- uses: actions/upload-artifact@v4
|
|
16
|
+
with: {name: dist, path: dist}
|
|
17
|
+
pypi:
|
|
18
|
+
needs: build
|
|
19
|
+
runs-on: ubuntu-latest
|
|
20
|
+
environment:
|
|
21
|
+
name: pypi
|
|
22
|
+
url: https://pypi.org/p/usb-rea
|
|
23
|
+
permissions:
|
|
24
|
+
id-token: write
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/download-artifact@v4
|
|
27
|
+
with: {name: dist, path: dist}
|
|
28
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
usb_rea-0.2.1/.gitignore
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## v0.2.1 — 2026-10-16
|
|
6
|
+
- multi-packet transactions: actions that send several packets (e.g. write + apply) are inferred as one command; the driver sends all packets in order
|
|
7
|
+
- cross-packet checksums (a later packet carrying a checksum of an earlier one)
|
|
8
|
+
- fix: `mcp` is now a core dependency, so a plain `pip install usb-rea` gives a working `usb-rea mcp`
|
|
9
|
+
|
|
10
|
+
- MCP tools: richer descriptions (side effects, privileges, when-to-use) and `pydantic.Field` parameter
|
|
11
|
+
descriptions so Glama TDQS can score the schema; analysis tools accept the same `labels` string/file
|
|
12
|
+
as the CLI for marker-free captures
|
|
13
|
+
|
|
14
|
+
## v0.2.0 — 2026-10-15
|
|
15
|
+
Wireshark dissector generator, HID descriptor decoding, wider inference, marker-free captures.
|
|
16
|
+
|
|
17
|
+
- `usb-rea dissector` (MCP tool `wireshark_dissector`): generate a Wireshark Lua dissector for the inferred protocol —
|
|
18
|
+
command names in the Info column, decoded values, counters, lookup labels, checksum verification with expert info;
|
|
19
|
+
tested against real tshark 4.4 in CI. `usb-rea demo` now writes `<device>.lua` too
|
|
20
|
+
- synthetic captures: submission and completion now share one URB id and use proper usbmon data flags,
|
|
21
|
+
so Wireshark pairs requests with responses (GET DESCRIPTOR responses and VID/PID are decoded)
|
|
22
|
+
- `--labels`: analyse plain Wireshark/USBPcap captures without markers by splitting traffic at the pauses between clicks
|
|
23
|
+
- inference: 32-bit LE/BE linear values and packed bitfields (contiguous bits beside non-zero const bits,
|
|
24
|
+
including 12-bit spans across two LE bytes); synthetic `dial` device + encode round-trip tests
|
|
25
|
+
- HID report descriptor decoder (`usb-rea hid`, MCP tool `hid_descriptor`): items, reports, bit-exact fields,
|
|
26
|
+
report IDs, Push/Pop, extended usages, payload decoding; checked against the HID 1.11 boot mouse/keyboard descriptors
|
|
27
|
+
- reports include a HID section and tag each inferred command with its HID report when the capture has GET_DESCRIPTOR 0x22
|
|
28
|
+
- Dockerfile and glama.json so the MCP server can be listed on Glama
|
|
29
|
+
- `usb-rea demo --device dial` (32-bit value + bitfield demo); README capability table, MCP tool list and roadmap
|
|
30
|
+
updated for 32-bit/bitfield inference and `hid_descriptor`
|
|
31
|
+
- registry metadata: `server.json` (official MCP Registry, `mcp-name` marker in README) and `smithery.yaml`
|
|
32
|
+
- listed on Glama (score badge in README); MCP install snippets now use the git URL so they work before the PyPI release
|
|
33
|
+
|
|
34
|
+
## v0.1.0 — 2026-10-14
|
|
35
|
+
First public release.
|
|
36
|
+
|
|
37
|
+
- pcap/pcapng parser for Linux usbmon (DLT 189/220) and Windows USBPcap (DLT 249), pure Python
|
|
38
|
+
- action markers (`usb-rea mark`) and per-action diffing with background-traffic filtering
|
|
39
|
+
- inference: constants, command ids, linear 8/16-bit LE/BE values, multi-value actions, lookup tables,
|
|
40
|
+
rolling counters, sum/xor/CRC8/CRC16 checksums; leave-one-out guard against coincidental fits
|
|
41
|
+
- Markdown evidence report: every claim linked to Wireshark frame numbers
|
|
42
|
+
- standalone pyusb driver generator (bulk/interrupt and control/HID SET_REPORT)
|
|
43
|
+
- confirmed-only replay, CLI, MCP server (capture, mark, diff, report, scaffold, replay)
|
|
44
|
+
- validation: synthetic devices, liquidctl Kraken X, rivalcfg Rival 100, public Wireshark captures
|
usb_rea-0.2.1/Dockerfile
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Stdio MCP server for Glama / local Docker use.
|
|
2
|
+
# Build: docker build -t usb-rea .
|
|
3
|
+
# Run: docker run --rm -i usb-rea (analysis of capture files)
|
|
4
|
+
# Live capture/replay needs host USB access: add --privileged -v /dev/bus/usb:/dev/bus/usb
|
|
5
|
+
FROM python:3.12-slim-bookworm
|
|
6
|
+
|
|
7
|
+
# libusb is needed by pyusb for replay/scaffolded drivers; analysis works without it
|
|
8
|
+
RUN apt-get update && apt-get install -y --no-install-recommends libusb-1.0-0 \
|
|
9
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
10
|
+
|
|
11
|
+
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 PIP_DISABLE_PIP_VERSION_CHECK=1
|
|
12
|
+
WORKDIR /app
|
|
13
|
+
COPY pyproject.toml README.md LICENSE ./
|
|
14
|
+
COPY src ./src
|
|
15
|
+
RUN pip install --no-cache-dir ".[mcp]"
|
|
16
|
+
|
|
17
|
+
RUN useradd --create-home --uid 1000 app
|
|
18
|
+
USER app
|
|
19
|
+
WORKDIR /home/app
|
|
20
|
+
|
|
21
|
+
ENTRYPOINT ["usb-rea", "mcp"]
|
usb_rea-0.2.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Emir Mrkaljević
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
usb_rea-0.2.1/PKG-INFO
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: usb-rea
|
|
3
|
+
Version: 0.2.1
|
|
4
|
+
Summary: Reverse Engineer any USB gadget: capture, diff, infer, and generate open drivers with evidence
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: mcp<3,>=1.2
|
|
9
|
+
Requires-Dist: pyusb>=1.2
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: mcp<3,>=1.2; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
13
|
+
Provides-Extra: mcp
|
|
14
|
+
Requires-Dist: mcp<3,>=1.2; extra == 'mcp'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# usb-rea — Reverse Engineer any USB gadget
|
|
18
|
+
|
|
19
|
+
<!-- mcp-name: io.github.emirmrkaljevic/usb-rea -->
|
|
20
|
+
|
|
21
|
+
[](https://github.com/emirmrkaljevic/usb-rea/actions/workflows/ci.yml)  [](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea)
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
> Status: alpha (v0.2.0). Validated on synthetic devices, on protocols documented by liquidctl and rivalcfg, and on public Wireshark captures — see [docs/VALIDATION.md](docs/VALIDATION.md). Not yet tested on a physical gadget: your captures are welcome.
|
|
28
|
+
|
|
29
|
+
## Try it in 10 seconds (no hardware needed)
|
|
30
|
+
```bash
|
|
31
|
+
pip install git+https://github.com/emirmrkaljevic/usb-rea
|
|
32
|
+
usb-rea demo --device fan
|
|
33
|
+
```
|
|
34
|
+
```
|
|
35
|
+
fan 100% 0x5a | counter (+1 per message) | 0x10 | u16be round(value * 1 + 0) | 0x00 | u16le crc16_modbus(bytes[0:6])
|
|
36
|
+
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])
|
|
37
|
+
mode 100% 0x5a | counter (+1 per message) | 0x30 | lookup 1→0x11, 2→0x22, 3→0x47 | 0x00 | 0x00 | u16le crc16_modbus(bytes[0:6])
|
|
38
|
+
```
|
|
39
|
+
It writes a synthetic capture of a fan controller, recovers the protocol, and writes `report.md` (every claim linked to Wireshark frame numbers) and a working pyusb driver that encodes values it never saw.
|
|
40
|
+
|
|
41
|
+
`--device lamp` (default) is a simple 8-bit device; `--device dial` shows 32-bit values and packed bitfields:
|
|
42
|
+
```
|
|
43
|
+
position 100% 0xa5 | 0x01 | u32le round(value * 1 + 0) | sum8(bytes[0:6])
|
|
44
|
+
level 100% 0xa5 | 0x02 | bits[0:4] = round(value * 1 + 0) | 0x00 | sum8(bytes[0:4])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## What it can infer
|
|
48
|
+
| Role | Example |
|
|
49
|
+
|---|---|
|
|
50
|
+
| constant header / command id | `5a`, `10` vs `20` vs `30` |
|
|
51
|
+
| linear values, 8-, 16- or 32-bit LE/BE, several per action | `fan 2650` → `0a 5a`; `color 255 24 0`; `position 50000` → `50 c3 00 00` |
|
|
52
|
+
| packed bitfields (bits beside constant flag bits, incl. 12-bit spans over two bytes) | `level 7` → `b7` = flags `b0` + bits[0:4] |
|
|
53
|
+
| lookup tables (non-linear choices) | DPI 250→`08`, 500→`07` … |
|
|
54
|
+
| rolling counters | sequence byte +1 per message |
|
|
55
|
+
| checksums | sum8, xor8, neg/inv sum8, sum16, CRC8, CRC8/MAXIM, CRC16 CCITT-FALSE/XMODEM/MODBUS/KERMIT |
|
|
56
|
+
|
|
57
|
+
Linear fits must survive leave-one-out cross-checks; otherwise usb-rea falls back to an honest lookup table and says so.
|
|
58
|
+
|
|
59
|
+
## Real device workflow (Linux)
|
|
60
|
+
```bash
|
|
61
|
+
sudo modprobe usbmon
|
|
62
|
+
usb-rea capture start -o lamp.pcapng
|
|
63
|
+
usb-rea mark -m lamp.pcapng.actions.json "brightness 0%" # then click it in the vendor app
|
|
64
|
+
usb-rea mark -m lamp.pcapng.actions.json "brightness 50%" # ...repeat for 3+ values per setting
|
|
65
|
+
usb-rea capture stop
|
|
66
|
+
usb-rea report lamp.pcapng -o report.md
|
|
67
|
+
usb-rea scaffold lamp.pcapng -o lamp_driver.py
|
|
68
|
+
usb-rea dissector lamp.pcapng -o lamp.lua # Wireshark dissector for the same protocol
|
|
69
|
+
```
|
|
70
|
+
Captured on Windows with USBPcap/Wireshark and no markers? Click each action with a short pause in between, then just list them in order — usb-rea splits the traffic at the pauses:
|
|
71
|
+
```bash
|
|
72
|
+
usb-rea report lamp.pcapng --labels "brightness 0%; brightness 50%; brightness 100%; power off; power on"
|
|
73
|
+
usb-rea scaffold lamp.pcapng --labels labels.txt -o lamp_driver.py # one label per line
|
|
74
|
+
```
|
|
75
|
+
(Markers, if you have them, are a JSON list of `{"label", "start", "end"}` epoch timestamps.)
|
|
76
|
+
|
|
77
|
+
## MCP server
|
|
78
|
+
`pip install "usb-rea[mcp] @ git+https://github.com/emirmrkaljevic/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.
|
|
79
|
+
|
|
80
|
+
With [uv](https://docs.astral.sh/uv/) no install step is needed:
|
|
81
|
+
```json
|
|
82
|
+
{"mcpServers": {"usb-rea": {"command": "uvx", "args": ["--from", "usb-rea[mcp] @ git+https://github.com/emirmrkaljevic/usb-rea", "usb-rea", "mcp"]}}}
|
|
83
|
+
```
|
|
84
|
+
Once usb-rea is on PyPI, `--from "usb-rea[mcp]"` is enough. The server is also listed on [Glama](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea), which builds it from the repo's `Dockerfile` and checks that it starts and lists its tools.
|
|
85
|
+
|
|
86
|
+
## How it works
|
|
87
|
+
1. `usb-rea capture start` — record traffic (usbmon / USBPcap)
|
|
88
|
+
2. `usb-rea mark "brightness 50%"` — label each action you take in the vendor app
|
|
89
|
+
3. `usb-rea diff` — which bytes change per action, value correlation, checksums, counters
|
|
90
|
+
4. `usb-rea infer` — message templates + evidence report
|
|
91
|
+
5. `usb-rea scaffold` — generate a pyusb driver and replay tests
|
|
92
|
+
6. `usb-rea dissector` — write a Wireshark Lua dissector for the inferred protocol
|
|
93
|
+
7. `usb-rea replay` — send packets to the device (only with explicit confirmation)
|
|
94
|
+
8. `usb-rea hid` — decode a HID report descriptor from a capture (GET_DESCRIPTOR 0x22) or from hex, and decode report payloads
|
|
95
|
+
9. `usb-rea mcp` — the same tools as an MCP server for AI agents
|
|
96
|
+
|
|
97
|
+
### Wireshark dissector
|
|
98
|
+
`usb-rea dissector capture.pcapng -o lamp.lua` turns the inferred protocol into a Wireshark Lua plugin, so you can keep
|
|
99
|
+
capturing and read the traffic as commands instead of hex:
|
|
100
|
+
```bash
|
|
101
|
+
wireshark -X lua_script:lamp.lua capture.pcapng
|
|
102
|
+
tshark -X lua_script:lamp.lua -r capture.pcapng -Y 'usbrea.command == "brightness"'
|
|
103
|
+
```
|
|
104
|
+
Each matching packet gets a `usbrea` tree with the command name, decoded values (`usbrea.brightness.value`), counters,
|
|
105
|
+
lookup labels, and a checksum check (`usbrea.checksum_ok`, with an expert warning on mismatch); the Info column reads
|
|
106
|
+
`brightness 50`. It works for bulk, interrupt and control/HID SET_REPORT transfers, from usbmon or USBPcap, and needs no
|
|
107
|
+
bit library (Wireshark 3.x and 4.x). `-n NAME` changes the filter prefix. CI runs every generated dissector through tshark.
|
|
108
|
+
|
|
109
|
+
### HID devices
|
|
110
|
+
Most RGB keyboards, mice, headsets and LCD gadgets are HID with a vendor-defined page. If the capture includes the
|
|
111
|
+
report descriptor (start capturing, then replug the device), the report gets a section with every report's ID, size and
|
|
112
|
+
fields, and each inferred command is tagged with the HID report it travels in. Without a capture:
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
usb-rea hid /sys/class/hidraw/hidraw0/device/report_descriptor --items
|
|
116
|
+
usb-rea hid capture.pcapng --decode "10ff0d1d000000" --type Output
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Roadmap
|
|
120
|
+
- [x] pcap/pcapng parser (usbmon + USBPcap)
|
|
121
|
+
- [x] action markers and per-action diffing
|
|
122
|
+
- [x] checksum detection (sum8, xor8, neg/inv sum8, CRC8, CRC16 variants)
|
|
123
|
+
- [x] evidence-backed protocol report
|
|
124
|
+
- [x] driver scaffolding (pyusb, bulk/interrupt + control/HID SET_REPORT)
|
|
125
|
+
- [x] confirmed replay
|
|
126
|
+
- [x] MCP server
|
|
127
|
+
- [x] rolling counters, multi-byte (16/32-bit LE/BE) values, lookup tables, multi-value actions
|
|
128
|
+
- [x] packed bitfields
|
|
129
|
+
- [x] validation against documented protocols (liquidctl, rivalcfg)
|
|
130
|
+
- [x] HID report descriptor decoding (`usb-rea hid`), with captured commands mapped to their HID reports
|
|
131
|
+
- [ ] emulated-device CI (dummy_hcd + configfs HID; script in `scripts/gadget_e2e.sh`)
|
|
132
|
+
- [ ] real-hardware case studies
|
|
133
|
+
- [x] multi-packet transactions (write + apply), including checksums over earlier packets
|
|
134
|
+
- [ ] state machines (request/response handshakes)
|
|
135
|
+
|
|
136
|
+
## Legal
|
|
137
|
+
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.
|
|
138
|
+
|
|
139
|
+
## License
|
|
140
|
+
MIT
|
usb_rea-0.2.1/README.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# usb-rea — Reverse Engineer any USB gadget
|
|
2
|
+
|
|
3
|
+
<!-- mcp-name: io.github.emirmrkaljevic/usb-rea -->
|
|
4
|
+
|
|
5
|
+
[](https://github.com/emirmrkaljevic/usb-rea/actions/workflows/ci.yml)  [](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea)
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
> Status: alpha (v0.2.0). Validated on synthetic devices, on protocols documented by liquidctl and rivalcfg, and on public Wireshark captures — see [docs/VALIDATION.md](docs/VALIDATION.md). Not yet tested on a physical gadget: your captures are welcome.
|
|
12
|
+
|
|
13
|
+
## Try it in 10 seconds (no hardware needed)
|
|
14
|
+
```bash
|
|
15
|
+
pip install git+https://github.com/emirmrkaljevic/usb-rea
|
|
16
|
+
usb-rea demo --device fan
|
|
17
|
+
```
|
|
18
|
+
```
|
|
19
|
+
fan 100% 0x5a | counter (+1 per message) | 0x10 | u16be round(value * 1 + 0) | 0x00 | u16le crc16_modbus(bytes[0:6])
|
|
20
|
+
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])
|
|
21
|
+
mode 100% 0x5a | counter (+1 per message) | 0x30 | lookup 1→0x11, 2→0x22, 3→0x47 | 0x00 | 0x00 | u16le crc16_modbus(bytes[0:6])
|
|
22
|
+
```
|
|
23
|
+
It writes a synthetic capture of a fan controller, recovers the protocol, and writes `report.md` (every claim linked to Wireshark frame numbers) and a working pyusb driver that encodes values it never saw.
|
|
24
|
+
|
|
25
|
+
`--device lamp` (default) is a simple 8-bit device; `--device dial` shows 32-bit values and packed bitfields:
|
|
26
|
+
```
|
|
27
|
+
position 100% 0xa5 | 0x01 | u32le round(value * 1 + 0) | sum8(bytes[0:6])
|
|
28
|
+
level 100% 0xa5 | 0x02 | bits[0:4] = round(value * 1 + 0) | 0x00 | sum8(bytes[0:4])
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## What it can infer
|
|
32
|
+
| Role | Example |
|
|
33
|
+
|---|---|
|
|
34
|
+
| constant header / command id | `5a`, `10` vs `20` vs `30` |
|
|
35
|
+
| linear values, 8-, 16- or 32-bit LE/BE, several per action | `fan 2650` → `0a 5a`; `color 255 24 0`; `position 50000` → `50 c3 00 00` |
|
|
36
|
+
| packed bitfields (bits beside constant flag bits, incl. 12-bit spans over two bytes) | `level 7` → `b7` = flags `b0` + bits[0:4] |
|
|
37
|
+
| lookup tables (non-linear choices) | DPI 250→`08`, 500→`07` … |
|
|
38
|
+
| rolling counters | sequence byte +1 per message |
|
|
39
|
+
| checksums | sum8, xor8, neg/inv sum8, sum16, CRC8, CRC8/MAXIM, CRC16 CCITT-FALSE/XMODEM/MODBUS/KERMIT |
|
|
40
|
+
|
|
41
|
+
Linear fits must survive leave-one-out cross-checks; otherwise usb-rea falls back to an honest lookup table and says so.
|
|
42
|
+
|
|
43
|
+
## Real device workflow (Linux)
|
|
44
|
+
```bash
|
|
45
|
+
sudo modprobe usbmon
|
|
46
|
+
usb-rea capture start -o lamp.pcapng
|
|
47
|
+
usb-rea mark -m lamp.pcapng.actions.json "brightness 0%" # then click it in the vendor app
|
|
48
|
+
usb-rea mark -m lamp.pcapng.actions.json "brightness 50%" # ...repeat for 3+ values per setting
|
|
49
|
+
usb-rea capture stop
|
|
50
|
+
usb-rea report lamp.pcapng -o report.md
|
|
51
|
+
usb-rea scaffold lamp.pcapng -o lamp_driver.py
|
|
52
|
+
usb-rea dissector lamp.pcapng -o lamp.lua # Wireshark dissector for the same protocol
|
|
53
|
+
```
|
|
54
|
+
Captured on Windows with USBPcap/Wireshark and no markers? Click each action with a short pause in between, then just list them in order — usb-rea splits the traffic at the pauses:
|
|
55
|
+
```bash
|
|
56
|
+
usb-rea report lamp.pcapng --labels "brightness 0%; brightness 50%; brightness 100%; power off; power on"
|
|
57
|
+
usb-rea scaffold lamp.pcapng --labels labels.txt -o lamp_driver.py # one label per line
|
|
58
|
+
```
|
|
59
|
+
(Markers, if you have them, are a JSON list of `{"label", "start", "end"}` epoch timestamps.)
|
|
60
|
+
|
|
61
|
+
## MCP server
|
|
62
|
+
`pip install "usb-rea[mcp] @ git+https://github.com/emirmrkaljevic/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.
|
|
63
|
+
|
|
64
|
+
With [uv](https://docs.astral.sh/uv/) no install step is needed:
|
|
65
|
+
```json
|
|
66
|
+
{"mcpServers": {"usb-rea": {"command": "uvx", "args": ["--from", "usb-rea[mcp] @ git+https://github.com/emirmrkaljevic/usb-rea", "usb-rea", "mcp"]}}}
|
|
67
|
+
```
|
|
68
|
+
Once usb-rea is on PyPI, `--from "usb-rea[mcp]"` is enough. The server is also listed on [Glama](https://glama.ai/mcp/servers/emirmrkaljevic/usb-rea), which builds it from the repo's `Dockerfile` and checks that it starts and lists its tools.
|
|
69
|
+
|
|
70
|
+
## How it works
|
|
71
|
+
1. `usb-rea capture start` — record traffic (usbmon / USBPcap)
|
|
72
|
+
2. `usb-rea mark "brightness 50%"` — label each action you take in the vendor app
|
|
73
|
+
3. `usb-rea diff` — which bytes change per action, value correlation, checksums, counters
|
|
74
|
+
4. `usb-rea infer` — message templates + evidence report
|
|
75
|
+
5. `usb-rea scaffold` — generate a pyusb driver and replay tests
|
|
76
|
+
6. `usb-rea dissector` — write a Wireshark Lua dissector for the inferred protocol
|
|
77
|
+
7. `usb-rea replay` — send packets to the device (only with explicit confirmation)
|
|
78
|
+
8. `usb-rea hid` — decode a HID report descriptor from a capture (GET_DESCRIPTOR 0x22) or from hex, and decode report payloads
|
|
79
|
+
9. `usb-rea mcp` — the same tools as an MCP server for AI agents
|
|
80
|
+
|
|
81
|
+
### Wireshark dissector
|
|
82
|
+
`usb-rea dissector capture.pcapng -o lamp.lua` turns the inferred protocol into a Wireshark Lua plugin, so you can keep
|
|
83
|
+
capturing and read the traffic as commands instead of hex:
|
|
84
|
+
```bash
|
|
85
|
+
wireshark -X lua_script:lamp.lua capture.pcapng
|
|
86
|
+
tshark -X lua_script:lamp.lua -r capture.pcapng -Y 'usbrea.command == "brightness"'
|
|
87
|
+
```
|
|
88
|
+
Each matching packet gets a `usbrea` tree with the command name, decoded values (`usbrea.brightness.value`), counters,
|
|
89
|
+
lookup labels, and a checksum check (`usbrea.checksum_ok`, with an expert warning on mismatch); the Info column reads
|
|
90
|
+
`brightness 50`. It works for bulk, interrupt and control/HID SET_REPORT transfers, from usbmon or USBPcap, and needs no
|
|
91
|
+
bit library (Wireshark 3.x and 4.x). `-n NAME` changes the filter prefix. CI runs every generated dissector through tshark.
|
|
92
|
+
|
|
93
|
+
### HID devices
|
|
94
|
+
Most RGB keyboards, mice, headsets and LCD gadgets are HID with a vendor-defined page. If the capture includes the
|
|
95
|
+
report descriptor (start capturing, then replug the device), the report gets a section with every report's ID, size and
|
|
96
|
+
fields, and each inferred command is tagged with the HID report it travels in. Without a capture:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
usb-rea hid /sys/class/hidraw/hidraw0/device/report_descriptor --items
|
|
100
|
+
usb-rea hid capture.pcapng --decode "10ff0d1d000000" --type Output
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Roadmap
|
|
104
|
+
- [x] pcap/pcapng parser (usbmon + USBPcap)
|
|
105
|
+
- [x] action markers and per-action diffing
|
|
106
|
+
- [x] checksum detection (sum8, xor8, neg/inv sum8, CRC8, CRC16 variants)
|
|
107
|
+
- [x] evidence-backed protocol report
|
|
108
|
+
- [x] driver scaffolding (pyusb, bulk/interrupt + control/HID SET_REPORT)
|
|
109
|
+
- [x] confirmed replay
|
|
110
|
+
- [x] MCP server
|
|
111
|
+
- [x] rolling counters, multi-byte (16/32-bit LE/BE) values, lookup tables, multi-value actions
|
|
112
|
+
- [x] packed bitfields
|
|
113
|
+
- [x] validation against documented protocols (liquidctl, rivalcfg)
|
|
114
|
+
- [x] HID report descriptor decoding (`usb-rea hid`), with captured commands mapped to their HID reports
|
|
115
|
+
- [ ] emulated-device CI (dummy_hcd + configfs HID; script in `scripts/gadget_e2e.sh`)
|
|
116
|
+
- [ ] real-hardware case studies
|
|
117
|
+
- [x] multi-packet transactions (write + apply), including checksums over earlier packets
|
|
118
|
+
- [ ] state machines (request/response handshakes)
|
|
119
|
+
|
|
120
|
+
## Legal
|
|
121
|
+
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.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Validation
|
|
2
|
+
|
|
3
|
+
usb-rea is validated without anyone's hardware in four ways. All of them run in CI (`pytest`).
|
|
4
|
+
|
|
5
|
+
| Suite | What it proves | File |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| Synthetic lamp | constant header, command byte, scaled value, sum8, over bulk and HID SET_REPORT | `tests/test_infer.py` |
|
|
8
|
+
| Synthetic fan controller | rolling counter, 16-bit big-endian value, 3-value RGB, lookup table, CRC16/MODBUS | `tests/test_infer_advanced.py` |
|
|
9
|
+
| liquidctl Kraken X (1e71:170e) | recovers `02 4d <channel> 00 <duty>` exactly as liquidctl's `kraken2.py` builds it | `tests/test_validation.py` |
|
|
10
|
+
| rivalcfg Rival 100 (1038:1702) | recovers non-linear DPI/polling lookup tables and RGB color as in rivalcfg's `rival100.py` | `tests/test_validation.py` |
|
|
11
|
+
| CRC catalogue | CRC8/CRC16 variants match the standard `123456789` check values | `tests/test_checksums.py` |
|
|
12
|
+
| Wireshark dissector | generated Lua plugin labels every command in tshark 4.4, values match, checksums verify, a corrupted checksum is flagged; Lua checksum helpers match the Python ones | `tests/test_dissector.py` |
|
|
13
|
+
| Real captures | parser handles public usbmon/USBPcap captures (downloaded on demand) | `tests/test_public_pcaps.py` |
|
|
14
|
+
|
|
15
|
+
The driver generated from each capture is also executed against a mocked pyusb device and must emit
|
|
16
|
+
byte-identical packets for values that were **never captured**.
|
|
17
|
+
|
|
18
|
+
## Not yet covered
|
|
19
|
+
* Emulated USB gadget (dummy_hcd + raw-gadget). It needs a kernel with `CONFIG_USB_DUMMY_HCD` and
|
|
20
|
+
`CONFIG_USB_RAW_GADGET`; the development box runs a sandboxed kernel without them. Script planned in
|
|
21
|
+
`scripts/gadget_e2e.sh` for anyone with a normal Linux machine.
|
|
22
|
+
* Real hardware. Captures + action labels from users are very welcome (open an issue).
|
|
Binary file
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Regenerate with: vhs docs/demo.tape
|
|
2
|
+
Output docs/demo.gif
|
|
3
|
+
Set FontSize 15
|
|
4
|
+
Set Width 1100
|
|
5
|
+
Set Height 640
|
|
6
|
+
Set Theme "Dracula"
|
|
7
|
+
Set TypingSpeed 45ms
|
|
8
|
+
Hide
|
|
9
|
+
Type `export PATH=$PWD/.venv/bin:$PATH PS1='$ ' && cd $(mktemp -d) && clear` Enter
|
|
10
|
+
Show
|
|
11
|
+
Type "# a fan controller with a Windows-only app: capture + click labels -> protocol + driver" Enter
|
|
12
|
+
Sleep 600ms
|
|
13
|
+
Type "usb-rea demo --device fan -o fan" Enter
|
|
14
|
+
Sleep 2500ms
|
|
15
|
+
Type "sed -n '/^## .fan/,/^Evidence/p' fan/report.md" Enter
|
|
16
|
+
Sleep 3500ms
|
|
17
|
+
Type `python -c 'import fan.fan_driver as d; print(d.encode(d.FAN, [3100], seq=7).hex(" "))'` Enter
|
|
18
|
+
Sleep 3500ms
|
usb_rea-0.2.1/glama.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "usb-rea"
|
|
7
|
+
version = "0.2.1"
|
|
8
|
+
description = "Reverse Engineer any USB gadget: capture, diff, infer, and generate open drivers with evidence"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
dependencies = ["pyusb>=1.2", "mcp>=1.2,<3"]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
dev = ["pytest>=8", "mcp>=1.2,<3"]
|
|
16
|
+
mcp = ["mcp>=1.2,<3"]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
usb-rea = "usb_rea.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.hatch.build.targets.wheel]
|
|
22
|
+
packages = ["src/usb_rea"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Emulated-device end-to-end test (EXPERIMENTAL, not run in CI yet).
|
|
3
|
+
#
|
|
4
|
+
# Needs a normal Linux kernel with dummy_hcd, libcomposite (usb_f_hid) and usbmon, plus root.
|
|
5
|
+
# Creates a virtual HID "lamp" on dummy_hcd via configfs, captures real kernel USB traffic with
|
|
6
|
+
# usbmon while writing lamp commands to it, runs usb-rea on the capture, and checks that the
|
|
7
|
+
# device side received exactly what the generated driver would send.
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
[[ $EUID -eq 0 ]] || { echo "run as root"; exit 1; }
|
|
10
|
+
command -v dumpcap >/dev/null || { echo "need dumpcap (apt install tshark)"; exit 1; }
|
|
11
|
+
modprobe dummy_hcd libcomposite usbmon
|
|
12
|
+
|
|
13
|
+
G=/sys/kernel/config/usb_gadget/usbrea_lamp
|
|
14
|
+
cleanup() {
|
|
15
|
+
[[ -n "${CAP_PID:-}" ]] && kill "$CAP_PID" 2>/dev/null || true
|
|
16
|
+
if [[ -d $G ]]; then
|
|
17
|
+
echo "" > $G/UDC 2>/dev/null || true
|
|
18
|
+
rm -f $G/configs/c.1/hid.usb0; rmdir $G/configs/c.1/strings/0x409 $G/configs/c.1 \
|
|
19
|
+
$G/functions/hid.usb0 $G/strings/0x409 $G 2>/dev/null || true
|
|
20
|
+
fi
|
|
21
|
+
}
|
|
22
|
+
trap cleanup EXIT
|
|
23
|
+
|
|
24
|
+
mkdir -p $G && cd $G
|
|
25
|
+
echo 0x1234 > idVendor; echo 0xabcd > idProduct
|
|
26
|
+
mkdir -p strings/0x409; echo usb-rea > strings/0x409/manufacturer; echo "Fake Lamp" > strings/0x409/product
|
|
27
|
+
mkdir -p functions/hid.usb0
|
|
28
|
+
echo 0 > functions/hid.usb0/protocol; echo 0 > functions/hid.usb0/subclass; echo 5 > functions/hid.usb0/report_length
|
|
29
|
+
# vendor-defined report: 5 bytes in, 5 bytes out
|
|
30
|
+
printf '\x06\x00\xff\x09\x01\xa1\x01\x15\x00\x26\xff\x00\x75\x08\x95\x05\x09\x01\x81\x02\x95\x05\x09\x01\x91\x02\xc0' \
|
|
31
|
+
> functions/hid.usb0/report_desc
|
|
32
|
+
mkdir -p configs/c.1/strings/0x409; echo lamp > configs/c.1/strings/0x409/configuration
|
|
33
|
+
ln -s functions/hid.usb0 configs/c.1/
|
|
34
|
+
UDC=$(ls /sys/class/udc | grep dummy | head -1); echo "$UDC" > UDC
|
|
35
|
+
sleep 1
|
|
36
|
+
cd - >/dev/null
|
|
37
|
+
|
|
38
|
+
BUS=$(for d in /sys/bus/usb/devices/*; do [[ -f $d/idVendor && $(cat $d/idVendor) == 1234 ]] && cat $d/busnum; done | head -1)
|
|
39
|
+
HIDRAW=/dev/$(ls /sys/class/hidraw | while read h; do grep -q 1234 /sys/class/hidraw/$h/device/uevent 2>/dev/null && echo $h; done | head -1)
|
|
40
|
+
OUT=$(mktemp -d)
|
|
41
|
+
dumpcap -q -i usbmon$BUS -w $OUT/lamp.pcapng & CAP_PID=$!
|
|
42
|
+
sleep 1
|
|
43
|
+
cat /dev/hidg0 > $OUT/device_side.bin & READER=$!
|
|
44
|
+
|
|
45
|
+
python3 - "$HIDRAW" "$OUT" <<'PY'
|
|
46
|
+
import json, sys, time
|
|
47
|
+
from usb_rea.synth import encode
|
|
48
|
+
hidraw, out = sys.argv[1], sys.argv[2]
|
|
49
|
+
marks = []
|
|
50
|
+
with open(hidraw, "wb", buffering=0) as f:
|
|
51
|
+
for label in ["brightness 0%", "brightness 30%", "brightness 60%", "brightness 100%",
|
|
52
|
+
"hue 0", "hue 90", "hue 300"]:
|
|
53
|
+
name, val = label.split()[0], float(label.split()[1].rstrip("%"))
|
|
54
|
+
start = time.time(); time.sleep(0.2)
|
|
55
|
+
f.write(b"\x00" + encode(name, val)) # report id 0 + payload
|
|
56
|
+
time.sleep(0.3); marks.append({"label": label, "start": start, "end": time.time()})
|
|
57
|
+
json.dump(marks, open(f"{out}/lamp.pcapng.actions.json", "w"))
|
|
58
|
+
PY
|
|
59
|
+
sleep 1; kill $CAP_PID; CAP_PID=; kill $READER || true
|
|
60
|
+
usb-rea report $OUT/lamp.pcapng -o $OUT/report.md
|
|
61
|
+
python3 - "$OUT" <<'PY'
|
|
62
|
+
import sys
|
|
63
|
+
from usb_rea.pipeline import analyze
|
|
64
|
+
from usb_rea.synth import encode
|
|
65
|
+
p = analyze(f"{sys.argv[1]}/lamp.pcapng")
|
|
66
|
+
c = {c.name: c for c in p.commands}
|
|
67
|
+
assert c["brightness"].encode(45) == encode("brightness", 45), c["brightness"].fields
|
|
68
|
+
assert c["hue"].encode(200) == encode("hue", 200)
|
|
69
|
+
print("gadget e2e OK:", sys.argv[1])
|
|
70
|
+
PY
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-09-29/server.schema.json",
|
|
3
|
+
"name": "io.github.emirmrkaljevic/usb-rea",
|
|
4
|
+
"description": "Reverse engineer USB gadgets: capture traffic, infer the protocol, generate a pyusb driver.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "https://github.com/emirmrkaljevic/usb-rea",
|
|
7
|
+
"source": "github"
|
|
8
|
+
},
|
|
9
|
+
"version": "0.2.0",
|
|
10
|
+
"packages": [
|
|
11
|
+
{
|
|
12
|
+
"registryType": "pypi",
|
|
13
|
+
"identifier": "usb-rea",
|
|
14
|
+
"version": "0.2.0",
|
|
15
|
+
"transport": {
|
|
16
|
+
"type": "stdio"
|
|
17
|
+
},
|
|
18
|
+
"runtimeHint": "uvx",
|
|
19
|
+
"packageArguments": [
|
|
20
|
+
{
|
|
21
|
+
"type": "positional",
|
|
22
|
+
"value": "mcp"
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|