Skip to main content
Glama
DVSProductions

siglent-mcp

README.md
# Siglent SPD1305X MCP server

[![CI](https://github.com/DVSProductions/siglent-mcp/actions/workflows/ci.yml/badge.svg)](https://github.com/DVSProductions/siglent-mcp/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue.svg)](pyproject.toml)
[![MCP](https://img.shields.io/badge/MCP-server-orange.svg)](https://modelcontextprotocol.io)

Drive a Siglent SPD1305X (or any SPD1000X series programmable DC power supply)
from Claude Code over Ethernet. No EasyPower, no NI-VISA, no drivers. The supply
is an LXI-style instrument that accepts plain SCPI on a raw TCP socket (port
**5025**); this server wraps that as MCP tools using only the Python standard
library, so it runs anywhere Python does.

## Tools

| Tool | What it does |
|------|--------------|
| `psu_info` | `*IDN?` + firmware, network config, setpoints, live V/I/P, output state, CV/CC mode, safe limits |
| `get_measurements` | live measured **voltage / current / power** at the terminals, plus output state and setpoints |
| `monitor` | sample V/I/P over a time window: min/max/mean, CV/CC transitions, decimated preview, optional CSV |
| `set_safety_limits` | declare the safe V/A envelope; **required before output can be enabled**, and it caps the setpoints |
| `set_voltage` | program the output voltage setpoint (with readback); guarded (see Safety model) |
| `set_current` | program the output current limit (with readback); guarded (see Safety model) |
| `set_output` | enable / disable the physical output (returns resulting state + live measurement) |
| `save_state` | store present settings into an instrument memory slot (1..5, `*SAV`) |
| `recall_state` | recall settings from an instrument memory slot (1..5, `*RCL`) |
| `scpi` | raw SCPI escape hatch for anything else |

## Safety model (for agents driving the supply)

The control tools are gated so an autonomous agent cannot energize a circuit
carelessly:

- **Declare limits before energizing.** `set_output(True)` is **refused** until
  `set_safety_limits(max_voltage, max_current)` has been called this session.
  There is no environment default; the agent must consciously commit to a
  ceiling each time the server starts.
- **Setpoints are capped.** After limits are declared, `set_voltage` and
  `set_current` refuse any value above them. To go higher you must raise the
  limit on purpose. Enabling the output also re-checks that the present
  setpoints are within the limits.
- **No silent live edits.** While the output is ON, `set_voltage` and
  `set_current` are refused unless you pass `IKNOWTHEOUTPUTISONDOITANYWAYS=True`.
  The flag is deliberately loud and hard to pass by accident, and it stands out
  in logs, so a live change to a powered load is always an explicit decision.
- **`monitor` never changes anything.** It only reads, so it is always safe to
  run.

## Setup

Install the one dependency (the MCP SDK):

```sh
pip install "mcp[cli]"
```

...or install the package itself (adds a `siglent-mcp` console script):

```sh
pip install .
```

Find the supply's IP on the unit under **System → LAN**, or query it over the
network. This unit is at `192.168.178.209`. Confirm the link with:

```sh
python -c "import socket; s=socket.create_connection(('192.168.178.209',5025),3); s.sendall(b'*IDN?\n'); print(s.recv(200))"
```

You should see `Siglent Technologies,SPD1305X,...`.

## Register with Claude Code

```sh
claude mcp add --env SIGLENT_HOST=192.168.178.209 --scope user siglent -- python D:/ryzzenDL/siglent-mcp/siglent_mcp.py
```

> The `--env` flag must have another option (here `--scope user`) between it and
> the server name, or the CLI mis-parses the name. If `python` isn't on PATH,
> use the full `python.exe` path or `-- cmd /c python D:/...`.

If you `pip install`ed the package, use the console script instead of a path:

```sh
claude mcp add --env SIGLENT_HOST=192.168.178.209 --scope user siglent -- siglent-mcp
```

Or add it to a `.mcp.json` (project scope), see [`.mcp.example.json`](.mcp.example.json):

```json
{
  "mcpServers": {
    "siglent": {
      "type": "stdio",
      "command": "python",
      "args": ["D:/ryzzenDL/siglent-mcp/siglent_mcp.py"],
      "env": { "SIGLENT_HOST": "192.168.178.209" }
    }
  }
}
```

Then `claude mcp list` to confirm it connects, and ask Claude things like
*"what is the supply outputting right now?"*, *"set it to 5 volts, 0.5 amp limit
and turn the output on"*, or *"read the current and power"*.

## Environment variables

| Var | Default | Meaning |
|-----|---------|---------|
| `SIGLENT_HOST` | `192.168.178.209` | supply IP |
| `SIGLENT_PORT` | `5025` | raw SCPI socket port |
| `SIGLENT_TIMEOUT` | `5` | socket timeout (seconds) |
| `SIGLENT_CHANNEL` | `CH1` | channel token (the SPD1305X is single channel) |
| `SIGLENT_VMAX` | `30.0` | hardware voltage ceiling (safe limits and setpoints cannot exceed it) |
| `SIGLENT_IMAX` | `5.0` | hardware current ceiling (safe limits and setpoints cannot exceed it) |
| `SIGLENT_OUT_DIR` | `./captures` | where `monitor` writes CSV files |

To reuse this server for a different SPD1000X model, override `SIGLENT_VMAX` /
`SIGLENT_IMAX` (for example `16` / `8` for an SPD1168X).

## SCPI reference (confirmed on this unit, firmware 2.1.1.9)

| Purpose | SCPI | Notes |
|---------|------|-------|
| Identity | `*IDN?` | `Siglent Technologies,SPD1305X,<serial>,<fw>,<hw>` |
| Firmware | `SYSTem:VERSion?` | |
| Set voltage | `CH1:VOLTage 5.000` | bare `VOLTage 5.000` also works |
| Set current limit | `CH1:CURRent 0.500` | bare `CURRent 0.500` also works |
| Read voltage setpoint | `CH1:VOLTage?` | the programmed value |
| Read current limit | `CH1:CURRent?` | |
| Measure output voltage | `MEASure:VOLTage? CH1` | live at the terminals; 0 while output off |
| Measure output current | `MEASure:CURRent? CH1` | |
| Measure output power | `MEASure:POWEr? CH1` | |
| Output on / off | `OUTPut CH1,ON` / `OUTPut CH1,OFF` | **write only, there is no `OUTPut?` query** |
| Status register | `SYSTem:STATus?` | hex; **bit 4 = output ON, bit 0 = CV(0)/CC(1)** |
| Error queue | `SYSTem:ERRor?` | `0  No Error` when clean |
| Save / recall | `*SAV n` / `*RCL n` | slot 1..5 |
| Network | `IPaddr?` `MASKaddr?` `GATEaddr?` `DHCP?` | |

## Notes / gotchas

- **The output state is not queryable directly.** There is no `OUTPut?` query;
  read `SYSTem:STATus?` and test **bit 4** (this server does that for you and
  reports `output_on`).
- **CV vs CC** comes from **bit 0** of `SYSTem:STATus?` (0 = constant voltage,
  1 = constant current). Under no load the supply sits in CV.
- **The LAN service is single client and fragile.** The port-5025 SCPI server
  tolerates one connection and can wedge if a client disconnects abruptly or
  reconnects in a rapid burst; both the raw socket and the VXI-11 service then
  reset every new client even though the unit still answers ping. This server
  avoids that by keeping **one persistent, lock serialized connection**, closing
  it gracefully, and pacing reconnects. Do not point multiple clients at the
  supply at once, and do not fire tools concurrently.
- **If it does wedge** (rare, usually only from an unclean kill), the fix is a
  **power cycle** of the unit; ping and the network stack stay up, but the
  instrument services need the reset. Toggling the output from the front panel
  is harmless meanwhile.
- **`set_output` energizes the terminals** at the present voltage setpoint. It
  returns the live measured voltage/current so you can confirm what happened.
- **Setpoints are independent of output state.** Changing voltage/current while
  the output is off just stages the value; it applies when you enable output.
- The unit powers on with the **output off** by default (a front panel setting).

## Contributing

Issues and PRs welcome, see [CONTRIBUTING.md](CONTRIBUTING.md). CI lints with
[ruff](https://docs.astral.sh/ruff/) and runs an import smoke test on Python
3.10 to 3.13 (no supply required).

## License

[MIT](LICENSE) © Valentino Saitz