scpi-mcp
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@scpi-mcp Capture the signal on channel 1"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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 abstractbase.pyinterface.rigol_ds1000z.pyis a thin wrapper over the (complete) library;mock.pyimplements the same interface with zero hardware. Adding an instrument is one new file here and zero changes totools/.tools/โ instrument-agnostic MCP tools that call onlybase.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.

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.

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 toolsacq_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
Scored across 18 tools
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.
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.
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.
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
Related MCP Connectors
MCP server for building and testing AI agents with multi-model experimentation and insights.
The Remote MCP server acts as a standardized bridge between LLM applications (like Claude, ChatGPT, and Cursor) and external services, enabling AI agents to access external tools and resources. Its primary capability is providing a centralized search tool to discover other MCP servers and their respective tools. Unlike local implementations, it runs remotely with OAuth authentication and permission controls for security.
MCP server for your apps' tools and custom tools, plus hosted AI agents and approval-gated workflows
MCP server connecting AI agents to 100+ apps (Gmail, Slack, Notion, GitHub) via one-click OAuth.
Related MCP Servers
- AlicenseAqualityDmaintenanceAn 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.1316 npm9MIT
- AlicenseNot gradedqualityDmaintenanceAI-native oscilloscope control. A pure-Python MCP service that lets LLM agents control Keysight EDUX1052G oscilloscopes via a compact DSL.MIT
- AlicenseNot gradedqualityCmaintenanceAn MCP server for controlling Rigol oscilloscopes from an AI assistant. It translates MCP tool calls into SCPI commands over PyVISA.1MIT
- AlicenseNot gradedqualityDmaintenanceAn 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