Skip to main content
Glama
tinysa-mcp
by ikatkov

tinySA MCP and CLI

A standalone, headless Python USB driver shared by a command-line tool and an MCP server. No TinySA Saver, Qt, or GUI is required. Designed for scripting and agent-controlled measurements on macOS, Linux, and Windows. Automated and hardware verification procedures are described in VALIDATION.md. Console guide translates the firmware's command list into routine measurement operations.

Install

Python 3.11+ and uv are required for the development setup:

git clone https://github.com/ikatkov/tinysa-mcp.git
cd tinysa-mcp
uv sync --locked
uv run tinysa ports
uv run tinysa info

For a command available outside the project directory:

uv tool install .
tinysa --help

Alternatively, python3 -m venv .venv and .venv/bin/python -m pip install -e . install the runtime without uv. The Python API is TinySA(USBTransport(port)); always call close() in a finally block.

Related MCP server: pico-mcp

USB preparation

Select analyzer/input mode on the tinySA, set CONFIG → CONNECTION → USB if present, and connect a data-capable cable. Close TinySA Saver, NanoVNA Saver, serial terminals, and other clients. Only one process should use the instrument. Serial baud rate is 115200 (USB CDC generally ignores it).

Auto-detection actively tries USB serial ports: products named tinySA first, then devices with STM32 USB ID 0483:5740, then other USB serial endpoints. It sends an empty prompt request and info, accepting the first instrument that identifies as tinySA. Busy, silent, and unrelated devices are closed and skipped. Each prompt and identity probe has a deadline of at most two seconds. Bluetooth and built-in console ports are skipped. If nothing matches, the error lists attempted ports and why each failed. If multiple tinySAs are attached, choose one explicitly instead of relying on the deterministic first match.

To override detection, use tinysa --port PORT info with a port returned by tinysa ports, or set TINYSA_PORT. An explicit port is verified and never falls back to another device. The account needs permission to open the serial device. Discovery opens no serial device.

On macOS, these answer different questions:

system_profiler SPUSBDataType       # all USB devices and bus topology
tinysa ports                       # serial endpoints and candidate identities
python3 -m serial.tools.list_ports -v  # pyserial's serial inventory

Measure and script

Global options (--port, --json, timeouts) go before the subcommand:

# Save the current actual/displayed trace to CSV + metadata, with an ASCII chart.
tinysa capture
tinysa capture --output measurements.csv --width 120

# Set bandwidth explicitly (Hz API, kHz console command).
tinysa configure --rbw 10k --attenuation auto --unit dbm

# New scan, not a snapshot. Firmware leaves the instrument paused by default.
tinysa --scan-timeout 120 scan --start 500k --stop 25M --points 450
tinysa resume

# Resume the configured display sweep immediately after a fresh acquisition.
tinysa scan --start 1M --stop 10M --points 100 --resume-after

# Complete measurement and artifact paths as JSON; no chart on stdout.
tinysa --json capture --no-chart > measurement.json

# CSV to stdout for pipelines; no metadata file is created for stdout export.
tinysa capture --output - > spectrum.csv

# Repeated snapshots: one CSV + metadata per acquisition; NDJSON with --json.
tinysa --json record --count 10 --interval 2 --directory readings > captures.ndjson
tinysa record --count 0 --interval 1   # until Ctrl-C

# A completed fresh scan for every record (interval is a minimum, not a sampling rate).
tinysa --json record --fresh --start 500k --stop 25M --points 450 --count 10 --interval 2

# Strongest sampled local maxima, optionally enforce frequency separation.
tinysa --json peaks --limit 10 --min-level -90 --min-separation 100k

tinysa status
tinysa console-help

Each saved CSV has exactly frequency_hz,level_dbm followed by all sampled points. Its .json sidecar records schema version, source, device/firmware identity, UTC start/end times, settings readback, state effects, and a peak summary. Default files are timestamped in ./readings; TINYSA_EXPORT_DIR or --directory changes this. Existing CSV and metadata files are protected. Timestamps describe host acquisition and transfer times; individual sample timestamps are not provided by the USB protocol.

The ASCII chart bins samples to terminal columns and preserves each bin's minimum and maximum. Narrow spurs remain visible; screen geometry cannot be reproduced exactly at terminal resolution. The CSV retains all samples. Zero span uses sample order on the horizontal axis, not calibrated time. Use --no-chart, --width, and --height. With --json --chart or --output - --chart, the chart goes to stderr.

Exit status: 0 success, 1 measurement/connection/file error, 2 argument syntax error, 130 Ctrl-C. With --json, runtime errors have an error field. Successful single commands produce one JSON object; record produces one per capture. Parser errors still use argparse's stderr. No retries on a failed scan.

Use with an AI agent

The server uses the official MCP Python SDK and stdio transport. After uv tool install ., make tinysa-mcp available on the MCP client's PATH. For clients with a JSON mcpServers configuration:

{
  "mcpServers": {
    "tinysa": {
      "command": "tinysa-mcp",
      "args": []
    }
  }
}

