Skip to main content
Glama
rigol-mcp
by erebusnz

rigol-mcp

MCP server for controlling Rigol DS1000Z and DHO series oscilloscopes over LAN or USB. Exposes the scope as a set of tools that Claude (or any MCP client) can call to take measurements, configure the instrument, and capture screenshots — entirely through natural language.

Scope

Example: unknown signal characterisation in Claude

Unknown signal (square wave into LCR trap), wrong channel enabled, invalid timebase/voltage/trigger. Claude identifies the signal type, corrects the setup, and characterises the waveform.

Scope

Supported Hardware

Rigol DS1000Z / MSO1000Z series (8-bit):

Model

Channels

Notes

DS1054Z

4 analog

Most common, 50 MHz

DS1074Z

4 analog

70 MHz

DS1074Z-S

4 analog + signal gen

DS1104Z

4 analog

100 MHz

DS1104Z-S

4 analog + signal gen

MSO1054Z

4 analog + 16 digital

MSO variant

MSO1074Z

4 analog + 16 digital

MSO1104Z

4 analog + 16 digital

Rigol DHO series (12-bit):

Model

Channels

Notes

DHO924S

4 analog + signal gen

250 MHz

Other DHO models (DHO900/1000/4000 families) likely work with the same SCPI dialect but are not verified.

The scope connects either over your local network via Ethernet (rear panel RJ45) or over USB (rear panel USB-B device port). LAN is the default; USB is used when RIGOL_USB is set (see Configuration). USB transport has been validated on DS1000Z; DHO support has so far been validated over LAN only.

Related MCP server: SkippyMCP

Requirements

  • Python 3.11+

  • uv

  • A Rigol DS1000Z (LAN or USB) or DHO series scope (LAN) connected to your computer

  • For LAN: SCPI over TCP/IP enabled on the scope (on by default)

  • For USB: a VISA driver on the scope's USB interface — either the native USBTMC driver (e.g. from Rigol UltraSigma / any NI-VISA runtime) or WinUSB via Zadig (see USB connection)

Installation

git clone https://github.com/erebusnz/rigol-mcp
cd rigol-mcp
uv sync

Scope Network Setup

On the scope, go to Utility → IO Setting → LAN and note the IP address (or assign a static one). The scope listens on port 5555 for raw SCPI commands — no additional configuration is needed.

Replace 192.168.1.123 with the IP address of your scope in all instructions below.

Verify in browser: http://192.168.1.123/DS1000Z_WelcomePage.html

Verify connectivity before using as MCP:

python -c "import pyvisa; rm = pyvisa.ResourceManager('@py'); s = rm.open_resource('TCPIP0::192.168.1.123::5555::SOCKET'); s.write_termination='\n'; s.read_termination='\n'; print(s.query('*IDN?'))"

You should see something like:

RIGOL TECHNOLOGIES,DS1054Z,DS1ZA123456789,00.04.04.SP4

Configuration

Set the scope IP for MCP via environment variable:

export RIGOL_IP=192.168.1.123

Or create a .env file (copy from .env.example):

RIGOL_IP=192.168.1.123

Optional:

Variable

Default

Description

RIGOL_IP

(required for LAN)

Scope IP address

RIGOL_USB

(unset)

Set to 1 to connect over USB instead of LAN. The first Rigol USB scope is found automatically.

RIGOL_USB_SERIAL

(unset)

When several Rigol scopes are on USB, pin a specific one by serial number.

RIGOL_ENABLE_SEND_RAW

(unset)

Set to 1 to enable the send_raw tool (arbitrary SCPI). Off by default — see Tools.

RIGOL_SCREENSHOT_DIR

screenshots/

Directory for saved PNG screenshots

RIGOL_WAVEFORM_DIR

waveforms/

Parent directory for full acquisition exports

USB connection

Set RIGOL_USB=1 to connect over USB instead of LAN. The server looks for a Rigol scope on USB (vendor ID 0x1AB1) and connects to it. If more than one scope is connected, set RIGOL_USB_SERIAL to choose which one by serial number.

