MHS — operating real hardware
The hardware capability, qwen-mm-plugins-mhs. It implements an MHS host:
one fixed six-tool surface the model uses to operate any device, with everything device-specific
pushed behind an adapter.
For background on MHS, see Anthropic's Previewing the Model Hardware Standard (August 27, 2026).
The point of the split is that the adapter belongs to the hardware, not to this plugin. Whoever owns a camera writes and runs its adapter; the plugin only needs to know where it is. So the tools the model sees never change as hardware is added, and this repository contains no driver for anything.
| Tool | What it answers |
|---|---|
mhs_discover | What devices exist, of what type, in what state, with which capabilities |
mhs_meta_info | What one device is, what each capability takes, and what it will refuse |
mhs_read | A sensor value, a current setting, a camera frame |
mhs_write | Send a command — the only tool that changes the physical world |
mhs_health_check | Is it alive, right now (never cached) |
mhs_reset | Return to a known-good state, or stop it immediately |
Try it without hardware
The capability ships a mock adapter with two simulated devices, backed by the shared
adapter_server.py HTTP + MessagePack layer. It lives
beside the Skill, so it is present in a repository checkout and in a plugin install (which copies the
whole capability directory, Skill included). It is not part of the pip/uvx wheel, which packages only
the server module.
# install the adapter dependency in your Python environment
python3 -m pip install "msgpack>=1.1,<2"
# from a checkout
python3 src/capabilities/mhs/skill/references/mock_adapter.py --port 8800
# from a plugin install, under the installed skill directory
python3 <skills>/qwen-mm-plugins-mhs/references/mock_adapter.py --port 8800
Register it:
mkdir -p ~/.qwen-mm-plugins
cat > ~/.qwen-mm-plugins/mhs-devices.json <<'JSON'
{"adapters": [{"name": "mock", "url": "http://127.0.0.1:8800"}]}
JSON
Then ask naturally:
What hardware can you see?
Take a picture with the camera and tell me what's in it.
Set the camera exposure to 75 and take another picture. Did it get brighter?
Turn the lamp on.
Set the exposure to 5000.
What you should see:
- "Set the exposure to 5000" is refused. The mock camera declares a hard limit of 1–100 ms, and the
host enforces it before the request leaves the process.
confirm=truecannot override a hard limit. - "Turn the lamp on" is refused once, because the lamp marks
poweras requiring confirmation, and succeeds when the model re-issues it withconfirm=true. - Gain above 32 dB is a soft limit: refused, but overridable with
confirm=true. mhs_reseton the lamp reports that this device does not implement reset — legal in MHS, and something the user needs to know, because it means that lamp cannot be stopped through the model.
Configuration
~/.qwen-mm-plugins/mhs-devices.json (override the path with QWEN_MM_MHS_DEVICES):
{
"adapters": [
{ "name": "lab-cam", "url": "http://192.168.1.20:8800" },
{ "name": "arm", "url": "https://arm.internal", "timeout": 30,
"auth": { "type": "bearer", "token_env": "ARM_TOKEN" } }
]
}
Only the name of the environment variable holding a token goes in this file — never the token.
Every mhs_discover call reloads this file and queries each adapter; device lists and health are
never cached. An empty adapters list is valid. QWEN_MM_MHS_CACHE_TTL (default 60s) applies only
to metadata, which discovery and reset also invalidate.
Requests are sent with proxies explicitly disabled: hardware is normally on the LAN, and an ambient
HTTP_PROXY intercepting those calls looks exactly like broken hardware.
Installing
claude plugin marketplace add https://github.com/QwenLM/Qwen-MM-Plugins
claude plugin install qwen-mm-plugins-mhs@qwen-mm-plugins
No cloud key and no system tools — the host uses stdlib HTTP and MessagePack. Pair it with
qwen-mm-plugins-api or qwen-mm-plugins-core if you want the model to analyze the frames it reads.
Letting the model write the adapter
Because the registry file is re-read on every call and devices are discovered from adapters at runtime, an adapter written mid-session works immediately — no restart, no change to this plugin. The Skill tells the model it may do this when the user has reachable hardware with no adapter for it:
This box has network interfaces but no MHS adapter. Write one and show me eth0's link state.
The model reads the protocol spec, writes an adapter, starts it, checks it against the protocol,
adds it to the registry, and calls mhs_discover. A worked run against a real host NIC:
mhs_discover → host/eth0, host/lo, host/dummy0 (device_type network_interface)
mhs_read link → operstate = up · carrier = True · mtu = 1450 bytes · speed = 100000 Mb/s
mhs_read counters → rx_bytes = 2548886186 · tx_packets = 19099118 · rx_errors = 0
mhs_write link → Error: capability 'link' on host/eth0 is read-only
mhs_reset → Error: host/eth0 does not implement reset (HTTP 405)
Preserve existing adapter entries when registering a new one. Write the updated JSON to a temporary
file in the same directory, then atomically replace the registry. The next mhs_discover call sees
new or removed registrations, changed addresses, and disconnected adapters without a refresh flag.
Note the last two. The adapter declared every capability direction: "read", so the host refused the
write locally — the request never reached the adapter — and reset returned an honest 405 rather than
pretending to bounce a live interface. The Skill instructs the model to start read-only for exactly this
reason: a read-only declaration is host-enforced, so it makes an accidental write unreachable, and it
costs nothing to add later.
An adapter the model wrote has had no review. The Skill requires it to tell you what the adapter talks to and which capabilities can change physical state before using it on anything that moves.
Writing an adapter for real hardware
An adapter is a plain HTTP server answering six routes. Any language; no SDK.
-
Read
adapter_protocol.md— the complete contract. -
Copy
adapter_server.pybeside your device implementation. Usemock_adapter.pyas an example ofsummary,meta,health,read, andwritemethods, plus optionalreset, and replace simulated state with real I/O. -
Check it before trusting it:
python3 -m qwen_mm_plugins_mhs.verify http://127.0.0.1:8800Read-only — it never writes and never resets — and it validates through the host's own protocol module, so a PASS means the host agrees with your adapter rather than a second copy of the rules agreeing. It reports missing
blockskeys, malformed image payloads, forgottendirectionfields, writable parameters with no declared bound, and error paths that answer 200 instead of 404. Exit status is non-zero on any failure, so it drops straight into CI. -
Declare
safety_limitshonestly. The host enforces what you declare, so this is the cheapest place to make an unsafe command impossible. Keep enforcing them in the adapter too — host-side checking exists so a bad guess costs a round trip, not a machine. -
Mark every write a person should intend with
requires_confirm: true. -
Add a line to the registry file. Nothing in this plugin changes.
Safety
These tools move physical things. The Skill instructs the model to read a device's metadata before its
first write, to treat a hard-limit refusal as information rather than an obstacle, to run
mhs_health_check before retrying a failed write, and to reach for mhs_reset mode=estop first when
something looks wrong. That is guidance, not a guarantee — the enforceable part is what your adapter
declares and what it refuses.
Cases
Unitree Go2: camera, state and bounded motion
This case contains a standalone adapter for the Go2 onboard computer. Start it in
read-only mode, connect the MHS host through SSH, and ask for the current front
camera frame and robot state. JPEG frames preserve aspect ratio with a maximum
long edge of 640 pixels. Movement requires explicitly starting the adapter with
--allow-motion and an operator present.
Follow the case setup and code guide. The case includes the server entry point, camera implementation, device implementation, SDK integration, pinned dependencies and offline tests. The setup guide includes commands to check out the complete case directory.
The tests use a fake SDK and generated frames; no physical robot is actuated by the cookbook build or test suite. Check firmware-specific state handling and stopping behavior on your own robot before enabling motion.