USB detection is automatic unless --port or TINYSA_PORT specifies a device. Exports default to readings under the server's working directory; choose the destination at runtime with --export-dir or TINYSA_EXPORT_DIR. If the client cannot find the command, configure its PATH or supply the executable location in that client's configuration. tinysa serve is equivalent. USB opens lazily on the first instrument tool, so discovery works when no instrument is attached. The server retains the connection until disconnect_device or shutdown. Diagnostics go to stderr; stdout is reserved for MCP. There is no network listener or GUI.

For Codex, register the installed command with:

codex mcp add tinysa -- tinysa-mcp
codex mcp get tinysa

In the Codex configuration, set tool_timeout_sec = 180.0 in [mcp_servers.tinysa] to allow the server's 120-second scan deadline plus readout overhead.

For Claude Code, register it for your user account across projects:

claude mcp add --scope user --transport stdio tinysa -- tinysa-mcp
claude mcp get tinysa

Use /mcp in Claude Code to inspect the connection. Client registrations and export destinations belong in each user's configuration rather than in the repository.

Tools:

Tool

Purpose / effects

list_devices

Serial inventory; no USB open

get_device_info

Verified model and firmware identity

get_status

Current state and raw settings readback

capture_spectrum

Current trace; temporarily pauses and restores running state

scan_spectrum

Fresh scan; replaces trace and leaves paused unless resume_after

configure_measurement

Explicit RBW/attenuation/dBm changes and readback

find_spectrum_peaks

Snapshot plus sampled local maxima

pause_sweep, resume_sweep

Explicit display control

disconnect_device

Release the serial connection

get_console_help

Commands actually available on the attached firmware

capture_spectrum and scan_spectrum support save=true, an optional simple .csv filename, and include_points=false for compact results. File names stay within the configured export directory. Tools return structured JSON plus MCP text content. Read tinysa://guide before measurement; tinysa://export-directory shows the artifact directory. Measurement tools declare state changes in MCP annotations. Acquisition tool results also include ten sampled local maxima from that exact capture, so an agent can discuss peaks without taking a second measurement.

Example agent request: “Identify the tinySA, read its settings, capture the current trace and save it, then report the five strongest sampled peaks with frequencies.”

Expert raw console access is opt-in: start the server with --allow-raw to expose execute_console_command, or use tinysa command --allow-raw 'rbw 10'. Arbitrary commands can alter calibration, persistent settings, and generator output. Binary commands (capture, scanraw, sd_read) and reset are excluded from this text tool.

Measurement semantics and limits

  • A snapshot reads frequencies and actual data 2 while paused, then restores the earlier running state. It does not initiate a new scan. It requires firmware with status. A previously paused display remains paused. Pausing can interrupt an ongoing sweep; the displayed trace can contain samples from different sweep passes. Use a fresh scan when a complete acquisition matters. Frequent snapshot polling can keep interrupting the display; use record --fresh for repeated completed acquisitions.

  • A scan uses scan START STOP POINTS 3. Frequencies and actual levels arrive in one response, including the legacy zero imaginary component on some firmware. The old point count is restored afterwards. Resume the display before a subsequent snapshot if the scan used a different point count. Display settings and frequency arrays are not a full transactional instrument-state snapshot. Within a connected driver session, snapshots after a different-point-count scan are refused until the sweep is resumed. Across independent CLI invocations, the driver cannot know earlier console operations; resume explicitly before capture.

  • Data follows the selected display unit. This API requires dBm and rejects other units instead of mislabelling them. configure --unit dbm changes the display explicitly. Input/analyzer mode is selected by the user, not changed automatically.

  • Existing trace processing (average, max hold, etc.) can affect data; raw calc readback accompanies every capture. A fresh scan does not reset this processing.

  • Basic allows up to 290 scan points; Ultra family up to 450. Frequencies accept integer Hz up to a validation ceiling of 12 GHz; usable ranges depend on the actual model, input mode, firmware, and RF front end. This ceiling is not a claim that every instrument supports 12 GHz.

  • RBW requests are Hz; the console accepts kHz. Firmware chooses available filters; read actual bandwidth from rbw_reply / rbw_after_reply. Some settings take effect at the next sweep. Query replies are retained verbatim for firmware differences.

  • Peak finding uses local sampled maxima, plateaus, and frequency separation. It does not interpolate frequencies, compute integrated power, or identify the origin of spurs. A reported peak can be at a scan edge.

USB reliability and troubleshooting

The transport reads until a complete ch> prompt, even if split across packets or without a newline. Compound operations share one lock. POSIX exclusive opens prevent cooperating clients from sharing the port; close already-running GUIs yourself. Replies have a 1 MiB bound and a finite deadline. Ordinary timeout is 5 seconds; fresh scan timeout defaults to 120 seconds and accepts up to 3600. A timeout closes the connection; the instrument may still be scanning and its state is then unknown. Wait for completion and reconnect rather than immediately starting another scan.

