devusb-mcp
by JacobBeningo
README.md
# devusb
[](https://github.com/JacobBeningo/devusb/actions/workflows/ci.yml)
Agent-friendly USB power control for the embedded bench, built for the
Agentic Firmware Workbench. Switch, monitor, and power-cycle USB devices —
from the command line or from an AI agent via MCP.
Typical use: an agent flashing and testing firmware hits a hung debug probe or
wedged dev board. Instead of asking a human to unplug it, the agent runs
`power_cycle` and carries on.
## Supported hardware
devusb drives two kinds of hub through one interface.
**Yepkit YKUSH family** — dedicated bench hardware, spoken to directly over
USB HID (`hidapi`). No vendor utility needed. Addressed by serial number.
| Hub | Switched ports | Status |
|-----|----------------|--------|
| YKUSH3 | 3 | verified on hardware |
| YKUSH | 3 | same protocol, untested |
| YKUSH XS | 1 | same protocol, untested |
**Generic power-switchable hubs** — any hub advertising per-port power
switching (`ppps`), driven through
[uhubctl](https://github.com/mvp/uhubctl). Many ordinary hubs and docks
qualify, so this often works with hardware you already own. Addressed by
uhubctl *location* (e.g. `35-2.1.4`), since ordinary hubs rarely carry
unique serial numbers. Verified on Realtek RTS5411 and GenesysLogic hubs.
## Install
```bash
pip install .
```
Requires Python 3.10+. For generic hub support, also install uhubctl:
```bash
brew install uhubctl # macOS
sudo apt install uhubctl # Debian/Ubuntu
```
YKUSH control never depends on uhubctl — if it is missing, generic hubs are
simply absent from the listing.
### Linux: udev rules
On Linux, non-root access to the hub's HID interface needs a udev rule:
```bash
sudo cp udev/99-devusb.rules /etc/udev/rules.d/
sudo udevadm control --reload-rules && sudo udevadm trigger
```
Then unplug and replug the hub. macOS and Windows need no extra setup.
## CLI
```bash
devusb list # every controllable hub, both backends
devusb list --ykush-only # skip generic hubs
devusb status # power state of each port
devusb off 1 # cut power to port 1
devusb on 1 # restore power
devusb cycle 1 --hold 10 # power-cycle: off 10 s, back on
devusb on all # every port
devusb --json status # machine-readable output
```
`--hub` picks which hub to talk to — a YKUSH serial or a generic hub
location:
```bash
devusb --hub Y3N14127 status # YKUSH by serial
devusb --hub 35-2.1.4 status # generic hub by location
devusb --hub 35-2.1.4 cycle 2 # cycle a port on that hub
```
With exactly one YKUSH attached, `--hub` is optional. **Generic hubs must
always be named**: they share the listing with the rest of the machine, so
devusb will not pick one for you.
## Safety
Cutting power to the wrong port has real consequences — an unmounted disk, a
dark monitor, a dropped network link. Two rules apply everywhere:
- Never cut power to a device mid-flash.
- After power returns, give the device a few seconds to re-enumerate before
talking to it.
### The protected-port guard
Generic hubs get an extra guard, because a typical machine lists a lot of
hubs that have nothing to do with the bench. devusb refuses to switch off a
port whose downstream subtree contains storage, a display, input hardware, or
networking:
```console
$ devusb --hub 35-4.1 off 1
error: refusing to cut power to 35-4.1 port 1: network device attached
(0bda:8153 Realtek USB 10/100/1000 LAN 1010000CD). Pass force to override.
```
The check follows the whole subtree, not just the directly attached device —
cutting a port kills everything below it, including devices on a downstream
hub. `devusb list` marks guarded ports, and `devusb status` shows what is
attached to each port so you can see what a port number really means:
```console
$ devusb --hub 35-4.1 status
port 1: on [0bda:8153 Realtek USB 10/100/1000 LAN 1010000CD] (protected)
port 2: on [05e3:0620 GenesysLogic USB3.2 Hub, USB 3.20, 4 ports, ppps] (protected)
port 3: on
port 4: on [0bda:0411 Generic 4-Port USB 3.0 Hub, USB 3.00, 4 ports, ppps]
```
Pass `--force` (CLI) or `force=True` (MCP) to override. Detection is
best-effort keyword matching on what uhubctl reports, so treat it as a
seatbelt, not a proof: check `status` before switching an unfamiliar port.
YKUSH hubs have no guard — every port is a dedicated bench port.
### Interoperability with uhubctl
The YKUSH3 is built on Microchip hub silicon (USB5744/USB2744) that also
supports generic per-port power switching, so a YKUSH can appear under *both*
backends. Don't drive the same hub both ways: the YKUSH firmware doesn't
track changes made behind its back, and port numbering differs between the
two paths (e.g. YKUSH port 1 is internal hub port 3). Pick one controller per
hub — prefer the `ykush` backend, which is purpose-built.
## MCP server
`devusb-mcp` runs an MCP server over stdio exposing `list_hubs`,
`port_status`, `port_on`, `port_off`, and `power_cycle` — with agent-facing
safety guidance baked into the tool descriptions, including the instruction
to call `port_status` before switching a generic hub port and to confirm with
the user before forcing past the guard.
Register with Claude Code:
```bash
claude mcp add --scope user devusb devusb-mcp
```
Any MCP-capable agent host works the same way — point it at the
`devusb-mcp` executable.
## Roadmap
- **Acroname programmable hubs** — the premium lab option.
- **Paired USB 2.0 / 3.x port switching** for generic hubs: a physical port
often appears as separate hub instances per speed, and cutting only one
leaves the device half-powered. uhubctl handles this pairing; devusb
currently exposes each instance separately.
## Development
```bash
pip install -e ".[dev]"
pytest
```
The suite runs against fake HID and uhubctl layers — no hardware required.
The uhubctl fake renders real uhubctl output so the parser is exercised.
Before pushing, run it once with the host's tools hidden:
```bash
env PATH=/usr/bin:/bin "$(command -v python3)" -m pytest -q
```
The fakes are meant to be hermetic, but a developer machine has uhubctl
installed and CI does not — so a test that quietly reaches past the fake to
the real binary still passes locally and fails in CI. Stripping PATH catches
that before you push. (`$(command -v python3)` resolves the interpreter
*before* PATH is emptied; a bare `pytest` would no longer be findable.)
## License
MIT
This server cannot be deployed
Maintenance
ActivityStale
ResponsivenessNo issues