Skip to main content
Glama
Schimmilab

pico-mcp

by Schimmilab
README.md
# pico-mcp

A lean MCP (Model Context Protocol) server for **PicoScope 5000A** USB oscilloscopes
(developed and verified on a **5442B**). It lets an LLM/agent drive the scope: discover
the device, configure channels, run the built-in signal generator, capture blocks, take
measurements, and sweep a frequency response.

Built because the existing third-party server was too buggy (a `find_all_units()` discovery
that crashes the process on the ps6000a driver, `ctypes.byref` errors on `GetUnitInfo`, a
device-handle leak, and an `is_connected` check on a field that is never set). This server
talks to the **ps5000a** driver directly; every code path was verified against real hardware.

## macOS Apple Silicon note (important)

> **Changed 2026-08-01 — no Rosetta needed any more.** PicoScope 7 T&M **7.2.24** ships
> **arm64** libraries. Earlier versions were x86_64, which is why this project used to
> require a Rosetta venv. If you followed the old instructions, see *Migrating from the
> Rosetta setup* below — it fails in a confusing way, because the old x86 venv keeps
> working right up until the app is updated.

There is still one thing to do: PicoScope no longer installs a separate
`PicoSDK.framework`, so `ctypes.util.find_library("ps5000a")` finds nothing. Link the
libraries from inside the app into `~/lib`, which is on Python's default macOS library
search path (`DEFAULT_LIBRARY_FALLBACK` in `ctypes.macholib.dyld`) — **no `sudo`, and no
`DYLD_LIBRARY_PATH` in your MCP config**, which matters because the app path contains both
a space and an `&`.

On Windows/Linux with a natively installed PicoSDK, none of this applies.

## Setup

```bash
git clone https://github.com/Schimmilab/pico-mcp.git
cd pico-mcp

python3 -m venv .venv
.venv/bin/pip install -e .
```

macOS only — make the driver discoverable (symlinks, so a PicoScope update carries over):

```bash
SRC="/Applications/PicoScope 7 T&M.app/Contents/MonoBundle"
mkdir -p ~/lib
ln -sfn "$SRC/libps5000a.dylib"   ~/lib/libps5000a.dylib
ln -sfn "$SRC/libps5000a.2.dylib" ~/lib/libps5000a.2.dylib   # libps5000a.dylib links against this

python3 -c "import ctypes.util; print(ctypes.util.find_library('ps5000a'))"
# -> /Users/<you>/lib/libps5000a.dylib
```

Quick hardware test (close the PicoScope desktop app first — a scope can only be opened by
one program at a time):

```bash
.venv/bin/python -c "from pico_mcp.scope import probe_devices; print(probe_devices())"
# -> [{'model': 'PS5000A', 'variant': '5442B', 'serial': '...'}]
```

**Verify against a connected scope, not just an import.** `import pico_mcp.server`
succeeds even when no driver can be loaded — it says nothing about whether the thing works.

## Migrating from the Rosetta setup

```bash
rm -rf .venv-x86            # x86 Python cannot load the new arm64 libraries
python3 -m venv .venv
.venv/bin/pip install -e .
# then the ~/lib symlinks above, then re-register (note: no --env any more)
claude mcp remove pico
claude mcp add pico --scope user -- /ABSOLUTE/PATH/TO/pico-mcp/.venv/bin/pico-mcp
```

## Register in Claude Code

```bash
claude mcp add pico --scope user -- /ABSOLUTE/PATH/TO/pico-mcp/.venv/bin/pico-mcp
```

(Replace `/ABSOLUTE/PATH/TO/` with your clone location.) A newly registered server is only
picked up by a **new** Claude Code session — the MCP connections are fixed at session start.

## Tools

| Tool | Purpose |
|------|---------|
| `list_devices` | Find connected scopes (model / variant / serial) |
| `connect` / `disconnect` | Open (resolution 8/12/14/15/16 bit) / release the device |
| `device_info` | Model, variant, serial, resolution, configured channels |
| `set_channel` | Channel A–D: enable, DC/AC, range (0.01–20 V), analog offset |
| `set_signal_generator` / `stop_signal_generator` | Built-in AWG: sine/square/triangle/ramp, frequency, amplitude |
| `capture_block` | Block capture of all enabled channels → Vpp/min/max/mean/rms, estimated frequency, downsampled waveform (≤240 points) |
| `frequency_sweep` | Frequency response: log-spaced AWG sweep, gain `out/in` per frequency (linear + dB), −3 dB band edges. Timebase chosen automatically per frequency |

## Example: measuring a transformer

Connect AWG/GEN → channel A (input) → device-under-test → channel B (output), enable both
channels, then ask the agent to run a `frequency_sweep`. The server drives each frequency,
captures both channels and reports the gain B/A — yielding the −3 dB bandwidth, pass-band
flatness and any resonances. (This was the first real use: characterising a 1:1 audio
isolation transformer — flat 17 Hz–1.4 MHz, one mild resonance at 380 kHz.)

## Notes

- Close the **PicoScope desktop app** before connecting — exclusive device access.
- `capture_block` returns a **downsampled** waveform (no megasample dumps); measurements are
  computed from the full-resolution data.
- Code changes take effect after the MCP host reloads the server (e.g. restart the session).
  The driver layer can be tested without a reload by running it directly in the venv.

## License

MIT

## Maintainer

Schimmi — https://schimmilab.de
Issues und Pull Requests willkommen.