Skip to main content
Glama
masahiro-999

oscilloscope-mcp

by masahiro-999

oscilloscope-mcp

MCP server that lets an AI agent (Claude Code, Cursor, custom) operate a bench oscilloscope over its LAN / SCPI interface — to close the "RTL sim PASS / HW FAIL" gap by observing physical pin behavior directly.

4-channel demo capture — agent-driven setup + screenshot of a RIGOL DS1104Z

One MCP-driven capture: an AI agent set the timebase, V/div, per-channel offset, channel labels, and trigger on a RIGOL DS1104Z, then pulled the screenshot back to the user. The four traces are GPIO 26 / 19 / 13 / 17 on a Raspberry Pi 4 driven at 50 / 100 / 200 / 500 Hz via pigpio DMA waveform output — the scope reads each one back exactly. The status bar along the bottom (Freq=50.0 Hz / 100 Hz / 200 Hz / 500 Hz) is the scope's own measurement, confirming the entire loop end-to-end. All SCPI traffic was issued by the agent through this MCP server's tools.

PNG screenshots are saved separately for user review with optional cursor / channel-label annotation, so the agent's structured fact extraction (numbers + caveats) is always cross-checkable by a human.

Currently ships drivers for the RIGOL DS1000Z series. The single SCPI dialect covers DS1054Z / DS1074Z / DS1104Z, but only DS1104Z is verified on real hardware at the moment — other DS1000Z variants ride the same code path and a profile YAML, but they have not been conformance-tested yet. The plug-in architecture lets you add a new vendor / model by dropping in two files: a SCPI-dialect driver subclass and a capability profile YAML. See Adding a new instrument below.

Install

git clone https://github.com/masahiro-999/oscilloscope-mcp.git
cd oscilloscope-mcp
pip install -e .

Related MCP server: siglent-sds-mcp

Run as MCP server

# Set the scope's network address.
export SCOPE_MCP_HOST=<your-scope-ip>     # e.g. 192.168.1.42

# Start the server (stdio transport, default).
python -m oscilloscope_mcp

# Or use the console script.
oscilloscope-mcp

Register with Claude Code

claude mcp add oscilloscope -- python -m oscilloscope_mcp

Then the scope_query, scope_screenshot, scope_trigger, scope_measure, scope_measure_stat, scope_channel, scope_timebase, scope_waveform, scope_acquire, scope_capture, scope_compare, and scope_viewer tools become available to any session that connects to this MCP server.

Tools exposed

MCP tool

Purpose

scope_query

Raw SCPI command passthrough. Returns the response plus a caveats[] warning that observation limits are not auto-checked on this path.

scope_screenshot

Save a PNG of the current scope screen for user review. Optionally place manual cursors (Δt / ΔV) and channel labels (DIR / STP / NXT / CLK) to make the image easy to read.

scope_trigger

Read or configure the trigger — every trigger type the instrument supports. Call with no arguments to read the current state (returns mode, globals, the mode's params, and accepted_params describing what's settable). To set, pass mode and/or the global sweep / coupling / holdoff_s, and put all type-specific parameters in a params object (e.g. {"source":"CHAN1","slope":"NEG","nth_edge":2,"idle_s":2e-3} for an Nth-edge trigger). mode is validated against the profile's full type list (6 standard + 9 option-licensed DS1000Z types), and each params key/value is validated against that mode's schema (enum membership, numeric range, channel source). Unsupported types/params are rejected with the valid list; option-licensed types emit a caveats[] warning. Pass action to control acquisition — RUN / STOP / SINGLE (arm one shot) / FORCE (force a trigger; NORMAL/SINGLE sweep only); status (TD/WAIT/RUN/AUTO/STOP) reflects the result.

scope_measure

Read scope-side automatic measurements (:MEAS:ITEM?) for a list of items on a source channel. Single-source items: VMAX VMIN VPP VTOP VBASE VAMP VAVG VRMS OVERSHOOT PRESHOOT PERIOD FREQUENCY RTIME FTIME PWIDTH NWIDTH PDUTY NDUTY; two-source (require source2): RDELAY FDELAY RPHASE FPHASE. Un-measurable values come back as null with a caveat.

scope_measure_stat

Read measurement statistics (:MEASure:STATistic:ITEM) for one or more items on a source — accumulated current/max/min/average/deviation/count rather than a single instantaneous value. stat_types defaults to all six (CURRent MAXimum MINimum AVERages DEViation COUNt); pass a subset to limit the query. mode optionally sets the statistics mode (DIFFerence / EXTRemum). reset=True clears accumulated data before querying. Returns {source, statistics: [{item, current, max, min, avg, dev, count}], caveats}.

scope_channel

Read or configure an analog channel's vertical setup. channel (1-based) is required. Pass any subset of scale_v_per_div, offset_v, coupling (AC/DC/GND), display, probe, bw_limit (20M/OFF), invert, units. Validated against the profile; 20 MHz BW limit emits a caveat.

