siglent-sdg-mcp
Controls Siglent SDG waveform generators over TCP/SCPI through MCP.
Connect/disconnect and identify the generator.
Read and configure channel output state, load impedance, and polarity.
Read and configure basic waveforms (sine, square, ramp, pulse, noise, ARB, DC, PRBS) with frequency, amplitude, offset, phase, duty cycle, etc.
Read and configure modulation (AM, DSB-AM, FM, PM, PWM, ASK, FSK, PSK).
Read and configure frequency sweeps and burst mode.
Select built-in or user-defined arbitrary waveforms.
Reset to defaults, copy settings between channels, configure sync output, and equalize channel phase.
Send read-only SCPI queries as an escape hatch.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@siglent-sdg-mcp Set channel 1 to output a 1kHz sine wave with 2V amplitude."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
siglent-sdg-mcp
A Model Context Protocol (MCP) server that lets AI assistants control Siglent SDG waveform generators over your local network. Connect Claude to your bench and generate, modulate, and configure signals through natural language.
Overview
This MCP server communicates with Siglent SDG waveform generators via SCPI commands over TCP sockets (port 5025). No VISA drivers or NI-MAX installation required — just a network connection to your generator.

Key features:
20 tools covering output control, waveform generation, modulation, sweep, burst, arbitrary waveforms, and more (21 with the opt-in raw SCPI tool)
Dual-channel support (C1 and C2)
8 waveform types: sine, square, ramp, pulse, noise, arbitrary, DC, PRBS
8 modulation types: AM, DSB-AM, FM, PM, PWM, ASK, FSK, PSK
Auto-connect on startup via environment variable
Query queue serializes commands automatically — tools can safely run in parallel
Read-only SCPI query escape hatch; raw SCPI commands are opt-in
Security guardrails: command-injection protection, query-only
scpi_query, connect-target allowlist (see Security)
Related MCP server: jds6600-mcp
Compatibility
Status | Model |
Tested | SDG1032X |
Expected to work | SDG1000X series (SDG1062X, etc.) |
May work | Other Siglent SDG models with SCPI over TCP support |
The server uses standard SCPI commands from the SDG series programming guide. Other Siglent models that support the same command set over port 5025 should work with little or no modification.
Quick Start
You need a Siglent waveform generator accessible on your network (TCP port 5025). Pick one of the three options below and add the config to your .mcp.json (in your project directory, or ~/.claude/.mcp.json for global access).
Replace 192.168.1.126 with your generator's IP address.
Option A: Docker (recommended)
No Node.js installation required. Works on Linux, macOS, and Windows (via WSL2 or Docker Desktop).
{
"mcpServers": {
"siglent-sdg": {
"type": "stdio",
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "SIGLENT_SDG_IP=192.168.1.126",
"ghcr.io/magnusjohansson/siglent-sdg-mcp:latest"
]
}
}
}Option B: npx
Requires Node.js 20+. Downloads and runs the package automatically.
{
"mcpServers": {
"siglent-sdg": {
"type": "stdio",
"command": "npx",
"args": ["-y", "siglent-sdg-mcp"],
"env": {
"SIGLENT_SDG_IP": "192.168.1.126"
}
}
}
}Option C: Clone and build
git clone https://github.com/magnusjohansson/siglent-sdg-mcp.git
cd siglent-sdg-mcp
npm install
npm run build{
"mcpServers": {
"siglent-sdg": {
"type": "stdio",
"command": "node",
"args": ["/path/to/siglent-sdg-mcp/build/index.js"],
"env": {
"SIGLENT_SDG_IP": "192.168.1.126"
}
}
}
}Replace /path/to/siglent-sdg-mcp with the actual path to your clone.
Environment Variables
Variable | Required | Default | Description |
| No | — | Generator IP address for auto-connect on startup |
| No | — | Fallback if |
| No |
| TCP port (only change if your setup differs) |
| No |
| Fallback if |
| No | — | Comma-separated hosts the |
| No |
| Register the raw |
Auto-Connect Behavior
If SIGLENT_SDG_IP (or SIGLENT_IP) is set, the server attempts to connect to the generator immediately after starting. This runs in the background and does not block the MCP server — Claude can start using other tools right away. If the generator is offline or unreachable, the server logs a warning and you can connect manually later using the connect tool.
If neither variable is set, the server starts without a connection. Use the connect tool to connect when ready. In that case connect only accepts private-LAN IP addresses; see Security.
Using with Other AI Clients
The Quick Start examples above use Claude Code's .mcp.json format, which includes a "type": "stdio" field. Other AI clients use the same JSON structure but without the "type" field and with different config file locations.
Claude Desktop
Edit claude_desktop_config.json:
Windows:
%APPDATA%\Claude\claude_desktop_config.jsonmacOS:
~/Library/Application Support/Claude/claude_desktop_config.json
Docker
{
"mcpServers": {
"siglent-sdg": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "SIGLENT_SDG_IP=192.168.1.126",
"ghcr.io/magnusjohansson/siglent-sdg-mcp:latest"
]
}
}
}npx
{
"mcpServers": {
"siglent-sdg": {
"command": "npx",
"args": ["-y", "siglent-sdg-mcp"],
"env": {
"SIGLENT_SDG_IP": "192.168.1.126"
}
}
}
}Note: You must fully restart Claude Desktop after changing the config file.
Cursor
Edit one of:
User-level:
~/.cursor/mcp.json(available across all projects)Project-level:
.cursor/mcp.json(shared with your team via version control)
You can also add servers through the UI: Settings > Cursor Settings > MCP > Add new global MCP server.
Docker
{
"mcpServers": {
"siglent-sdg": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "SIGLENT_SDG_IP=192.168.1.126",
"ghcr.io/magnusjohansson/siglent-sdg-mcp:latest"
]
}
}
}npx
{
"mcpServers": {
"siglent-sdg": {
"command": "npx",
"args": ["-y", "siglent-sdg-mcp"],
"env": {
"SIGLENT_SDG_IP": "192.168.1.126"
}
}
}
}Windsurf
Edit mcp_config.json:
Windows:
%USERPROFILE%\.codeium\windsurf\mcp_config.jsonmacOS/Linux:
~/.codeium/windsurf/mcp_config.json
You can also configure servers through the UI: Cascade panel > MCP icon > Manage MCP Servers > View raw config.
Docker
{
"mcpServers": {
"siglent-sdg": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "SIGLENT_SDG_IP=192.168.1.126",
"ghcr.io/magnusjohansson/siglent-sdg-mcp:latest"
]
}
}
}npx
{
"mcpServers": {
"siglent-sdg": {
"command": "npx",
"args": ["-y", "siglent-sdg-mcp"],
"env": {
"SIGLENT_SDG_IP": "192.168.1.126"
}
}
}
}Google Antigravity
Configuration is managed through the IDE's UI:
Open the Agent pane on the right side of the workspace
Click the
...button at the topSelect MCP Servers
Click Manage MCP Servers
Click View raw config
Add the configuration below and save
Docker
{
"mcpServers": {
"siglent-sdg": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "SIGLENT_SDG_IP=192.168.1.126",
"ghcr.io/magnusjohansson/siglent-sdg-mcp:latest"
]
}
}
}npx
{
"mcpServers": {
"siglent-sdg": {
"command": "npx",
"args": ["-y", "siglent-sdg-mcp"],
"env": {
"SIGLENT_SDG_IP": "192.168.1.126"
}
}
}
}Replace 192.168.1.126 with your generator's IP address in all examples above.
ChatGPT Desktop
ChatGPT Desktop only supports remote HTTPS MCP servers (called "connectors"), not local stdio servers. Since this MCP server uses stdio transport, it is not directly compatible with ChatGPT Desktop.
Tools
20 tools across 9 categories by default; scpi_command is only registered when SIGLENT_ALLOW_RAW_SCPI=true.
Category | Tool | Description |
Connection |
| Connect to waveform generator over TCP |
| Close the connection | |
| Query device ID (manufacturer, model, serial, firmware) | |
Output |
| Read output state, load impedance, and polarity |
| Turn output on/off, set load impedance and polarity | |
Basic Waveform |
| Read waveform parameters (type, frequency, amplitude, etc.) |
| Set waveform type, frequency, amplitude, offset, phase, duty cycle, and more | |
Modulation |
| Read modulation settings (type, source, depth/deviation) |
| Set modulation type, source, frequency, depth, deviation, and carrier parameters | |
Sweep |
| Read sweep parameters (time, frequency range, mode, direction) |
| Set sweep time, start/stop frequencies, mode, direction, trigger, and marker | |
Burst |
| Read burst parameters (mode, period, trigger, cycle count) |
| Set burst mode, period, trigger source, cycle count, delay, and gate polarity | |
Arbitrary Waveform |
| Read the current arbitrary waveform selection |
| Set arbitrary waveform by index (built-in) or name (user-defined) | |
Utility |
| Reset generator to factory defaults (*RST) |
| Copy all parameters from one channel to another | |
| Configure sync output signal | |
| Synchronize phase of both channels | |
SCPI |
| Send a read-only SCPI query (single header ending in |
| Opt-in. Send a single raw SCPI command (no response expected) |
Example Conversations
Generate a basic waveform
You: Output a 1 kHz sine wave at 2 Vpp on channel 1.
Claude calls
configure_basic_wavewithchannel: "C1",waveform_type: "SINE",frequency: 1000,amplitude: 2, thenconfigure_outputwithchannel: "C1",state: "ON".
Set up modulation
You: Add AM modulation to channel 1 with 80% depth at 100 Hz.
Claude calls
configure_modulationwithchannel: "C1",state: "ON",type: "AM",depth: 80,frequency: 100.
Configure a frequency sweep
You: Sweep channel 2 from 100 Hz to 10 kHz over 5 seconds.
Claude calls
configure_sweepwithchannel: "C2",state: "ON",start: 100,stop: 10000,time: 5, thenconfigure_outputwithchannel: "C2",state: "ON".
Check current settings
You: What's the current waveform setup on both channels?
Claude calls
get_basic_waveon both C1 and C2 in parallel and reports the waveform type, frequency, amplitude, and other settings for each channel.
Copy channel configuration
You: Make channel 2 match channel 1's settings.
Claude calls
copy_channelwithsource: "C1",destination: "C2".
Set up burst mode
You: Configure channel 1 for 5-cycle bursts triggered externally.
Claude calls
configure_burstwithchannel: "C1",state: "ON",burst_mode: "NCYC",cycles: "5",trigger_source: "EXT".
Security
An MCP server turns a language model's output into actions. For this server those actions are physical: a wrong command can put 20 Vpp into a 3.3 V circuit. Tool arguments are therefore treated as untrusted input, whether they came from your request, from a document the model read, or from a prompt injection.
What the server enforces, regardless of what the model was told:
Guardrail | What it prevents |
Command-boundary check on every string written to the socket: no control characters (incl. newlines), no | Smuggling extra SCPI commands through any parameter ( |
Strict parameter formats for the free-text parameters ( | Breaking out of a parameter or a quoted string |
| Using the "read-only" tool to change state ( |
Raw | An unrestricted command channel to the instrument, or to other SCPI devices on the LAN (power supplies often use port 5025 too) |
Connect allowlist: | Using the server as a TCP client to reach other services, e.g. |
What it does not protect against: |
Output voltage. This server does not limit amplitude or offset. The model can set anything the generator allows, up to 20 Vpp. To protect a circuit, set the generator's own limit on the front panel or with
C1:BSWV MAX_OUTPUT_AMP,<Vpp>(on models that support it). This backstop only holds while raw SCPI is off: withSIGLENT_ALLOW_RAW_SCPI=true, the model can sendMAX_OUTPUT_AMPitself and raise the limit. Whetherreset(*RST) restores the generator's default limit isn't documented; check it on your model before relying on the backstop. Server-side limits are being designed separately: a first version was reviewed and found to be bypassable, so it isn't shipped (see the threat model).Other clients on port 5025. Anyone with network access to the instrument can control it directly, without going through this server. The instrument has no authentication. Network segmentation is the control for that.
The full threat model, including residual risks, is in docs/threat-model.md.
Architecture
Claude Code <-- stdio/JSON-RPC --> siglent-sdg-mcp <-- TCP/SCPI --> Generator:5025Transport: MCP over stdio (JSON-RPC 2.0)
Protocol: SCPI commands over raw TCP sockets, newline-terminated
Query Queue: All SCPI queries are serialized through an internal queue. The generator processes one command at a time, so even when tools issue parallel requests, the queue ensures they're sent sequentially.
Auto-Connect: If
SIGLENT_SDG_IPis set, connects in the background on startup without blocking the MCP server.Security choke point: every command and query passes through
connection.ts, which applies the checks insrc/security/before anything is written to the socket.
Development
npm run build # Compile TypeScript
npm run watch # Watch mode — recompile on changes
npm run dev # Build and run
npm run inspector # Launch with MCP Inspector for debugging
npm test # Unit tests + end-to-end tests against the SCPI simulatorThe end-to-end tests run the real server and connection code over a real TCP socket against a small SCPI simulator (tests/simulator/), so no hardware is needed. Security tests assert on what actually reached the simulator, not only on the error returned.
To check against real hardware (never turns an output on, aborts unless both outputs are off, because channel coupling can carry C1 changes to C2, uses 1 Vpp on C1, and changes C1's waveform settings):
npm run hardware-check -- 192.168.1.126Releasing
Publishing is triggered by publishing a GitHub Release, not by pushing a tag:
Bump
versioninpackage.jsonand add a section toCHANGELOG.md, then merge tomain.Create a release (draft first) with tag
vX.Y.Zonmain, using the changelog section as the release notes.Publish the release. This runs the npm publish (tests first, with provenance) and the GHCR image build.
The npm workflow fails if the tag doesn't match the package.json version. A pre-release publishes to the npm next dist-tag, and doesn't move the Docker latest tag. A draft release publishes nothing.
Docker (local build)
Build the image locally:
docker build -t siglent-sdg-mcp .Then use the local image in your .mcp.json:
{
"mcpServers": {
"siglent-sdg": {
"type": "stdio",
"command": "docker",
"args": [
"run", "--rm", "-i",
"-e", "SIGLENT_SDG_IP=192.168.1.126",
"siglent-sdg-mcp"
]
}
}
}Project Structure
src/
index.ts # Entry point: stdio transport, auto-connect
server.ts # MCP server factory, tool registration
config.ts # Environment parsing, effective security settings
connection.ts # TCP socket manager with query queue (security choke point)
security/
scpi-guard.ts # Command-boundary and query-only checks
host-policy.ts # Connect-target allowlist
tools/
connection.ts # connect, disconnect, identify
output.ts # get_output, configure_output
basic-wave.ts # get_basic_wave, configure_basic_wave
modulation.ts # get_modulation, configure_modulation
sweep.ts # get_sweep, configure_sweep
burst.ts # get_burst, configure_burst
arbitrary.ts # get_arbitrary_wave, set_arbitrary_wave
utility.ts # reset, copy_channel, configure_sync, equal_phase
scpi.ts # scpi_query, scpi_command (opt-in)
tests/
unit/ # Guard, host policy and config tests
integration/ # End-to-end security tests via MCP client
simulator/ # SCPI simulator (TCP)Troubleshooting
"Not connected to waveform generator"
The generator isn't connected yet. Either set SIGLENT_SDG_IP in your .mcp.json env for auto-connect, or use the connect tool manually.
Connection timeout
Verify the generator's IP address (check the generator's Utility > Interface menu)
Ensure port 5025 is accessible (try
telnet <generator-ip> 5025from your machine)Check that no firewall is blocking the connection
Close other SCPI clients (another MCP server instance, a second Claude session, NI-MAX, scripts). The SDG1032X accepts more than one TCP connection, so a second client does not fail to connect. It interferes instead (see below)
Query timeout
Some SCPI queries can take a few seconds. The default timeout is 5 seconds. For scpi_query, you can increase the timeout with the timeout_ms parameter.
Timeouts on queries that normally answer instantly (identify, get_output) usually mean another client is talking to the generator at the same time. A common cause is a second Claude Code session whose MCP config also starts this server with SIGLENT_SDG_IP set, so both auto-connect. The server serializes its own queries, but it can't coordinate with other processes, and their commands and replies interleave on the instrument. Close the other client and reconnect.
Docker: can't reach the waveform generator
By default, Docker containers can reach LAN devices via the bridge network (NAT). If the container can't connect to your generator:
Verify the generator is reachable from your host:
telnet 192.168.1.126 5025On Linux, try adding
--network hostto the Docker args:"args": ["run", "--rm", "-i", "--network", "host", "-e", "SIGLENT_SDG_IP=192.168.1.126", "ghcr.io/magnusjohansson/siglent-sdg-mcp:latest"]Note:
--network hostdoes not work on macOS or Windows Docker Desktop.
Docker: wrong architecture / exec format error
The published image supports linux/amd64 and linux/arm64. Docker should pull the correct one automatically. If you see an exec format error, pull explicitly:
docker pull --platform linux/amd64 ghcr.io/magnusjohansson/siglent-sdg-mcp:latest"CHDR" appears in responses
This shouldn't happen — the server sets CHDR OFF on connect. If you see command headers in responses, try disconnecting and reconnecting.
License
MIT — see LICENSE for details.
Available Tools
20 toolsconfigure_basic_waveA
Configure the basic waveform parameters of a channel. Multiple parameters can be set in a single call. The command builds a single BSWV command with all specified parameters.
Inputs: channel (required), duty, fall, mean, +14 more.
configure_burstB
Configure burst mode on a channel. State must be set to ON before other parameters. Builds a single BTWV command with all specified parameters.
Inputs: channel (required), delay, state, cycles, +15 more.
configure_modulationA
Configure modulation on a channel. Set state first (ON), then type and parameters. Commands are sent sequentially: state, then type, then type-specific parameters, then carrier parameters.
Inputs: channel (required), type, depth, state, +11 more.
configure_outputA
Configure the output of a channel. Can turn output on/off, set load impedance, and set polarity.
Inputs: channel (required), load, state, polarity.
configure_sweepA
Configure sweep on a channel. State must be set to ON before other parameters. Builds a single SWWV command with all specified parameters.
Inputs: channel (required), span, stop, time, +20 more.
configure_syncC
Configure the sync output signal for a channel.
Inputs: state (required), channel (required), type.
connectA
Connect to a Siglent SDG waveform generator over TCP. Returns device identification on success. Only hosts and ports allowed by the server configuration can be used.
Inputs: host, port.
copy_channelA
Copy all parameters from one channel to another.
Inputs: source (required), destination (required).
disconnectA
Disconnect from the waveform generator.
Inputs: no named parameters.
equal_phaseB
Synchronize the phase of both channels (EQPHASE). Aligns the phase relationship between C1 and C2.
Inputs: no named parameters.
get_arbitrary_waveARead-only
Get the current arbitrary waveform selection for a channel. Returns the waveform index and name.
Inputs: channel (required).
get_basic_waveARead-only
Get the basic waveform parameters of a channel, including waveform type, frequency, amplitude, offset, phase, duty cycle, etc.
Inputs: channel (required).
get_burstARead-only
Get the burst wave parameters of a channel, including state, period, trigger source, burst mode (gate/ncycle), cycle count, carrier settings, and delay.
Inputs: channel (required).
get_modulationARead-only
Get the modulation parameters of a channel. Returns all modulation settings including type, source, frequency, depth/deviation, and carrier parameters.
Inputs: channel (required).
get_outputARead-only
Get the output state of a channel, including on/off status, load impedance, and polarity.
Inputs: channel (required).
get_sweepARead-only
Get the sweep parameters of a channel, including state, time, start/stop frequencies, sweep mode, direction, trigger source, carrier settings, and marker.
Inputs: channel (required).
identifyARead-only
Query the waveform generator identification (*IDN?). Returns manufacturer, model, serial number, and firmware version.
Inputs: no named parameters.
resetADestructive
Reset the waveform generator to factory default settings (*RST).
Inputs: no named parameters.
scpi_queryARead-only
Send a read-only SCPI query to the waveform generator and return the response. Use this as an escape hatch for queries not covered by other tools. The command must be a single SCPI header ending in '?' (e.g. 'C1:BSWV?'); anything that could change instrument state is rejected. Note: CHDR is set to OFF, so responses contain only values (no headers).
Inputs: command (required), timeout_ms.
set_arbitrary_waveA
Set the arbitrary waveform for a channel by index number or by name. Use index for built-in waveforms (e.g. 2=StairUp, 18=Sinc, 26=Cardiac) or name for user-defined waveforms.
Inputs: channel (required), name, index.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 20 tools
Most tools map cleanly to distinct subsystems: output, basic wave, arbitrary wave, modulation, sweep, burst, and sync. The main ambiguity is between set_arbitrary_wave and configure_basic_wave, and scpi_query overlaps with all get_* tools as a read-only escape hatch, though the descriptions clarify these boundaries.
The set is predominantly consistent snake_case with clear verb_noun patterns like configure_output, get_output, configure_sweep, and get_sweep. Minor deviations are set_arbitrary_wave versus the configure_* family, and scpi_query using a noun-first form, but the overall convention remains readable.
Twenty tools is borderline heavy for this server. The coverage is justified by multiple waveform-generator subsystems, but the count sits in the 16-25 range where the surface starts to feel expansive rather than tightly scoped.
Core signal-generation workflows are well covered with get/configure pairs for output, basic wave, arbitrary wave, modulation, sweep, and burst, plus connection, reset, copy, and phase alignment. Minor gaps remain: there is no get_sync counterpart to configure_sync, and no way to define custom arbitrary waveforms beyond selecting built-in or named ones.
Maintenance
Related MCP Connectors
MCP server for Gainium — manage trading bots, deals, and balances via AI assistants
MCP server for Producer/Riffusion AI music generation
MCP server for AI dialogue using various LLM models via AceDataCloud
Related MCP Servers
- AlicenseAqualityDmaintenanceAn MCP server that enables AI assistants to control Siglent SDS oscilloscopes over a local network using SCPI commands. It allows users to measure signals, configure channel and acquisition settings, and capture waveforms or screenshots through natural language.1316 npm9MIT
- AlicenseNot gradedqualityDmaintenanceMCP server for the JDS6600 signal generator, enabling vintage tape recorder calibration via auto-detection and advanced waveform controls.MIT
- AlicenseNot gradedqualityDmaintenanceAn MCP server that connects AI assistants to the Owon DS1102 oscilloscope, enabling waveform capture, measurement retrieval, and remote control of scope settings.GPL 3.0
- FlicenseNot gradedqualityDmaintenanceMCP server for controlling SIGLENT oscilloscopes and signal generators via USBTMC, enabling waveform generation, scope configuration, measurement, and evidence reporting for electronics competitions.1-