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.
This commit is contained in:
parent
866aa46134
commit
3a7e6887aa
51
src/mcltspice/_app.py
Normal file
51
src/mcltspice/_app.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""The shared FastMCP application instance.
|
||||||
|
|
||||||
|
Tools, resources, and prompts are defined across several modules (tools_*.py,
|
||||||
|
resources.py, prompts.py); each does `from ._app import mcp` and decorates with
|
||||||
|
this single instance. server.py imports those modules so their decorators run
|
||||||
|
and register everything onto this `mcp` before main() serves it.
|
||||||
|
|
||||||
|
This module imports nothing from the tool modules, which keeps the registration
|
||||||
|
graph acyclic.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
|
mcp = FastMCP(
|
||||||
|
name="mcltspice",
|
||||||
|
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
|
||||||
|
- Analyze noise: spectral density, spot noise, RMS, noise figure, 1/f corner
|
||||||
|
- 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
|
||||||
|
- Publish circuits as interactive notebooks on SpiceBook
|
||||||
|
|
||||||
|
LTspice runs via Wine on Linux. Simulations execute in batch mode
|
||||||
|
and results are parsed from binary .raw files.
|
||||||
|
|
||||||
|
SpiceBook integration lets you share simulation results as runnable
|
||||||
|
notebooks at spicebook.warehack.ing. Use spicebook_publish to convert
|
||||||
|
an LTspice netlist to ngspice format and create a shareable notebook.
|
||||||
|
""",
|
||||||
|
)
|
||||||
@ -16,11 +16,13 @@ import math
|
|||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from fastmcp import FastMCP
|
|
||||||
from fastmcp.prompts import Message
|
from fastmcp.prompts import Message
|
||||||
|
|
||||||
|
# Domain modules register their tools/resources/prompts on the shared mcp
|
||||||
|
# instance at import time. Imported for that side effect only.
|
||||||
|
from . import spicebook_tools # noqa: F401,E402
|
||||||
|
from ._app import mcp
|
||||||
from .batch import (
|
from .batch import (
|
||||||
run_monte_carlo,
|
run_monte_carlo,
|
||||||
run_parameter_sweep,
|
run_parameter_sweep,
|
||||||
@ -29,7 +31,6 @@ from .batch import (
|
|||||||
from .config import (
|
from .config import (
|
||||||
LTSPICE_EXAMPLES,
|
LTSPICE_EXAMPLES,
|
||||||
LTSPICE_LIB,
|
LTSPICE_LIB,
|
||||||
SPICEBOOK_URL,
|
|
||||||
validate_installation,
|
validate_installation,
|
||||||
)
|
)
|
||||||
from .diff import diff_schematics as _diff_schematics
|
from .diff import diff_schematics as _diff_schematics
|
||||||
@ -55,11 +56,6 @@ from .power_analysis import compute_efficiency, compute_power_metrics
|
|||||||
from .raw_parser import parse_raw_file
|
from .raw_parser import parse_raw_file
|
||||||
from .runner import run_netlist, run_simulation
|
from .runner import run_netlist, run_simulation
|
||||||
from .schematic import modify_component_value, parse_schematic
|
from .schematic import modify_component_value, parse_schematic
|
||||||
from .spicebook import (
|
|
||||||
SpiceBookClient,
|
|
||||||
build_notebook_cells,
|
|
||||||
ltspice_to_ngspice,
|
|
||||||
)
|
|
||||||
from .stability import compute_stability_metrics
|
from .stability import compute_stability_metrics
|
||||||
from .templates import (
|
from .templates import (
|
||||||
ASC_TEMPLATES,
|
ASC_TEMPLATES,
|
||||||
@ -81,46 +77,6 @@ from .waveform_math import (
|
|||||||
)
|
)
|
||||||
from .waveform_query import analyze_signal, extract_waveform
|
from .waveform_query import analyze_signal, extract_waveform
|
||||||
|
|
||||||
mcp = FastMCP(
|
|
||||||
name="mcltspice",
|
|
||||||
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
|
|
||||||
- Analyze noise: spectral density, spot noise, RMS, noise figure, 1/f corner
|
|
||||||
- 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
|
|
||||||
- Publish circuits as interactive notebooks on SpiceBook
|
|
||||||
|
|
||||||
LTspice runs via Wine on Linux. Simulations execute in batch mode
|
|
||||||
and results are parsed from binary .raw files.
|
|
||||||
|
|
||||||
SpiceBook integration lets you share simulation results as runnable
|
|
||||||
notebooks at spicebook.warehack.ing. Use spicebook_publish to convert
|
|
||||||
an LTspice netlist to ngspice format and create a shareable notebook.
|
|
||||||
""",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# SIMULATION TOOLS
|
# SIMULATION TOOLS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@ -2198,266 +2154,6 @@ Common failure modes:
|
|||||||
""")]
|
""")]
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# SPICEBOOK INTEGRATION TOOLS
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
_NETLIST_EXTENSIONS = {".cir", ".net", ".sp", ".spice", ".asc"}
|
|
||||||
_MAX_NETLIST_SIZE = 10 * 1024 * 1024 # 10 MB
|
|
||||||
|
|
||||||
|
|
||||||
def _spicebook_error(e: Exception) -> dict:
|
|
||||||
"""Format a SpiceBook error into a consistent response dict."""
|
|
||||||
if isinstance(e, httpx.ConnectError):
|
|
||||||
return {
|
|
||||||
"error": f"SpiceBook not reachable at {SPICEBOOK_URL}",
|
|
||||||
"hint": "Check connectivity or set SPICEBOOK_URL env var",
|
|
||||||
}
|
|
||||||
if isinstance(e, httpx.TimeoutException):
|
|
||||||
return {"error": "SpiceBook request timed out"}
|
|
||||||
if isinstance(e, httpx.HTTPStatusError):
|
|
||||||
body = e.response.text[:500] if e.response else ""
|
|
||||||
return {"error": f"HTTP {e.response.status_code}", "detail": body}
|
|
||||||
return {"error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
def _read_netlist(
|
|
||||||
netlist_text: str | None, netlist_path: str | None
|
|
||||||
) -> tuple[str | None, dict | None]:
|
|
||||||
"""Resolve netlist from text or file path. Returns (text, error_dict)."""
|
|
||||||
if netlist_text and netlist_path:
|
|
||||||
return None, {"error": "Provide netlist_text or netlist_path, not both"}
|
|
||||||
if netlist_text:
|
|
||||||
if len(netlist_text) > _MAX_NETLIST_SIZE:
|
|
||||||
return None, {"error": f"Netlist text too large (max {_MAX_NETLIST_SIZE} bytes)"}
|
|
||||||
return netlist_text, None
|
|
||||||
if netlist_path:
|
|
||||||
p = Path(netlist_path).resolve()
|
|
||||||
if p.suffix.lower() not in _NETLIST_EXTENSIONS:
|
|
||||||
return None, {
|
|
||||||
"error": f"Unsupported file type '{p.suffix}'. "
|
|
||||||
f"Expected: {', '.join(sorted(_NETLIST_EXTENSIONS))}"
|
|
||||||
}
|
|
||||||
if not p.exists():
|
|
||||||
return None, {"error": f"File not found: {p}"}
|
|
||||||
try:
|
|
||||||
size = p.stat().st_size
|
|
||||||
if size > _MAX_NETLIST_SIZE:
|
|
||||||
return None, {"error": f"File too large ({size} bytes, max {_MAX_NETLIST_SIZE})"}
|
|
||||||
return p.read_text(encoding="utf-8"), None
|
|
||||||
except (OSError, UnicodeDecodeError) as e:
|
|
||||||
return None, {"error": f"Cannot read {p}: {e}"}
|
|
||||||
return None, {"error": "Provide netlist_text or netlist_path"}
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def spicebook_publish(
|
|
||||||
netlist_text: str | None = None,
|
|
||||||
netlist_path: str | None = None,
|
|
||||||
title: str = "Untitled Circuit",
|
|
||||||
description: str | None = None,
|
|
||||||
tags: list[str] | None = None,
|
|
||||||
run: bool = False,
|
|
||||||
) -> dict:
|
|
||||||
"""Publish an LTspice netlist as an interactive SpiceBook notebook.
|
|
||||||
|
|
||||||
Converts the netlist from LTspice dialect to ngspice, builds
|
|
||||||
notebook cells, and creates the notebook on SpiceBook. Returns
|
|
||||||
the notebook ID and URL.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
netlist_text: Netlist as a string (mutually exclusive with netlist_path)
|
|
||||||
netlist_path: Path to a .cir/.net file on disk
|
|
||||||
title: Notebook title
|
|
||||||
description: Optional description shown in the intro cell
|
|
||||||
tags: Optional list of tags for categorization
|
|
||||||
run: If True, SpiceBook runs the simulation on publish
|
|
||||||
"""
|
|
||||||
text, err = _read_netlist(netlist_text, netlist_path)
|
|
||||||
if err:
|
|
||||||
return err
|
|
||||||
assert text is not None # _read_netlist guarantees text when err is None
|
|
||||||
|
|
||||||
converted, warnings = ltspice_to_ngspice(text)
|
|
||||||
cells = build_notebook_cells(
|
|
||||||
title=title,
|
|
||||||
description=description,
|
|
||||||
netlist=converted,
|
|
||||||
warnings=warnings if warnings else None,
|
|
||||||
)
|
|
||||||
|
|
||||||
client = SpiceBookClient()
|
|
||||||
try:
|
|
||||||
result = await client.compose(
|
|
||||||
title=title,
|
|
||||||
cells=cells,
|
|
||||||
tags=tags,
|
|
||||||
run=run,
|
|
||||||
)
|
|
||||||
except (httpx.HTTPError, OSError, ValueError) as e:
|
|
||||||
return _spicebook_error(e)
|
|
||||||
|
|
||||||
notebook_id = result.get("id", "")
|
|
||||||
return {
|
|
||||||
"notebook_id": notebook_id,
|
|
||||||
"url": f"{SPICEBOOK_URL}/notebooks/{notebook_id}",
|
|
||||||
"cell_count": len(cells),
|
|
||||||
"conversion_warnings": warnings,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def spicebook_list() -> dict:
|
|
||||||
"""List all notebooks on SpiceBook.
|
|
||||||
|
|
||||||
Returns notebook summaries including id, title, tags, and modified date.
|
|
||||||
"""
|
|
||||||
client = SpiceBookClient()
|
|
||||||
try:
|
|
||||||
notebooks = await client.list_notebooks()
|
|
||||||
except (httpx.HTTPError, OSError) as e:
|
|
||||||
return _spicebook_error(e)
|
|
||||||
return {"notebooks": notebooks, "count": len(notebooks)}
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def spicebook_get(
|
|
||||||
notebook_id: str,
|
|
||||||
) -> dict:
|
|
||||||
"""Get a full SpiceBook notebook with all cells and outputs.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
notebook_id: The notebook ID to retrieve
|
|
||||||
"""
|
|
||||||
client = SpiceBookClient()
|
|
||||||
try:
|
|
||||||
return await client.get_notebook(notebook_id)
|
|
||||||
except (httpx.HTTPError, OSError, ValueError) as e:
|
|
||||||
return _spicebook_error(e)
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def spicebook_delete(
|
|
||||||
notebook_id: str,
|
|
||||||
) -> dict:
|
|
||||||
"""Delete a SpiceBook notebook.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
notebook_id: The notebook ID to delete
|
|
||||||
"""
|
|
||||||
client = SpiceBookClient()
|
|
||||||
try:
|
|
||||||
await client.delete_notebook(notebook_id)
|
|
||||||
except (httpx.HTTPError, OSError, ValueError) as e:
|
|
||||||
return _spicebook_error(e)
|
|
||||||
return {"deleted": True, "notebook_id": notebook_id}
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.tool()
|
|
||||||
async def spicebook_simulate(
|
|
||||||
netlist_text: str | None = None,
|
|
||||||
netlist_path: str | None = None,
|
|
||||||
) -> dict:
|
|
||||||
"""Run a netlist via SpiceBook's ngspice engine without creating a notebook.
|
|
||||||
|
|
||||||
Useful for testing ngspice compatibility before publishing.
|
|
||||||
Converts the netlist from LTspice dialect to ngspice first.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
netlist_text: Netlist as a string (mutually exclusive with netlist_path)
|
|
||||||
netlist_path: Path to a .cir/.net file on disk
|
|
||||||
"""
|
|
||||||
text, err = _read_netlist(netlist_text, netlist_path)
|
|
||||||
if err:
|
|
||||||
return err
|
|
||||||
assert text is not None
|
|
||||||
|
|
||||||
converted, warnings = ltspice_to_ngspice(text)
|
|
||||||
|
|
||||||
client = SpiceBookClient()
|
|
||||||
try:
|
|
||||||
result = await client.simulate(converted)
|
|
||||||
except (httpx.HTTPError, OSError) as e:
|
|
||||||
return _spicebook_error(e)
|
|
||||||
|
|
||||||
if warnings:
|
|
||||||
result["conversion_warnings"] = warnings
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# SPICEBOOK RESOURCE & PROMPT
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.resource("spicebook://status")
|
|
||||||
async def spicebook_status() -> str:
|
|
||||||
"""SpiceBook service status -- URL and reachability."""
|
|
||||||
client = SpiceBookClient()
|
|
||||||
ok = await client.health()
|
|
||||||
status = "reachable" if ok else "unreachable"
|
|
||||||
return f"SpiceBook URL: {SPICEBOOK_URL}\nStatus: {status}"
|
|
||||||
|
|
||||||
|
|
||||||
@mcp.prompt()
|
|
||||||
def publish_to_spicebook(
|
|
||||||
circuit_description: str = "",
|
|
||||||
netlist_path: str = "",
|
|
||||||
) -> list:
|
|
||||||
"""Step-by-step guide for publishing a circuit to SpiceBook.
|
|
||||||
|
|
||||||
Walks through: verify locally in LTspice, test ngspice compatibility
|
|
||||||
via SpiceBook, then publish as an interactive notebook.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
circuit_description: What circuit you want to publish
|
|
||||||
netlist_path: Path to the netlist or schematic
|
|
||||||
"""
|
|
||||||
path_note = (
|
|
||||||
f"Netlist/schematic: {netlist_path}"
|
|
||||||
if netlist_path
|
|
||||||
else "First, identify or create the netlist."
|
|
||||||
)
|
|
||||||
desc_note = (
|
|
||||||
f"Circuit: {circuit_description}"
|
|
||||||
if circuit_description
|
|
||||||
else "No description given -- describe the circuit for the notebook."
|
|
||||||
)
|
|
||||||
|
|
||||||
return [Message(role="user", content=f"""Publish a circuit to SpiceBook as an interactive notebook.
|
|
||||||
|
|
||||||
{path_note}
|
|
||||||
{desc_note}
|
|
||||||
|
|
||||||
Workflow:
|
|
||||||
1. **Verify locally** -- simulate the circuit with LTspice:
|
|
||||||
- Use simulate or simulate_netlist to confirm it works
|
|
||||||
- Use get_waveform / analyze_waveform to check results
|
|
||||||
- Fix any issues before publishing
|
|
||||||
|
|
||||||
2. **Test ngspice compatibility** -- dry run on SpiceBook:
|
|
||||||
- Use spicebook_simulate with the netlist
|
|
||||||
- Check for conversion warnings (LTspice -> ngspice differences)
|
|
||||||
- If errors occur, adjust the netlist (remove LTspice-specific constructs)
|
|
||||||
|
|
||||||
3. **Publish** -- create the notebook:
|
|
||||||
- Use spicebook_publish with a descriptive title and tags
|
|
||||||
- Include a description explaining the circuit purpose and key specs
|
|
||||||
- Set run=True if you want SpiceBook to execute the simulation
|
|
||||||
|
|
||||||
4. **Verify** -- check the published notebook:
|
|
||||||
- Use spicebook_get to confirm the notebook was created
|
|
||||||
- Visit the URL to see the interactive version
|
|
||||||
|
|
||||||
Tips:
|
|
||||||
- Add tags like "filter", "amplifier", "power" for discoverability
|
|
||||||
- The converter handles .backanno, Rser=, Windows paths automatically
|
|
||||||
- Use spicebook_list to see all published notebooks
|
|
||||||
- Use spicebook_delete to remove test notebooks
|
|
||||||
""")]
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ENTRY POINT
|
# ENTRY POINT
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
265
src/mcltspice/spicebook_tools.py
Normal file
265
src/mcltspice/spicebook_tools.py
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
"""SpiceBook integration: publish/list/get/delete/simulate tools, plus a
|
||||||
|
status resource and a publishing prompt. Registers onto the shared mcp instance.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastmcp.prompts import Message
|
||||||
|
|
||||||
|
from ._app import mcp
|
||||||
|
from .config import SPICEBOOK_URL
|
||||||
|
from .spicebook import (
|
||||||
|
SpiceBookClient,
|
||||||
|
build_notebook_cells,
|
||||||
|
ltspice_to_ngspice,
|
||||||
|
)
|
||||||
|
|
||||||
|
_NETLIST_EXTENSIONS = {".cir", ".net", ".sp", ".spice", ".asc"}
|
||||||
|
_MAX_NETLIST_SIZE = 10 * 1024 * 1024 # 10 MB
|
||||||
|
|
||||||
|
|
||||||
|
def _spicebook_error(e: Exception) -> dict:
|
||||||
|
"""Format a SpiceBook error into a consistent response dict."""
|
||||||
|
if isinstance(e, httpx.ConnectError):
|
||||||
|
return {
|
||||||
|
"error": f"SpiceBook not reachable at {SPICEBOOK_URL}",
|
||||||
|
"hint": "Check connectivity or set SPICEBOOK_URL env var",
|
||||||
|
}
|
||||||
|
if isinstance(e, httpx.TimeoutException):
|
||||||
|
return {"error": "SpiceBook request timed out"}
|
||||||
|
if isinstance(e, httpx.HTTPStatusError):
|
||||||
|
body = e.response.text[:500] if e.response else ""
|
||||||
|
return {"error": f"HTTP {e.response.status_code}", "detail": body}
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _read_netlist(
|
||||||
|
netlist_text: str | None, netlist_path: str | None
|
||||||
|
) -> tuple[str | None, dict | None]:
|
||||||
|
"""Resolve netlist from text or file path. Returns (text, error_dict)."""
|
||||||
|
if netlist_text and netlist_path:
|
||||||
|
return None, {"error": "Provide netlist_text or netlist_path, not both"}
|
||||||
|
if netlist_text:
|
||||||
|
if len(netlist_text) > _MAX_NETLIST_SIZE:
|
||||||
|
return None, {"error": f"Netlist text too large (max {_MAX_NETLIST_SIZE} bytes)"}
|
||||||
|
return netlist_text, None
|
||||||
|
if netlist_path:
|
||||||
|
p = Path(netlist_path).resolve()
|
||||||
|
if p.suffix.lower() not in _NETLIST_EXTENSIONS:
|
||||||
|
return None, {
|
||||||
|
"error": f"Unsupported file type '{p.suffix}'. "
|
||||||
|
f"Expected: {', '.join(sorted(_NETLIST_EXTENSIONS))}"
|
||||||
|
}
|
||||||
|
if not p.exists():
|
||||||
|
return None, {"error": f"File not found: {p}"}
|
||||||
|
try:
|
||||||
|
size = p.stat().st_size
|
||||||
|
if size > _MAX_NETLIST_SIZE:
|
||||||
|
return None, {"error": f"File too large ({size} bytes, max {_MAX_NETLIST_SIZE})"}
|
||||||
|
return p.read_text(encoding="utf-8"), None
|
||||||
|
except (OSError, UnicodeDecodeError) as e:
|
||||||
|
return None, {"error": f"Cannot read {p}: {e}"}
|
||||||
|
return None, {"error": "Provide netlist_text or netlist_path"}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def spicebook_publish(
|
||||||
|
netlist_text: str | None = None,
|
||||||
|
netlist_path: str | None = None,
|
||||||
|
title: str = "Untitled Circuit",
|
||||||
|
description: str | None = None,
|
||||||
|
tags: list[str] | None = None,
|
||||||
|
run: bool = False,
|
||||||
|
) -> dict:
|
||||||
|
"""Publish an LTspice netlist as an interactive SpiceBook notebook.
|
||||||
|
|
||||||
|
Converts the netlist from LTspice dialect to ngspice, builds
|
||||||
|
notebook cells, and creates the notebook on SpiceBook. Returns
|
||||||
|
the notebook ID and URL.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
netlist_text: Netlist as a string (mutually exclusive with netlist_path)
|
||||||
|
netlist_path: Path to a .cir/.net file on disk
|
||||||
|
title: Notebook title
|
||||||
|
description: Optional description shown in the intro cell
|
||||||
|
tags: Optional list of tags for categorization
|
||||||
|
run: If True, SpiceBook runs the simulation on publish
|
||||||
|
"""
|
||||||
|
text, err = _read_netlist(netlist_text, netlist_path)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
assert text is not None # _read_netlist guarantees text when err is None
|
||||||
|
|
||||||
|
converted, warnings = ltspice_to_ngspice(text)
|
||||||
|
cells = build_notebook_cells(
|
||||||
|
title=title,
|
||||||
|
description=description,
|
||||||
|
netlist=converted,
|
||||||
|
warnings=warnings if warnings else None,
|
||||||
|
)
|
||||||
|
|
||||||
|
client = SpiceBookClient()
|
||||||
|
try:
|
||||||
|
result = await client.compose(
|
||||||
|
title=title,
|
||||||
|
cells=cells,
|
||||||
|
tags=tags,
|
||||||
|
run=run,
|
||||||
|
)
|
||||||
|
except (httpx.HTTPError, OSError, ValueError) as e:
|
||||||
|
return _spicebook_error(e)
|
||||||
|
|
||||||
|
notebook_id = result.get("id", "")
|
||||||
|
return {
|
||||||
|
"notebook_id": notebook_id,
|
||||||
|
"url": f"{SPICEBOOK_URL}/notebooks/{notebook_id}",
|
||||||
|
"cell_count": len(cells),
|
||||||
|
"conversion_warnings": warnings,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def spicebook_list() -> dict:
|
||||||
|
"""List all notebooks on SpiceBook.
|
||||||
|
|
||||||
|
Returns notebook summaries including id, title, tags, and modified date.
|
||||||
|
"""
|
||||||
|
client = SpiceBookClient()
|
||||||
|
try:
|
||||||
|
notebooks = await client.list_notebooks()
|
||||||
|
except (httpx.HTTPError, OSError) as e:
|
||||||
|
return _spicebook_error(e)
|
||||||
|
return {"notebooks": notebooks, "count": len(notebooks)}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def spicebook_get(
|
||||||
|
notebook_id: str,
|
||||||
|
) -> dict:
|
||||||
|
"""Get a full SpiceBook notebook with all cells and outputs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
notebook_id: The notebook ID to retrieve
|
||||||
|
"""
|
||||||
|
client = SpiceBookClient()
|
||||||
|
try:
|
||||||
|
return await client.get_notebook(notebook_id)
|
||||||
|
except (httpx.HTTPError, OSError, ValueError) as e:
|
||||||
|
return _spicebook_error(e)
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def spicebook_delete(
|
||||||
|
notebook_id: str,
|
||||||
|
) -> dict:
|
||||||
|
"""Delete a SpiceBook notebook.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
notebook_id: The notebook ID to delete
|
||||||
|
"""
|
||||||
|
client = SpiceBookClient()
|
||||||
|
try:
|
||||||
|
await client.delete_notebook(notebook_id)
|
||||||
|
except (httpx.HTTPError, OSError, ValueError) as e:
|
||||||
|
return _spicebook_error(e)
|
||||||
|
return {"deleted": True, "notebook_id": notebook_id}
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.tool()
|
||||||
|
async def spicebook_simulate(
|
||||||
|
netlist_text: str | None = None,
|
||||||
|
netlist_path: str | None = None,
|
||||||
|
) -> dict:
|
||||||
|
"""Run a netlist via SpiceBook's ngspice engine without creating a notebook.
|
||||||
|
|
||||||
|
Useful for testing ngspice compatibility before publishing.
|
||||||
|
Converts the netlist from LTspice dialect to ngspice first.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
netlist_text: Netlist as a string (mutually exclusive with netlist_path)
|
||||||
|
netlist_path: Path to a .cir/.net file on disk
|
||||||
|
"""
|
||||||
|
text, err = _read_netlist(netlist_text, netlist_path)
|
||||||
|
if err:
|
||||||
|
return err
|
||||||
|
assert text is not None
|
||||||
|
|
||||||
|
converted, warnings = ltspice_to_ngspice(text)
|
||||||
|
|
||||||
|
client = SpiceBookClient()
|
||||||
|
try:
|
||||||
|
result = await client.simulate(converted)
|
||||||
|
except (httpx.HTTPError, OSError) as e:
|
||||||
|
return _spicebook_error(e)
|
||||||
|
|
||||||
|
if warnings:
|
||||||
|
result["conversion_warnings"] = warnings
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.resource("spicebook://status")
|
||||||
|
async def spicebook_status() -> str:
|
||||||
|
"""SpiceBook service status -- URL and reachability."""
|
||||||
|
client = SpiceBookClient()
|
||||||
|
ok = await client.health()
|
||||||
|
status = "reachable" if ok else "unreachable"
|
||||||
|
return f"SpiceBook URL: {SPICEBOOK_URL}\nStatus: {status}"
|
||||||
|
|
||||||
|
|
||||||
|
@mcp.prompt()
|
||||||
|
def publish_to_spicebook(
|
||||||
|
circuit_description: str = "",
|
||||||
|
netlist_path: str = "",
|
||||||
|
) -> list:
|
||||||
|
"""Step-by-step guide for publishing a circuit to SpiceBook.
|
||||||
|
|
||||||
|
Walks through: verify locally in LTspice, test ngspice compatibility
|
||||||
|
via SpiceBook, then publish as an interactive notebook.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
circuit_description: What circuit you want to publish
|
||||||
|
netlist_path: Path to the netlist or schematic
|
||||||
|
"""
|
||||||
|
path_note = (
|
||||||
|
f"Netlist/schematic: {netlist_path}"
|
||||||
|
if netlist_path
|
||||||
|
else "First, identify or create the netlist."
|
||||||
|
)
|
||||||
|
desc_note = (
|
||||||
|
f"Circuit: {circuit_description}"
|
||||||
|
if circuit_description
|
||||||
|
else "No description given -- describe the circuit for the notebook."
|
||||||
|
)
|
||||||
|
|
||||||
|
return [Message(role="user", content=f"""Publish a circuit to SpiceBook as an interactive notebook.
|
||||||
|
|
||||||
|
{path_note}
|
||||||
|
{desc_note}
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
1. **Verify locally** -- simulate the circuit with LTspice:
|
||||||
|
- Use simulate or simulate_netlist to confirm it works
|
||||||
|
- Use get_waveform / analyze_waveform to check results
|
||||||
|
- Fix any issues before publishing
|
||||||
|
|
||||||
|
2. **Test ngspice compatibility** -- dry run on SpiceBook:
|
||||||
|
- Use spicebook_simulate with the netlist
|
||||||
|
- Check for conversion warnings (LTspice -> ngspice differences)
|
||||||
|
- If errors occur, adjust the netlist (remove LTspice-specific constructs)
|
||||||
|
|
||||||
|
3. **Publish** -- create the notebook:
|
||||||
|
- Use spicebook_publish with a descriptive title and tags
|
||||||
|
- Include a description explaining the circuit purpose and key specs
|
||||||
|
- Set run=True if you want SpiceBook to execute the simulation
|
||||||
|
|
||||||
|
4. **Verify** -- check the published notebook:
|
||||||
|
- Use spicebook_get to confirm the notebook was created
|
||||||
|
- Visit the URL to see the interactive version
|
||||||
|
|
||||||
|
Tips:
|
||||||
|
- Add tags like "filter", "amplifier", "power" for discoverability
|
||||||
|
- The converter handles .backanno, Rser=, Windows paths automatically
|
||||||
|
- Use spicebook_list to see all published notebooks
|
||||||
|
- Use spicebook_delete to remove test notebooks
|
||||||
|
""")]
|
||||||
@ -2,12 +2,12 @@
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from mcltspice.server import _read_netlist
|
|
||||||
from mcltspice.spicebook import (
|
from mcltspice.spicebook import (
|
||||||
SpiceBookClient,
|
SpiceBookClient,
|
||||||
build_notebook_cells,
|
build_notebook_cells,
|
||||||
ltspice_to_ngspice,
|
ltspice_to_ngspice,
|
||||||
)
|
)
|
||||||
|
from mcltspice.spicebook_tools import _read_netlist
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# ltspice_to_ngspice converter tests
|
# ltspice_to_ngspice converter tests
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user