MCP adapter: expose Labwire instruments as MCP tools for AI agents
Project description
labwire-mcp
The Labwire MCP adapter: connects to one or more Labwire Instrument Servers over WebSocket and exposes every declared instrument command as an MCP tool, so Claude and other MCP hosts can drive (simulated) lab hardware natively.
pip install labwire-mcp
labwire-mcp ws://127.0.0.1:9520 [ws://... ...]
See examples/mcp-config.json for a Claude-style MCP server configuration.
MCP 2026-07-28, supported from day one
The adapter runs on the MCP Python SDK v2 and serves both protocol eras from the same process with no configuration:
| Client | Handshake | Confirmation flow | Long commands |
|---|---|---|---|
| 2026-07-28 era | per-request metadata, server/discover |
elicitation round trip (below) | returned as tasks, if the client declares the tasks extension |
| Handshake era (2025-11-25 and earlier) | classic initialize |
confirmation parameter on the tool |
ordinary blocking tool call |
Every adapter test runs twice, once per era, against the same server object. List responses carry the cache hints the 2026 revision requires: tool and resource lists cache generously (the surface is fixed for the life of the process), resource reads not at all (a deck read is live instrument state).
What a tool carries
Tool names are <model>__<command>. The command's params_schema (already
JSON Schema, SPEC 7.2) becomes the tool's inputSchema verbatim, UCUM unit
codes included. Each description states the command's safety class (S0 to
S3) and its cancel semantics sentence, so an agent knows what cancel can
physically do before committing to a call, not after.
Human in the loop (2026-07-28 era)
An S2 tool call (costly or irreversible) that arrives without a
confirmation is not failed. The adapter returns input_required with an
elicitation showing the exact command and parameter values; the MCP host
surfaces it to the human; on approval the adapter injects the deployment's
standing confirmation and submits. Decline means nothing is submitted. The
agent never handles the confirmation token.
S3 (hazardous) goes further: the first call runs the server's refusal path,
and the elicitation carries the resulting request id plus the exact
labwire grant approve command an operator must run out of band. The human
types the freshly minted grant id into the elicitation; the retry submits
with that authorization. The adapter cannot pre-hold an S3 grant; each one
is bound to the exact command and parameter values.
The standing S2 confirmation is read from the LABWIRE_MCP_CONFIRMATION
environment variable at startup, never a CLI flag (flags are visible in
process listings). Without it, S2 calls keep the legacy parameter path.
Stated plainly: both paths prove a human approved this call with these
parameters. Neither proves who; the signed record says
identity_verified: false either way. Cryptographic operator identity is a
roadmap item, not a shipped feature.
Runnable end to end with zero hardware:
uv run examples/mcp_confirmation.py
Long-running commands as tasks (experimental)
For 2026-era clients that declare the io.modelcontextprotocol/tasks
extension, a command whose estimated_duration_s is 10 s or more (the
threshold is configurable via build_server) returns a task instead of
blocking. The client polls tasks/get; tasks/cancel is served
cooperatively. Clients that do not declare the extension never see a task.
The SDK does not ship this extension, so it is implemented here from the ext-tasks specification text. The 2026-07-28 changelog calls tasks an official extension while the ext-tasks repository labels itself experimental; we treat the text as less stable than core and say so. TODO-VERIFY: the extension's final status.
The status mapping has two points that will surprise reviewers, both required by the extension's own text:
- Task
failedis reserved for JSON-RPC faults only. An instrument-level failure (hardware fault, interlock trip) is a task that completed, withisError: trueinside the CallToolResult. - A
cancelledtask carries no result field. When yourtasks/cancelends a run, the settlement outcome (SPEC 8.3: what physically happened) is summarized instatusMessage, and the signed run bundle stays on the instrument host. Every other terminal path delivers the full record.
tasks/cancel is cooperative by the extension's design, which maps
Labwire's cancel semantics exactly: a command declared
cancel_semantics: "none" acknowledges the cancel and runs to completion
(the ack promises nothing); abort and between_steps commands get a real
Labwire cancel and settle honestly. One trap from the spec: a client MAY
drop all task state after sending cancel and never fetch the outcome; the
signed bundle persists on the instrument host regardless.
Labwire's accepted, running, and canceling states all surface as working
(the extension has no queued or cancel-pending status); statusMessage
carries the truth. Tasks emit no progress notifications (the extension
forbids it); live telemetry stays on Labwire's own channels.
Limitations
- The
input_requiredround trip keys itsrequest_stateto in-process memory, and task state is in-process too. The stdio adapter is one process, so this is exact, but an adapter restart forgets pending approvals and task handles. The signed run bundles do not vanish; they are on the instrument host. - The adapter holds the standing S2 confirmation for the deployment. Anyone who can reach the adapter's environment can approve S2 commands; neither confirmation path identifies the approver (see above).
- The tasks extension is experimental upstream and may change; ours tracks the published text, not a shipped SDK implementation.
- Verified against simulated instruments only, never physical hardware.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file labwire_mcp-0.5.0.tar.gz.
File metadata
- Download URL: labwire_mcp-0.5.0.tar.gz
- Upload date:
- Size: 17.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3acc116b77dd1d8851c00360afa2d345294005d5af627450f0e1d28366819f98
|
|
| MD5 |
abc5b261138389f5cac5d79d646e21a6
|
|
| BLAKE2b-256 |
c43805579f4e39b7f887e9929be3d8abfd7fac5bb4fb96cac0050971dd9ab1af
|
Provenance
The following attestation bundles were made for labwire_mcp-0.5.0.tar.gz:
Publisher:
release.yml on benchwire/labwire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
labwire_mcp-0.5.0.tar.gz -
Subject digest:
3acc116b77dd1d8851c00360afa2d345294005d5af627450f0e1d28366819f98 - Sigstore transparency entry: 2280932934
- Sigstore integration time:
-
Permalink:
benchwire/labwire@3218d5a2f9f576b48e5be9e57c18c944548a1456 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/benchwire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3218d5a2f9f576b48e5be9e57c18c944548a1456 -
Trigger Event:
release
-
Statement type:
File details
Details for the file labwire_mcp-0.5.0-py3-none-any.whl.
File metadata
- Download URL: labwire_mcp-0.5.0-py3-none-any.whl
- Upload date:
- Size: 16.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
51a28a4a47966fcec886e9a140f386c35ba96fbe1c80fdd1d1337f3716cdf808
|
|
| MD5 |
dd4059f1fc25517079d715c6e51234f6
|
|
| BLAKE2b-256 |
86d5a815545be86d2a15abd91ea81898c4739d020023e0a196c5ea707c698a14
|
Provenance
The following attestation bundles were made for labwire_mcp-0.5.0-py3-none-any.whl:
Publisher:
release.yml on benchwire/labwire
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
labwire_mcp-0.5.0-py3-none-any.whl -
Subject digest:
51a28a4a47966fcec886e9a140f386c35ba96fbe1c80fdd1d1337f3716cdf808 - Sigstore transparency entry: 2280932963
- Sigstore integration time:
-
Permalink:
benchwire/labwire@3218d5a2f9f576b48e5be9e57c18c944548a1456 -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/benchwire
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3218d5a2f9f576b48e5be9e57c18c944548a1456 -
Trigger Event:
release
-
Statement type: