Skip to main content
Glama

scpi-mcp

LabLink gives agents a SCPI wire; scpi-mcp gives agents oscilloscope expertise.

scpi-mcp is a Model Context Protocol server that lets an agent operate an oscilloscope โ€” not just push SCPI strings at one. It exposes goal-level tools (capture this signal, measure that, characterize a channel) backed by a clean, instrument-agnostic interface. All vendor SCPI lives in the underlying instrument library; the MCP layer stays a thin, uniform wrapper with no raw-SCPI escape hatch.

        .-"""-. 
       / .===. \      Skippi  ๐Ÿงšโ€โ™‚๏ธ
       \/ 6 6 \/      the artistic scope gremlin
       ( \___/ )      "every waveform is a portrait of the magic pixies"
    ___ooo___ooo___

Skippi is the mascot โ€” the artistic gremlin who lives in the scope and paints pictures of the magic pixies (the ones who carry every electron down the probe). When you see a clean trace, that's Skippi's latest masterpiece.

Scope

First instrument: Rigol DS1054Z (unlocked to DS1104Z base) โ€” 4 analog channels, 100 MHz. No :SOURce (function generator) or :LA (logic analyzer) support. Reachable over USB or LAN (TCPIP::<ip>::INSTR).

Related MCP server: keyscope-mcp

Architecture

  • transport/ โ€” owns discovery. Enumerates USB, falls back to best-effort LAN auto-discovery, then to an IP prompt; resolves a single VISA resource string and hands it to the instrument layer. Discovery never leaks upward.

  • instruments/ โ€” vendor-specific backends behind one abstract base.py interface. rigol_ds1000z.py is a thin wrapper over the (complete) library; mock.py implements the same interface with zero hardware. Adding an instrument is one new file here and zero changes to tools/.

  • tools/ โ€” instrument-agnostic MCP tools that call only base.py.

  • config.py โ€” permission tiers (read_only / read_config / full), enforced at the tool layer. The server refuses; the model never decides.

Status

Live hardware, active bench use. The Rigol DS1054Z is connected and scpi-mcp is driving real measurements โ€” see Skippi in the Wild for captures. The mock backend remains available for development and CI.

Quickstart

uv sync
uv run pytest          # all green against the mock, zero hardware
uv run scpi-mcp        # start the MCP server (mock backend by default)

Skippi in the Wild

Real captures from the bench, analyzed live via scpi-mcp.

Power-off โ€” relay coil with freewheel diode

The setup: A 24 V relay coil (with freewheel diode installed) powered from a bench supply. Probed at the moment of power-off to observe inductive kickback. CH1 watches the supply rail; CH2 sits across the freewheel diode.

Prompts used:

Grab the scope screen.
What are the min and max voltages on each channel?
Is that spike on CH2 actually 60 V, or is the scale messing with me?

scpi-mcp called capture_screen and measure_snapshot, then explained the visual illusion: CH2 is set to 1 V/div, so its 5.36 V kickback spike fills 5+ of the 8 vertical divisions and looks enormous. CH1 is at 8 V/div, so the 24 V rail only occupies ~3 divisions despite being far larger in absolute terms. No 60 V spike โ€” the freewheel diode clamped it cleanly to V_supply + V_f โ‰ˆ 24.7 V, right where it should be.

Power-off capture โ€” relay coil, freewheel diode installed

500 ms/div (5 s full sweep). CH1 yellow, 8 V/div: 24 V supply decaying from 26.9 V โ†’ 2.24 V. CH2 cyan, 1 V/div: freewheel diode kickback, max 5.36 V, min โˆ’2.08 V.


Power-off โ€” bare coil, no freewheel diode

The setup: The same 24 V relay coil, but with the freewheel diode removed โ€” a bare inductor. Powered from a bench supply (100 nF across the rail to tame switch-mode ripple). Two 10x probes straddle the disconnect on the high side: CH1 on the supply side, CH2 on the coil side. Power is cut by pulling the + lead, and a single-shot trigger catches the unclamped kickback.

Prompts used:

Prime the scope for a single-shot on CH2 falling โ€” I'll pull the + lead.
We didn't clear the clipping โ€” reset V/div on CH2 and re-arm for another one-shot.
Got it โ€” grab the screencap and give me the true peak.

scpi-mcp armed a single-shot falling-edge trigger on CH2, watched the trigger status, and grabbed the screen plus both channel waveforms the instant it fired. This time the scale fought back the other way: the kick kept overflowing the window โ€” it railed at ยฑ130 V, then again at โˆ’304 V โ€” so scpi-mcp stepped CH2 coarser to 100 V/div until the whole transient fit. With no freewheel diode to absorb it, the collapsing field threw a โˆ’436 V spike with a +240 V overshoot โ€” a ~676 Vpp ring, roughly 18x the 24 V rail โ€” then bled off exponentially back toward the rail over ~1 ms. Compare the clamped case above, where the diode held the same coil to โ‰ˆ24.7 V.

Power-off capture โ€” bare coil, freewheel diode removed

200 ยตs/div. CH1 yellow, 20 V/div: supply side holds ~24 V with a small coupled ring at the break. CH2 cyan, 100 V/div: bare-coil kickback โ€” peak โˆ’436 V, +240 V overshoot (~676 Vpp), then exponential bleed-back. No freewheel diode; contrast the โ‰ˆ24.7 V clamped result above.

License

MIT

Available Tools

18 tools
acq_configureA

Set acquisition type and/or memory depth. Disruptive โ€” needs confirm=True.

Deeper memory captures more points at a given time base; averaging/peak modes trade update rate for noise rejection or glitch capture.

Inputs: confirm, acq_type, memory_depth. Returns: object

acq_forceA

Force a trigger now, ignoring the trigger condition. Needs confirm=True.

Inputs: confirm. Returns: object

acq_runA

Start continuous (free-running) acquisition. Disruptive โ€” needs confirm=True.

Inputs: confirm. Returns: object

acq_singleA

Arm a single-shot acquisition: capture one trigger then stop. Needs confirm=True.

Pair with set_trigger to catch a one-off transient (e.g. a power-on spike).

Inputs: confirm. Returns: object

acq_stopA

Stop acquisition, freezing the current trace. Disruptive โ€” needs confirm=True.

Inputs: confirm. Returns: object

capability_reportA

Report the connected instrument's detected capabilities.

Returns channel count/ids, bandwidth, source & logic support, max memory
depth, and installed options. Takes no arguments; requires a prior
self_config. Use it to learn what the scope can do before driving it.

Inputs: no named parameters. Returns: object

capture_alignedA

Capture multiple channels on the same time base, time-aligned.

The instrument shares one time base across channels, so a sequence of reads is already time-aligned. (Real hardware reads them back-to-back; the mock is instantaneous.) Use this to compare two signals sample-for-sample.

Inputs: channels (required), deep, include_samples. Returns: object

capture_memoryB

Capture the deep-memory waveform for one channel (full sample-rate record).

Inputs: channel (required), points, include_samples. Returns: object

capture_screenA

Capture the on-screen waveform for one channel (~1200 points).

Inputs: channel (required), include_samples. Returns: object

characterize_signalA

Characterize the signal on channel โ€” the FLAGSHIP goal-level tool.

Runs the probe โ†’ vertical fit โ†’ horizontal fit โ†’ trigger hunt โ†’ characterize
loop, settling and re-verifying after each adjustment (bounded by
:data:`MAX_SETTLE_ITERATIONS`). Returns the discovered signal parameters, a
full measurement snapshot, and a confidence flag. When ``export_path`` is
given, the deep-memory capture is rendered to a PNG there.

Inputs: channel (required), export_path. Returns: object

identifyA

Return the connected instrument's identity (vendor, model, serial, firmware).

Takes no arguments; requires a prior self_config.

Inputs: no named parameters. Returns: object

measureA

Take a single measurement of one quantity on one channel.

Inputs: kind (required), channel (required). Returns: object

measure_seriesA

Poll one measurement repeatedly, returning a short time series.

Useful for watching a value drift or settle (e.g. a ramping duty cycle).
``interval_s`` is honored via the instrument's settle hook so the mock stays
instant in tests while real hardware actually waits between reads.

Inputs: kind (required), channel (required), samples, interval_s. Returns: object

measure_snapshotC

Take several measurements at once on one channel (one read per quantity).

Inputs: channel (required), kinds. Returns: object

self_configA

Discover/connect an instrument and attach it to the session.

Call this first โ€” every other tool needs a connected instrument. Runs the
transport cascade (USB โ†’ LAN โ†’ manual host); pass ``host`` to skip discovery
and target a known address.

Inputs: host. Returns: object

set_channelA

Set a channel's vertical configuration (enable, V/div, offset, coupling).

Only the arguments you pass are changed; omit any to leave it as-is. Use this to fit a signal on screen before measuring or capturing.

Inputs: channel (required), scale, offset, enabled, +1 more. Returns: object

set_timebaseA

Set the horizontal time base (seconds/div and trigger offset).

Controls how many periods of the signal are shown; widen s/div to see slow events, narrow it to resolve fast ones. Shared across all channels.

Inputs: scale, offset. Returns: object

set_triggerA

Configure the edge trigger (source channel, level, slope).

Sets up a stable trigger so repetitive signals hold still, or so a
single-shot (see acq_single) fires on the right edge of a one-off event.

Inputs: level (required), source (required), slope. Returns: object

Tool Schema Changelog

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

Loading tool schema changelogโ€ฆ

    TDQS

    A3.9/5.0

    Scored across 18 tools

    Disambiguation5/5

    Each tool targets a distinct oscilloscope operation. Acquisition, capture, measurement, and configuration tools have clear, non-overlapping purposes. Even similar tools like acq_run and acq_single are differentiated by continuous vs single-shot acquisition.

    Naming Consistency4/5

    Tools predominantly follow a consistent verb_noun pattern (e.g., set_channel, capture_screen). Minor deviations like 'identify' and 'self_config' slightly break the pattern but remain intuitive. Overall, naming is predictable and readable.

    Tool Count5/5

    18 tools comprehensively cover essential oscilloscope operations without redundancy. The count is well-scoped for a domain-specific server, providing enough granularity for complex workflows while remaining manageable.

    Completeness4/5

    Core lifecycle tasks (connect, configure, acquire, capture, measure) are fully covered. Advanced features like FFT or automated calibration are absent but not critical for basic SCPI control. Minor gaps exist but do not hinder primary use cases.

    Maintenance

    ActivityInactive
    ResponsivenessOutdated data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      D
      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.
      13
      16 npm
      9
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      AI-native oscilloscope control. A pure-Python MCP service that lets LLM agents control Keysight EDUX1052G oscilloscopes via a compact DSL.
      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
      Not graded
      quality
      D
      maintenance
      An MCP server that connects AI assistants to the Owon DS1102 oscilloscope, enabling waveform capture, measurement retrieval, and remote control of scope settings.
      GPL 3.0