scope_timebase

Read or configure the horizontal (timebase) setup. Pass any subset of s_per_div, offset_s, mode (MAIN/XY/ROLL). Validated against the profile; long timebase emits a memory-downgrade caveat.

scope_waveform

Capture one channel and return a compact threshold-quantized event stream, not raw ADC samples. Raw volts pass through a Schmitt-trigger comparator (threshold_v ± hysteresis_v/2) into a 0/1 stream, then run-length-encoded into runs [(t_us, level, dur_us)] and edges [(t_us, channel, RISE/FALL)] (times in µs, trigger at t=0). Hysteresis is mandatory — a single threshold on a noisy edge fragments into useless micro-pulses. A hysteresis_v over 20 % of the signal peak-to-peak emits a detection-miss caveats[] warning; an over-budget stream is truncated with a caveat. Args: source (default CHAN1), mode (NORMal or RAW — RAW reads the full acquisition memory up to 24M points instead of the ~1200-point screen decimation; scope must be stopped first), threshold_v (1.5), hysteresis_v (0.1).

scope_acquire

Read or configure the acquisition modetype (NORMal/AVERages/PEAK/HRESolution), averages (2..1024 powers of 2), and memory_depth (AUTO or numeric, validated per active channel count). Call with no args to read (returns type, averages, memory_depth, sample_rate). Selecting AVERages emits a caveats[] warning that the update rate is reduced.

scope_capture

Declarative single-shot capture — configure, arm, wait, and read in one call. Composes trigger/channel/timebase/acquire setup, arms :SINGle, polls until triggered (or timeout_s), then reads per-channel waveforms (quantized to runs/edges/bus_runs), scope-side measurements, and an optional screenshot. Use sweep='AUTO' or 'NORMAL' to skip arm+poll and read the current screen memory immediately. Set waveform_mode='RAW' to read the full acquisition memory after the trigger fires (scope is already stopped after SINGLE). Returns {triggered, channels, waveform, bus_runs, measurements, screenshot_path, caveats}.

scope_compare

Sim vs HW reference diff (P4) — the project's core goal. Takes a golden edge/run list from your RTL simulator and compares it against hardware (live capture or provided data). Returns {matched, shifted[{ref_t_us, hw_t_us, delta_us, kind}], missing[{t_us, kind}], added[{t_us, kind}], first_divergence_us, summary, caveats}. Two modes: live (captures from scope if hw_runs/hw_edges not provided) or offline (pass pre-captured data directly, no scope connection needed). tolerance_us (default 0.05 = 50 ns) sets how close two same-kind edges must be to count as a match. Signal-agnostic: works on SPI, I2C, UART, ULPI, or any digital signal.

scope_viewer

Generate a self-contained interactive HTML waveform viewer. Reads RAW waveform data (scope must be stopped) and embeds full analog voltage samples — no downsampling — into an HTML file. Features: per-channel ON/OFF toggle, per-channel V/div dropdown (10mV–100V), draggable GND offset markers (▶), T/div dropdown with 1-2-5 auto-stepping on scroll-zoom, drag-to-pan, vertical cursor snapped to sample points with fixed voltage readout, trigger position marker (▼T at t=0, derived from :TRIG:POS?). depth controls observation window centered on the trigger: "low" (30k pts, ~120µs, ~1s transfer), "mid" (300k, ~1.2ms, ~3s), "high" (3M+, ~12ms, ~17s). Browser renders millions of points via per-pixel min/max envelope when zoomed out, individual samples + dots when zoomed in.

Layout