Malformed values (including the older firmware's -:.000000e+01 formatter bug), non-finite values, length mismatches, and incorrect endpoints are reported as errors. Values are never guessed or silently repaired. If a device is silent, check physical USB connection mode and cable, then power cycle. Narrow RBW and repeated/long sweeps can require much more time than a simple settings query.

Development

uv sync --locked
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv build

Unit tests use a fragmented fake serial device. The stdio MCP test launches a real server process against a pseudo-terminal instrument, exercising initialization, tool schemas, structured measurements, error delivery, resources, and shutdown. No hardware is required for tests.

uv run python examples/client.py demonstrates a real MCP client capturing and exporting the current trace. uv run python examples/measure.py demonstrates the direct Python API. uv run python tools/verify_hardware.py performs opt-in hardware verification with one fresh scan and restores the earlier running/paused state.

The implementation was written independently after reviewing the experimental tinySA_mcp project, the official USB interface, Ultra console examples, and firmware source. It avoids a mandatory Tkinter GUI and arbitrary short sleeps for reply framing. Those firmware commands vary by release; console-help and settings readback describe the attached instrument.

Available Tools

11 tools
capture_spectrumB

Snapshot current actual dBm trace; briefly pause and restore previous running state.

Does not guarantee a fresh scan. save writes CSV and metadata to the export directory.

Inputs: save, filename, include_points. Returns: object

configure_measurementA

Explicitly set bandwidth in Hz, attenuation in dB, or dBm display units; read back settings.

Firmware rounds bandwidth to available filters. None leaves a setting unchanged. Auto and explicit value are mutually exclusive. Changes are not rolled back on error.

Inputs: rbw_hz, rbw_auto, unit_dbm, attenuation_db, +1 more. Returns: object

disconnect_deviceA

Release USB; the next device tool reconnects. Does not change the current sweep state.

Inputs: no named parameters. Returns: object

find_spectrum_peaksB

Snapshot current trace and find strongest sampled local maxima; temporarily pauses readout.

Inputs: limit, min_level_dbm, min_separation_hz. Returns: object

get_console_helpA
Read-only

Read the connected firmware's actual supported console commands.

Inputs: no named parameters. Returns: object

get_device_infoB
Read-only

Connect lazily, verify tinySA identity, and report firmware and maximum scan points.

Inputs: no named parameters. Returns: object

get_statusA
Read-only

Read sweep state, bandwidth, attenuation, display units, and processing settings.

Inputs: no named parameters. Returns: object

list_devicesA
Read-only

List serial ports and USB candidates. Does not connect; STM32 IDs can also be NanoVNAs.

Inputs: no named parameters. Returns: object

pause_sweepA

Pause the displayed sweep explicitly.

Inputs: no named parameters. Returns: object

resume_sweepA

Resume the instrument's configured display sweep (including frequency array restoration).

Inputs: no named parameters. Returns: object

scan_spectrumA

Acquire a new dBm spectrum with paired frequencies/levels; default leaves device paused.

    Endpoints are integer Hz. Basic <=290 points, Ultra <=450. resume_after restarts the
    configured display sweep. No automatic change to input mode, RBW, or processing.
    

Inputs: stop_hz (required), start_hz (required), save, points, +3 more. Returns: object

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A3.7/5.0

    Scored across 11 tools

    Disambiguation4/5

    The sweep-control pair (pause_sweep/resume_sweep) and lifecycle tools (list_devices/get_device_info/disconnect_device) are clearly distinct. However, capture_spectrum, scan_spectrum, and find_spectrum_peaks all deal with trace acquisition and could be confused, though the descriptions do clarify snapshot vs. fresh scan vs. peak analysis.

    Naming Consistency5/5

    All tools use consistent snake_case verb_noun or verb patterns (list_devices, get_status, configure_measurement, scan_spectrum). No mixed conventions or vague single-word verbs.

    Tool Count5/5

    11 tools is well-scoped for a device-control server, covering connection, status, configuration, acquisition, and sweep control without redundancy.

    Completeness4/5

    The surface covers the full device lifecycle: list, connect/info, disconnect, status, configuration, acquisition, pause/resume, and console help. Minor gaps exist (configuration is limited to bandwidth/attenuation/units with no broader settings, and saving is folded into capture_spectrum rather than a dedicated export tool).

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      A
      maintenance
      Enables AI agents to control Copper Mountain Vector Network Analyzers over TCP/IP SCPI, offering 45 tools for sweep configuration, calibration, measurement, and Touchstone export.
      49
      2
      AGPL 3.0
    • A
      license
      Not graded
      quality
      C
      maintenance
      Enables LLMs to control a PicoScope 5000A USB oscilloscope for signal generation, block capture, measurements, and frequency response sweeps.
      1
      MIT
    • A
      license
      B
      quality
      B
      maintenance
      Enables AI agents to process RF signals, perform spectral analysis, and execute radar DSP primitives such as Range-Doppler processing and CFAR detection, supporting both file-based and live SDR capture.
      14
      MIT
    • F
      license
      A
      quality
      B
      maintenance
      Enables AI assistants to operate a Fishball7020/PlutoSky software-defined radio, including tuning, spectrum analysis, band scanning, IQ capture, FPGA filtering, and optional transmission.
      16
      4
      -