RIGOL_USB=1
# RIGOL_USB_SERIAL=DS1ZA000000000   # only needed if more than one scope is on USB

Windows USB instructions

The scope's USB interface needs a VISA-compatible driver. The server auto-detects whichever you have installed — pick one:

Option A — install the WinUSB driver with Zadig. The server talks to the scope through the bundled pure-Python pyvisa-py (@py) backend (pyusb + libusb); no extra Rigol software needed. Do this once per scope:

  1. Connect the scope over USB and power it on.

  2. Download and run Zadig.

  3. Choose Options → List All Devices.

  4. Select "DS1000Z Series" in the device dropdown.

  5. Set the target driver to WinUSB and click Install Driver (or Replace Driver).

  6. Wait for "The driver was installed successfully", then set RIGOL_USB=1.

Option B — install Rigol UltraSigma, which bundles the driver. UltraSigma installs the standard "USB Test and Measurement Device" (USBTMC) driver. Once it's installed, the server reaches the scope through the NI-VISA (@ivi) backend — nothing else to configure.

Note: the two drivers are mutually exclusive on a given USB interface. Installing WinUSB (Option A) means UltraSigma can no longer see the scope over USB until you revert the driver in Device Manager, and vice versa. LAN access is unaffected either way.

Linux / macOS USB

Works without needing drivers using the pyvisa-py (@py) backend.

  • macOS: typically works as-is once RIGOL_USB=1 is set; no driver to install.

  • Linux: give your user permission to claim the device with a udev rule, then replug it:

    # /etc/udev/rules.d/60-rigol.rules
    SUBSYSTEM=="usb", ATTRS{idVendor}=="1ab1", MODE="0660", GROUP="plugdev"
    sudo udevadm control --reload-rules && sudo udevadm trigger

    (Make sure your user is in the plugdev group: sudo usermod -aG plugdev $USER, then log out and back in.)

    If the kernel's usbtmc module has already claimed the scope, unbind or blacklist it so libusb can take the interface — pyvisa-py does not detach it automatically.

USB is currently hardware-tested on Windows only; Linux/macOS use the standard libusb setup above.

Claude Desktop / Claude Code Setup

Add to your .mcp.json (or Claude Desktop MCP config):

{
  "mcpServers": {
    "rigol": {
      "command": "uv",
      "args": ["run", "rigol-mcp"],
      "cwd": "/path/to/rigol-mcp",
      "env": {
        "RIGOL_IP": "192.168.1.123"
      }
    }
  }
}

For USB, replace the RIGOL_IP entry in env with "RIGOL_USB": "1" (see USB connection).

Tools

Identification & State

Tool

Description

idn

Identify the instrument — make, model, serial, firmware

get_scope_state

Snapshot of all channel configs, timebase, and trigger settings

get_acquisition_info

Read-only acquisition status, actual sample rate, memory-depth setting, enabled channels, and implemented transfer modes (DS1000Z acquisition metadata)

disconnect

Release this server's cached VISA connection without opening one or sending acquisition commands; later scope tools may reconnect

Acquisition Control

Tool

Description

run

Start continuous acquisition

stop

Stop and freeze display

single

Arm for one trigger event, then stop

autoscale

Auto-configure timebase, vertical scale, and trigger

Configuration

Tool

Description

set_channel

Set scale (V/div), offset, coupling (AC/DC/GND), probe ratio, on/off

set_timebase

Set time/div and trigger offset

set_trigger

Configure edge trigger: source, slope (POS/NEG/RFAL), level

Measurement

Tool

Description

measure

Query any single-channel measurement: VMAX, VMIN, VPP, VTOP, VBASE, VAMP, VAVG, VRMS, PVRMS, VUPPER, VMID, VLOWER, VARIANCE, FREQUENCY, PERIOD, PWIDTH, NWIDTH, PDUTY, NDUTY, RTIME, FTIME, OVERSHOOT, PRESHOOT, PSLEWRATE, NSLEWRATE, TVMAX, TVMIN, MAREA, MPAREA, PPULSES, NPULSES, PEDGES, NEDGES

measure_between

Query delay or phase between two channels. DS1000Z: RDELAY, FDELAY, RPHASE, FPHASE. DHO: RRDELAY/RFDELAY/FRDELAY/FFDELAY, RRPHASE/RFPHASE/FRPHASE/FFPHASE (DS1000Z names auto-map to the homogeneous-edge DHO equivalents)

get_waveform

Download and analyse waveform data (NORM screen buffer: up to 1200 pts on DS1000Z, 1000 on DHO); returns text analysis by default, raw time/voltage arrays with raw_data=true

export_acquisition

Save complete stopped DS1000Z acquisition memory as binary samples and a JSON manifest; returns paths, counts, timing, and integrity warnings

Full acquisition-memory export

get_waveform(raw_data=true) changes the response representation only: it still reads the NORM screen buffer. For offline filtering or protocol analysis, use export_acquisition with {"channels": ["CHAN1", "CHAN2", "CHAN3", "CHAN4"]}. Omit channels to export all currently enabled analog channels. output_dir optionally selects the parent directory on the server's machine. Each export gets a unique subdirectory.

The scope must already report STOP. A single acquisition that has finished is suitable; WAIT, RUN, and AUTO are rejected. Stopping is a separate, explicit action. The instrument does not expose a capture timestamp or completion history through this API, so STOP alone cannot prove that a previous trigger completed. Keep the front panel and other SCPI clients idle throughout the export. No run, single, autoscale, channel-enable, or acquisition-setting writes are issued by the export.

The tool selects RAW/BYTE through the DS1000Z driver, queries each channel's preamble, and streams the complete record in chunks of at most 250,000 samples. It checks range readback, binary block lengths, file sizes, stopped status, and metadata consistency. Waveform source, mode, format, and range are restored and checked afterward; restoration failures appear in the warnings. If any scope I/O fails, including a text query or command write, or a binary block has invalid framing, restoration is skipped and the connection is invalidated: a delayed or partial reply could otherwise be mistaken for the next SCPI query's response. Local filesystem failures do not by themselves prevent restoration. The tool reports when transfer settings were not restored; it does not reconnect or retry that export. DHO memory export is currently unsupported and never falls back to the screen buffer. AUTO memory depth is reported as a setting; actual record lengths come from RAW preambles rather than a presumed installed memory option.

The returned manifest_path identifies a JSON file beside CHAN1.bin, etc. Samples are unsigned 8-bit codes, without a SCPI header. The manifest contains instrument and acquisition settings, UTC export timestamps, preambles, channel units and probe factors, requested/exported ranges (one-based, inclusive), chunk/sample counts, and completeness. Duration is the time between first and last saved sample: (points - 1) * interval. Each channel has its own trigger-relative time axis; alignment differences are flagged. Preamble scaling already includes the configured probe ratio; do not multiply it again. Transfer and conversion follow the Rigol programming guide, waveform commands.

Read an export locally in Python (NumPy is needed only for this example):

import json
from pathlib import Path
import numpy as np

path = Path("waveforms/acquisition-EXAMPLE/manifest.json")  # use returned manifest_path
manifest = json.loads(path.read_text(encoding="utf-8"))
assert manifest["complete"], manifest.get("error")
channel = manifest["channels"]["CHAN1"]
codes = np.fromfile(path.parent / channel["file"], dtype=np.uint8)
assert codes.size == channel["points"]
p = channel["preamble"]
times_s = p["xorigin"] + (np.arange(codes.size) - p["xreference"]) * p["xincrement"]
values = (codes.astype(float) - p["yorigin"] - p["yreference"]) * p["yincrement"]
# values use channel["settings"]["units"] (normally VOLT).

