Talk to your spectrum analyzer.

Let Claude and other AI assistants set frequency, amplitude, bandwidth and sweeps on UNI-T UTS series analyzers, over plain SCPI on your LAN.

  • MIT licensed
  • 66 tools
  • Verified on UTS3036B
  • TypeScript · Node 22+

marker.exe

M1 1.805 GHz
LVL -32.0 dBm
Illustrative signal. Averaged trace = teal, live trace = purple.

Say what you want. The analyzer does it.

Typed tools turn a plain-English request into the right SCPI commands, check the result, and tell you what really happened.

natural-language.txt

Ask, don't type SCPI

"Center 100 MHz, 10 MHz span." The assistant picks the tools, sets the values and reads them back.

no-drivers.sys

No drivers needed

Raw SCPI over TCP port 5025. No VISA, no vendor software, just a network connection to the analyzer.

safety.dll

Safe by design

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.

hardware.log

Verified on real hardware

Developed against a UNI-T UTS3036B (firmware V1.04.0040). Every quirk we found is written down.

errors.txt

Errors you can act on

Not connected, lost connection, timeout and instrument errors each get a clear message that names the command involved.

portable.js

Pure JavaScript

TypeScript on Node 22+, with just the MCP SDK and zod as dependencies and no native modules.

Try saying this

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.

tool call: set_frequency

{ "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.

66 tools, one tidy toolbox

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.

Connection

  • 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.

Raw SCPI

  • 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.

Frequency

  • 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).

Amplitude

  • 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.

Bandwidth, sweep and detector

  • 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.

Acquisition

  • 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…

Trace

  • 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…

Markers

  • 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).

Limit lines

  • 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).

Tracking generator

  • 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.

Measurement function module

  • 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.

Analog demodulation module

  • 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).

EMI function

  • 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).

Measurements

  • 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…

Screenshot

  • 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.

Built for real hardware

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.

Sweeps that just finish

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?.

Settings you can trust

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.

Errors that name the right command

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.

Quick sequences stay safe

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.

Safe by default

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.

Built carefully

It drives real lab equipment, so the process is strict.

build.log

  • Test-driven: failing tests are committed before the code that fixes them
  • Every change is a pull request tied to an issue
  • A contract test runs every tool through the same failures: not connected, timeout, garbage reply, instrument error
  • A docs test fails if a tool is missing from the reference
  • CodeQL scans every pull request
  • Instrument settings are restored after every hardware test

Quick start

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.

Claude Desktop: download the .mcpb and double-click

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 Code

from npm (macOS / Linux)

claude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- npx -y uni-t-uts-mcp

from npm (Windows)

claude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- cmd /c npx -y uni-t-uts-mcp

Or from the release tarball (the same files as the npm package):

terminal (macOS / Linux)

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.tgz

terminal (Windows)

claude 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.tgz

Windows 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".

Claude Desktop (JSON), Cursor, VS Code and others

mcp config (JSON, macOS / Linux)

{
  "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" }
    }
  }
}

mcp config (JSON, Windows)

{
  "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" }
    }
  }
}

Other ways to install

from the git tag

npx -y github:Reid-n0rc/uni-t-uts-mcp#v0.5.0

Docker (amd64 and arm64)

docker run -i --rm -e UTS_IP=192.168.1.100 ghcr.io/reid-n0rc/uni-t-uts-mcp:0.5.0

from a clone (prints your exact config)

git 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

Point it at your analyzer

environment.ini

VariableMeaning
UTS_IPAnalyzer host or IP. If set, the server connects at startup; otherwise ask the assistant to use the connect tool.
UTS_PORTTCP port, default 5025.

Add to your client

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 Code

  • Recommended: add this repository as a marketplace and install the plugin (two commands, inside Claude Code or with 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).
  • Or run the 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).
  • UTS_IP is optional: without it, tell the assistant "connect to the analyzer at 192.168.1.100".

Plugin (recommended)

/plugin marketplace add Reid-n0rc/uni-t-uts-mcp
/plugin install uni-t-uts@uni-t-uts-mcp

Plugin from a shell

claude plugin marketplace add Reid-n0rc/uni-t-uts-mcp
claude plugin install uni-t-uts@uni-t-uts-mcp

npx (macOS / Linux)

claude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- npx -y uni-t-uts-mcp

npx (Windows)

claude mcp add uni-t-uts -e UTS_IP=192.168.1.100 -- cmd /c npx -y uni-t-uts-mcp

