2026-02-10 23:35:53 -07:00

1963 lines
60 KiB
Python

"""FastMCP server for LTspice circuit simulation automation.
This server provides tools for:
- Running SPICE simulations on schematics and netlists
- Extracting and analyzing waveform data
- Creating circuits programmatically
- Modifying schematic components
- Browsing component libraries, models, and examples
- Design rule checks and circuit comparison
"""
import csv
import io
import json
import math
import tempfile
from pathlib import Path
import numpy as np
from fastmcp import FastMCP
from . import __version__
from .asc_generator import (
generate_inverting_amp,
)
from .asc_generator import (
generate_rc_lowpass as generate_rc_lowpass_asc,
)
from .asc_generator import (
generate_voltage_divider as generate_voltage_divider_asc,
)
from .batch import (
run_monte_carlo,
run_parameter_sweep,
run_temperature_sweep,
)
from .config import (
LTSPICE_EXAMPLES,
LTSPICE_LIB,
validate_installation,
)
from .diff import diff_schematics as _diff_schematics
from .drc import run_drc as _run_drc
from .log_parser import parse_log
from .models import (
search_models as _search_models,
)
from .models import (
search_subcircuits as _search_subcircuits,
)
from .netlist import (
Netlist,
buck_converter,
colpitts_oscillator,
common_emitter_amplifier,
differential_amplifier,
h_bridge,
inverting_amplifier,
ldo_regulator,
non_inverting_amplifier,
rc_lowpass,
voltage_divider,
)
from .optimizer import (
ComponentRange,
OptimizationTarget,
format_engineering,
optimize_component_values,
)
from .power_analysis import compute_efficiency, compute_power_metrics
from .raw_parser import parse_raw_file
from .runner import run_netlist, run_simulation
from .schematic import modify_component_value, parse_schematic
from .stability import compute_stability_metrics
from .touchstone import parse_touchstone, s_param_to_db
from .waveform_expr import WaveformCalculator
from .waveform_math import (
compute_bandwidth,
compute_fft,
compute_peak_to_peak,
compute_rise_time,
compute_rms,
compute_settling_time,
compute_thd,
)
mcp = FastMCP(
name="mcp-ltspice",
instructions="""
LTspice MCP Server - Circuit simulation automation.
Use this server to:
- Run SPICE simulations on .asc schematics or .cir netlists
- Extract waveform data (voltages, currents) from simulation results
- Analyze signals: FFT, THD, RMS, bandwidth, settling time
- Create circuits from scratch using the netlist builder
- Create circuits from 10 pre-built templates (list_templates)
- Modify component values in schematics programmatically
- Browse LTspice's component library (6500+ symbols)
- Search 2800+ SPICE models and subcircuits
- Access example circuits (4000+ examples)
- Run design rule checks before simulation
- Compare schematics to see what changed
- Export waveform data to CSV
- Extract DC operating point (.op) and transfer function (.tf) data
- Measure stability (gain/phase margins from AC loop gain)
- Compute power and efficiency from voltage/current waveforms
- Evaluate waveform math expressions (V*I, gain, dB, etc.)
- Optimize component values to hit target specs automatically
- Generate .asc schematic files (graphical format)
- Run parameter sweeps, temperature sweeps, and Monte Carlo analysis
- Handle stepped simulations: list runs, extract per-run data
- Parse Touchstone (.s2p) S-parameter files
LTspice runs via Wine on Linux. Simulations execute in batch mode
and results are parsed from binary .raw files.
""",
)
# ============================================================================
# SIMULATION TOOLS
# ============================================================================
@mcp.tool()
async def simulate(
schematic_path: str,
timeout_seconds: float = 300,
) -> dict:
"""Run an LTspice simulation on a schematic file.
Executes any simulation directives (.tran, .ac, .dc, .op, etc.)
found in the schematic. Returns available signal names and
the path to the .raw file for waveform extraction.
Args:
schematic_path: Absolute path to .asc schematic file
timeout_seconds: Maximum time to wait for simulation (default 5 min)
"""
result = await run_simulation(
schematic_path,
timeout=timeout_seconds,
parse_results=True,
)
response = {
"success": result.success,
"elapsed_seconds": result.elapsed_seconds,
"error": result.error,
}
if result.raw_data:
response["variables"] = [
{"name": v.name, "type": v.type} for v in result.raw_data.variables
]
response["points"] = result.raw_data.points
response["plotname"] = result.raw_data.plotname
response["raw_file"] = str(result.raw_file) if result.raw_file else None
if result.log_file and result.log_file.exists():
log = parse_log(result.log_file)
if log.measurements:
response["measurements"] = log.get_all_measurements()
if log.errors:
response["log_errors"] = log.errors
return response
@mcp.tool()
async def simulate_netlist(
netlist_path: str,
timeout_seconds: float = 300,
) -> dict:
"""Run an LTspice simulation on a netlist file (.cir or .net).
Args:
netlist_path: Absolute path to .cir or .net netlist file
timeout_seconds: Maximum time to wait for simulation
"""
result = await run_netlist(
netlist_path,
timeout=timeout_seconds,
parse_results=True,
)
response = {
"success": result.success,
"elapsed_seconds": result.elapsed_seconds,
"error": result.error,
}
if result.raw_data:
response["variables"] = [
{"name": v.name, "type": v.type} for v in result.raw_data.variables
]
response["points"] = result.raw_data.points
response["raw_file"] = str(result.raw_file) if result.raw_file else None
if result.log_file and result.log_file.exists():
log = parse_log(result.log_file)
if log.measurements:
response["measurements"] = log.get_all_measurements()
if log.errors:
response["log_errors"] = log.errors
return response
# ============================================================================
# WAVEFORM & ANALYSIS TOOLS
# ============================================================================
@mcp.tool()
def get_waveform(
raw_file_path: str,
signal_names: list[str],
max_points: int = 1000,
run: int | None = None,
) -> dict:
"""Extract waveform data from a .raw simulation results file.
For transient analysis, returns time + voltage/current values.
For AC analysis, returns frequency + magnitude(dB)/phase(degrees).
For stepped simulations (.step, .mc, .temp), specify `run` (1-based)
to extract a single run's data. Omit `run` to get all data combined.
Args:
raw_file_path: Path to .raw file from simulation
signal_names: Signal names to extract, e.g. ["V(out)", "I(R1)"]
max_points: Maximum data points (downsampled if needed)
run: Run number (1-based) for stepped simulations (None = all data)
"""
raw = parse_raw_file(raw_file_path)
# Extract specific run if requested
if run is not None:
if not raw.is_stepped:
return {"error": "Not a stepped simulation - no multiple runs available"}
if run < 1 or run > raw.n_runs:
return {"error": f"Run {run} out of range (1..{raw.n_runs})"}
raw = raw.get_run_data(run)
x_axis = raw.get_time()
x_name = "time"
if x_axis is None:
x_axis = raw.get_frequency()
x_name = "frequency"
total_points = len(x_axis) if x_axis is not None else raw.points
step = max(1, total_points // max_points)
result = {
"x_axis_name": x_name,
"x_axis_data": [],
"signals": {},
"total_points": total_points,
"returned_points": 0,
"is_stepped": raw.is_stepped,
"n_runs": raw.n_runs,
}
if x_axis is not None:
sampled = x_axis[::step]
if np.iscomplexobj(sampled):
result["x_axis_data"] = sampled.real.tolist()
else:
result["x_axis_data"] = sampled.tolist()
result["returned_points"] = len(result["x_axis_data"])
for name in signal_names:
data = raw.get_variable(name)
if data is not None:
sampled = data[::step]
if np.iscomplexobj(sampled):
result["signals"][name] = {
"magnitude_db": [
20 * math.log10(abs(x)) if abs(x) > 0 else -200 for x in sampled
],
"phase_degrees": [math.degrees(math.atan2(x.imag, x.real)) for x in sampled],
}
else:
result["signals"][name] = {"values": sampled.tolist()}
return result
@mcp.tool()
def list_simulation_runs(raw_file_path: str) -> dict:
"""List runs in a stepped simulation (.step, .mc, .temp).
Returns run count and boundary information for multi-run .raw files.
Args:
raw_file_path: Path to .raw file from simulation
"""
raw = parse_raw_file(raw_file_path)
result = {
"is_stepped": raw.is_stepped,
"n_runs": raw.n_runs,
"total_points": raw.points,
"plotname": raw.plotname,
"variables": [{"name": v.name, "type": v.type} for v in raw.variables],
}
if raw.is_stepped and raw.run_boundaries:
runs = []
for i in range(raw.n_runs):
start, end = raw._run_slice(i + 1)
runs.append(
{
"run": i + 1,
"start_index": start,
"end_index": end,
"points": end - start,
}
)
result["runs"] = runs
return result
@mcp.tool()
def analyze_waveform(
raw_file_path: str,
signal_name: str,
analyses: list[str],
settling_tolerance_pct: float = 2.0,
settling_final_value: float | None = None,
rise_low_pct: float = 10.0,
rise_high_pct: float = 90.0,
fft_max_harmonics: int = 50,
thd_n_harmonics: int = 10,
) -> dict:
"""Analyze a signal from simulation results.
Run one or more analyses on a waveform. Available analyses:
- "rms": Root mean square value
- "peak_to_peak": Min, max, peak-to-peak swing, mean
- "settling_time": Time to settle within tolerance of final value
- "rise_time": 10%-90% rise time (configurable)
- "fft": Frequency spectrum via FFT
- "thd": Total Harmonic Distortion
Args:
raw_file_path: Path to .raw file
signal_name: Signal to analyze, e.g. "V(out)"
analyses: List of analysis types to run
settling_tolerance_pct: Tolerance for settling time (default 2%)
settling_final_value: Target value (None = use last sample)
rise_low_pct: Low threshold for rise time (default 10%)
rise_high_pct: High threshold for rise time (default 90%)
fft_max_harmonics: Max harmonics to return in FFT
thd_n_harmonics: Number of harmonics for THD calculation
"""
raw = parse_raw_file(raw_file_path)
time = raw.get_time()
signal = raw.get_variable(signal_name)
if signal is None:
return {
"error": f"Signal '{signal_name}' not found. Available: "
f"{[v.name for v in raw.variables]}"
}
# Use real parts for time-domain analysis
if np.iscomplexobj(time):
time = time.real
if np.iscomplexobj(signal):
signal = np.abs(signal)
results = {"signal": signal_name}
for analysis in analyses:
if analysis == "rms":
results["rms"] = compute_rms(signal)
elif analysis == "peak_to_peak":
results["peak_to_peak"] = compute_peak_to_peak(signal)
elif analysis == "settling_time":
if time is not None:
results["settling_time"] = compute_settling_time(
time,
signal,
final_value=settling_final_value,
tolerance_percent=settling_tolerance_pct,
)
elif analysis == "rise_time":
if time is not None:
results["rise_time"] = compute_rise_time(
time,
signal,
low_pct=rise_low_pct,
high_pct=rise_high_pct,
)
elif analysis == "fft":
if time is not None:
results["fft"] = compute_fft(
time,
signal,
max_harmonics=fft_max_harmonics,
)
elif analysis == "thd":
if time is not None:
results["thd"] = compute_thd(
time,
signal,
n_harmonics=thd_n_harmonics,
)
return results
@mcp.tool()
def measure_bandwidth(
raw_file_path: str,
signal_name: str,
ref_db: float | None = None,
) -> dict:
"""Measure -3dB bandwidth from an AC analysis result.
Computes the frequency range where the signal is within 3dB
of its peak (or a specified reference level).
Args:
raw_file_path: Path to .raw file from AC simulation
signal_name: Signal to measure, e.g. "V(out)"
ref_db: Reference level in dB (None = use peak)
"""
raw = parse_raw_file(raw_file_path)
freq = raw.get_frequency()
signal = raw.get_variable(signal_name)
if freq is None:
return {"error": "Not an AC analysis - no frequency data found"}
if signal is None:
return {"error": f"Signal '{signal_name}' not found"}
# Convert complex signal to magnitude in dB
mag_db = np.array([20 * math.log10(abs(x)) if abs(x) > 0 else -200 for x in signal])
return compute_bandwidth(freq.real, mag_db, ref_db=ref_db)
@mcp.tool()
def export_csv(
raw_file_path: str,
signal_names: list[str] | None = None,
output_path: str | None = None,
max_points: int = 10000,
) -> dict:
"""Export simulation waveform data to CSV format.
Args:
raw_file_path: Path to .raw file
signal_names: Signals to export (None = all)
output_path: Where to save CSV (None = auto-generate in /tmp)
max_points: Maximum rows to export
"""
raw = parse_raw_file(raw_file_path)
# Determine x-axis
x_axis = raw.get_time()
x_name = "time"
if x_axis is None:
x_axis = raw.get_frequency()
x_name = "frequency"
# Select signals
if signal_names is None:
signal_names = [
v.name for v in raw.variables if v.name not in (x_name, "time", "frequency")
]
# Downsample
total = raw.points
step = max(1, total // max_points)
# Build CSV
buf = io.StringIO()
writer = csv.writer(buf)
# Header
if x_axis is not None and np.iscomplexobj(x_axis):
headers = [x_name]
else:
headers = [x_name]
for name in signal_names:
data = raw.get_variable(name)
if data is not None:
if np.iscomplexobj(data):
headers.extend([f"{name}_magnitude_db", f"{name}_phase_deg"])
else:
headers.append(name)
writer.writerow(headers)
# Data rows
indices = range(0, total, step)
for i in indices:
row = []
if x_axis is not None:
row.append(x_axis[i].real if np.iscomplexobj(x_axis) else x_axis[i])
for name in signal_names:
data = raw.get_variable(name)
if data is not None:
if np.iscomplexobj(data):
val = data[i]
row.append(20 * math.log10(abs(val)) if abs(val) > 0 else -200)
row.append(math.degrees(math.atan2(val.imag, val.real)))
else:
row.append(data[i])
writer.writerow(row)
csv_content = buf.getvalue()
# Save to file
if output_path is None:
raw_name = Path(raw_file_path).stem
output_path = str(Path(tempfile.gettempdir()) / f"{raw_name}.csv")
Path(output_path).write_text(csv_content)
return {
"output_path": output_path,
"rows": len(indices),
"columns": headers,
}
# ============================================================================
# STABILITY ANALYSIS TOOLS
# ============================================================================
@mcp.tool()
def analyze_stability(
raw_file_path: str,
signal_name: str,
) -> dict:
"""Measure gain margin and phase margin from AC loop gain data.
Computes Bode plot (magnitude + phase) and finds the crossover
frequencies where gain = 0 dB and phase = -180 degrees.
Args:
raw_file_path: Path to .raw file from AC simulation
signal_name: Loop gain signal, e.g. "V(out)" or "V(loop_gain)"
"""
raw = parse_raw_file(raw_file_path)
freq = raw.get_frequency()
signal = raw.get_variable(signal_name)
if freq is None:
return {"error": "Not an AC analysis - no frequency data found"}
if signal is None:
return {
"error": f"Signal '{signal_name}' not found. Available: "
f"{[v.name for v in raw.variables]}"
}
return compute_stability_metrics(freq.real, signal)
# ============================================================================
# DC OPERATING POINT & TRANSFER FUNCTION TOOLS
# ============================================================================
@mcp.tool()
def get_operating_point(log_file_path: str) -> dict:
"""Extract DC operating point results from a simulation log.
The .op analysis computes all node voltages and branch currents
at the DC bias point. Results include device operating points
(transistor Gm, Id, Vgs, etc.) when available.
Run a simulation with .op directive first, then pass the log file.
Args:
log_file_path: Path to .log file from simulation
"""
log = parse_log(log_file_path)
if not log.operating_point:
return {
"error": "No operating point data found in log. "
"Ensure the simulation uses a .op directive.",
"log_errors": log.errors,
}
# Separate node voltages from branch currents/device params
voltages = {}
currents = {}
other = {}
for name, value in log.operating_point.items():
if name.startswith("V(") or name.startswith("v("):
voltages[name] = value
elif name.startswith("I(") or name.startswith("i(") or name.startswith("Ix("):
currents[name] = value
else:
other[name] = value
return {
"voltages": voltages,
"currents": currents,
"device_params": other,
"total_entries": len(log.operating_point),
}
@mcp.tool()
def get_transfer_function(log_file_path: str) -> dict:
"""Extract .tf (transfer function) results from a simulation log.
The .tf analysis computes:
- Transfer function (gain or transresistance)
- Input impedance at the source
- Output impedance at the output node
Run a simulation with .tf directive first (e.g., ".tf V(out) V1"),
then pass the log file.
Args:
log_file_path: Path to .log file from simulation
"""
log = parse_log(log_file_path)
if not log.transfer_function:
return {
"error": "No transfer function data found in log. "
"Ensure the simulation uses a .tf directive, "
"e.g., '.tf V(out) V1'.",
"log_errors": log.errors,
}
# Identify the specific components
result: dict = {"raw_data": log.transfer_function}
for name, value in log.transfer_function.items():
name_lower = name.lower()
if "transfer_function" in name_lower:
result["transfer_function"] = value
elif "output_impedance" in name_lower:
result["output_impedance_ohms"] = value
elif "input_impedance" in name_lower:
result["input_impedance_ohms"] = value
return result
# ============================================================================
# POWER ANALYSIS TOOLS
# ============================================================================
@mcp.tool()
def analyze_power(
raw_file_path: str,
voltage_signal: str,
current_signal: str,
) -> dict:
"""Compute power metrics from voltage and current waveforms.
Returns average power, RMS power, peak power, and power factor.
Args:
raw_file_path: Path to .raw file from transient simulation
voltage_signal: Voltage signal name, e.g. "V(out)"
current_signal: Current signal name, e.g. "I(R1)"
"""
raw = parse_raw_file(raw_file_path)
time = raw.get_time()
voltage = raw.get_variable(voltage_signal)
current = raw.get_variable(current_signal)
if time is None:
return {"error": "Not a transient analysis - no time data found"}
if voltage is None:
return {"error": f"Voltage signal '{voltage_signal}' not found"}
if current is None:
return {"error": f"Current signal '{current_signal}' not found"}
return compute_power_metrics(time, voltage, current)
@mcp.tool()
def compute_efficiency_tool(
raw_file_path: str,
input_voltage_signal: str,
input_current_signal: str,
output_voltage_signal: str,
output_current_signal: str,
) -> dict:
"""Compute power conversion efficiency.
Compares input power to output power for regulators, converters, etc.
Args:
raw_file_path: Path to .raw file from transient simulation
input_voltage_signal: Input voltage, e.g. "V(vin)"
input_current_signal: Input current, e.g. "I(Vin)"
output_voltage_signal: Output voltage, e.g. "V(out)"
output_current_signal: Output current, e.g. "I(Rload)"
"""
raw = parse_raw_file(raw_file_path)
time = raw.get_time()
if time is None:
return {"error": "Not a transient analysis"}
v_in = raw.get_variable(input_voltage_signal)
i_in = raw.get_variable(input_current_signal)
v_out = raw.get_variable(output_voltage_signal)
i_out = raw.get_variable(output_current_signal)
for name, sig in [
(input_voltage_signal, v_in),
(input_current_signal, i_in),
(output_voltage_signal, v_out),
(output_current_signal, i_out),
]:
if sig is None:
return {"error": f"Signal '{name}' not found"}
return compute_efficiency(time, v_in, i_in, v_out, i_out)
# ============================================================================
# WAVEFORM EXPRESSION TOOLS
# ============================================================================
@mcp.tool()
def evaluate_waveform_expression(
raw_file_path: str,
expression: str,
max_points: int = 1000,
) -> dict:
"""Evaluate a math expression on simulation waveforms.
Supports: +, -, *, /, abs(), sqrt(), log10(), dB()
Signal names reference variables from the .raw file.
Examples:
"V(out) * I(R1)" - instantaneous power
"V(out) / V(in)" - voltage gain
"dB(V(out))" - magnitude in dB
Args:
raw_file_path: Path to .raw file
expression: Math expression using signal names
max_points: Maximum data points to return
"""
raw = parse_raw_file(raw_file_path)
calc = WaveformCalculator(raw)
try:
result = calc.calc(expression)
except ValueError as e:
return {"error": str(e), "available_signals": calc.available_signals()}
# Get x-axis
x_axis = raw.get_time()
x_name = "time"
if x_axis is None:
x_axis = raw.get_frequency()
x_name = "frequency"
total = len(result)
step = max(1, total // max_points)
response = {
"expression": expression,
"total_points": total,
"returned_points": len(result[::step]),
}
if x_axis is not None:
sampled_x = x_axis[::step]
response["x_axis_name"] = x_name
response["x_axis_data"] = (
sampled_x.real.tolist() if np.iscomplexobj(sampled_x) else sampled_x.tolist()
)
response["values"] = result[::step].tolist()
response["available_signals"] = calc.available_signals()
return response
# ============================================================================
# OPTIMIZER TOOLS
# ============================================================================
@mcp.tool()
async def optimize_circuit(
netlist_template: str,
targets: list[dict],
component_ranges: list[dict],
max_iterations: int = 20,
) -> dict:
"""Automatically optimize component values to hit target specifications.
Runs real LTspice simulations in a loop, adjusting component values
using binary search (single component) or coordinate descent (multiple).
Args:
netlist_template: Netlist text with {ComponentName} placeholders
(e.g., {R1}, {C1}) that get substituted each iteration.
targets: List of target specs, each with:
- signal_name: Signal to measure (e.g., "V(out)")
- metric: One of "bandwidth_hz", "rms", "peak_to_peak",
"settling_time", "gain_db", "phase_margin_deg"
- target_value: Desired value
- weight: Importance weight (default 1.0)
component_ranges: List of tunable components, each with:
- component_name: Name matching {placeholder} (e.g., "R1")
- min_value: Minimum value in base units
- max_value: Maximum value in base units
- preferred_series: Optional "E12", "E24", or "E96" for snapping
max_iterations: Max simulation iterations (default 20)
"""
opt_targets = [
OptimizationTarget(
signal_name=t["signal_name"],
metric=t["metric"],
target_value=t["target_value"],
weight=t.get("weight", 1.0),
)
for t in targets
]
opt_ranges = [
ComponentRange(
component_name=r["component_name"],
min_value=r["min_value"],
max_value=r["max_value"],
preferred_series=r.get("preferred_series"),
)
for r in component_ranges
]
result = await optimize_component_values(
netlist_template,
opt_targets,
opt_ranges,
max_iterations,
)
return {
"best_values": {k: format_engineering(v) for k, v in result.best_values.items()},
"best_values_raw": result.best_values,
"best_cost": result.best_cost,
"iterations": result.iterations,
"targets_met": result.targets_met,
"final_metrics": result.final_metrics,
"history_length": len(result.history),
}
# ============================================================================
# BATCH SIMULATION TOOLS
# ============================================================================
@mcp.tool()
async def parameter_sweep(
netlist_text: str,
param_name: str,
start: float,
stop: float,
num_points: int = 10,
timeout_seconds: float = 300,
) -> dict:
"""Sweep a parameter across a range of values.
Runs multiple simulations, substituting the parameter value each time.
The netlist should contain a .param directive for the parameter.
Args:
netlist_text: Netlist with .param directive
param_name: Parameter to sweep (e.g., "Rval")
start: Start value
stop: Stop value
num_points: Number of sweep points
timeout_seconds: Per-simulation timeout
"""
values = np.linspace(start, stop, num_points).tolist()
result = await run_parameter_sweep(
netlist_text,
param_name,
values,
timeout=timeout_seconds,
)
return {
"success_count": result.success_count,
"failure_count": result.failure_count,
"total_elapsed": result.total_elapsed,
"parameter_values": result.parameter_values,
"raw_files": [str(r.raw_file) if r.raw_file else None for r in result.results],
}
@mcp.tool()
async def temperature_sweep(
netlist_text: str,
temperatures: list[float],
timeout_seconds: float = 300,
) -> dict:
"""Run simulations at different temperatures.
Args:
netlist_text: Netlist text
temperatures: List of temperatures in degrees C
timeout_seconds: Per-simulation timeout
"""
result = await run_temperature_sweep(
netlist_text,
temperatures,
timeout=timeout_seconds,
)
return {
"success_count": result.success_count,
"failure_count": result.failure_count,
"total_elapsed": result.total_elapsed,
"parameter_values": result.parameter_values,
"raw_files": [str(r.raw_file) if r.raw_file else None for r in result.results],
}
@mcp.tool()
async def monte_carlo(
netlist_text: str,
n_runs: int,
tolerances: dict[str, float],
timeout_seconds: float = 300,
seed: int | None = None,
) -> dict:
"""Run Monte Carlo analysis with component tolerances.
Randomly varies component values within tolerance using a normal
distribution, then runs simulations for each variant.
Args:
netlist_text: Netlist text
n_runs: Number of Monte Carlo iterations
tolerances: Component tolerances, e.g. {"R1": 0.05} for 5%
timeout_seconds: Per-simulation timeout
seed: Optional RNG seed for reproducibility
"""
result = await run_monte_carlo(
netlist_text,
n_runs,
tolerances,
timeout=timeout_seconds,
seed=seed,
)
return {
"success_count": result.success_count,
"failure_count": result.failure_count,
"total_elapsed": result.total_elapsed,
"parameter_values": result.parameter_values,
"raw_files": [str(r.raw_file) if r.raw_file else None for r in result.results],
}
# ============================================================================
# SCHEMATIC GENERATION TOOLS
# ============================================================================
@mcp.tool()
def generate_schematic(
template: str,
output_path: str | None = None,
r: str | None = None,
c: str | None = None,
r1: str | None = None,
r2: str | None = None,
vin: str | None = None,
rin: str | None = None,
rf: str | None = None,
opamp_model: str | None = None,
) -> dict:
"""Generate an LTspice .asc schematic file from a template.
Available templates and their parameters:
- "rc_lowpass": r (resistor, default "1k"), c (capacitor, default "100n")
- "voltage_divider": r1 (top, default "10k"), r2 (bottom, default "10k"),
vin (input voltage, default "5")
- "inverting_amp": rin (input R, default "10k"), rf (feedback R,
default "100k"), opamp_model (default "UniversalOpamp2")
Args:
template: Template name
output_path: Where to save (None = auto in /tmp)
r: Resistor value (rc_lowpass)
c: Capacitor value (rc_lowpass)
r1: Top resistor (voltage_divider)
r2: Bottom resistor (voltage_divider)
vin: Input voltage (voltage_divider)
rin: Input resistor (inverting_amp)
rf: Feedback resistor (inverting_amp)
opamp_model: Op-amp model name (inverting_amp)
"""
if template == "rc_lowpass":
params: dict[str, str] = {}
if r is not None:
params["r"] = r
if c is not None:
params["c"] = c
sch = generate_rc_lowpass_asc(**params)
elif template == "voltage_divider":
params = {}
if r1 is not None:
params["r1"] = r1
if r2 is not None:
params["r2"] = r2
if vin is not None:
params["vin"] = vin
sch = generate_voltage_divider_asc(**params)
elif template == "inverting_amp":
params = {}
if rin is not None:
params["rin"] = rin
if rf is not None:
params["rf"] = rf
if opamp_model is not None:
params["opamp_model"] = opamp_model
sch = generate_inverting_amp(**params)
else:
return {
"error": f"Unknown template '{template}'. "
f"Available: rc_lowpass, voltage_divider, inverting_amp"
}
if output_path is None:
output_path = str(Path(tempfile.gettempdir()) / f"{template}.asc")
saved = sch.save(output_path)
return {
"success": True,
"output_path": str(saved),
"schematic_preview": sch.render()[:500],
}
# ============================================================================
# TOUCHSTONE / S-PARAMETER TOOLS
# ============================================================================
@mcp.tool()
def read_touchstone(file_path: str) -> dict:
"""Parse a Touchstone (.s1p, .s2p, .snp) S-parameter file.
Returns S-parameter data, frequency points, and port information.
Args:
file_path: Path to Touchstone file
"""
try:
data = parse_touchstone(file_path)
except (ValueError, FileNotFoundError) as e:
return {"error": str(e)}
# Convert S-parameter data to a more digestible format
s_params = {}
for i in range(data.n_ports):
for j in range(data.n_ports):
key = f"S{i + 1}{j + 1}"
s_data = data.data[:, i, j]
s_params[key] = {
"magnitude_db": s_param_to_db(s_data).tolist(),
}
return {
"filename": data.filename,
"n_ports": data.n_ports,
"n_frequencies": len(data.frequencies),
"freq_range_hz": [float(data.frequencies[0]), float(data.frequencies[-1])],
"reference_impedance": data.reference_impedance,
"s_parameters": s_params,
"comments": data.comments[:5], # First 5 comment lines
}
# ============================================================================
# SCHEMATIC TOOLS
# ============================================================================
@mcp.tool()
def read_schematic(schematic_path: str) -> dict:
"""Read and parse an LTspice schematic file.
Returns component list, net names, and SPICE directives.
Args:
schematic_path: Path to .asc schematic file
"""
sch = parse_schematic(schematic_path)
return {
"version": sch.version,
"components": [
{
"name": c.name,
"symbol": c.symbol,
"value": c.value,
"x": c.x,
"y": c.y,
"attributes": c.attributes,
}
for c in sch.components
],
"nets": [f.name for f in sch.flags],
"directives": sch.get_spice_directives(),
"wire_count": len(sch.wires),
}
@mcp.tool()
def edit_component(
schematic_path: str,
component_name: str,
new_value: str,
output_path: str | None = None,
) -> dict:
"""Modify a component's value in a schematic.
Args:
schematic_path: Path to .asc schematic file
component_name: Instance name like "R1", "C2", "M1"
new_value: New value string, e.g., "10k", "100n", "2N7000"
output_path: Where to save (None = overwrite original)
"""
try:
sch = modify_component_value(
schematic_path,
component_name,
new_value,
output_path,
)
comp = sch.get_component(component_name)
return {
"success": True,
"component": component_name,
"new_value": new_value,
"output_path": output_path or schematic_path,
"symbol": comp.symbol if comp else None,
}
except ValueError as e:
return {"success": False, "error": str(e)}
@mcp.tool()
def diff_schematics(
schematic_a: str,
schematic_b: str,
) -> dict:
"""Compare two schematics and show what changed.
Reports component additions, removals, value changes,
directive changes, and wire/net topology differences.
Args:
schematic_a: Path to "before" .asc file
schematic_b: Path to "after" .asc file
"""
diff = _diff_schematics(schematic_a, schematic_b)
return diff.to_dict()
@mcp.tool()
def run_drc(schematic_path: str) -> dict:
"""Run design rule checks on a schematic.
Checks for common issues:
- Missing ground connection
- Floating nodes
- Missing simulation directive
- Voltage source loops
- Missing component values
- Duplicate component names
- Unconnected components
Args:
schematic_path: Path to .asc schematic file
"""
result = _run_drc(schematic_path)
return result.to_dict()
# ============================================================================
# NETLIST BUILDER TOOLS
# ============================================================================
@mcp.tool()
def create_netlist(
title: str,
components: list[dict],
directives: list[str],
output_path: str | None = None,
) -> dict:
"""Create a SPICE netlist programmatically and save to a .cir file.
Build circuits from scratch without needing a graphical schematic.
The created .cir file can be simulated with simulate_netlist.
Args:
title: Circuit title/description
components: List of component dicts, each with:
- name: Component name (R1, C1, V1, M1, X1, etc.)
- nodes: List of node names (use "0" for ground)
- value: Value or model name
- params: Optional extra parameters string
directives: List of SPICE directives, e.g.:
[".tran 10m", ".ac dec 100 1 1meg",
".meas tran vmax MAX V(out)"]
output_path: Where to save .cir file (None = auto in /tmp)
Example components:
[
{"name": "V1", "nodes": ["in", "0"], "value": "AC 1"},
{"name": "R1", "nodes": ["in", "out"], "value": "10k"},
{"name": "C1", "nodes": ["out", "0"], "value": "100n"}
]
"""
nl = Netlist(title=title)
for comp in components:
nl.add_component(
name=comp["name"],
nodes=comp["nodes"],
value=comp["value"],
params=comp.get("params", ""),
)
for directive in directives:
nl.add_directive(directive)
# Determine output path
if output_path is None:
safe_title = "".join(c if c.isalnum() else "_" for c in title)[:30]
output_path = str(Path(tempfile.gettempdir()) / f"{safe_title}.cir")
saved = nl.save(output_path)
return {
"success": True,
"output_path": str(saved),
"netlist_preview": nl.render(),
"component_count": len(nl.components),
}
# ============================================================================
# CIRCUIT TEMPLATE TOOLS
# ============================================================================
# Registry of netlist templates with parameter metadata
_TEMPLATES: dict[str, dict] = {
"voltage_divider": {
"func": voltage_divider,
"description": "Resistive voltage divider with .op or custom analysis",
"params": {"v_in": "5", "r1": "10k", "r2": "10k", "sim_type": "op"},
},
"rc_lowpass": {
"func": rc_lowpass,
"description": "RC lowpass filter with AC sweep",
"params": {"r": "1k", "c": "100n", "f_start": "1", "f_stop": "1meg"},
},
"inverting_amplifier": {
"func": inverting_amplifier,
"description": "Inverting op-amp (gain = -Rf/Rin), +/-15V supply",
"params": {"r_in": "10k", "r_f": "100k", "opamp_model": "LT1001"},
},
"non_inverting_amplifier": {
"func": non_inverting_amplifier,
"description": "Non-inverting op-amp (gain = 1 + Rf/Rin), +/-15V supply",
"params": {"r_in": "10k", "r_f": "100k", "opamp_model": "LT1001"},
},
"differential_amplifier": {
"func": differential_amplifier,
"description": "Diff amp: Vout = (R2/R1)*(V2-V1), +/-15V supply",
"params": {
"r1": "10k",
"r2": "10k",
"r3": "10k",
"r4": "10k",
"opamp_model": "LT1001",
},
},
"common_emitter_amplifier": {
"func": common_emitter_amplifier,
"description": "BJT common-emitter with voltage divider bias",
"params": {
"rc": "2.2k",
"rb1": "56k",
"rb2": "12k",
"re": "1k",
"cc1": "10u",
"cc2": "10u",
"ce": "47u",
"vcc": "12",
"bjt_model": "2N2222",
},
},
"buck_converter": {
"func": buck_converter,
"description": "Step-down DC-DC converter with MOSFET switch",
"params": {
"ind": "10u",
"c_out": "100u",
"r_load": "10",
"v_in": "12",
"duty_cycle": "0.5",
"freq": "100k",
"mosfet_model": "IRF540N",
"diode_model": "1N5819",
},
},
"ldo_regulator": {
"func": ldo_regulator,
"description": "LDO regulator: Vout = Vref * (1 + R1/R2)",
"params": {
"opamp_model": "LT1001",
"r1": "10k",
"r2": "10k",
"pass_transistor": "IRF9540N",
"v_in": "8",
"v_ref": "2.5",
},
},
"colpitts_oscillator": {
"func": colpitts_oscillator,
"description": "LC oscillator: f ~ 1/(2pi*sqrt(L*Cseries))",
"params": {
"ind": "1u",
"c1": "100p",
"c2": "100p",
"rb": "47k",
"rc": "1k",
"re": "470",
"vcc": "12",
"bjt_model": "2N2222",
},
},
"h_bridge": {
"func": h_bridge,
"description": "4-MOSFET H-bridge motor driver with dead time",
"params": {
"v_supply": "12",
"r_load": "10",
"mosfet_model": "IRF540N",
},
},
}
@mcp.tool()
def create_from_template(
template_name: str,
params: dict[str, str] | None = None,
output_path: str | None = None,
) -> dict:
"""Create a circuit netlist from a pre-built template.
Available templates:
- voltage_divider: params {v_in, r1, r2, sim_type}
- rc_lowpass: params {r, c, f_start, f_stop}
- inverting_amplifier: params {r_in, r_f, opamp_model}
- non_inverting_amplifier: params {r_in, r_f, opamp_model}
- differential_amplifier: params {r1, r2, r3, r4, opamp_model}
- common_emitter_amplifier: params {rc, rb1, rb2, re, cc1, cc2, ce, vcc, bjt_model}
- buck_converter: params {ind, c_out, r_load, v_in, duty_cycle, freq, mosfet_model, diode_model}
- ldo_regulator: params {opamp_model, r1, r2, pass_transistor, v_in, v_ref}
- colpitts_oscillator: params {ind, c1, c2, rb, rc, re, vcc, bjt_model}
- h_bridge: params {v_supply, r_load, mosfet_model}
All parameter values are optional -- defaults are used if omitted.
Args:
template_name: Template name from the list above
params: Optional dict of parameter overrides (all values as strings)
output_path: Where to save .cir file (None = auto in /tmp)
"""
template = _TEMPLATES.get(template_name)
if template is None:
return {
"error": f"Unknown template '{template_name}'",
"available_templates": [
{"name": k, "description": v["description"], "params": v["params"]}
for k, v in _TEMPLATES.items()
],
}
# Build kwargs from params, converting duty_cycle to float for buck_converter
kwargs: dict = {}
if params:
for k, v in params.items():
if k not in template["params"]:
return {
"error": f"Unknown parameter '{k}' for template '{template_name}'",
"valid_params": template["params"],
}
# duty_cycle needs to be float, not string
if k == "duty_cycle":
kwargs[k] = float(v)
else:
kwargs[k] = v
nl = template["func"](**kwargs)
if output_path is None:
output_path = str(Path(tempfile.gettempdir()) / f"{template_name}.cir")
saved = nl.save(output_path)
return {
"success": True,
"template": template_name,
"description": template["description"],
"output_path": str(saved),
"netlist_preview": nl.render(),
"component_count": len(nl.components),
"params_used": {**template["params"], **(params or {})},
}
@mcp.tool()
def list_templates() -> dict:
"""List all available circuit templates with their parameters and defaults.
Returns template names, descriptions, and the parameters each accepts
with their default values.
"""
return {
"templates": [
{
"name": name,
"description": info["description"],
"params": info["params"],
}
for name, info in _TEMPLATES.items()
],
"total_count": len(_TEMPLATES),
}
# ============================================================================
# LIBRARY & MODEL TOOLS
# ============================================================================
@mcp.tool()
def list_symbols(
category: str | None = None,
search: str | None = None,
limit: int = 50,
) -> dict:
"""List available component symbols from LTspice library.
Args:
category: Filter by category (e.g., "Opamps", "Comparators")
search: Search term for symbol name (case-insensitive)
limit: Maximum results to return
"""
symbols = []
sym_dir = LTSPICE_LIB / "sym"
if not sym_dir.exists():
return {"error": "Symbol library not found", "symbols": [], "total_count": 0}
for asy_file in sym_dir.rglob("*.asy"):
rel_path = asy_file.relative_to(sym_dir)
cat = str(rel_path.parent) if rel_path.parent != Path(".") else "misc"
name = asy_file.stem
if category and cat.lower() != category.lower():
continue
if search and search.lower() not in name.lower():
continue
symbols.append({"name": name, "category": cat, "path": str(asy_file)})
symbols.sort(key=lambda x: x["name"].lower())
total = len(symbols)
return {"symbols": symbols[:limit], "total_count": total, "returned_count": min(limit, total)}
@mcp.tool()
def list_examples(
category: str | None = None,
search: str | None = None,
limit: int = 50,
) -> dict:
"""List example circuits from LTspice examples library.
Args:
category: Filter by category folder
search: Search term for example name
limit: Maximum results to return
"""
examples = []
if not LTSPICE_EXAMPLES.exists():
return {"error": "Examples not found", "examples": [], "total_count": 0}
for asc_file in LTSPICE_EXAMPLES.rglob("*.asc"):
rel_path = asc_file.relative_to(LTSPICE_EXAMPLES)
cat = str(rel_path.parent) if rel_path.parent != Path(".") else "misc"
name = asc_file.stem
if category and cat.lower() != category.lower():
continue
if search and search.lower() not in name.lower():
continue
examples.append({"name": name, "category": cat, "path": str(asc_file)})
examples.sort(key=lambda x: x["name"].lower())
total = len(examples)
return {"examples": examples[:limit], "total_count": total, "returned_count": min(limit, total)}
@mcp.tool()
def get_symbol_info(symbol_path: str) -> dict:
"""Get detailed information about a component symbol.
Reads the .asy file to extract pin names, attributes, and description.
Args:
symbol_path: Path to .asy symbol file
"""
path = Path(symbol_path)
if not path.exists():
return {"error": f"Symbol not found: {symbol_path}"}
content = path.read_text(errors="replace")
lines = content.split("\n")
info = {
"name": path.stem,
"pins": [],
"attributes": {},
"description": "",
"prefix": "",
"spice_prefix": "",
}
for line in lines:
line = line.strip()
if line.startswith("PIN"):
parts = line.split()
if len(parts) >= 5:
info["pins"].append(
{
"x": int(parts[1]),
"y": int(parts[2]),
"justification": parts[3],
"rotation": parts[4] if len(parts) > 4 else "0",
}
)
elif line.startswith("PINATTR PinName"):
pin_name = line.split(None, 2)[2] if len(line.split()) > 2 else ""
if info["pins"]:
info["pins"][-1]["name"] = pin_name
elif line.startswith("SYMATTR"):
parts = line.split(None, 2)
if len(parts) >= 3:
attr_name = parts[1]
attr_value = parts[2]
info["attributes"][attr_name] = attr_value
if attr_name == "Description":
info["description"] = attr_value
elif attr_name == "Prefix":
info["prefix"] = attr_value
elif attr_name == "SpiceModel":
info["spice_prefix"] = attr_value
return info
@mcp.tool()
def search_spice_models(
search: str | None = None,
model_type: str | None = None,
limit: int = 50,
) -> dict:
"""Search for SPICE .model definitions in the library.
Finds transistors, diodes, and other discrete devices.
Args:
search: Search term for model name (case-insensitive)
model_type: Filter by type: NPN, PNP, NMOS, PMOS, D, NJF, PJF
limit: Maximum results
"""
models = _search_models(search=search, model_type=model_type, limit=limit)
return {
"models": [
{
"name": m.name,
"type": m.type,
"source_file": m.source_file,
"parameters": m.parameters,
}
for m in models
],
"total_count": len(models),
}
@mcp.tool()
def search_spice_subcircuits(
search: str | None = None,
limit: int = 50,
) -> dict:
"""Search for SPICE .subckt definitions (op-amps, ICs, etc.).
Args:
search: Search term for subcircuit name
limit: Maximum results
"""
subs = _search_subcircuits(search=search, limit=limit)
return {
"subcircuits": [
{
"name": s.name,
"pins": s.pins,
"pin_names": s.pin_names,
"description": s.description,
"source_file": s.source_file,
"n_components": s.n_components,
}
for s in subs
],
"total_count": len(subs),
}
@mcp.tool()
def check_installation() -> dict:
"""Verify LTspice and Wine are properly installed."""
ok, msg = validate_installation()
from .config import LTSPICE_DIR, LTSPICE_EXE, WINE_PREFIX
return {
"valid": ok,
"message": msg,
"paths": {
"ltspice_dir": str(LTSPICE_DIR),
"ltspice_exe": str(LTSPICE_EXE),
"wine_prefix": str(WINE_PREFIX),
"lib_dir": str(LTSPICE_LIB),
"examples_dir": str(LTSPICE_EXAMPLES),
},
"lib_exists": LTSPICE_LIB.exists(),
"examples_exist": LTSPICE_EXAMPLES.exists(),
}
# ============================================================================
# RESOURCES
# ============================================================================
@mcp.resource("ltspice://symbols")
def resource_symbols() -> str:
"""All available LTspice symbols organized by category."""
result = list_symbols(limit=10000)
return json.dumps(result, indent=2)
@mcp.resource("ltspice://examples")
def resource_examples() -> str:
"""All LTspice example circuits."""
result = list_examples(limit=10000)
return json.dumps(result, indent=2)
@mcp.resource("ltspice://status")
def resource_status() -> str:
"""Current LTspice installation status."""
return json.dumps(check_installation(), indent=2)
# ============================================================================
# PROMPTS
# ============================================================================
@mcp.prompt()
def design_filter(
filter_type: str = "lowpass",
topology: str = "rc",
cutoff_freq: str = "1kHz",
) -> str:
"""Guide through designing and simulating a filter circuit.
Args:
filter_type: lowpass, highpass, bandpass, or notch
topology: rc (1st order), rlc (2nd order), or sallen-key (active)
cutoff_freq: Target cutoff frequency with units
"""
return f"""Design a {filter_type} filter with these requirements:
- Topology: {topology}
- Cutoff frequency: {cutoff_freq}
Workflow:
1. Use create_netlist to build the circuit
2. Add .ac analysis directive for frequency sweep
3. Add .meas directive for -3dB bandwidth
4. Simulate with simulate_netlist
5. Use measure_bandwidth to verify cutoff frequency
6. Use get_waveform to inspect the frequency response
7. Adjust component values with create_netlist if needed
Tips:
- For RC lowpass: f_c = 1/(2*pi*R*C)
- For 2nd order: Q controls peaking, Butterworth Q=0.707
- Use search_spice_models to find op-amp models for active filters
"""
@mcp.prompt()
def analyze_power_supply(schematic_path: str = "") -> str:
"""Guide through analyzing a power supply circuit.
Args:
schematic_path: Path to the power supply schematic
"""
path_instruction = (
f"The schematic is at: {schematic_path}"
if schematic_path
else "First, identify or create the power supply schematic."
)
return f"""Analyze a power supply circuit for key performance metrics.
{path_instruction}
Workflow:
1. Use read_schematic to understand the circuit topology
2. Use run_drc to check for design issues
3. Simulate with .tran analysis (include load step if applicable)
4. Use analyze_waveform with these analyses:
- "peak_to_peak" on output for ripple measurement
- "settling_time" for transient response
- "fft" on output to identify noise frequencies
5. If AC analysis available, use measure_bandwidth for loop gain
Key metrics to extract:
- Output voltage regulation (DC accuracy)
- Ripple voltage (peak-to-peak on output)
- Load transient response (settling time after step)
- Efficiency (input power vs output power)
"""
@mcp.prompt()
def debug_circuit(schematic_path: str = "") -> str:
"""Guide through debugging a circuit that isn't working.
Args:
schematic_path: Path to the problematic schematic
"""
path_instruction = (
f"The schematic is at: {schematic_path}"
if schematic_path
else "First, identify the schematic file."
)
return f"""Systematic approach to debugging a circuit.
{path_instruction}
Step 1 - Validate the schematic:
- Use run_drc to catch obvious issues (missing ground, floating nodes)
- Use read_schematic to review component values and connections
Step 2 - Check simulation setup:
- Verify simulation directives are correct
- Check that models/subcircuits are available (search_spice_models)
Step 3 - Run and analyze:
- Simulate the circuit
- Use get_waveform to inspect key node voltages
- Compare expected vs actual values at each stage
Step 4 - Isolate the problem:
- Use edit_component to simplify (replace active devices with ideal)
- Use diff_schematics to track what changes fixed the issue
- Re-simulate after each change
Common issues:
- Wrong node connections (check wire endpoints)
- Missing bias voltages or ground
- Component values off by orders of magnitude
- Wrong model (check with search_spice_models)
"""
@mcp.prompt()
def optimize_design(
circuit_type: str = "filter",
target_spec: str = "1kHz bandwidth",
) -> str:
"""Guide through optimizing a circuit to meet target specifications.
Args:
circuit_type: Type of circuit (filter, amplifier, regulator, oscillator)
target_spec: Target specification to achieve
"""
return f"""Optimize a {circuit_type} circuit to achieve: {target_spec}
Workflow:
1. Start with a template: use list_templates to see available circuits
2. Create the initial circuit with create_from_template
3. Simulate and measure the current performance
4. Use optimize_circuit to automatically tune component values:
- Define target metrics (bandwidth, gain, settling time, etc.)
- Specify component ranges with preferred E-series values
- Let the optimizer iterate (typically 10-20 simulations)
5. Verify the optimized design with a full simulation
6. Run Monte Carlo (monte_carlo tool) to check yield with tolerances
Tips:
- Start with reasonable initial values from the template
- Use E24 or E96 series for resistors/capacitors
- For filters: target bandwidth_hz metric
- For amplifiers: target gain_db and phase_margin_deg
- For regulators: target settling_time and peak_to_peak (ripple)
"""
@mcp.prompt()
def monte_carlo_analysis(
circuit_description: str = "RC filter",
n_runs: str = "100",
) -> str:
"""Guide through Monte Carlo tolerance analysis.
Args:
circuit_description: What circuit to analyze
n_runs: Number of Monte Carlo iterations
"""
return f"""Run Monte Carlo tolerance analysis on: {circuit_description}
Number of runs: {n_runs}
Workflow:
1. Create or identify the netlist for your circuit
2. Use monte_carlo tool with component tolerances:
- Resistors: typically 1% (0.01) or 5% (0.05)
- Capacitors: typically 10% (0.1) or 20% (0.2)
- Inductors: typically 10% (0.1)
3. For each completed run, extract key metrics:
- Use get_waveform on each raw file
- Use analyze_waveform for RMS, peak-to-peak, etc.
- Use measure_bandwidth for filter circuits
4. Compute statistics across all runs:
- Mean and standard deviation of each metric
- Min/max (worst case)
- Yield: what percentage meet spec?
Tips:
- Use list_simulation_runs to understand stepped data
- For stepped simulations, use get_waveform with run parameter
- Start with fewer runs (10-20) to verify setup, then scale up
- Set seed for reproducible results during development
- Typical component tolerances:
- Metal film resistors: 1%
- Ceramic capacitors: 10-20%
- Electrolytic capacitors: 20%
- Inductors: 10-20%
"""
@mcp.prompt()
def circuit_from_scratch(
description: str = "audio amplifier",
) -> str:
"""Guide through creating a complete circuit from scratch.
Args:
description: What circuit to build
"""
return f"""Build a complete circuit from scratch: {description}
Approach 1 - Use a template (recommended for common circuits):
1. Use list_templates to see available circuit templates
2. Use create_from_template with custom parameters
3. Simulate with simulate_netlist
4. Analyze results with get_waveform and analyze_waveform
Approach 2 - Build from components:
1. Use create_netlist to define components and connections
2. Use search_spice_models to find transistor/diode models
3. Use search_spice_subcircuits to find op-amp/IC models
4. Add simulation directives (.tran, .ac, .dc, .op, .tf)
5. Simulate and analyze
Approach 3 - Graphical schematic:
1. Use generate_schematic for supported topologies (rc_lowpass,
voltage_divider, inverting_amp)
2. The .asc file can be opened in LTspice GUI for editing
3. Simulate with the simulate tool
Verification workflow:
1. Run run_drc to check for design issues before simulating
2. Start with .op analysis to verify DC bias point
3. Run .tf analysis for gain and impedance
4. Run .ac analysis for frequency response
5. Run .tran analysis for time-domain behavior
6. Use diff_schematics to compare design iterations
"""
# ============================================================================
# ENTRY POINT
# ============================================================================
def main():
"""Run the MCP server."""
print(f"\U0001f50c mcp-ltspice v{__version__}")
print(" LTspice circuit simulation automation")
ok, msg = validate_installation()
if ok:
print(f" \u2713 {msg}")
else:
print(f" \u26a0 {msg}")
mcp.run()
if __name__ == "__main__":
main()