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

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.