SpiceMCP
Provides tools for inspecting, statically checking, simulating, and parsing simulation logs of LTspice circuits, enabling LLMs to reason over netlists and identify circuit errors.
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., "@SpiceMCPCheck my low-pass filter netlist for floating pins and missing ground."
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.
SPICE MCP client
Debug LTspice circuits by talking to an LLM about the netlist, not a screenshot.
An MCP server exposes the circuit as structured text — components, nodes, values,
directives, static checks, simulation logs — so the model reasons over topology instead of
pixels. That is cheaper in input tokens and gives far more reliable answers, because the
model can see that R1 connects to NC_01 rather than squinting at a wire that looks
close enough to a pin.
Milestone 1 of a broader effort to build MCP servers for EDA tools. LTspice is the open sandbox for validating the approach before Cadence-class tools.
Requirements
Windows. LTspice has no native Linux build, and batch invocation is Windows-only in v1. Reading and statically checking an existing
.net/.cirworks anywhere; converting a.ascor running a simulation needs the executable.LTspice (developed against 26.0.2). Auto-detected at the per-user
%LOCALAPPDATA%\Programs\ADI\LTspice\LTspice.exeand the olderProgram Files\LTCpaths. Override withLTSPICE_EXEif yours is elsewhere.Python 3.11+ (developed on 3.13).
Related MCP server: SPICEBridge
Setup
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txtTo pick the environment back up in a later session, that middle line is the one you want:
.venv\Scripts\activate(In Git Bash it is source .venv/Scripts/activate; in PowerShell,
.venv\Scripts\Activate.ps1.)
A TAMU AI Gateway token. The LLM is Claude reached through the TAMU AI Gateway, which handles authentication and billing and talks to the model provider on its own side. One bearer token in
TAMU_API_KEYis the whole requirement: no AWS account, no AWS credentials, no region, no boto3.
Then copy .env.example to .env:
copy .env.example .envOnly TAMU_API_KEY is required; every other setting has a working default
(us.anthropic.claude-opus-5 at https://gateway.api.tamu.ai). .env is git-ignored, and
the token never enters the app's config object — it is read from the environment at the
moment the SDK client is built, so there is nothing for a stray debug print, a repr() or a
traceback to leak. What the app records is a label: TAMU_API_KEY (48 chars), never a
fingerprint of the value.
Choosing a model
SPICE_MCP_MODEL names the model the gateway should route to, and says nothing about
this machine. us.anthropic.claude-opus-5 means the gateway reaches Claude via Bedrock on
its side, using its own account — you need no Bedrock access of your own. The us. prefix
is billed about 10% above the global. equivalent, which is worth noting in any cost
write-up so the number is not read as base pricing.
Usage and spend are the gateway's to report — GET /v1/tamus/billing on the same token.
Nothing in this repo needs to reconcile a bill.
The app needs the route to support tool use, which is a property of the endpoint as much as the model. To prove a real round trip end to end:
python scripts\probe_tool_calling.py REM native
python scripts\probe_tool_calling.py <model id> --prompted-json REM the fallbackThree stages — a plain completion with non-zero usage, a genuine tool_use block with
parsed input, and a tool_result reply that closes the loop with stop_reason: "end_turn".
Stage 3 checks the final answer actually contains the value the tool returned, so a model
that answers from its own imagination fails. This sends real requests, so it spends real
tokens against your gateway account — a few, not a session's worth. It lives outside the
app's dependency tree; pip install -r scripts\requirements.txt is enough to run it.
Point SPICE_MCP_BASE_URL at a different gateway root if you need to. Leave the version
segment off: the SDK appends /v1/messages itself.
Two tool-calling modes
SPICE_MCP_TOOL_MODE picks how the model is asked to call tools:
mode | how it works | when |
| the MCP tools go in the Messages API | the gateway's |
| the tool catalogue goes in the system prompt; the model replies with one JSON object, and results come back framed as | a route that accepts |
That second failure is not hypothetical, and it is what makes the endpoint matter more than
the model: the same gateway's /v1/chat/completions route, asked for the same model,
accepts a tools array and then answers as though it had none. The failure is silent — the
model narrates the tool call in its visible text, the turn "succeeds", and no tool ever runs.
prompted_json keeps the app working on such a route without pretending the native channel
is open.
The mode is explicit configuration, never inferred from a model name. A misspelling is
rejected at startup rather than falling back to the default, because falling back is exactly
the silent-wrong-answer this setting exists to avoid. The running mode is shown in the app's
header. Use probe_tool_calling.py --prompted-json to check the fallback on a model before
switching to it; nothing about a native pass or failure changes when you do.
The parser is deliberately strict: the reply must begin with the JSON object (decoded from
index 0 — no scanning prose for something brace-shaped), type must be tool_call or
answer, and the name must be one of the tools actually supplied. Anything else earns a
correction message rather than an execution. Arbitrary prose and antml-style markup are
never interpreted as tool calls.
Running the app
python -m spice_mcp_app --folder fixturesOpen a folder, pick a circuit, and describe the symptom. The model has the seven tools
below and will read, check and simulate on its own. When it proposes a value change you get
a before/after diff with Apply and Reject; applying writes the .asc and
immediately re-simulates, because a fix that was never verified is not a finished fix.
--debug turns up logging. The header shows running token totals; Export log saves the
session JSON wherever you want it.
--file <path> opens a single circuit: it selects it and runs the static checks on load, and
its folder populates the sidebar. That is what the Explorer entry below uses.
Launch from Explorer
Registering a right-click entry removes the three manual steps between "something looks wrong" and "ask about it":
python scripts\install_context_menu.py REM install
python scripts\install_context_menu.py --status REM what is actually registered
python scripts\install_context_menu.py --uninstall REM removeThen right-click any .asc and choose Debug with SPICE MCP. It opens the schematic in
LTspice and the client, with that circuit already selected and its static checks already on
screen.
On Windows 11 this lives in the legacy menu — right-click, then Show more options (or
Shift+right-click). The new compact menu is populated only by packaged (MSIX) apps implementing
the IExplorerCommand COM interface; there is no registry setting that promotes a classic verb
into it.
Details worth knowing:
Per-user, no administrator rights, reversible. Everything goes under
HKCU\Software\Classes\SystemFileAssociations\.asc\shell\, which *adds* a verb. It creates no ProgID, never touchesHKCU\Software\Classes\.asc, and stays clear ofshell\open, so LTspice's own association and what double-clicking a schematic does are unchanged. The verb is markedNeverDefaultso it can never be promoted to the double-click action.The absolute paths are baked into the registry, because the registry cannot give a command a working directory. Move or rename the repo, or rebuild
.venv, and the entry stops working until you re-run the installer.--statusdiagnoses exactly that.It runs
pythonw.exe, so no console window flashes. The cost is that a failure before the window exists has nowhere to print, so the launcher shows a message box for fatal errors and writeslaunch.logat the repo root.The apps come up together when you start from the schematic. Opening LTspice on its own from the Start menu will not summon the client — nothing watches for it.
Editing in LTspice at the same time
The circuit is bound once, at launch. Nothing polls LTspice, and the client only ever reads the
.asc from disk, so:
Before asking, save in LTspice (Ctrl+S) — unsaved GUI edits are invisible here. The app says so once per session when it notices LTspice running.
After applying a fix, LTspice will not notice the external edit. It holds its own copy and will write it back over yours if you save from the GUI. Use File ▸ Revert to reload the patched file. The app warns about this in the conversation whenever LTspice is running.
The write itself is never blocked on this — you asked for the fix and the file is yours. The warning exists so a fix cannot quietly disappear later.
Running the MCP server
Normally the desktop app spawns it. To run it standalone (it speaks MCP over stdio, so it will just sit there waiting for a client — that is correct):
python -m spice_mcp_serverSPICE_MCP_LOG_LEVEL=DEBUG turns up the diagnostics, which go to stderr; stdout is
the protocol wire and must stay clean.
The repo ships a .mcp.json, so the server can also be driven straight from Claude Code
against the fixtures without the app existing yet.
Nothing about the server is Claude Code specific — it is a plain MCP stdio server and any
MCP host can spawn it. .mcp.json is just one host's registration file. Its command is
relative to the repo root, so a host that launches from somewhere else needs either an
absolute path to .venv\Scripts\python.exe or the repo set as the working directory. The
real constraints are the server's own: Windows, and LTspice for anything touching a .asc.
Tools
Tool | What it does |
| Structured view of a circuit: components, nodes, values, nets, directives, subcircuits. Accepts |
| Findings with no simulation: missing ground, floating pins, isolated sections, no DC path to ground, duplicate refs, missing/malformed values, the |
| Runs |
| Parses a |
| Changes one component's value in a |
| Compares two circuits by reference designator: value changes, rewires, added/removed parts, net and directive changes. |
| Writes a real SPICE netlist to a path you choose. Refuses to overwrite. |
Never trust the exit code
Two LTspice behaviours, both verified against 26.0.2, make the obvious success signals useless:
A failed run can exit 0.
ERROR: Node n1 is floating and connected to current source I1exits 0. A missing ground exits 1. There is no consistent mapping.A
.rawfile is written even when the analysis failed. The over-defined-matrix failure still produced a complete-looking waveform file.
So succeeded is derived from the log text alone. returncode is carried in the result
for diagnostics only, and is documented as such in the tool schema so the model doesn't
reach for it.
The log messages themselves are not uniform either — of the five distinct failures the
parser handles, only two carry an ERROR:/WARNING: prefix, and one arrives in a
compile-style path(line): message form. A grep for ERROR would miss the two most
serious failures outright.
The .asc vs .net distinction
.asc is the schematic and the source of truth — it carries the GUI coordinates, so a
fix written back there stays usable in LTspice. .net is the flattened netlist. Prefer
pointing tools at the .asc.
Beware: LTspice's Export Netlist menu item (and -PCBnetlist) writes an ExpressPCB
netlist, not SPICE. The RCLP.net in this repo is one of those. The server detects the
format, and falls back to converting the sibling .asc when there is one rather than
misparsing PCB connectivity as a circuit.
Your files are never written to
LTspice writes -netlist and -b output next to the input file, which means running it
on your schematic silently overwrites your own .net. Every invocation therefore stages
the input into a scratch directory under %TEMP%\spice_mcp_work\ first, and all
artifacts land there. tests/test_ltspice.py pins this down — it is a regression test for
a real incident, not a hypothetical.
The model cannot edit your schematic on its own
Exactly one tool writes to your files, and the approval for it is registered by the
Apply button, not by the model. If the model calls patch_component_value with
apply=true off its own bat, the app refuses the call and tells it to show you a diff
instead. Approvals are single-use and specific to one file, one reference designator and one
value — approving C1 → 100n does not authorise R1, or a second write of the same value.
tests/test_llm.py asserts the refused call never reaches the server; the end-to-end script
also asks the model to bypass the gate and checks the file comes back byte-identical.
The gate sits on the executor, below both tool-calling modes, so prompted_json buys the
model no extra reach — the same test drives the same unapproved write through both.
When a write does happen it is surgical: the one SYMATTR Value line belonging to that
component is rewritten, reusing the file's own indentation and line terminator. Encoding,
line endings and every other byte are preserved, so the schematic still opens in the LTspice
GUI — which is the whole point of patching the .asc rather than a netlist.
Tests
python -m pytest288 tests, ~42s. Parser and static-check tests run without LTspice installed; schematic
tests skip automatically if the executable is not found. Nothing in the suite calls the
network, so running it costs nothing — the live-model checks are the two scripts,
probe_tool_calling.py and app_smoke.py (a headless nine-stage end-to-end run against
a temp copy of wrong_value_lowpass.asc).
Staying free is enforced, not hoped for: an autouse fixture deletes TAMU_API_KEY (the
developer's own .env holds a live one), strips every AWS_* and SPICE_MCP_* variable,
points HOME/USERPROFILE at a temp directory, and neuters .env loading. Without it a
mis-wired test could reach the live gateway and spend money, and a "credentials are missing"
test would pass on a fresh checkout while failing on a configured machine.
The fixture matrix in tests/test_checks.py asserts the exact set of checks each
circuit produces. Equality rather than membership is deliberate: it makes the suite a
false-positive guard, so a new check that misfires on the known-good circuit fails loudly
instead of quietly training everyone to ignore the output.
fixtures/ holds deliberately broken circuits, each documenting its own bug in a TEXT
comment:
Fixture | Bug | Caught by |
| None — exists so checks can be proven silent on a correct circuit. | — |
|
| static |
| Loop is closed but there is no node 0. | static + sim (singular matrix) |
|
| static only |
| Two ideal sources of different value in parallel. | sim only (over-defined matrix) |
| Correct topology, | neither |
The last three are the interesting ones, because they pull in different directions:
no_dc_path.ascis flagged by the static check but simulates fine — LTspice exits 0, solves the.op"by inspection", and only warns. The DC operating point is not actually determined, and the simulator doesn't care.source_conflict.ascis the mirror image: statically clean, but the simulation genuinely fails.wrong_value_lowpass.ascpasses both. It is out of spec by 10× and nothing but circuit reasoning will catch it. That is the fixture that tests whether the model understands the circuit rather than just parsing errors.
Together they're the argument for the two-stage design: neither pass subsumes the other.
RCLP.asc is fixture 0 and genuinely broken two ways — R1 has a floating pin and is not
in the signal path, so Vin never drives Vout. It is worth running yourself, because
it simulates "successfully": the .ac analysis completes with no warning at all. Only
the static check notices that the circuit cannot possibly do what it looks like it does.
Layout
spice_mcp_server/ MCP server. Knows nothing about LLMs.
models.py pydantic return models — these ARE the tool output schemas
netlist.py SPICE parsing; ExpressPCB detection
checks.py static checks
logparse.py .log parsing — where the real diagnosis usually lives
ltspice.py exe discovery, staged -netlist / -b invocation, timeouts
asc.py byte-preserving .asc value patching
diff.py before/after circuit comparison
spice_mcp_app/ desktop UI + LLM client + MCP client
config.py model/gateway URL/credential-style resolution; holds no credential
session.py the token log; atomic write after every turn
llm.py gateway client, MCP→Anthropic tool mapping, agent loop
mcp_client.py stdio client; holds one server subprocess open
api.py the JS bridge — and the approval gate
launch.py the Explorer entry point: opens LTspice, then the window
web/ index.html, style.css, app.js
spice_mcp_launch.py repo-root shim the registry command points at
scripts/ standalone utilities, own requirements.txt
install_context_menu.py adds/removes the right-click verb (HKCU, no admin)
fixtures/ deliberately broken circuits
sessions/ per-session token logs (git-ignored)The two-process split is the point: the server never learns what an LLM is, so it can be reused by any MCP host and later retargeted at other EDA tools.
Session logs
Each debug session writes sessions/<uuid>.json with per-turn token counts. Cost
comparison against the old screenshot workflow is done externally — the app only
records, it does not analyse. The gateway reports per-call spend on its own side
(GET /v1/tamus/billing), so the log and the bill can be reconciled against each other.
{
"session_id": "…", "started_at": "…", "model": "…", "circuit_file": "…\\RCLP.asc",
"turns": [{"role": "assistant", "text": "…", "tool_calls": [],
"input_tokens": 4211, "output_tokens": 96}],
"total_input_tokens": 41996, "total_output_tokens": 1572, "resolved": true
}The file is rewritten atomically after every turn, so a crash mid-session still leaves a
complete log. tool_calls is present only on turns that made them. resolved is the
Mark resolved button — it is how you tell, later, which sessions actually ended in a fix.
The schema is fixed by that external comparison, so it deliberately does not grow keys:
usage also carries cache_read_input_tokens and cache_creation_input_tokens, and since
prompt caching is not enabled they are dropped rather than logged. An assistant turn that
arrives with no usage at all is logged as zero and warned about — a silently null count
would under-report cost without anything visibly failing.
Notes for future work
mcpmust stay>=2.1. The 2.x API (MCPServer, return-annotation-derived schemas) is a rewrite; most tutorials online target v1 and will not run.Never send a
thinkingparameter. Adaptive thinking is on by default for Opus 5;budget_tokensis rejected with a 400, and explicitly disabling thinking makes the model occasionally write a tool call into its visible text rather than emitting atool_useblock — which is the exact failure this transport was migrated to fix..gitattributesexempts*.asc/*.asy/*.netfrom EOL normalisation.RCLP.ascis bare-LF whileRCLP.netis CRLF, andtext=autowould rewrite schematic bytes.-I<path>must be the last LTspice argument, after the filename, with no space. Placed earlier, LTspice hangs forever instead of erroring.
This server cannot be deployed
Maintenance
Related MCP Connectors
Find your AI agent's likely failure mode, get runtime settings, and clarify ambiguous prompts.
Electronic component datasheets for AI agents — specs, pinouts, package data on demand.
Deterministic reasoning stack for AI agents: simulate, decide & compute, plus cross-domain tools.
Codebase intelligence for agents: 152 structured artifacts across 21 programs, one call.
Related MCP Servers
- FlicenseCqualityCmaintenanceThis server enables LLMs to design, simulate, and debug electronic circuits using LTspice via natural language commands. It automates netlist generation, library component validation, and iterative error correction for SPICE simulations.21-
- AlicenseBqualityCmaintenanceAI-powered circuit design through simulation — an MCP server that gives language models direct access to SPICE circuit simulation via ngspice, enabling natural language circuit description and automated netlist generation, simulation, measurement, and spec verification.2836GPL 3.0
- AlicenseAqualityBmaintenanceAn MCP server that connects LLM assistants to real circuit simulation: LTspice and ngspice, plus direct editing of LTspice .asc schematics. Simulation results come back as structured numbers so the assistant can design, verify, and iterate on circuits.4843GPL 3.0
- AlicenseNot gradedqualityDmaintenanceGenerates, simulates, and inspects LTspice circuits via MCP tools and resources, providing structured JSON interfaces for AI agents.MIT