Interrupted transfers retain validated contiguous chunks and an explicitly incomplete manifest. The response reports the failure and saved counts; it never embeds large arrays or retries the entire export automatically. A process crash can leave extra bytes beyond the last manifest checkpoint: treat incomplete files as partial and use only reported counts. File system failures may prevent a final manifest update; initial manifests are always marked incomplete. After an export has started, a failed final manifest write returns complete=false, manifest_current=false, manifest_error, and the file path. persisted_points reports the last successful checkpoint, while channel points reports validated samples written during this attempt. In this case recovery_manifest supplies the current conversion metadata and counts without any waveform arrays. Preserve that response because the on-disk manifest is stale. The original transfer error, if any, is retained separately from the manifest error. Sample/time subranges, CSV, and NPZ are not implemented.

Cursors

Tool

Description

set_cursors

Set cursor mode (MANUAL/TRACK/OFF) and time positions in seconds

get_cursor_values

Read cursor positions (in seconds) and all delta/amplitude readouts

Utility

Tool

Description

screenshot

Capture display as PNG — returns image inline and saves to disk

send_raw

Send any SCPI command directly (escape hatch). Disabled by default — set RIGOL_ENABLE_SEND_RAW=1 to expose it, since arbitrary SCPI can leave the scope in any state.

check_error

Query the SCPI error queue

Example Prompts

Basic measurement session:

"Connect to the scope, check what's configured, then measure the frequency and Vpp on channel 1."

Signal characterisation:

"Stop the scope, download the waveform from channel 2, and tell me the rise time, overshoot percentage, and estimated fundamental frequency."

Setup from scratch:

"Set channel 1 to 2V/div DC coupling with a 10x probe, set the timebase to 1ms/div, trigger on channel 1 rising edge at 1V, then run and take a screenshot."

Cursor measurement:

"Put manual cursors on the first rising edge of the signal on channel 1 — cursor A at the 10% level and cursor B at the 90% level — and read the rise time from the delta."

Transient / ringing characterisation:

"There's a damped oscillation on channel 1 after a step edge. Stop the scope, measure Vpp, Vmax, Vmin, and Vrms, then estimate the ring frequency and how many cycles it takes to decay."

Iterative debugging:

"I'm verifying the gain of an amplifier. Channel 1 is the input, channel 2 is the output. The expected gain is 20 dB. Figure out whether it's within spec."

Unknown signal characterisation:

"There's an unfamiliar signal on channel 1. I don't know its frequency, amplitude, or shape. Keep adjusting the timebase and vertical scale until you have a stable, well-framed view of at least two full cycles, then give me a complete characterisation of what you see."

Architecture

Claude / MCP client
        │  MCP protocol (stdio)
rigol_mcp.server      ← tool definitions, request routing
        │  Python function calls
rigol_mcp.scope       ← VISA connection, SCPI command helpers
        │  SCPI over TCP/IP (port 5555) or USBTMC
Rigol DS1000Z         ← 192.168.1.123  /  USB

The VISA connection is cached across tool calls (one connection per server session) and reconnects automatically on communication errors.

Scope I/O is strictly synchronous. One server lock covers each operation, including export transfers, restoration, and retry cleanup; disconnect takes that same lock and waits for the active operation to finish. There are no background scope workers or parallel channel downloads. Cancellation cannot interrupt an in-progress synchronous transfer midway through its I/O.

Use disconnect when this server should release its cached USB/LAN access. It closes the session without clearing the device or changing acquisition settings, and does not open a connection when none exists. A later scope tool can reconnect, so avoid queuing more scope calls after disconnecting. This lock and disconnect apply to this server process only: use one active server/client for a physical instrument. They cannot prevent a separate program or another MCP server from accessing the scope.

SCPI Transport

By default the server connects using raw socket VISA (TCPIP0::<ip>::5555::SOCKET), not VXI-11. This avoids the NI-VISA dependency and works with the pure-Python pyvisa-py backend. It also eliminates the VXI-11 handshake overhead, making individual commands faster.

When RIGOL_USB is set, the server instead connects over USBTMC (USB0::0x1AB1::<model>::<serial>::INSTR), discovering the scope automatically. It auto-selects whichever VISA backend can see the scope:

  • @py (pyvisa-py + pyusb + bundled libusb) — for a USB interface bound to WinUSB (no NI-VISA needed).

  • @ivi (NI-VISA / IVI VISA) — for the native USBTMC driver (e.g. installed with Rigol UltraSigma).

