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 toolMHP call
mhp_finddirectory/search, or a scan of cards in a small lab
mhp_describedevice/describe {detail, select}, default summary; with resource= it calls resources/read
mhp_readsignals/read
mhp_writesettings/write
mhp_invokeactions/invoke, optionally waiting for the job
mhp_jobjobs/status or jobs/cancel
mhp_estopsafety/estop on one device, or every device this session touched
mhp_runruns a script against the Lab client; returns stdout only (§3.4)
mhp_labfleet 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"))