简体中文 · Documentation · Home · Client integration · Device integration · JSON Schemas
This reference explains v0.1 requests, responses, field meanings, and how this repository's runtime handles requests. The current server accepts calls through local MCP stdio. The caller handles experiment planning and sequences of operations.
See the client guide for connection and invocation steps, and the backend guide for device configuration, capability definitions, and SDK integration.
Callers first discover device instances, then query the capabilities each instance offers. These identifiers distinguish the model, instance, implementation, and requested operation:
| Field | Meaning |
|---|---|
model_id |
The device model |
device_id |
A locally configured device instance, used to select the device in a request |
backend_id |
The backend implementation that handles requests for the instance |
capability_id |
A capability offered by the instance, used to select the operation in a request |
Instances of the same model can use different backends and offer different capabilities. Read the actual instance's description through describe_device(device_id) before calling it.
Each returned capability includes capability_id, version, description, input_schema, result_schema, and completion_criteria. The two schemas define the input and successful result formats; completion_criteria describes the conditions for completion. Capability schemas use JSON Schema Draft 2020-12.
An instance's execution_mode states how it handles calls: offline validation, mock, official simulation, or hardware execution. Instances do not have to progress through these modes in order. The caller selects an instance through device_id and uses its declared mode. Input formats, result formats, and completion conditions can differ between modes, so follow the capability definitions returned by that instance. See the backend guide for the modes and completion evidence for interpreting results.
A capability can also declare access_level to describe the control it exposes to the caller; see the five access levels. Bundled device functions are primarily L3; the OT-2 delay helper has no declared access level. The level follows the published interface. A multi-device client's call sequence and the commands generated inside a backend do not change the capability's declared level.
| Data | Tools that return it | Main contents |
|---|---|---|
DeviceDescriptor |
describe_device |
Device identifiers, provider, execution mode, capabilities and versions, input/result schemas, optional state schema, and cancellation support |
DeviceState |
get_device_state |
Backend availability, local occupancy, the associated run, and optional observations with timestamps and sources |
ExecutionRecord |
submit_execution, get_execution, cancel_execution |
A request, its effective arguments and versions, execution status, progress, result, error, and completion evidence |
The list_devices response contains descriptor summaries with a state snapshot nested under each item's state. Thus, item.state.state in that list corresponds to DeviceState.state in a separate state query.
Device-specific fields are defined by input_schema, result_schema, and state_schema. Backends declare the operations they support and the data they can report. Observations such as temperature, speed, or sample location can be omitted when unavailable.
The descriptor_revision is a SHA-256 content identifier computed from the static description, capability schemas, versions, and declared configuration version. Availability and occupancy changes do not change it. This repository's runtime fixes descriptions at startup; restart the server after changing capability declarations or static configuration.
A backend can declare configuration_version; credentials stay in local configuration. Each execution records the descriptor_revision and capability_version it used, so callers can check the interface on which the request was based.
A request can include expected_descriptor_revision. If it differs from the current revision, the server returns descriptor_changed before execution and the caller should query the description again. Omitting it uses the current revision. The revision checks the interface definition; device calibration and readiness require device feedback.
| Data | Field | Default or meaning when omitted |
|---|---|---|
| Device description | provider, configuration_version |
null: not declared |
| Device description | state_schema |
null: no schema declared for observation values |
| Device description | cancellation_supported |
false: cancellation support is not declared |
| Capability | access_level |
null: access level not declared |
| Capability | defaults_policy |
not_applied: schema defaults are not inserted automatically |
| Capability | result_schema |
{"type":"object"}; backends can declare a more specific successful result format |
| Capability | limitations |
Empty list: no limitations listed; integrators should still document known limitations |
| Device state | observation |
null: no observation currently available |
| Execution record | progress, result |
Empty objects until populated |
Device and capability identifiers, input schemas, and execution modes must be explicit. Each capability's required list determines mandatory experimental parameters. That capability also declares its operating limits and defaults.
| Tool | Input | Successful data |
|---|---|---|
list_devices |
Optional device_class |
Instance summaries with nested state snapshots |
describe_device |
device_id |
DeviceDescriptor |
get_device_state |
device_id |
DeviceState |
submit_execution |
request |
An accepted execution record, including run_id |
get_execution |
run_id |
The execution's latest record |
cancel_execution |
run_id |
The execution record after requesting cancellation |
Common device tools return responses in MCP structuredContent and equivalent JSON text, with their format declared through outputSchema:
| Situation | MCP isError |
Response contents |
|---|---|---|
| Tool call succeeds | false |
{"ok":true,"data":...} |
| Tool call is rejected | true |
{"ok":false,"error":{"code":...,"message":...,"details":{...}}} |
| Query successfully retrieves a failed or unresolved execution | false |
ok=true; the execution's status and error are in data.status and data.error |
After calling get_execution, read the execution's status to determine its outcome. See errors and schemas for error codes and where they appear.
Invalid MCP argument shapes or unexpected server errors can produce responses directly from the MCP SDK, with a different format from the table above. The call() helper in the client examples extracts data on success and raises an exception when the tool call fails.
The submit_execution tool accepts a request with required fields device_id, capability_id, and arguments. The optional expected_descriptor_revision checks the descriptor revision; idempotency_key supports retrying the same request.
The arguments field is the complete object described by the selected capability's input_schema. That schema determines field names, nesting, required fields, and whether extra fields are allowed. Inputs use standard JSON; NaN and Infinity are rejected.
The following complete tool arguments use a centrifuge's offline-validation instance. This capability uses parameters for operation settings and bindings for sample references:
{
"request": {
"device_id": "agilent_centrifuge_773-validation",
"capability_id": "agilent_centrifuge_773.centrifugation",
"arguments": {
"parameters": {
"speed": 1000,
"duration": 30,
"acceleration_percent": 100,
"deceleration_percent": 100
},
"bindings": {"sample": {"resource_id": "sample-plate"}}
},
"idempotency_key": "my-centrifugation-1"
}
}This capability specifies speed in rpm and duration in seconds. Its bindings.sample.resource_id is a sample reference agreed between the caller and local backend. The offline backend checks the reference structure. For hardware integration, verify resources and positions against local device and sample configuration. Whether references include position, sample_id, or arrays also depends on the target capability's schema.
Other capabilities can use different argument structures. For example, ot2-sim uses setup + liquids + parameters, while the plate reader adapter example uses plate_id + wavelength_nm.
Backends use x-scp-unit on numeric schema fields to declare units. The same convention applies to inputs, results, and state observations. For example, an object's properties can contain:
{
"volume": {
"type": "number",
"exclusiveMinimum": 0,
"maximum": 1000,
"x-scp-unit": "uL"
}
}Submit {"volume":50} to mean 50 uL; bounds and defaults use that unit too. Supply "50 uL" or {"value":50,"unit":"uL"} only if the capability's schema accepts that form. For arrays, annotate the numeric items schema.
The x-scp-unit annotation describes the unit. JSON Schema validators check declared types and constraints without converting values. Common project conventions include uL, mL, L, s, min, degC, nm, mm, rpm, %, ppm, and mbar; the reader example uses AU for absorbance. Devices can also use other documented units.
An absent annotation means the unit is undeclared, not dimensionless. Callers should confirm units from the target capability's schema, and integrators should document units for the fields they use. Counts can be explained by field names and descriptions; use 1 to declare a dimensionless unit explicitly. Some bundled shaking parameters and focal-height fields have undeclared units; confirm their conventions before using them.
Devices supporting multiple units can define inputs such as {"volume":{"value":0.05,"unit":"mL"}}, declaring allowed units and appropriate bounds in their schemas. The backend implements any necessary conversions. Converting rotational speed to relative centrifugal acceleration also requires device geometry, which should be considered during integration.
A capability's defaults_policy states how schema default annotations are handled:
| Policy | Behavior | Bundled examples |
|---|---|---|
not_applied |
Default policy: pass arguments as supplied; default is descriptive |
Offline-validation instances |
backend_resolved |
The backend fills published optional defaults through resolve_arguments(), then validates the effective arguments again |
Demonstration heater and OT-2 backend |
Under both policies, fields listed in required must be supplied explicitly. Declaring a default does not make a required field optional.
For the demonstration heater in the client guide, omitting the optional duration_s makes the backend fill in one second. Execution record excerpt:
{
"request": {"arguments": {"target_celsius": 37}},
"effective_arguments": {"target_celsius": 37, "duration_s": 1}
}The request.arguments field preserves the original input; effective_arguments contains the arguments passed to execute(). Report actual device feedback, such as measured temperature, through results or observations, separately from requested settings.
The DeviceState.state field describes a device instance's availability and occupancy in the current server. The ExecutionRecord.status field describes one execution.
Device state |
Meaning |
|---|---|
idle |
The backend is available and no unreleased execution occupies the instance |
busy |
An execution is accepted or running, including while cancellation confirmation is pending |
unavailable |
No execution occupies the instance, but the backend is unavailable |
needs_attention |
An execution has a final status but has not released occupancy; verification and recovery are needed |
The available field separately reports backend availability. The base backend returns true by default, meaning the software implementation is available; hardware readiness requires device feedback. The reported_at field is the state snapshot's generation time. The fields source="local_execution_runtime" and scope="this_server_process" indicate that the current server process records this state.
Backends can supply device observations in observation:
| Field | Meaning |
|---|---|
values |
Observation values defined by the device's state_schema |
observed_at |
Original observation time, retained when cached data is queried again |
source |
Data source, such as a device SDK or simulator |
kind |
Data kind: measured, device_reported, simulated, or declared |
Publishing observations requires a declared state_schema, which the runtime uses to validate values. Return observation=null when no observation is available. Requested settings and actual readings should use distinct fields with clear meanings.
State queries only read availability and cached observations. Occupancy is established when a request is accepted; necessary execution conditions still need to be checked during request validation and execution.
On submission, the runtime checks the request and local availability, then accepts the execution and reserves the instance. Each instance can have at most one unreleased execution. Conflicts return device_busy without queuing. Different instances can execute concurrently.
Execution statuses change as follows:
stateDiagram-v2
[*] --> accepted
accepted --> running
accepted --> cancel_requested
running --> succeeded
running --> failed
running --> unknown
running --> cancel_requested
cancel_requested --> cancelled
cancel_requested --> succeeded
cancel_requested --> failed
cancel_requested --> unknown
The accepted status means the request passed validation and was registered. Use the returned run_id to query subsequent status. The succeeded status means the call succeeded; its mode, result, and completion evidence explain what completed. Successful results are validated against result_schema.
Cancellation can be requested when the device description declares cancellation_supported=true. Continue querying after cancel_requested. The backend returns cancelled only after confirming that the operation stopped. If the operation finishes concurrently, it can return succeeded instead. The final status can also be failed or unknown; handle the reported outcome.
The current OT-2 simulation implementation does not support cancellation. Interrupting the server process does not establish that hardware stopped; verify this through device feedback.
A backend returns unknown when it cannot establish the outcome. Unhandled hardware-backend exceptions also become unknown. The device stays in needs_attention, and the runtime does not retry automatically.
After an execution ends, the backend uses BackendResult.device_released to indicate whether its reservation can be released:
- Hardware backends explicitly return
device_released=trueafter confirming that the device can accept another operation. Successful, failed, and cancelled executions all need separate release confirmation. - Software backends default to releasing occupancy on known final outcomes and can explicitly retain it.
- The
unknownstatus always retains occupancy. The final execution record stores the actualdevice_releasedvalue.
The current runtime has no interface for manually clearing unreleased occupancy. Hardware integrations should provide verification and recovery methods in their control software or backend.
The updated_at field changes when execution status or progress changes; querying does not change it. The finished_at field records when the local execution coroutine ends. Device feedback is still needed to confirm that a physical operation stopped.
A capability's completion_criteria declares the conditions for completion. The execution record's completion_evidence stores the evidence supplied by that call. Interpret results together with execution_mode and the capability's result_schema:
execution_mode |
Example completion_evidence.kind |
Meaning of success |
|---|---|---|
schema_validation |
schema_validation |
The request passed the published input schema |
mock |
mock_trace |
The demonstration software flow completed |
official_simulation |
official_simulator_runlog |
Official software simulation completed |
hardware |
For example, device_observation |
Actual feedback supports the capability's declared completion conditions |
The first three kind values come from bundled backends; the hardware row is an implementation example. Custom backends should document the meaning of their evidence. Keep the backend, result format, and completion conditions consistent. Detailed evidence can be placed in completion_evidence.details.
The physical_execution=true flag identifies a hardware execution path. A backend can set completion_evidence.physical_completion_verified=true only when actual feedback sufficiently supports completion. The runtime rejects this claim from nonhardware backends. All bundled backends run in software.
Provide an optional idempotency_key to retry the same request, for example when a submission receives no response. The key applies to one device_id within one server process:
| Submission | Behavior |
|---|---|
| Retry the same request with the original key | Return the existing execution and its run_id |
| Submit a different request with the original key | Return idempotency_conflict |
| Use a new key or omit the key | Handle it as a new request, subject to argument and occupancy checks |
Request comparison ignores object key order; other content is compared as JSON. Values that become equal after unit conversion are not treated as the same request. Preserve the original request contents when retrying.
Execution records, occupancy, and deduplication records stay in the current server's memory. Keep the same MCP session for submission and querying. After the server exits or restarts, old run_id values cannot be queried and old keys cannot prevent duplicate execution. Independently started servers track occupancy separately, so one server should manage a given physical device. Verify actual device and experiment state before deciding to execute again.
The current version has no durable queue, reconnection recovery, cross-process lock, event stream, or global sample ledger. Integrators can implement these features when needed. Simulation JSON files saved separately by OT-2-specific tools are not common execution records.
When a tool call is rejected, its response contains error. Errors occurring after acceptance appear in the execution record's error. Both contain code, message, and details.
| Error code | Common cause | Location |
|---|---|---|
unknown_device, unknown_capability |
The device is not registered or does not offer the selected capability | Tool response |
invalid_arguments |
Arguments do not meet capability requirements | Tool response |
backend_unavailable, device_busy |
The backend is unavailable or an unreleased execution still occupies the instance | Tool response |
descriptor_changed |
The requested descriptor revision differs from the current revision | Tool response |
idempotency_conflict |
The same key was used for a different request | Tool response |
unknown_run |
The current server has no record of this execution | Tool response |
cancellation_unsupported |
The backend does not declare support for cancelling an active execution | Tool response |
invalid_backend_state, invalid_backend_defaults |
Backend state or default-argument handling does not match its declarations | Tool response |
backend_error, simulation_failed, execution_interrupted |
Backend exception, simulation failure, or local execution interruption | Execution record |
Backends can add documented error codes. See the JSON Schema directory for public message schemas and their MCP resource paths.
The OT-2-specific helper tools directly return validation data, code, or simulation records in their own formats, without the common device tools' ok/data envelope.
These calls do not go through submit_execution, reserve a device instance, or produce a common execution record queryable through get_execution. The simulate_protocol tool returns after simulation finishes. Use its run_id with read_simulation_run to read the saved simulation record.
After the base installation, run from the repository root:
uv run --no-sync scp-lab checkFor a custom device directory, add --devices-dir ./lab-devices. The command reads registered descriptions and states, checking data structures, identifiers, JSON Schemas, unit annotation formats, and observation values against state_schema. It reports per-instance results and declared limitations.
Excerpt from a passing result:
{
"status": "passed",
"execution_performed": false
}This means description and state format checks passed; no device operation was performed. An optional simulator being unavailable is not a format error. Backend read_state() methods should return only availability and cached observations.
During integration, also check units and parameter ranges, and make actual calls to verify argument handling, results, completion evidence, and supported cancellation flows. This keeps capability definitions consistent with backend behavior. See the hardware integration steps.