The two backends frame USBTMC reads differently, so block reads (waveform/screenshot) are read by exact byte count on @py and via native message reads (read_raw) on @ivi. Transport and backend selection live in rigol_mcp.scope.get_scope / _open_usb_scope.

Testing

Unit tests are fully offline — the VISA layer is faked, so no instrument is required.

uv run --extra test pytest      # or: uv sync --extra test && uv run pytest

Limitations

  • USB driver setup is platform-specific: Windows needs WinUSB (via Zadig) or NI-VISA/UltraSigma; Linux needs libusb access (a udev rule) or NI-VISA; macOS typically works through libusb with no setup. The server auto-detects the backend — see USB connection. LAN needs no driver setup on any platform.

  • No support for math channels, digital channels (MSO), or protocol decode in the current tools yet — use send_raw for those

  • get_waveform uses the NORMAL screen buffer; export_acquisition supports full RAW memory on DS1000Z only. Hardware validation against a calibrated signal remains necessary for absolute time/voltage accuracy.

License

MIT — see LICENSE.

Available Tools

17 tools
autoscaleA

Run the scope's auto-setup (timebase, vertical scale, trigger). Takes a few seconds; call get_scope_state afterwards to see the resulting configuration.

Inputs: no named parameters.

check_errorA

Query the SCPI error queue. Returns the error if present, or 'No error' if clear.

Inputs: no named parameters.

get_cursor_valuesB

Read current cursor mode and all cursor readouts. AX_s and BX_s are time positions in seconds.

Inputs: no named parameters.

get_scope_stateA

Return a snapshot of the scope's current configuration: active channels (scale, offset, coupling, probe), timebase, and trigger. Call this at the start of a session to understand the current setup.

Inputs: no named parameters.

get_waveformA

Download and analyse the current waveform for a channel (screen buffer, ~1200 points). Stop or single-trigger the scope first for consistent data. By default returns a plain-text analysis: signal shape, frequency/period, amplitude, DC offset, cycle count, and data-quality warnings (e.g. mid-cycle edges, invalid frequency). Set raw_data=true to get the full time/voltage JSON arrays instead. After reading, act on any warnings — if FREQUENCY would be 9.9E37 widen the timebase; if edges are not near the DC mean, adjust offset so right edge = N×(period/2) − 6×scale.

Inputs: channel (required), raw_data.

idnA

Identify the instrument. Returns make, model, serial, and firmware version. Use to verify connectivity before starting a measurement session.

Inputs: no named parameters.

measureA

Query a single-source built-in measurement on a channel. Stop acquisition first for stable readings. channel: CHAN1–CHAN4. item: VMAX, VMIN, VPP, VTOP, VBASE, VAMP, VAVG, VRMS, FREQUENCY, PERIOD, PWIDTH, NWIDTH, PDUTY, NDUTY, RTIME, FTIME, OVERSHOOT, PRESHOOT, PSLEWRATE, NSLEWRATE, TVMAX, TVMIN, VUPPER, VMID, VLOWER, VARIANCE, PVRMS, PPULSES, NPULSES, PEDGES, NEDGES. A return value of 9.9E37 is the scope's invalid/overflow sentinel — it means the measurement could not be computed (e.g. FREQUENCY returns 9.9E37 when the timebase is too narrow to show a complete cycle; widen scale and retry). For delay or phase between two channels use measure_between.

Inputs: channel (required), item (required).

measure_betweenA

Query a two-source delay or phase measurement between two channels. source1 is the reference channel, source2 is the measured channel. item: RDELAY (rising-edge delay, seconds), FDELAY (falling-edge delay, seconds), RPHASE (rising-edge phase, degrees), FPHASE (falling-edge phase, degrees). Stop acquisition first for stable readings.

Inputs: source1 (required), source2 (required), item (required).

runB

Start continuous acquisition. Returns trigger status after the command.

Inputs: no named parameters.

screenshotA

Capture a screenshot of the oscilloscope display. Returns the image and the absolute path where the PNG was saved.

