Ask, don't type SCPI
"Center 100 MHz, 10 MHz span." The assistant picks the tools, sets the values and reads them back.
Tap anywhere to start the soundtrack
Let Claude and other AI assistants set frequency, amplitude, bandwidth and sweeps on UNI-T UTS series analyzers, over plain SCPI on your LAN.
Typed tools turn a plain-English request into the right SCPI commands, check the result, and tell you what really happened.
"Center 100 MHz, 10 MHz span." The assistant picks the tools, sets the values and reads them back.
Raw SCPI over TCP port 5025. No VISA, no vendor software, just a network connection to the analyzer.
Resets, network changes, panel locks and file writes need an explicit confirm. Every setter reads the value back and warns when the instrument clamped it.
Developed against a UNI-T UTS3036B (firmware V1.04.0040). Every quirk we found is written down.
Not connected, lost connection, timeout and instrument errors each get a clear message that names the command involved.
TypeScript on Node 22+, with just the MCP SDK and zod as dependencies and no native modules.
An illustration of one round trip. The tool call and result are what the assistant sends and receives.
You
Set the center to 100 MHz with a 10 MHz span.
{ "center_hz": 100000000, "span_hz": 10000000 }// result
{ "state": { "center_hz": 100000000, "span_hz": 10000000,
"start_hz": 95000000, "stop_hz": 105000000, ... },
"warnings": [] }
Assistant
Done. The analyzer is centered on 100 MHz with a 10 MHz span (95 to 105 MHz), and it accepted both values as asked.
This list is generated from the server's own tool definitions, so it is always current. Read-only tools are marked as such to the assistant; everything that changes the instrument goes through the safety checks.
connectConnect to a UNI-T UTS series spectrum analyzer over auto (default: VXI-11 when the instrument offers it, else raw SCPI/TCP), VXI-11 or raw SCPI/TCP.discover_instrumentsFind UNI-T / VXI-11 spectrum analyzers on the local network without knowing their IP address: broadcasts a portmapper query on each local IPv4 interface and identifies…disconnectDisconnect from the spectrum analyzer (the instrument named by name, default: the default one).list_instrumentsList the instruments this server knows by name (the default one, those from UTS_INSTRUMENTS and any added with connect name=...): whether each is connected, where…identifyQuery the analyzer identification (*IDN?): manufacturer, model, serial number and firmware version.scpi_querySend one raw SCPI query (it must end in '?') to the analyzer and return the reply.scpi_commandSend a raw SCPI command (no reply expected) to the analyzer.get_frequencyGet the analyzer's frequency settings: center, span, start, stop, center step (and whether it is automatic) and offset, all in Hz.set_frequencySet frequency parameters (all in Hz).get_amplitudeGet the analyzer's amplitude settings: reference level (in the current power unit), power unit, scale per division (dB), Y scale type, reference level offset (dB)…set_amplitudeSet amplitude parameters.get_bandwidthGet resolution and video bandwidth (Hz) with their auto flags, the VBW/RBW ratio and the filter shape (gaussian or flattop).set_bandwidthSet RBW, VBW, the VBW/RBW ratio and the filter shape.get_sweepGet sweep settings: sweep time (s) with auto flag and auto rule, sweep type (sweep/fft) with auto flag, sweep points (the trace length)…set_sweepSet sweep points, sweep time, sweep type, trigger source and video trigger level.get_detectorGet the detector (sample, positive, negative, normal, average) and its auto flag for one trace (1-6) or all six, via :SENSe:DETector:TRACe<n>.set_detectorSet the detector of one trace (1-6): sample, positive, negative, normal or average (turns that trace's automatic detector off), or switch automatic selection on/off.get_acquisitionGet the acquisition mode: continuous (true) or single sweep (false), plus the sweep time (s) and its auto flag.set_acquisitionSwitch between continuous sweep (continuous=true, the instrument's normal state) and single sweep mode (continuous=false, :INITiate:CONTINUOUS OFF).single_sweepSwitch to single sweep mode, run exactly one sweep and wait until it is complete, so the trace is final and can be read.start_sweepStart (:INITiate:IMMediate) or restart (:INITiate:RESTart) a sweep.wait_for_sweep_completeWait for a sweep started with start_sweep(wait=false) to finish: over the raw transport it stays silent for the remaining sweep time (the instrument ignores input while…get_traceGet the trace configuration: the selected (current) trace, and for each trace 1-6 (or only trace) its type (write, average, max_hold, min_hold)…set_traceSet trace configuration: select the current trace, and for one trace set its type (write, average, max_hold, min_hold)…restart_traceRestart max hold, min hold or averaging by re-sending the trace's current type (:TRACe<n>:TYPE <current type>).set_trace_mathSet trace math for a trace (:CALCulate:MATH): result trace <- operand_a (op) operand_b.get_trace_dataRead the amplitude values of one trace (1-6, default the selected trace) with the computed frequency axis, the power unit, the trace type and summary statistics (min…get_markerRead one marker: mode (off, normal, delta, fixed), trace, and its frequency (x, Hz) and level (y, current power unit, dBm by default) together.set_markerSet a marker's mode, trace, reference marker (delta mode) and/or position.marker_searchMove a marker with a search and return its new frequency and level together.marker_to_instrumentUse a marker's frequency or level to set an instrument parameter: center, step (center frequency step; turns automatic step off), start, stop or reference_level.markers_all_offTurn every marker off (:CALCulate:MARKer:AOFF) and confirm it by reading each marker's mode.get_peak_criteriaRead the peak search criteria: threshold (dBm) and excursion (dB), each with its automatic flag.set_peak_criteriaSet the peak search criteria.get_marker_functionRead a marker's function (off, noise, band_power, band_density) and the band span used by the band functions.set_marker_functionSet a marker's function: noise (noise level per Hz), band_power (power in the band), band_density (power density in the band) or off.get_marker_ndbRead a marker's N-dB bandwidth measurement: whether it is on, the N value (dB below the marker) and, when on, the measured bandwidth in Hz.set_marker_ndbTurn a marker's N-dB bandwidth measurement on or off and/or set N (dB below the marker, -140 to -0.01, e.g. -3 or -6).get_frequency_counterRead the marker frequency counter: whether it is on and its gate time in seconds.set_frequency_counterTurn the marker frequency counter on or off and/or set its gate time (1 us to 500 ms).get_limit_lineRead one limit line: whether it is displayed, its type (upper or lower), the trace it is tested against, the margin and its on/off state…set_limit_lineCreate or edit a limit line.delete_limit_lineDelete the points of one limit line (line) or of all limit lines (all: true).get_tracking_generatorGet the tracking generator (the manual's 'trace source') state: output on/off, output level in dBm, level offset in dB, reference trace…set_tracking_generatorSet the tracking generator (trace source): output on/off, level (dBm), level offset (dB), reference trace, normalization and its reference level.get_instrument_moduleGet the active measurement function module: spectrum_analyzer, emi or analog_demod.select_instrument_moduleSwitch the active measurement function module (SA/EMI/AD) and read it back.enter_analog_demodSwitch to the analog demodulation (AD) module and read it back.exit_analog_demodLeave the AD module and return to spectrum analyzer mode (:INSTrument:SELect SA).get_ad_settingsGet a read-only snapshot of the AD (analog demodulation) module's frequency, bandwidth and demod-timing settings: current demodulation type (AM/FM, via :CONFigure?)…set_ad_frequencyConfigure the AD module's center frequency, RF span, resolution bandwidth and demodulation bandwidth (DBW).set_ad_demod_timingConfigure the AD module's demodulation window sweep time, demodulation time (or automatic), and averaging state/count.set_ad_filtersSet one AD (analog demodulation) audio filter family: low-pass, high-pass or band-pass/weighting (PM p.158-160).set_ad_audioSet the AD (analog demodulation) module's speaker and/or system volume (PM p.158-159, p.9).get_emi_settingsGet a read-only snapshot of the EMI module configuration: standard, scan sequence, the 10-band scan table, meter/trace detectors, trace types, search/peak settings…set_emi_scan_tableEdit the EMI module's 10-band scan table: per band, enabled, start_hz, stop_hz, rbw_hz/rbw_auto, time_s/time_auto, attenuation_db/attenuation_auto…set_emi_measurementConfigure the EMI module's measurement: standard (none/cispr), sweep sequence and re-measure mode, trace/meter/final detectors, meter dwell time, averaging…get_emi_limit_lineRead one EMI limit line: whether it is displayed, its type (upper or lower), the trace it is tested against, the margin and its on/off state…set_emi_limit_lineCreate or edit an EMI limit line.delete_emi_limit_lineDelete the points of one EMI limit line (line) or of all EMI limit lines (all: true).get_measurementReport whether the instrument is in spectrum analyzer mode, or check one measurement.configure_measurementEnter a measurement (channel_power, time_power, obw, acpr, toi, cn or harmonics) and set its parameters; the instrument stays in the measurement until exit_measurement.read_measurementRead the result of the active measurement.exit_measurementLeave the measurement and return to spectrum analyzer mode (:CONFigure:SANalyzer).measureOne-shot measurement that leaves the instrument as it found it: from spectrum analyzer mode it saves the frequency…screenshotCapture the analyzer's screen and return it as a PNG image.Missing something? The raw SCPI tools are an escape hatch for any command in the programming guide. Full parameter docs live in the tools reference.
Every one of the 66 tools was tested against a real UNI-T UTS3036B running firmware V1.04.0040, the latest published. The server handles the analyzer's timing quirks so you do not have to. The details are in verified-hardware.md.
Start a single sweep and the tool returns when the trace is ready.
Behind the scenes: the analyzer ignores commands while it sweeps, so the server waits out the sweep time, then checks for completion with *OPC?.
Every setter reads the value back and tells you what the analyzer actually applied.
Behind the scenes: the analyzer quietly limits out-of-range values to the nearest valid one.
Failures say which command caused them, even when the analyzer reports a problem late.
Behind the scenes: an unsupported query posts its error a few seconds later, so the connection filters those stale errors out.
Commands are confirmed one at a time, so rapid-fire changes never confuse the analyzer.
Behind the scenes: lines sent back to back can produce phantom errors, so each command waits for an *OPC? reply.
Commands that reset the instrument, change its network settings or write files need your explicit confirmation.
Behind the scenes: they sit on a denylist and are refused unless the call sets confirm.
It drives real lab equipment, so the process is strict.
Works today with Node.js 22+ (or Docker), straight from npm and an analyzer reachable on your network. Use your analyzer's address in place of 192.168.1.100.
Attached to the v0.5.0 release as uni-t-uts-mcp-0.5.0.mcpb. Not yet verified inside Claude Desktop itself; the JSON configuration below always works.
Download uni-t-uts-mcp-0.5.0.mcpb from the GitHub release and double-click it. In the install dialog enter the analyzer address (optional: you can also say "connect to the analyzer at 192.168.1.100"), keep port 5025 and transport raw, and install. Claude Desktop already includes the Node.js it needs.
claude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- npx -y uni-t-uts-mcpclaude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- cmd /c npx -y uni-t-uts-mcpOr from the release tarball (the same files as the npm package):
claude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- npx -y https://github.com/Reid-n0rc/uni-t-uts-mcp/releases/download/v0.5.0/uni-t-uts-mcp-0.5.0.tgzclaude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- cmd /c npx -y https://github.com/Reid-n0rc/uni-t-uts-mcp/releases/download/v0.5.0/uni-t-uts-mcp-0.5.0.tgzWindows needs cmd /c because npx is a .cmd file. UTS_IP is optional: without it, tell the assistant "connect to the analyzer at 192.168.1.100".
{
"mcpServers": {
"uni-t-uts": {
"command": "npx",
"args": ["-y", "https://github.com/Reid-n0rc/uni-t-uts-mcp/releases/download/v0.5.0/uni-t-uts-mcp-0.5.0.tgz"],
"env": { "UTS_IP": "192.168.1.100" }
}
}
}{
"mcpServers": {
"uni-t-uts": {
"command": "cmd",
"args": ["/c", "npx", "-y", "https://github.com/Reid-n0rc/uni-t-uts-mcp/releases/download/v0.5.0/uni-t-uts-mcp-0.5.0.tgz"],
"env": { "UTS_IP": "192.168.1.100" }
}
}
}npx -y github:Reid-n0rc/uni-t-uts-mcp#v0.5.0docker run -i --rm -e UTS_IP=192.168.1.100 ghcr.io/reid-n0rc/uni-t-uts-mcp:0.5.0git clone https://github.com/Reid-n0rc/uni-t-uts-mcp.git
cd uni-t-uts-mcp
npm ci && npm run build
npm run client-config -- --host 192.168.1.100| Variable | Meaning |
|---|---|
UTS_IP | Analyzer host or IP. If set, the server connects at startup; otherwise ask the assistant to use the connect tool. |
UTS_PORT | TCP port, default 5025. |
Pick your MCP client for a copy-paste config. 192.168.1.100 is a placeholder for your analyzer's address. Each tab says what has and has not been verified.
claude plugin ... in a shell). The plugin runs npx -y uni-t-uts-mcp pinned to this release (0.5.0) and asks for the analyzer address, port and transport (all optional; they are stored in your Claude Code settings).claude mcp add command once in a terminal. Add --scope user to make it available in every project (local is the default, project writes .mcp.json)./plugin marketplace add Reid-n0rc/uni-t-uts-mcp
/plugin install uni-t-uts@uni-t-uts-mcpclaude plugin marketplace add Reid-n0rc/uni-t-uts-mcp
claude plugin install uni-t-uts@uni-t-uts-mcpclaude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- npx -y uni-t-uts-mcpclaude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- cmd /c npx -y uni-t-uts-mcpclaude mcp add uni-t-uts -- docker run -i --rm -e UTS_IP=192.168.1.100 ghcr.io/reid-n0rc/uni-t-uts-mcp:0.5.0{
"mcpServers": {
"uni-t-uts": {
"command": "npx",
"args": [
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}Plugin, marketplace and .mcp.json formats checked against the Claude Code docs and validated with claude plugin validate. claude plugin marketplace add and claude plugin install were run against a local checkout in an isolated Claude Code config directory (install and listing succeeded). Not yet verified: adding the marketplace from GitHub, and starting the server inside a Claude Code session (the pinned npm version must be published first). Source: https://code.claude.com/docs/en/plugins/marketplace-reference.md and https://code.claude.com/docs/en/mcp
Download the .mcpb (GitHub releases)
uni-t-uts-mcp-0.5.0.mcpb from the release and double-click it; the install dialog asks for the analyzer address. (Not verified in Claude Desktop yet.)~/Library/Application Support/Claude/claude_desktop_config.json, Windows %APPDATA%\Claude\claude_desktop_config.json, Linux ~/.config/Claude/claude_desktop_config.json. Restart Claude Desktop completely afterwards.{
"mcpServers": {
"uni-t-uts": {
"command": "npx",
"args": [
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}{
"mcpServers": {
"uni-t-uts": {
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}{
"mcpServers": {
"uni-t-uts": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"UTS_IP=192.168.1.100",
"ghcr.io/reid-n0rc/uni-t-uts-mcp:0.5.0"
]
}
}
}The claude_desktop_config.json format and paths are from the MCP docs. Installing the .mcpb in Claude Desktop itself is NOT verified yet (issue #86 tracks the manual check). Source: https://modelcontextprotocol.io/docs/develop/connect-local-servers
UTS_IP under env afterwards). On Windows use the config file below.~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project).{
"mcpServers": {
"uni-t-uts": {
"command": "npx",
"args": [
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}{
"mcpServers": {
"uni-t-uts": {
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}{
"mcpServers": {
"uni-t-uts": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"UTS_IP=192.168.1.100",
"ghcr.io/reid-n0rc/uni-t-uts-mcp:0.5.0"
]
}
}
}File locations and the install-link format are from Cursor's docs (https://cursor.com/docs/mcp/install-links). Not clicked in Cursor by the maintainer. Source: https://cursor.com/docs/context/mcp
.vscode/mcp.json. For your user profile run code --add-mcp with the JSON below, or use the Command Palette: MCP: Add Server.{
"servers": {
"uni-t-uts": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}{
"servers": {
"uni-t-uts": {
"type": "stdio",
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}code --add-mcp '{"name":"uni-t-uts","type":"stdio","command":"npx","args":["-y","uni-t-uts-mcp"],"env":{"UTS_IP":"192.168.1.100"}}'The servers format, file locations and code --add-mcp are from the VS Code docs (which note the servers format belongs to the Local harness, deprecated from February 2027). No one-click link: the vscode:mcp/install format is not in the official docs, so none is offered. Not run by the maintainer. Source: https://code.visualstudio.com/docs/agents/reference/mcp-configuration
{
"mcpServers": {
"uni-t-uts": {
"command": "npx",
"args": [
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}{
"mcpServers": {
"uni-t-uts": {
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}{
"mcpServers": {
"uni-t-uts": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"UTS_IP=192.168.1.100",
"ghcr.io/reid-n0rc/uni-t-uts-mcp:0.5.0"
]
}
}
}The mcpServers format is from the Windsurf docs. Their path has moved: the current docs say ~/.config/devin/mcp_config.json (macOS / Linux) or %AppData%/devin/mcp_config.json (Windows), older ones ~/.codeium/windsurf/mcp_config.json. Use Cascade's Open MCP config file action to find the right one. No install link is documented. Not run by the maintainer. Source: https://docs.devin.ai/windsurf/plugins/cascade/mcp
npx, arguments -y uni-t-uts-mcp, environment variable UTS_IP (optional, UTS_PORT defaults to 5025).npx is a .cmd file, use cmd with arguments /c npx -y uni-t-uts-mcp.{
"mcpServers": {
"uni-t-uts": {
"command": "npx",
"args": [
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}{
"mcpServers": {
"uni-t-uts": {
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"uni-t-uts-mcp"
],
"env": {
"UTS_IP": "192.168.1.100"
}
}
}
}{
"mcpServers": {
"uni-t-uts": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"-e",
"UTS_IP=192.168.1.100",
"ghcr.io/reid-n0rc/uni-t-uts-mcp:0.5.0"
]
}
}
}Plain MCP stdio: the client starts the command and talks over stdin/stdout. Key names differ per client; check its docs. Source: https://modelcontextprotocol.io/docs/develop/connect-local-servers
Progress is tracked in the open: see the roadmap issue, or jump in via CONTRIBUTING.