Port validated end-to-end on real Apple Silicon (macOS 26.5.2, LTspice XVII
17.2.4): full suite 534 passed / 8 skipped on native Mac; Linux unchanged
(524 unit + 18 integration via Wine). Platform-branches execution so Linux
behaves identically. See commits for details.
Hardware testing on a native Apple Silicon Mac (LTspice XVII 17.2.4) surfaced
that model/subcircuit-bearing circuits failed with 'Unable to find definition
of model' / 'Could not open library file': native Mac LTspice in batch mode
does NOT auto-include the standard device libs or search the lib dir the way
Wine does (Wine netlists the .asc itself on Linux, so this never showed there).
Two fixes:
- netlister.py: emit .lib standard.{bjt,mos,dio,jft} for each semiconductor
family present, since Mac LTspice won't auto-include them in batch mode.
- runner.py: _resolve_lib_paths() rewrites bare-name .lib/.include refs to
absolute paths under the Mac lib root before running -b (Mac LTspice can't
find them by name). darwin-only; Linux path untouched.
- test_netlister.py: skip the Wine-oracle comparison class when wine is absent
(it's a Linux-only oracle).
Validated on hardware: full suite 534 passed / 8 skipped on native macOS
(all 10 integration circuits simulate, incl. op-amp + BJT); Linux unchanged
(524 unit + 18 integration). Also answered Kevin's open questions on hardware:
#2 -- 'LTspice -b <netlist>' DOES run over bare SSH (Background session), so
mcltspice is fully headless on Mac since it always netlists itself; #3 --
examples are not pre-extracted on Mac (examples_exist reports false).
Platform-branches the Wine-specific execution so Linux behaves identically
while macOS drives LTspice.app natively. Implemented from muel9560's measured
spec + validated where possible against a Wine -netlist oracle on Linux.
- config.py: get_launch() abstraction (argv prefix + env + path translator);
IS_DARWIN, LTSPICE_BIN (env-overridable), LTSPICE_LIB_ROOT under
~/Library/Application Support/LTspice on macOS. Linux resolves to the
historical Wine paths unchanged.
- runner.py: darwin branch uses the native binary, POSIX paths (no Z: map),
plain -b (never -Run), and completion by polling for a stable .raw (the Mac
GUI app's exit code is unreliable). Linux keeps communicate()+wait_for.
- netlister.py (new): asc_to_netlist() converts a schematic to a netlist on
macOS, since the Mac CLI can't netlist a .asc. Union-find net extraction;
all 15 templates netlist, topology matches the Wine -netlist oracle.
- library_tools.py: check_installation reports platform/binary/lib-root/
version/gui-session on macOS.
- packaging/docs: drop 'Linux only'; document LTSPICE_BIN + GUI-session need.
Adversarial review fixes: restore kill+reap of the Wine subprocess on Linux
timeout (was orphaning the process -- a real regression); alias duplicate net
labels instead of aborting (sallen-key); synthesize NC_xx nodes for floating
pins (op-amps) to match the oracle; use asyncio.get_running_loop().
Linux unaffected: 476 unit + 10 integration (real Wine) still pass; +48 unit
+8 integration for the platform/netlister paths. macOS runtime (LTspice.app
invocation, GUI session, poll timing) needs validation on Kevin's hardware.
run_parameter_sweep/run_monte_carlo write each variant .cir into a work_dir,
then call run_netlist with that same work_dir -- which tried to shutil.copy2
the file onto itself (SameFileError), so every batch run failed before
simulating. Add _safe_copy() that skips when src and dst resolve to the same
path, used at all four staging-copy sites in run_simulation/run_netlist.
Caught by headless claude-cli testing of the batch tools (no batch test
existed). Adds test_runner.py (_safe_copy unit tests) and a TestBatchSweeps
integration class.
The 7 narrative prompts (design_filter, debug_circuit, etc.) are pure Message
builders with no tool dependencies -- relocate them to prompts.py, registered
on the shared mcp instance. server.py: 1876 -> 1568 lines.
ltspice://symbols, ltspice://examples, ltspice://status raised TypeError on
read -- they called list_symbols/list_examples/check_installation, which are
@mcp.tool() FunctionTool objects (not callable). Split those three into plain
*_impl functions + thin tool wrappers, and point the resources at the impls.
The other library tools move verbatim.
Adds test_resources.py to read every resource (regression guard). Library tool
outputs verified byte-identical old-vs-new. server.py: 2168 -> 1876 lines.
Introduce the multi-file FastMCP layout. The mcp instance moves to _app.py
(imports nothing from tool modules, keeping the registration graph acyclic),
and the self-contained SpiceBook domain (5 tools + status resource + publish
prompt + 2 helpers) moves to spicebook_tools.py. server.py imports it for the
registration side effect.
Tool/resource/prompt counts unchanged (42/6/8); names identical. Repointed
test_spicebook's _read_netlist import. server.py: 2472 -> 2168 lines.
17 tests: round_sig/compact_list rounding, downsample_indices stride vs
peak-preserving (a narrow spike must survive), and extract_waveform/
analyze_signal across AC/transient/x-range/error/all-analysis paths.
Move the shared JSON-compaction helpers (round_sig, compact_list,
downsample_indices) out of server.py into output_format.py, and extract the
get_waveform and analyze_waveform cores into waveform_query.py
(extract_waveform, analyze_signal) -- both take a parsed RawFile and return a
JSON-ready dict, so they test without a simulator. The tools keep only the
.raw read.
server.py: 2677 -> 2472 lines. Output verified byte-identical across
AC/transient/downsample/error/all-analysis cases.
11 tests over auto plot-type detection (bode/spectrum/time), missing-signal
and multi-signal-overlay errors, dimension/height defaults, downsampling, and
x-range clipping -- all with hand-built RawFiles, no .raw read or SVG write.
plot_waveform was the largest tool at 207 lines, interleaving signal
resolution, auto plot-type detection, X-range clipping, downsampling, and
svg_plot dispatch with the .raw read and SVG write. Move the RawFile -> SVG
transform into plotting.build_waveform_svg (returns the SVG string + metadata,
no filesystem); the tool keeps only the parse and write I/O.
server.py: 2799 -> 2677 lines. Output verified byte-identical (SVG sha256 +
metadata) to the prior version across bode/spectrum/time, x-range+downsample,
custom dimensions, and both error paths.
15 tests over build_effective_params, extract_metrics (AC + transient via
hand-built RawFiles), evaluate_targets (all comparison operators +
unmeasurable), and make_suggestions. This logic previously required a full
LTspice run to exercise.
tune_circuit interleaved ~260 lines of pure computation (param validation,
metric extraction, target evaluation, suggestion generation) with the async
Wine orchestration, so that logic could only be tested through a full
simulation. Split the four pure pieces into tuning.py:
build_effective_params, extract_metrics, evaluate_targets, make_suggestions
(plus UnknownParamError). The tool keeps the async generate-and-simulate
flow and error-dict shaping.
server.py: 2949 -> 2799 lines. Output verified byte-identical to the prior
version across AC, transient, and all error paths.
Covers the netlist-first precedence contract (names in both registries),
asc-only/netlist-only resolution, unknown -> None, and the deduped union
in all_template_names -- the lookup logic tune_circuit depends on.
The two inline registries (_TEMPLATES, _ASC_TEMPLATES) plus their ~30
generator-function imports were pure data living in server.py. Move them
to a dedicated templates.py as NETLIST_TEMPLATES / ASC_TEMPLATES, and add
resolve_template() (netlist-first precedence) + all_template_names() to
replace the '.get() or .get()' dual-lookup dance that tune_circuit used.
server.py: 3257 -> 2949 lines. Registries verified byte-identical to the
originals (keys, params, descriptions, func identities); netlist-first
precedence for names in both registries preserved; live tune_circuit /
create_from_template / generate_schematic unchanged.
Locks in the exact-match semantics that distinguish resolve_index from
get_variable, so the lookup used by tune_circuit and plot_waveform is now
covered without needing the live LTspice path.
tune_circuit and plot_waveform each hand-rolled exact case-insensitive
name->row-index resolution -- a manual 3-way loop and a next(enumerate)
generator respectively. Centralize as RawFile.resolve_index(), next to
the existing get_variable() (which does partial match, so it can't be
reused here without changing semantics).
No behavior change: exact case-insensitive match and the not-found error
paths (with available_signals) are preserved. Verified against the live
LTspice path -- tune_circuit bandwidth and plot_waveform resolution
unchanged.
get_waveform with 3 AC signals at 700 points produced ~92K chars of JSON,
exceeding MCP client token limits. Three fixes:
- Round output values (dB to 2dp, phase to 2dp, freq/time to 6 sig figs),
cutting JSON size ~63% (97K -> 36K chars for the same data)
- Peak-preserving downsampling for AC data: bucket-based selection keeps
the point with the largest magnitude deviation per bucket, so narrow
resonance peaks (high-Q filters) aren't lost by blind stride sampling
- Add x_min/x_max parameters to get_waveform and evaluate_waveform_expression
for zooming into a frequency/time range without excessive global point counts
Bridge mcltspice to SpiceBook (spicebook.warehack.ing) with 5 new MCP
tools (publish, list, get, delete, simulate), a status resource, and a
guided publish prompt. Includes LTspice-to-ngspice netlist conversion
(strips .backanno, Rser=, Windows paths) with structured warnings.
Path traversal protection on file reads (extension allowlist + resolve),
notebook ID validation, narrowed exception handling, and size limits.
35 unit tests + 4 live integration tests.
Dark mode had inverted gray scale (:root[data-theme="dark"] should
have been [data-theme="light"]), causing near-invisible text. Override
SocialIcons with Lucide GitBranch/Package icons instead of duplicate
external-link icons. Add footer source link to Gitea.
Bump opacity on section labels, knob labels, brand text, and
attribution bar. Slightly increase font sizes on smallest labels.
Maintains vintage aesthetic while meeting readable contrast levels.
Row-major data from raw_parser was indexed as column-major, producing
garbled plots for digital waveforms. Also adds signals parameter for
overlaying multiple time-domain traces with legend, and adaptive X-axis
labels (ns/µs/ms/s) based on time span.
Expose x_min/x_max/y_min/y_max, max_points, width/height, and title
override through the MCP tool. Data is clipped to X range before
stride-based downsampling for better zoomed resolution. All params
default to None/current behavior for backward compatibility.
8-position volume control (5%-40%, default 20%) between Source
and Vertical sections. Persists via localStorage, keyboard
accessible, speech volume scales proportionally.
Opening narration cycles through a 9-stop gradient (red → amber →
yellow → green → teal → blue → violet → pink → red) at 6s per
loop. Closing narration stays teal for visual hierarchy.
Replace disconnected bottom link with the show's closing narration
subverted: "We now return control of your television set to
oscilloscopemusic.com" — fades in after typewriter finishes with
a dramatic 1.2s delay. Also add oscilloscopemusic.com to the
always-visible attribution bar for N-Spheres tracks.
Clicking the Vertical or Horizontal knobs triggers The Outer Limits
(1963) opening narration as a typewriter overlay on the CRT display,
with Web Speech API narration. Dismiss via click, Escape, or knob
re-click. Works on both 465 and 545A skins.
Source knob cycles through 6 tracks: the original CC-licensed
Spirals intro plus 5 N-Spheres tracks (Function, Intersect,
Attractor, Flux, Core) by Fenderson & Hansi3D.
- 48kHz FLAC conversions served via git LFS (~189MB total)
- Rotary knob with CSS custom property composable transforms
- Dynamic attribution (CC link for Spirals, album credit for N-Spheres)
- Signal selection persisted in localStorage
- Loading state overlay while buffering larger tracks
- Skin-aware labels (465: "Source", 545A: "Input")
- Keyboard accessible (Enter/Space to cycle)
Second oscilloscope skin: 1959 Type 545A with blue-green hammertone
panel, cream silk-screened labels, Bakelite knobs, deeper CRT recess,
and ventilation holes. Click the model name to cycle between 465 and
545A skins. Selection persists via localStorage.
XY-mode Lissajous display renders stereo audio on a canvas
inside a warm champagne bezel with recessed CRT bay, labeled
control sections (Vertical/Horizontal/Trigger), rotary knobs,
and power LED. Uses modern AnalyserNode + rAF pipeline instead
of deprecated ScriptProcessor.
Audio: "Spirals" by Jerobeam Fenderson (CC BY-NC-SA 4.0)
Visual: Nick Watton, adapted from gist by rsp2k
COPY patches/ before npm ci so the postinstall Starlight
head.ts patch runs during install. Replace duplicate Tutorials
card on the landing page with a Concepts card.
Astro/Starlight documentation at docs/ with 21 pages:
- Getting started (prerequisites, Claude Code setup, first simulation)
- Tutorials (filter design, Monte Carlo yield)
- Reference (all 37 tools, 5 resources, 7 prompts)
- Concepts (LTspice on Linux, simulation types)
Docker infrastructure with dev/prod compose overlays, Caddy
reverse proxy for mcltspice.warehack.ing, and Makefile targets.
Includes patch for Starlight 0.37 head schema default bug.
Tools organized by category: simulation, waveform extraction,
signal analysis, noise, stability/power, schematic/netlist,
and library/templates. Added resources and prompts tables.
Rename package from mcp-ltspice/mcp_ltspice to mcltspice throughout:
source directory, imports, pyproject.toml, tests, and README.
Remove startup banner prints from main() since FastMCP handles
its own banner and stdout is the MCP JSON-RPC transport.
Point repo URL at git.supported.systems/MCP/mcltspice.
CE amplifier schematic: the input coupling cap CC_in was placed
horizontally (R90) at y=336 — the same y as the RB1-to-base bias
wire. Both cap pins sat on the wire, shorting the cap and allowing
Vin's 0V DC to override the bias divider, putting Q1 in cutoff.
Fix: move CC_in to vertical orientation (R0) above the base wire.
Now pinA=(400,256) and pinB=(400,320) are off the y=336 bias path.
The cap properly blocks DC while passing the 1kHz input signal.
Result: V(out) swings 2.2Vpp (gain ≈ 110) instead of stuck at Vcc.
Colpitts oscillator test: the schematic was actually working (V(out)
pp=2.05V) but the test's fallback variable selection picked V(n001)
(the Vcc rail, constant 12V) instead of V(out). Fix: look for V(out)
first since the schematic labels the collector with "out".
Integration tests: 4/4 pass, unit tests: 360/360 pass.
Add 7 new graphical schematic templates (differential amp, buck converter,
LDO regulator, H-bridge, common emitter, Colpitts oscillator) and rewrite
inverting amp to actually include an op-amp instead of just passive components.
Fix UniversalOpamp2 subcircuit error: the .asy symbol defines SpiceModel as
"level2", so SYMATTR Value must be omitted to let the built-in model name
resolve. Previously emitting SYMATTR Value UniversalOpamp2 caused LTspice
to search for a non-existent subcircuit.
Fix wire-through-pin routing bugs: vertical wires crossing intermediate
opamp/source pins auto-connect at those pins, creating unintended shorts.
Rerouted V1-to-In+ paths to avoid crossing In- pins in non-inverting,
common-emitter, Colpitts, and differential amp templates.
Refactor generate_schematic tool from hardcoded if/elif to registry dispatch
via _ASC_TEMPLATES dict, matching the _TEMPLATES pattern for netlists.
All 10 templates verified: simulate with zero errors and zero NC nodes.
255 tests pass, source lint clean.