Inputs: no named parameters.

send_rawA

Send an arbitrary SCPI command. Queries (ending with '?') return the response string; writes return empty string and auto-check the error queue. Use as an escape hatch when no dedicated tool covers the operation.

Inputs: command (required).

set_channelA

Configure a channel. Only specified parameters are changed. channel: CHAN1–CHAN4. scale_v_div: V/div. offset_v: volts. coupling: AC, DC, or GND. probe: attenuation ratio (1, 10, 100, …). Parameter names match get_scope_state output for easy round-tripping. Returns the resulting channel configuration.

Inputs: channel (required), display, scale_v_div, offset_v, +2 more.

set_cursorsC

Set cursor mode and/or X positions. mode: OFF, MANUAL, TRACK (omit to keep current mode). ax/bx: cursor A/B time positions in seconds. Returns the resulting cursor readouts.

Inputs: mode, ax, bx.

set_timebaseA

Set the horizontal timebase. scale_s_div: seconds per division (e.g. 0.001 for 1 ms/div). offset_s: shifts the display window; time_start = offset_s − 6×scale_s_div, time_end = offset_s + 6×scale_s_div. Trigger (t=0) is always a zero crossing when using edge trigger. To align the right edge to a zero crossing at time T: set offset_s = T − 6×scale_s_div. To put the trigger at the left edge of the screen: set offset_s = +6×scale_s_div. Parameter names match get_scope_state output for easy round-tripping. Returns the resulting timebase configuration.

Inputs: scale_s_div, offset_s.

set_triggerA

Configure edge trigger. source: CHAN1–CHAN4 or EXT. slope: POS (rising), NEG (falling), or RFAL (either). level: trigger level in volts. Returns the resulting trigger configuration.

Inputs: source, slope, level.

singleA

Arm the scope for a single acquisition; stops automatically after one trigger event. Returns trigger status. Note: acquisition does not complete until a trigger occurs — call stop or check trigger status before reading measurements.

Inputs: no named parameters.

stopA

Stop acquisition and freeze the display. Use before reading measurements or cursors for stable values. Returns trigger status after the command.

Inputs: no named parameters.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4/5.0

    Scored across 17 tools

    Disambiguation5/5

    Each tool has a clearly distinct purpose with no ambiguity. For example, get_waveform retrieves waveform data, measure queries built-in measurements, set_channel configures channels, and screenshot captures images. The descriptions clearly differentiate their functions, preventing misselection.

    Naming Consistency5/5

    Tool names follow a highly consistent verb_noun pattern throughout, such as get_scope_state, set_channel, measure_between, and check_error. This predictability makes it easy for agents to understand and use the toolset without confusion.

    Tool Count5/5

    With 17 tools, the count is well-scoped for controlling an oscilloscope, covering essential operations like configuration, measurement, data acquisition, and error handling. Each tool earns its place without feeling excessive or insufficient for the domain.

    Completeness5/5

    The toolset provides complete coverage for oscilloscope operations, including setup (autoscale, set_*), state inspection (get_*), measurements (measure, measure_between), data capture (get_waveform, screenshot), control (run, stop, single), and error handling (check_error, send_raw). No obvious gaps exist, enabling full lifecycle management.

    Maintenance

    ActivitySlowing
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      An MCP server that enables AI assistants to control Siglent SDS oscilloscopes over a local network using SCPI commands. It allows users to measure signals, configure channel and acquisition settings, and capture waveforms or screenshots through natural language.
      12
      16 npm
      11
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      An MCP server for controlling Rigol oscilloscopes from an AI assistant. It translates MCP tool calls into SCPI commands over PyVISA.
      1
      MIT
    • A
      license
      A
      quality
      C
      maintenance
      An MCP server that enables AI agents to operate an oscilloscope through high-level tools like signal capture and measurement, abstracting vendor-specific SCPI commands.
      18
      4
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      MCP server for controlling Hantek DSO2D15 and other DSO2000-family oscilloscopes via USB and SCPI, enabling waveform acquisition, measurements, and screen captures.
      1
      MIT