tinysa-mcp
# tinySA MCP and CLI
A standalone, headless Python USB driver shared by a command-line tool and an
[MCP](https://modelcontextprotocol.io/) 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](VALIDATION.md).
[Console guide](docs/CONSOLE_GUIDE.md) translates the firmware's command list into
routine measurement operations.
## Install
Python 3.11+ and [uv](https://docs.astral.sh/uv/) are required for the development setup:
```sh
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:
```sh
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.
## 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:
```sh
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:
```sh
# 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:
```json
{
"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:
```sh
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:
```sh
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
```sh
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](https://github.com/manahiyo831/tinySA_mcp), the
[official USB interface](https://tinysa.org/wiki/pmwiki.php?n=Main.USBInterface),
[Ultra console examples](https://tinysa.org/wiki/pmwiki.php?n=TinySA4.ConsoleCommands),
and [firmware source](https://github.com/erikkaashoek/tinySA). 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.
TDQS
Scored across 11 tools
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.
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.
11 tools is well-scoped for a device-control server, covering connection, status, configuration, acquisition, and sweep control without redundancy.
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).