oscilloscope-mcp/                 # repo (kebab-case)
├── README.md
├── ROADMAP.md
├── LICENSE
├── pyproject.toml
├── src/oscilloscope_mcp/         # Python package (snake_case)
│   ├── __init__.py
│   ├── __main__.py           # `python -m oscilloscope_mcp` entry
│   ├── server.py             # FastMCP server: tool registration + dispatch
│   ├── transport/
│   │   └── scpi_lan.py       # raw TCP SCPI client (zero third-party deps)
│   ├── instruments/
│   │   ├── _base.py          # Scope ABC — vendor-agnostic interface
│   │   ├── __init__.py       # MODEL_REGISTRY + open_scope() dispatch
│   │   ├── rigol_ds1000z.py  # RIGOL DS1054Z / DS1104Z driver
│   │   └── profiles/
│   │       ├── _ds1000z_family.yaml  # shared trigger + acquisition schema
│   │       ├── rigol_ds1104z.yaml
│   │       └── rigol_ds1054z.yaml
│   └── helpers/
│       ├── caveat_calc.py    # capability + current setting → caveats[]
│       ├── trigger.py        # trigger profile: validate / normalize / caveats
│       ├── measure.py        # measurement profile: validate / normalize / parse
│       ├── acquisition.py    # channel + timebase profile validation
│       ├── quantize.py       # raw volts → 0/1 stream (Schmitt hysteresis)
│       ├── rle.py            # digital stream → runs[(t_us, level, dur_us)]
│       ├── edges.py          # runs → edges[(t_us, ch, RISE/FALL)]
│       ├── bus.py            # multi-channel runs → bus_runs[(t_us, value, dur_us)]
│       ├── reference_diff.py # P4: sim vs HW edge diff (the project's raison d'etre)
│       ├── viewer.py         # self-contained HTML waveform viewer generator
│       ├── glitch_list.py    # P3: runs < min_width (runt/glitch detection)
│       ├── edge_interval_stats.py  # P3: jitter / clock stability stats
│       ├── pattern_search.py # P3: RLE bit-pattern template matching
│       ├── voltage_histogram.py    # P3: voltage distribution / bimodal detection
│       ├── fft_peaks.py      # P3: top-N frequency peaks (EMI / switching noise)
│       ├── causality_check.py      # P3: cross-channel "B follows A within N µs"
│       └── envelope_downsample.py  # P3: min/max decimation for visualization
└── tests/                    # unit + live conformance

Environment

Env var

Required

Meaning

SCOPE_MCP_HOST

yes

scope IP or hostname

SCOPE_MCP_PORT

no (default 5555)

SCPI TCP port

SCOPE_MCP_MODEL

no

model key (e.g. RIGOL_DS1104Z). If unset, *IDN? is parsed and matched against each profile's idn_match regex

Env-var prefix SCOPE_MCP_* is project-specific so it does not collide with other shells driving unrelated tools.

Adding a new instrument

Two new files plus one registry line:

  1. profilesrc/oscilloscope_mcp/instruments/profiles/<model>.yaml: capability (analog BW, sample rate per channel count, memory depth, idn_match regex).

  2. driversrc/oscilloscope_mcp/instruments/<vendor>_<series>.py: subclass oscilloscope_mcp.instruments._base.Scope, implement each abstract method using the vendor's SCPI dialect.

  3. registry line — add an entry to MODEL_REGISTRY in src/oscilloscope_mcp/instruments/__init__.py.

  4. conformance test — run

    SCOPE_MCP_HOST=<ip> \
    SCOPE_MCP_CONFORMANCE_MODEL=<MODEL_KEY> \
    pytest tests/test_instrument_conformance.py -v

No other module changes are required.

Limits (tool warnings)

All structured outputs include a caveats[] field. Operating outside a profile-declared limit (e.g. 4-channel mode forces 250 MSa/s on a DS1000Z, dropping practical observation to ~25 MHz) auto-emits a caveat. The agent must read caveats[] before trusting any number.

Status

P1 MVP — IDN / raw SCPI passthrough / screenshot with optional cursor + channel-label annotation. Verified end-to-end against a live RIGOL DS1104Z.

P1.5 quantize → RLEscope_waveform reads a channel in NORMal (screen) mode and returns a Schmitt-trigger-quantized runs / edges event stream (with optional multi-channel bus_runs via helpers/bus.py) instead of raw ADC samples. Pure helper modules (quantize, rle, edges, bus) are unit-tested without hardware; a live conformance test (gated by SCOPE_MCP_HOST) asserts the JSON shape on real hardware.

The phasing roadmap (P2 declarative capture → P3 reductions catalog → P4 reference_diff vs sim) is tracked in ROADMAP.md.

Security / trust model

This is a local-only stdio MCP server. It speaks to an MCP client over its parent process's stdin/stdout — it does not open any network port. The client (Claude Code, Cursor, your own agent) launches it as a subprocess, and the trust boundary is the local user account.

Within that boundary:

  • scope_query is a raw SCPI passthrough with no command validation. Any SCPI the client sends is forwarded to the scope. That is intentional — SCPI on a bench scope is overwhelmingly read-mostly, and even destructive setup changes (timebase, channel scale, trigger) are trivially undone — but it does mean an agent with this MCP server enabled can fully reconfigure your scope and read everything on its screen. Use accordingly.

  • The SCPI connection to the scope itself is unauthenticated TCP on the scope's LAN port (RIGOL DS1000Z uses 5555). Anything on the same network can already talk to the scope — installing this MCP server does not change that, it just gives the AI agent the same access you already have.

If you want to lock down the scope further, do it on the scope's network (VLAN, firewall) rather than at this layer. The MCP server is not the right place to police what an agent on the same machine can ask the scope to do.

Tests

# Unit tests (no hardware).
pytest tests/ -v

# Live conformance against a real scope.
SCOPE_MCP_HOST=<your-scope-ip> \
  pytest tests/test_instrument_conformance.py -v
A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/masahiro-999/oscilloscope-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server