Specification · section 5
Primitives
All requests are JSON-RPC 2.0. Method names are namespaced with /, as in MCP.
| Primitive | Methods | Direction |
|---|---|---|
| Lifecycle | initialize, ping |
client → server |
| Describe | device/describe {detail, select} |
client → server |
| Resources | resources/list, resources/read {path} |
client → server |
| Directory | directory/search, directory/get, directory/stats |
client → directory server |
| Signals | signals/list, signals/read, signals/subscribe |
client → server |
| Settings | settings/list, settings/write |
client → server |
| Actions | actions/list, actions/invoke, jobs/status, jobs/list, jobs/pause, jobs/resume, jobs/cancel |
client → server |
| Methods | methods/list, methods/find, methods/get, methods/save, methods/diff, methods/delete |
client → server |
| Safety | safety/limits, safety/estop, safety/reset |
client → server |
| Session | session/acquire, session/renew, session/release |
client → server |
| Notifications | notifications/signals/update, notifications/settings/changed, notifications/jobs/{started,progress,paused,resumed,cancelling,finished}, notifications/safety/{estop,fault,reset}, notifications/methods/{saved,retired} |
server → client |
| Elicitation | elicitation/confirm |
server → client (optional; see §7.2) |
5.1 initialize
→ {"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2026-09-12","clientInfo":{"name":"claude-lab","version":"1.0"}}}
← {"jsonrpc":"2.0","id":1,"result":{
"protocolVersion":"2026-09-12",
"device":{"id":"thermocycler-01","class":"thermocycler","make":"SimBio","model":"TC-96"},
"capabilities":{"signals":true,"settings":true,"actions":true,"methods":false,
"subscribe":true,"lease":true,"estop":true}}}
Servers MUST respond to initialize before any other method. Capabilities are booleans; a client MUST NOT call a method whose capability is false.
5.2 device/describe
Returns the descriptor (§4) at the requested detail tier (card | summary | full, §3.1) or the named items in select, plus "state": "idle" | "busy" | "fault" | "estop". Hosts SHOULD call this with summary before operating a device, then select the items they will use.
5.3 Signals (read)
→ {"method":"signals/read","params":{"names":["block_temperature","lid_closed"]}}
← {"result":{"values":{"block_temperature":22.0,"lid_closed":true},"ts":1788991266.5}}
names omitted means all signals. signals/subscribe asks the server to push notifications/signals/update for the named signals; over HTTP these arrive on the /events SSE stream, over stdio as interleaved notification lines.
5.4 Settings (write)
→ {"method":"settings/write","params":{"name":"target_temperature","value":95}}
← {"result":{"ok":true,"name":"target_temperature","value":95}}
→ {"method":"settings/write","params":{"name":"target_temperature","value":200}}
← {"error":{"code":-32010,"message":"target_temperature=200 above max 105","data":{"min":4,"max":105}}}
Optional params: approved: true (for confirm settings; set by the host only, after a person confirmed, §7.2), dryRun: true (run every check, touch nothing, return wouldWrite).
A write whose outcome is unknown (the command may have reached the device but the reply failed) latches the device in fault with data.outcome: "unknown". Errors raised before anything was sent (InvalidValue, LimitViolation, ...) do not change the device state.
5.5 Actions and jobs
actions/invoke returns immediately with a job. The device does the work; the agent polls or subscribes.
→ {"method":"actions/invoke","params":{"name":"run_protocol",
"params":{"steps":[{"temp":95,"hold_s":30},{"temp":58,"hold_s":30},{"temp":72,"hold_s":45}],"cycles":30}}}
← {"result":{"job":{"id":"job_04324008","action":"run_protocol","state":"queued","progress":0.0}}}
→ {"method":"jobs/status","params":{"id":"job_04324008"}}
← {"result":{"job":{"id":"job_04324008","state":"running","progress":0.43}}}
... later ...
← {"method":"notifications/jobs/finished","params":{"job":{"id":"job_04324008","state":"done",
"progress":1.0,"result":{"cycles_completed":30,"final_block_temperature":72}}}}
Job states: queued → running → done | failed | cancelled, plus waiting_operator for operator-run instruments (§9.1). A failed job carries outcome: "unknown" and puts the device in fault until a verified safety/reset. Busy is derived from all active jobs: a non-concurrent action is refused while any job runs, and a concurrent one may run alongside. A job's completion and the device state are published together. Adapters MUST report the backend's terminal outcome (a cancelled or aborted backend job is not done) and MUST NOT report a cancel the backend has not honoured.
5.6 Safety
safety/limitsreturns the effective limits, interlocks and approval levels in one call, for hosts that want to show the envelope to a human.safety/estopMUST stop all motion and energy output as fast as the hardware allows, cancel every running job, and put the device inestop. It MUST succeed regardless of lease, approval level or interlock state, and MUST NOT wait behind ordinary requests: the reference driver runs it outside the request lock, and the serial driver interrupts in-flight I/O before sending its stop commands. If the stop hook fails, the device stays inestopand the response saysstopped: false. It is the one call an agent may always make.safety/resetreturns the device toidleafterestoporfault. It MUST fail withDeviceBusywhile any job is still active, and MUST verify recovery first (the reference reads every signal). Only the lease holder, or anyone when the device is unleased, may reset.
5.7 Session leases
session/acquire {ttl} gives the calling session exclusive write and invoke rights until ttl seconds pass without session/renew, or until session/release. Every client has its own session id, so independent clients never share a lease. The reference client renews automatically with a heartbeat while it holds a lease, and each run gets its own session whose leases end with the run. If a lease is lost, the next actuating call fails with NotLeased, and the client must not assume it still has control. Reads, ping, describe, jobs/status, jobs/pause, jobs/cancel and safety/estop are never blocked by a lease; jobs/resume and safety/reset are. On a device that declares watchdog_s, operating it requires holding the lease (NotLeased with data.reason: "lease_required"); the reference client acquires it on first use.
5.8 Methods
The rules are in §4.5; this is the wire surface. Every device answers all six; a device that is not
method-capable answers list and find with nothing and refuses save with NotSupported.
| Method | Params | Result |
|---|---|---|
methods/list |
compound?, project?, action?, query?, retired? (default false) |
{methods: [card...], count, compounds, projects, actions, method_driven}; a card is the record without params |
methods/find |
compound (required), project?, action? |
{choice: one\|several\|none, method? \| candidates, ask?, why?, newProject?} (§4.5 M8) |
methods/get |
name (full id or bare), project?, version? |
the full record; an earlier version is reconstructed from history |
methods/save |
method: {name, project?, action, params, compounds, author?, notes?, source?, derived_from?}, note? |
{saved: card, validated: true}; the device assigns version, hash, timestamps and validated |
methods/diff |
a, b? (full ids or bare with project), aVersion?, bVersion? |
{a, b, action?, params: {key: {from, to}}, same} |
methods/delete |
name, project? |
{retired: true}; the file stays (§4.5 M4) |
actions/invoke gains method, project and compound (§4.5 M6, M7). methods/list, find, get and
diff are never blocked by a lease or by a running job; save and delete wait for the request lock like
any write and require no lease, since they touch files, not hardware.