Docker

claude 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

.mcp.json (project scope)

{
  "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

Claude Desktop

Download the .mcpb (GitHub releases)

  • Easiest: download 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.)
  • Or edit the config file (Settings > Developer > Edit Config): macOS ~/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.
  • UTS_IP is optional: without it, tell the assistant "connect to the analyzer at 192.168.1.100".

npx (macOS / Linux)

{
  "mcpServers": {
    "uni-t-uts": {
      "command": "npx",
      "args": [
        "-y",
        "uni-t-uts-mcp"
      ],
      "env": {
        "UTS_IP": "192.168.1.100"
      }
    }
  }
}

npx (Windows)

{
  "mcpServers": {
    "uni-t-uts": {
      "command": "cmd",
      "args": [
        "/c",
        "npx",
        "-y",
        "uni-t-uts-mcp"
      ],
      "env": {
        "UTS_IP": "192.168.1.100"
      }
    }
  }
}

Docker

{
  "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

Cursor

Add to Cursor

  • The install link opens Cursor's install dialog with the npx command (no analyzer address; tell the assistant where it is, or add UTS_IP under env afterwards). On Windows use the config file below.
  • Or put the JSON in ~/.cursor/mcp.json (all projects) or .cursor/mcp.json (one project).

npx (macOS / Linux)

{
  "mcpServers": {
    "uni-t-uts": {
      "command": "npx",
      "args": [
        "-y",
        "uni-t-uts-mcp"
      ],
      "env": {
        "UTS_IP": "192.168.1.100"
      }
    }
  }
}

npx (Windows)

{
  "mcpServers": {
    "uni-t-uts": {
      "command": "cmd",
      "args": [
        "/c",
        "npx",
        "-y",
        "uni-t-uts-mcp"
      ],
      "env": {
        "UTS_IP": "192.168.1.100"
      }
    }
  }
}

Docker

{
  "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

VS Code

  • Workspace: save the JSON as .vscode/mcp.json. For your user profile run code --add-mcp with the JSON below, or use the Command Palette: MCP: Add Server.
  • UTS_IP is optional: without it, tell the assistant "connect to the analyzer at 192.168.1.100".

.vscode/mcp.json (macOS / Linux)

{
  "servers": {
    "uni-t-uts": {
      "type": "stdio",
      "command": "npx",
      "args": [
        "-y",
        "uni-t-uts-mcp"
      ],
      "env": {
        "UTS_IP": "192.168.1.100"
      }
    }
  }
}

.vscode/mcp.json (Windows)

{
  "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 (macOS / Linux)

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

Windsurf

  • Open the file with Cascade's Open MCP config file action (or Settings > Tools > Windsurf Settings > Add Server) and add the entry.
  • UTS_IP is optional: without it, tell the assistant "connect to the analyzer at 192.168.1.100".

npx (macOS / Linux)

{
  "mcpServers": {
    "uni-t-uts": {
      "command": "npx",
      "args": [
        "-y",
        "uni-t-uts-mcp"
      ],
      "env": {
        "UTS_IP": "192.168.1.100"
      }
    }
  }
}

npx (Windows)

{
  "mcpServers": {
    "uni-t-uts": {
      "command": "cmd",
      "args": [
        "/c",
        "npx",
        "-y",
        "uni-t-uts-mcp"
      ],
      "env": {
        "UTS_IP": "192.168.1.100"
      }
    }
  }
}

Docker

{
  "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

Other stdio clients

  • Command npx, arguments -y uni-t-uts-mcp, environment variable UTS_IP (optional, UTS_PORT defaults to 5025).
  • On Windows, where npx is a .cmd file, use cmd with arguments /c npx -y uni-t-uts-mcp.

npx (macOS / Linux)

{
  "mcpServers": {
    "uni-t-uts": {
      "command": "npx",
      "args": [
        "-y",
        "uni-t-uts-mcp"
      ],
      "env": {
        "UTS_IP": "192.168.1.100"
      }
    }
  }
}

npx (Windows)

{
  "mcpServers": {
    "uni-t-uts": {
      "command": "cmd",
      "args": [
        "/c",
        "npx",
        "-y",
        "uni-t-uts-mcp"
      ],
      "env": {
        "UTS_IP": "192.168.1.100"
      }
    }
  }
}

Docker

{
  "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

Coming next

  • Several instruments at once

Progress is tracked in the open: see the roadmap issue, or jump in via CONTRIBUTING.