rei-lads-mcp
by fc0web
README.md
# rei-lads-mcp
> ⚠️ **v0.1.0-preview — early-feedback release. NOT for production use.**
> API is not stable before v1.0.0. Breaking changes may land in any 0.x version.
> X.509 certificate authentication is **not supported** in the 0.1.x line
> (username/password only). See [ROADMAP.md](ROADMAP.md) for Phase 5 items.
An MCP server that exposes **OPC UA LADS** (Laboratory Agnostic Device
Standard) instruments to LLM agents with two Rei-native disciplines
baked in:
1. **Approval-gated execution** — every state-changing tool validates its
operation name against a server-declared enum (LADS state-machine
methods, program templates). Arbitrary strings are rejected at the
MCP boundary, before any network call fires.
2. **Witnessed provenance** — every state-changing call emits a SHA-256
record (initiator, node, operation, params, samples, timestamp) that
is persisted to a local append-only JSONL and retrievable by prefix.
Modeled on the LADS reference implementations at
[`opcua-lads`](https://github.com/opcua-lads) and grounded in the
[Rei / D-FUMT](https://github.com/fc0web/rei-lang) approval-gate +
`witnessed by` design.
## Install
Requires Python 3.12+.
```bash
python -m venv .venv
./.venv/bin/pip install -r requirements.txt # Linux/macOS
./.venv/Scripts/pip install -r requirements.txt # Windows
```
## Quick start
Boot a LADS reference server (for example, `lads-viscometer` from
[`opcua-lads/lads-server-collection`](https://github.com/opcua-lads/lads-server-collection)),
then run:
```bash
python server.py
```
This starts a stdio MCP server. Point any MCP client at it.
Programmatic sanity check (no MCP client required):
```bash
python test_tools_list.py # 15 tools registered, schemas generated
python test_live_phase4.py # end-to-end against a running LADS server on
# opc.tcp://localhost:4840
```
## Tools (15)
**Connection**
- `lads_connect(alias, server_url, user?, password?, wait_timeout_s?, cert_path?, key_path?)`
- `lads_register_alias(alias, server_url, description?, user?, password?)`
- `lads_list_aliases()`
- `lads_connect_by_alias(alias, wait_timeout_s?)`
**Discovery** (read-only)
- `lads_list_devices(alias)`
- `lads_get_functional_units(alias, device_index)`
- `lads_get_sensor_value(alias, device_index, unit_index, function_index)`
- `lads_get_state(alias, device_index, unit_index)`
- `lads_get_state_machine_methods(alias, device_index, unit_index)`
- `lads_list_program_templates(alias, device_index, unit_index)`
- `lads_list_results(alias, device_index, unit_index)`
**State-changing** (emit witness records)
- `lads_call_state_machine_method(alias, device_index, unit_index, method_name)` — validates method_name against server-declared inventory
- `lads_start_program(alias, device_index, unit_index, template_name, properties?, samples?, supervisory_job_id?, supervisory_task_id?)` — validates template_name against ProgramManager templates
**Witness retrieval**
- `lads_list_witness_recent(limit?)`
- `lads_show_witness(sha256_prefix)` — min 4 characters
## Approval-gate demo
```python
lads_call_state_machine_method(alias, 0, 0, "Start")
# -> {invocation: "queued (async)", witness_hash: "..."}
lads_call_state_machine_method(alias, 0, 0, "ArbitraryExec")
# -> {error: "method 'ArbitraryExec' not declared by this state machine",
# valid_methods: ["Start", "StartProgram", "Stop"]}
```
The rejection happens at the MCP tool boundary — no OPC UA call is fired
for an operation name that the LADS server did not declare. Same pattern
for `lads_start_program(template_name=...)`.
## Witnessed provenance
Every state-changing tool call emits a record like:
```json
{
"initiator": "mcp-caller",
"node_alias": "viscometer",
"server_url": "opc.tcp://localhost:4840",
"device_browse_name": "myViscometer",
"unit_browse_name": "ViscometerUnit",
"operation": "StartProgram",
"template_name": "Analytical Method A (30rpm)",
"params": {"Speed": "30", "Duration": "60"},
"sample_context": [{"ContainerId": "vial-01", "SampleId": "sample-a",
"Position": "A1", "CustomData": "phase4-test"}],
"timestamp_utc": "2026-08-26T00:23:54.333266+00:00",
"phase": "accepted",
"sha256_hex": "174b5e1498a5d0cca55ce9291adf95f612161ed590b839258e700e994cba25ae"
}
```
Persisted to `~/.rei-lads/witness/YYYY-MM.jsonl` (env override
`REI_LADS_WITNESS_DIR`). Retrievable via the two `lads_*_witness_*`
tools by SHA-256 prefix.
## Related work
- [`opcua-lads/lads-server-collection`](https://github.com/opcua-lads/lads-server-collection) — LADS reference OPC UA servers (Viscometer / pH-Meter / Balance / Freezer / FT-NIR)
- [`opcua-lads/lads-client-py`](https://github.com/opcua-lads/lads-client-py) — the Python client this bridge wraps
- [`TECTOS-JP/lab-visa-mcp`](https://github.com/TECTOS-JP/lab-visa-mcp) — sibling MCP for PyVISA-controlled instruments
- [`fc0web/rei-lang`](https://github.com/fc0web/rei-lang) — the Rei language project this bridge implements the discipline of
- [Rei-Certified program](https://github.com/fc0web/rei-lang) (draft) — the compliance surface this bridge is a candidate G1 pilot for
## Preview status
- **Phase 1-4 spike complete** — 15 tools, L2 and L3 both green against
a live LADS viscometer sim
- **Phase 4 fixes that are already in code**: banner silencing at import,
`Connection.initialized` polling, `QualifiedName` unwrap
- **Phase 5 is not yet implemented** — see [ROADMAP.md](ROADMAP.md)
- **API stability**: not before v1.0.0 — expect breaking changes in 0.x
Full spike history is preserved in [SPIKE-HISTORY.md](SPIKE-HISTORY.md)
for the record.
## License
MIT — see [LICENSE](LICENSE). LADS ecosystem convention.
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md). External contributors will
require a signed CLA once the Rei-project CLA is finalized.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues