55 Commits

Author SHA1 Message Date
bd597a5a7e Bump version to 2026.07.03 (native macOS support) 2026-07-03 17:30:38 -06:00
1debe608d4 Merge macos-port: native macOS support (no Wine), hardware-validated
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.
2026-07-03 17:30:38 -06:00
47beacc9e1 macOS: resolve device-model libs (validated on real Apple Silicon)
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).
2026-07-03 15:46:26 -06:00
37caaf0310 Add native macOS support (no Wine) per Kevin's spec
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.
2026-07-03 12:35:18 -06:00
9c252606f7 Fix SameFileError in batch sweeps; guard self-copy in runner
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.
2026-06-21 22:24:24 -06:00
32a7a2a0dc Fix stale template count in server instructions (10 -> 15) 2026-06-21 21:58:17 -06:00
16cd0e3501 Decompose remaining tools into 6 domain modules; server.py is now an assembly file
Move all ~30 remaining tools out of server.py into domain modules that register
on the shared mcp instance:
- simulation_tools  (simulate, simulate_netlist)
- analysis_tools    (waveform extraction, stability, noise, DC op, power, expressions)
- optimize_tools    (optimize_circuit, tune_circuit, plot_waveform)
- batch_tools       (parameter/temperature sweeps, monte_carlo)
- schematic_tools   (generate/read/edit/diff schematic, drc, touchstone)
- netlist_tools     (create_netlist, create_from_template, list_templates)

server.py: 1568 -> 30 lines (imports-for-registration + main). Tool bodies moved
verbatim. 42 tools / 6 resources / 8 prompts register; 473 unit + 8 integration
tests pass; MCP protocol smoke test confirms tools served over stdio.
2026-06-21 21:06:23 -06:00
dfb94bfcb9 Move guided-workflow prompts into prompts.py
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.
2026-06-21 20:52:24 -06:00
ca6e15e751 Extract library_tools + resources; fix 3 dead resources
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.
2026-06-21 20:50:27 -06:00
3a7e6887aa Decompose server.py: shared _app.mcp + spicebook_tools module
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.
2026-06-21 19:54:23 -06:00
866aa46134 Merge refactor/server-extraction: extract tool logic into tested modules
Shrinks server.py from 3257 to 2472 lines (-24%) by moving tool bodies into
five focused, unit-testable modules, each extraction verified byte-identical
to the prior behavior:

- output_format.py  -- JSON-compaction helpers (round_sig/compact_list/downsample_indices)
- templates.py      -- circuit registries + resolve_template (netlist-first)
- tuning.py         -- tune_circuit pure logic
- plotting.py       -- plot_waveform core (RawFile -> SVG)
- waveform_query.py -- get_waveform + analyze_waveform cores
- RawFile.resolve_index -- dedup exact signal-index lookup

Tests: 412 -> 467 (unit) + 8 integration passing against real Wine+LTspice.
ruff clean.
2026-06-20 22:32:59 -06:00
90cd07d9bc Test output_format and waveform_query without a simulator
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.
2026-06-20 22:25:54 -06:00
1118ac2be1 Extract get_waveform/analyze_waveform cores; relocate output helpers
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.
2026-06-20 22:25:00 -06:00
35b12e61c4 Test build_waveform_svg without a simulator or filesystem
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.
2026-06-20 21:06:35 -06:00
b634eb9cd1 Extract plot_waveform core into plotting.py
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.
2026-06-20 21:06:02 -06:00
181d1cb925 Test tuning.py pure logic without a simulator
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.
2026-06-20 01:43:44 -06:00
dd6db95021 Extract tune_circuit pure logic into tuning.py
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.
2026-06-20 01:43:09 -06:00
cf1502cee9 Test templates registries and resolve_template precedence
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.
2026-06-20 00:45:47 -06:00
c71b3bbb4c Extract template registries into templates.py
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.
2026-06-20 00:45:24 -06:00
7c4f09cbb3 Test RawFile.resolve_index -- exact match, case-insensitivity, no partial
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.
2026-06-19 22:54:05 -06:00
d177a8a316 Add RawFile.resolve_index, dedup signal-index lookup in two tools
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.
2026-06-19 22:53:14 -06:00
351b89ada5 Sync uv.lock to pyproject version 2026.03.05.1 2026-06-19 22:53:14 -06:00
3ec422c9e1 Update publish docs -- sdist is now slim, publish both artifacts 2026-03-05 15:08:40 -07:00
950f05a62f Slim sdist from 182MB to 100KB -- exclude docs/audio from source distribution 2026-03-05 15:07:10 -07:00
902058f851 Fix waveform output bloat -- compact rounding, peak-preserving downsampling, range filter
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
2026-03-05 14:37:52 -07:00
6acf08f80d Add SpiceBook docs -- reference page, tutorial, and updated indexes
New pages:
- reference/spicebook: all 5 SpiceBook tools with params, returns, examples
- tutorials/publish-to-spicebook: end-to-end workflow guide

Updated pages:
- reference/index: add SpiceBook LinkCard, bump tool/resource/prompt counts
- reference/resources: add spicebook://status resource
- reference/prompts: add publish_to_spicebook prompt
- tutorials/index: add Publish to SpiceBook LinkCard
- sidebar: SpiceBook entry with "New" badge
2026-02-20 16:15:37 -07:00
71dfdc8d94 Add SpiceBook integration -- publish circuits as interactive notebooks
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.
2026-02-15 18:05:49 -07:00
4c75c76284 Fix dark mode contrast, add distinct social icons and footer repo link
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.
2026-02-15 14:07:58 -07:00
c69874c5cb Improve oscilloscope panel label contrast for accessibility
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.
2026-02-14 20:49:34 -07:00
b0db898ab4 Fix plot_waveform data indexing bug, add multi-signal overlay and adaptive axis labels
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.
2026-02-14 20:16:10 -07:00
8f0b9ad46a Add axis control, point limits, and dimension params to plot_waveform
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.
2026-02-14 18:04:40 -07:00
59677a6a7d Add signal attenuation knob to oscilloscope control panel
8-position volume control (5%-40%, default 20%) between Source
and Vertical sections. Persists via localStorage, keyboard
accessible, speech volume scales proportionally.
2026-02-13 09:12:52 -07:00
80a01a7326 Add animated rainbow gradient to Outer Limits easter egg text
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.
2026-02-13 07:03:36 -07:00
29fc250c73 Rework Outer Limits plug as narrative continuation
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.
2026-02-13 06:55:08 -07:00
cdd5a923a6 Add oscilloscopemusic.com plug to Outer Limits easter egg
After the typewriter finishes, a subtle link fades in at the bottom
of the CRT: "tell 'em warehack.ing sent 'cha" → oscilloscopemusic.com
2026-02-13 06:52:08 -07:00
ad630ff471 Add Outer Limits easter egg to oscilloscope knobs
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.
2026-02-13 06:46:46 -07:00
be88ea53b7 Add signal source selector to oscilloscope hero
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)
2026-02-13 06:16:47 -07:00
08e0ee3cba Add Tektronix 545A scope skin with switchable skin picker
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.
2026-02-13 03:44:16 -07:00
b7a370c1f4 Add Tektronix 465-inspired oscilloscope hero to docs landing page
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
2026-02-13 02:32:17 -07:00
3c2345282f Fix Dockerfile patch ordering and deduplicate landing cards
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.
2026-02-13 01:25:02 -07:00
f2c18982ae Add Starlight docs site with full tool reference
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.
2026-02-13 01:06:17 -07:00
f608fe5421 Add LICENSE file, add Python 3.13 classifier 2026-02-12 23:42:44 -07:00
125fa3901b Rewrite README with all 37 tools, 5 resources, 7 prompts
Tools organized by category: simulation, waveform extraction,
signal analysis, noise, stability/power, schematic/netlist,
and library/templates. Added resources and prompts tables.
2026-02-12 23:38:48 -07:00
cf8394fa6f Rename mcp-ltspice -> mcltspice, remove stdout banner
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.
2026-02-12 22:53:16 -07:00
0c545800f7 Merge phase6: SVG plots, tuning, templates, integration tests 2026-02-11 12:53:26 -07:00
b16c20c2ca Fix CE amp coupling cap routing and Colpitts test variable selection
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.
2026-02-11 06:01:30 -07:00
9b418a06c5 Add SVG plotting, circuit tuning, 5 new templates, fix prompts
- SVG waveform plots (svg_plot.py): pure-SVG timeseries, Bode, spectrum
  generation with plot_waveform MCP tool — no matplotlib dependency
- Circuit tuning tool (tune_circuit): single-shot simulate → measure →
  compare targets → suggest adjustments workflow for iterative design
- 5 new circuit templates: Sallen-Key lowpass, boost converter,
  instrumentation amplifier, current mirror, transimpedance amplifier
  (both netlist and .asc schematic generators, 15 total templates)
- Fix all 6 prompts to return list[Message] per FastMCP 2.x spec
- Add ltspice://templates and ltspice://template/{name} resources
- Add troubleshoot_simulation prompt
- Integration tests for RC lowpass and non-inverting amp (2/4 pass;
  CE amp and Colpitts oscillator have pre-existing schematic bugs)
- 360 unit tests passing, ruff clean
2026-02-11 05:13:50 -07:00
c56ce918b4 Expand .asc schematic templates to 10 topologies, fix opamp subcircuit
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.
2026-02-11 00:46:13 -07:00
1afa4f112b Add noise, template catalog, DC/TF tools and workflow prompts (35 tools)
New tools: analyze_noise, get_spot_noise, get_total_noise,
create_from_template, list_templates, get_operating_point,
get_transfer_function, list_simulation_runs.

Enhanced get_waveform with per-run extraction for stepped sims.
Added 3 new workflow prompts: optimize_design, monte_carlo_analysis,
circuit_from_scratch.
2026-02-10 23:39:29 -07:00
cfcd0ae221 Initial commit 2026-02-10 23:35:53 -07:00