keyscope-mcp
by AnterCreeper
README.md
# keyscope-mcp
**AI-native oscilloscope control.** A pure-Python MCP service that lets LLM agents control Keysight EDUX1052G oscilloscopes via a compact DSL.
```
┌─────────────┐ DSL/SCPI ┌──────────────┐ USBTMC ┌──────────┐
│ AI Agent │ ─────────────────→│ keyscope │ ─────────────→│ EDUX1052G│
│ (Claude/…) │←─ waveform/data───│ -mcp │←─ screenshot──│ │
└─────────────┘ └──────────────┘ └──────────┘
```
## Features
- **Single MCP tool** (`scope_exec`) — ~800 tokens context vs 3000+ for 12 separate tools
- **TCL-like DSL** — Positional args, implicit units (`1V`, `10ms`, `1kHz`)
- **Fail-fast by default** — Stops on first error; `continue_on_error` for batch jobs
- **Side-effect tagging** — ○ none / ◒ soft / ● hard for AI visibility
- **Three-tier capability** — VERIFIED / EXPERIMENTAL / EXCLUDED (security)
- **Snapshot persistence** — Save/load instrument state via LMDB
- **Thread-safe** — Per-device lock for concurrent AI sessions
## Release Status
- Current stable baseline: **`v0.1.0`**
- Stability promise for `v0.1.x`:
- Keep MCP tool name and input contract stable: `scope_exec(script, continue_on_error?)`
- Prefer additive output changes (new fields) over breaking field renames/removals
- Reserve potentially breaking interface changes for `v0.2.0+`
- Known limitation (LLM-facing): query syntax is not fully uniform across all commands yet (for example, `trig?` works while `chan 1?` may be inconsistent depending on parser path)
## Installation
```bash
pip install keyscope-mcp
# or from source
pip install -e .
```
### Local source install (recommended for development)
```bash
cd /root/keyscope-mcp
python3 -m venv .venv
. .venv/bin/activate
python -m pip install -U pip
python -m pip install -e .
```
Verify install:
```bash
keyscope --list-commands
python -m keyscope_mcp --list-commands
```
## Quick Start
### 1. MCP Server (default mode)
```bash
# Auto-detects USB device
python -m keyscope_mcp
# Or specify address explicitly
python -m keyscope_mcp --address "USB0::10893::923::CN60121247::0::INSTR"
```
### 2. CLI Modes
```bash
# Interactive REPL
keyscope -i
# One-shot command
keyscope -c "idn"
# Execute script file
keyscope examples/power_ripple.dsl
# Dry-run (parse only, no hardware)
keyscope -n -c "rst; chan 1 on 1V dc; time 1ms"
# List all commands
keyscope --list-commands
# List VISA devices
keyscope --list-devices
```
### 3. Python API
```python
from keyscope_mcp.executor import execute
result = execute("""
chan 1 on 1V dc
time 1ms
trig edge chan1 0.5 pos
wgen on sin 10k 3.3 0
run
meas 1 freq vpp
""")
print(result["results"][-1]["value"])
# → {"freq": 10000.0, "vpp": 3.28}
```
### 4. OpenCode MCP setup (local)
Use OpenCode interactive MCP setup:
```bash
opencode mcp add
```
When prompted:
- Name: `keyscope`
- Type: `local`
- Command: `/root/keyscope-mcp/.venv/bin/python -m keyscope_mcp`
Check registration:
```bash
opencode mcp list
```
## Smoke Test (minimal)
### A. No-hardware smoke test
```bash
PYTHONPATH=. .venv/bin/pytest tests/test_dsl.py -q
python -m keyscope_mcp -n -c "idn; chan 1 on 1V dc; time 1ms; meas 1 vpp"
```
### B. MCP tool smoke test in OpenCode
In an OpenCode chat, run:
```text
Use MCP tool `keyscope.scope_exec` with script:
help
```
Expected: command reference text is returned.
### C. Hardware connectivity check (optional)
```text
Use MCP tool `keyscope.scope_exec` with script:
idn
```
Expected: Keysight ID string (for example, `Keysight Technologies,EDUX1052G,...`).
## DSL Quick Reference
### Channel
```
chan 1 on 1V dc # CH1 on, 1V/div, DC coupling
chan 2 on 500mV ac bw20 # CH2 on, 500mV/div, AC, 20MHz BW limit
chan 1 off # Turn off
```
### Timebase
```
time 1ms # 1ms/div
time 10us -5ms main # 10us/div, -5ms offset, main mode
```
### Trigger
```
trig edge chan1 1.65 pos # Edge trigger on CH1, 1.65V, positive slope
trig auto # Auto trigger mode
trig holdoff 100ns # 100ns holdoff
```
### Acquisition
```
run # Continuous
sing # Single shot
stop # Stop
acq norm # Normal mode
acq aver 16 # Average 16 samples
```
### Measurements
```
meas 1 freq vpp # Frequency and Vpp on CH1
meas 1 all # All measurements
meas clear # Clear all
```
### Waveform Capture
```
wave 1 10k word # 10k points, WORD format
wave 1 max asc # Max points, ASCII (slow but human-readable)
```
### Waveform Generator
```
wgen on sin 10k 3.3 0 # Sine 10kHz 3.3Vpp 0V offset
wgen on dc 1.65 # DC offset only (no frequency)
wgen on squ 1M 5 0 # Square 1MHz 5Vpp
wgen off # Turn off
```
### Math / FFT
```
math fft 1 10kHz 100kHz hann # FFT of CH1, center 10kHz, span 100kHz
math sub 1 2 # CH1 - CH2 waveform subtraction
math off # Turn off math
```
### Cursors
```
curs on chan1 # Enable cursors on CH1
curs x 0us 50us # Set X cursors
curs y -1V 1V # Set Y cursors
curs? # Read cursor values
curs off # Disable
```
### Snapshot
```
save baseline # Save current setup
load baseline # Restore setup
list # List saved snapshots
```
### Screenshot
```
shot png # Capture PNG screenshot
shot bmp # Capture BMP (larger, faster)
```
### Utility
```
idn # Query identity
rst # Reset to factory defaults
opc # Wait for operation complete
err # Check error queue
auto # Autoscale
help # Show help
help chan # Help for specific command
```
## Measurement Sentinel
When a measurement cannot be made (e.g., frequency with no signal), Keysight returns `~9.9e37`. keyscope-mcp normalizes this to:
```json
{"freq": null, "freq_invalid": true}
```
This lets AI agents distinguish "no measurement" from "zero" or invalid data.
## Examples
See `examples/` directory:
- **`power_ripple.dsl`** — Switching regulator ripple measurement
- **`digital_si.dsl`** — Clock signal integrity analysis
- **`fft_spectrum.dsl`** — Harmonic content analysis
- **`frequency_sweep.py`** — Frequency response (Bode plot approximation)
## Hardware Setup
### Minimal Setup
```
PC USB ───→ EDUX1052G (USBTMC)
WaveGen OUT ──balun──→ CH1 (10x probe)
```
### Dual-Channel Setup (stereo audio)
```
PC Audio tip (left) ──→ CH1 (1x probe, 100-200mV/div)
PC Audio ring (right) ──→ CH2 (1x probe, 100-200mV/div)
PC Audio sleeve (gnd) ──→ scope ground
```
## Supported Hardware
| Model | Bandwidth | WaveGen | Verified |
|-------|-----------|---------|----------|
| EDUX1052G | 50–200MHz | 100Hz–12MHz | ✓ |
| DSOX1102G | 70–100MHz | 100Hz–12MHz | ✓* |
| Other InfiniiVision | — | — | Likely* |
*Compatible SCPI command set; may need capability flags for advanced features.
## Architecture
```
keyscope_mcp/
├── __main__.py # CLI entry point (MCP/REPL/script)
├── server.py # MCP server (stdio/sse)
├── dsl.py # DSL lexer, parser, 24-command registry
├── executor.py # Fail-fast script engine
├── scope.py # VISA connection, binary I/O, SCPI errors
├── units.py # Human-readable unit parsing (1V → 1.0, 1ms → 0.001)
├── persist.py # LMDB snapshot save/load with IDN validation
├── repl.py # Interactive REPL
└── help.py # Help text generation
```
## Testing
```bash
# Unit tests (no hardware required)
PYTHONPATH=. .venv/bin/pytest tests/test_dsl.py -v
# Device integration tests (requires EDUX1052G)
PYTHONPATH=. .venv/bin/pytest tests/test_device.py -v
# Stereo dual-channel device test only
PYTHONPATH=. .venv/bin/pytest tests/test_device.py -k stereo_audio_inputs -vv
# Manual stereo validation script
PYTHONPATH=. .venv/bin/python test_dual_channel.py
# XY oscilloscope music demo
PYTHONPATH=. .venv/bin/python examples/oscilloscope_music_demo.py --scale 50mV
```
Safety note:
- Audio output level is manual by design (scripts do not modify system volume).
- Start with low OS volume and increase gradually.
- Use scope vertical scale (`chan` settings or demo `--scale`) to improve visibility.
- Avoid headphones/speakers during high-level tuning.
## Troubleshooting
### No VISA devices found
```bash
# Check USB connection
lsusb | grep Keysight
# → Bus 001 Device 002: ID 2a8d:039b Keysight Technologies, Inc.
# Check kernel module
lsmod | grep usbtmc
# If present, may conflict with pyvisa-py backend:
sudo rmmod usbtmc
```
### Firmware hang after bad binary data
Physical replug required. Scope may need to re-initialize USBTMC state.
### Large waveform truncated
Fixed by `read_bytes()` with precise length parsing instead of `read_raw()`. See `scope.py:89-120`.
## License
MIT
## Contributing
Bug reports and PRs welcome. See `PLAN.md` for detailed architecture and SCPI taxonomy.
## Archive Handoff
`keyscope-mcp` is a small support asset for SiliconAIO / Noema / Autopilot, not a platform branch.
- Purpose: expose a Keysight oscilloscope as a single local MCP tool, `scope_exec`, backed by a compact DSL.
- Main entry: `python -m keyscope_mcp`
- Local install:
`python3 -m venv .venv && . .venv/bin/activate && python -m pip install -e .`
- OpenCode local MCP command:
`/root/keyscope-mcp/.venv/bin/python -m keyscope_mcp`
- No-hardware smoke test:
`PYTHONPATH=. .venv/bin/pytest tests/test_dsl.py -q`
`python -m keyscope_mcp -n -c "idn; chan 1 on 1V dc; time 1ms; meas 1 vpp"`
- Minimal MCP smoke test in OpenCode:
call `keyscope.scope_exec` with `help`
- Optional hardware check:
call `keyscope.scope_exec` with `idn`
- Known limits: stdio MCP only, Keysight-focused, no protocol-level CI, `.raw` is CLI-only expert path.
- If revisited later: prefer documentation and smoke-test maintenance, not architectural expansion.
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues