Specification · section 10
Three control surfaces
The same five primitives are reachable three ways. They compose: an agent uses MCP to explore and decide, writes a code file for the parts that must run fast or long, and uses the CLI to check on it.
MCP bridge
Eight generic tools, constant in the number of devices. The descriptor is the tool documentation, loaded on demand, so no per-device tool code is written.
Command line
The debugging and shell-scripting surface, and what an agent reaches for inside a Bash tool.
Code files
For work that must run for hours or faster than the model's reasoning loop. The devices execute; the model reads the result.
10.1 MCP bridge
| MCP tool | MHP call |
|---|---|
mhp_find | directory/search, or a scan of cards in a small lab |
mhp_describe | device/describe {detail, select}, default summary; with resource= it calls resources/read |
mhp_read | signals/read |
mhp_write | settings/write |
mhp_invoke | actions/invoke, optionally waiting for the job |
mhp_job | jobs/status or jobs/cancel |
mhp_estop | safety/estop on one device, or every device this session touched |
mhp_run | runs a script against the Lab client; returns stdout only (§3.4) |
mhp_lab | fleet management: scan, add, new, write, validate, list, remove (§9.1) |
resources mhp://<id>/descriptor, mhp://<id>/<path> | descriptor and package files of devices this session has opened |
Every tool ships input_examples. MHP errors surface as MCP tool results with isError: true and the structured mhpError body, so the model sees why a write was refused and can adjust. A refused call inside an mhp_run script returns the same body.
# any harness, scientist's laptop: one command
npx @sinkush/openmhp setup
# Claude Desktop / Claude Code, big lab: nothing loaded until searched
{"mcpServers": {"lab": {"command": "mhp-mcp", "args": ["--directory", "http://directory:18900"]}}}
# small lab: cards indexed at start, connections still lazy
{"mcpServers": {"lab": {"command": "mhp-mcp",
"args": ["thermo=http://bench-pc:18921", "arm=http://arm-pc:18921"]}}}
# remote harnesses: the same bridge over MCP Streamable HTTP
mhp-mcp --directory http://directory:18900 --http 18800 # POST http://host:18800/mcp
The bridge is the lab's MCP server. Any harness that speaks MCP over stdio or HTTP (Claude Code, Codex, OpenClaw, Hermes, Claude Science, Open Science, or a custom agent) connects to it and sees the same eight tools.
10.2 Command line
mhp http://bench:18921 describe
mhp http://bench:18921 read block_temperature lid_closed
mhp http://bench:18921 write target_temperature 95
mhp http://bench:18921 invoke run_protocol '{"steps":[...],"cycles":30}' --wait
mhp http://bench:18921 invoke open_lid --approved # after a human said yes
mhp http://bench:18921 estop "smoke from lid"
10.3 Code files
from openmhp.client import Lab
lab = Lab({"arm": "http://arm-pc:18921", "thermo": "http://bench:18921"})
arm, thermo = lab["arm"], lab["thermo"]
with arm, thermo: # leases on both
arm.write("speed", 31)
arm.wait(arm.invoke("pick_plate", location="deck_A1"))
arm.wait(arm.invoke("place_plate", location="thermocycler"))
result = thermo.wait(thermo.invoke("run_protocol", steps=PCR, cycles=31))
arm.wait(arm.invoke("pick_plate", location="thermocycler"))