oscilloscope-mcp
Click on "Install 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., "@oscilloscope-mcpMeasure frequency 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.
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.

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-mcpRegister with Claude Code
claude mcp add oscilloscope -- python -m oscilloscope_mcpThen 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 |
| Raw SCPI command passthrough. Returns the response plus a |
| 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. |
| Read or configure the trigger — every trigger type the instrument supports. Call with no arguments to read the current state (returns |
| Read scope-side automatic measurements ( |
| Read measurement statistics ( |
| Read or configure an analog channel's vertical setup. |
| Read or configure the horizontal (timebase) setup. Pass any subset of |
| Capture one channel and return a compact threshold-quantized event stream, not raw ADC samples. Raw volts pass through a Schmitt-trigger comparator ( |
| Read or configure the acquisition mode — |
| Declarative single-shot capture — configure, arm, wait, and read in one call. Composes trigger/channel/timebase/acquire setup, arms |
| 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 |
| 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 |
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 conformanceEnvironment
Env var | Required | Meaning |
| yes | scope IP or hostname |
| no (default 5555) | SCPI TCP port |
| no | model key (e.g. |
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:
profile —
src/oscilloscope_mcp/instruments/profiles/<model>.yaml: capability (analog BW, sample rate per channel count, memory depth,idn_matchregex).driver —
src/oscilloscope_mcp/instruments/<vendor>_<series>.py: subclassoscilloscope_mcp.instruments._base.Scope, implement each abstract method using the vendor's SCPI dialect.registry line — add an entry to
MODEL_REGISTRYinsrc/oscilloscope_mcp/instruments/__init__.py.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 → RLE — scope_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_queryis 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 -vThis server cannot be installed
Maintenance
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