From 8fd8c41b64ea94a4658375ac966baad810dad9e2 Mon Sep 17 00:00:00 2001 From: Qian Zhang Date: Sun, 14 Jun 2026 14:36:22 -0400 Subject: [PATCH] feat: promote powerio to a core dependency (closes #30) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit powerio is the cross-server exchange substrate — the pandapower, Egret, PyPSA, and ANDES bridge tools all build on its JSON transport and otherwise degrade to an install hint. Making it core is cheap (abi3 wheels for five platforms, zero required runtime deps, [mcp,matrix] resolving to numpy (core) + scipy (transitive via pandapower)) and removes the "is powerio installed?" branch from docs and agent behavior. - registry.py: add "powerio" to CORE; set extra=None on its Tool entry - pyproject.toml: move powerio[mcp,matrix]>=0.1.1 into core dependencies; drop the powerio extra and its entry in the opensource group - bridge tool docstrings (pandapower/Egret/PyPSA/ANDES): drop "Requires the powerio extra" notes — it is always available now - README: list PowerIO in the base install and the wizard pre-selection; drop it from the opensource extra list - tests: CORE set is {pandapower, pypsa, powerio}; powerio registry extra is None The graceful-degradation paths (_POWERIO_HINT, import guards) stay as insurance for platforms without a published wheel. Co-Authored-By: Claude Opus 4.8 (1M context) --- ANDES/andes_mcp.py | 8 ++++---- Egret/egret_mcp.py | 8 ++++---- PyPSA/pypsa_mcp.py | 8 ++++---- README.md | 8 ++++---- pandapower/panda_mcp.py | 8 ++++---- powermcp/registry.py | 10 +++++++--- pyproject.toml | 10 ++++++++-- tests/test_powerio_server.py | 5 +++-- tests/test_registry.py | 2 +- 9 files changed, 39 insertions(+), 28 deletions(-) diff --git a/ANDES/andes_mcp.py b/ANDES/andes_mcp.py index 4f4f922..5c81321 100644 --- a/ANDES/andes_mcp.py +++ b/ANDES/andes_mcp.py @@ -354,8 +354,8 @@ def load_network_from_json( Accepts the ``json`` string returned by the powerio server's parse_case or case_to_json tools. Converts the network to MATPOWER format, writes it to out_path (use a .m extension), and returns the path along with component - counts. Pass out_path to run_power_flow to run the simulation. Requires the - powerio extra (pip install 'powermcp[powerio]'). + counts. Pass out_path to run_power_flow to run the simulation. powerio is a + core dependency, so this is always available. Args: network_json: The JSON transport string from powerio @@ -399,8 +399,8 @@ def load_network_from_any( Reads MATPOWER .m, PSS/E .raw (v33), PowerWorld .aux, PowerModels JSON, or egret JSON via powerio and writes a MATPOWER file to out_path (use a .m - extension). Pass out_path to run_power_flow to run the simulation. Requires - the powerio extra (pip install 'powermcp[powerio]'). + extension). Pass out_path to run_power_flow to run the simulation. powerio + is a core dependency, so this is always available. Args: file_path: Path to the source case file diff --git a/Egret/egret_mcp.py b/Egret/egret_mcp.py index 8166d05..bfcdf28 100644 --- a/Egret/egret_mcp.py +++ b/Egret/egret_mcp.py @@ -214,8 +214,8 @@ def load_model_from_any(file_path: str, source_format: Optional[str] = None) -> egret JSON via powerio, converts it to egret JSON, validates it as an egret ModelData, and stages it to a temp file. Pass the returned `case_file` path to solve_ac_opf, solve_dc_opf, or - solve_unit_commitment_problem. Requires the powerio extra - (pip install 'powermcp[powerio]'). + solve_unit_commitment_problem. powerio is a core dependency, so this is + always available. Args: file_path: Path to the case file @@ -254,8 +254,8 @@ def load_model_from_json(network_json: str) -> Dict[str, Any]: case_to_json tools, so a case parsed once there feeds egret without re-reading the file. Converts it to egret JSON, validates it as an egret ModelData, and stages it to a temp file. Pass the returned `case_file` - path to the solver tools. Requires the powerio extra - (pip install 'powermcp[powerio]'). + path to the solver tools. powerio is a core dependency, so this is always + available. Args: network_json: The JSON transport string from powerio diff --git a/PyPSA/pypsa_mcp.py b/PyPSA/pypsa_mcp.py index bc8692d..1455034 100644 --- a/PyPSA/pypsa_mcp.py +++ b/PyPSA/pypsa_mcp.py @@ -727,8 +727,8 @@ def import_case_from_any( .nc extension); pass that path as network_name to the other tools. PyPSA's ppc import drops generator cost data; anything else it cannot represent is listed in the returned warnings. Branches with rating 0 are imported with - s_nom 0 unless overwrite_zero_s_nom supplies a value. Requires the powerio - extra (pip install 'powermcp[powerio]'). + s_nom 0 unless overwrite_zero_s_nom supplies a value. powerio is a core + dependency, so this is always available. Args: file_path: Path to the case file @@ -776,8 +776,8 @@ def import_case_from_json( a file around or re-parsing it. Expects source-valued tables (MW, degrees) as parse_case emits them, not the per-unit normalize_case form. Writes the network to output_path (use a .nc extension); pass that path as - network_name to the other tools. Requires the powerio extra - (pip install 'powermcp[powerio]'). + network_name to the other tools. powerio is a core dependency, so this is + always available. Args: network_json: The JSON transport string from powerio diff --git a/README.md b/README.md index aa6a2b5..f9f0f0d 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,12 @@ PowerMCP installs as a single Python package with an interactive CLI. Python 3.1 pip install powermcp ``` -The base install includes the two open-source engines that need no extra setup — **pandapower** and **PyPSA**. Every other tool is opt-in via an extra: +The base install includes the open-source engines that need no extra setup — **pandapower**, **PyPSA**, and **PowerIO** (the cross-server case-conversion substrate). Every other tool is opt-in via an extra: ```bash pip install powermcp[psse] # add PSS/E support pip install powermcp[andes,opendss] # add several tools at once -pip install powermcp[opensource] # all open-source tools (ANDES, Egret, OpenDSS, surge, HOPE, LTSpice, PowerIO) +pip install powermcp[opensource] # all open-source tools (ANDES, Egret, OpenDSS, surge, HOPE, LTSpice) pip install powermcp[all] # everything (closed-source tools still need the local software) ``` @@ -87,7 +87,7 @@ pip install powermcp[all] # everything (closed-source tools still powermcp install ``` -The wizard lets you pick tools (pandapower + PyPSA pre-selected), captures the local install path for any closed-source/EXE-based tools you choose (PSS/E, PSLF, PowerFactory, PSCAD, LTSpice), installs the right extras, and writes the MCP client configuration for **Claude Desktop**, **Claude Code**, and the **Codex CLI**. Use `--dry-run` to preview the changes, or `--yes` for a non-interactive core install. +The wizard lets you pick tools (pandapower + PyPSA + PowerIO pre-selected), captures the local install path for any closed-source/EXE-based tools you choose (PSS/E, PSLF, PowerFactory, PSCAD, LTSpice), installs the right extras, and writes the MCP client configuration for **Claude Desktop**, **Claude Code**, and the **Codex CLI**. Use `--dry-run` to preview the changes, or `--yes` for a non-interactive core install. In the interactive picker, move with ↑/↓ and **press SPACE to toggle each tool** before ENTER (ENTER alone keeps only the preselected tools). Prefer not to use the checkbox? Choose tools directly: @@ -126,7 +126,7 @@ These tools wrap commercial or locally-installed software, so PowerMCP stores th ### Case conversion between servers (PowerIO) -The `powerio` extra adds a conversion server backed by [powerio](https://github.com/eigenergy/powerio). It parses MATPOWER `.m`, PSS/E `.raw`, PowerWorld `.aux`, PowerModels JSON, and egret JSON into one format neutral network, converts between those formats with fidelity warnings, and builds the sparse matrices solvers need (B', B'', Y_bus, PTDF, LODF, Laplacian, LACPF). +PowerMCP ships a conversion server backed by [powerio](https://github.com/eigenergy/powerio) as a **core dependency** (no extra needed). It parses MATPOWER `.m`, PSS/E `.raw`, PowerWorld `.aux`, PowerModels JSON, and egret JSON into one format neutral network, converts between those formats with fidelity warnings, and builds the sparse matrices solvers need (B', B'', Y_bus, PTDF, LODF, Laplacian, LACPF). Its JSON transport is the exchange format between PowerMCP servers: parse a case once, pass the returned `json` string between tool calls, and load it anywhere. diff --git a/pandapower/panda_mcp.py b/pandapower/panda_mcp.py index 769dada..573ad3d 100644 --- a/pandapower/panda_mcp.py +++ b/pandapower/panda_mcp.py @@ -387,7 +387,7 @@ def load_network_from_any(file_path: str, source_format: Optional[str] = None) - Reads MATPOWER .m, PSS/E .raw (v33), PowerWorld .aux, PowerModels JSON, or egret JSON via powerio and converts it to a pandapower network, replacing the currently loaded one. Use this for case formats load_network does not - accept. Requires the powerio extra (pip install 'powermcp[powerio]'). + accept. powerio is a core dependency, so this is always available. Args: file_path: Path to the case file @@ -422,8 +422,8 @@ def load_network_from_json(network_json: str) -> Dict[str, Any]: case_to_json tools, so a case parsed once there loads here without passing a file around or re-parsing it. Expects source-valued tables (MW, degrees) as parse_case emits them, not the per-unit normalize_case form. Replaces - the currently loaded network. Requires the powerio extra - (pip install 'powermcp[powerio]'). + the currently loaded network. powerio is a core dependency, so this is + always available. Args: network_json: The JSON transport string from powerio @@ -452,7 +452,7 @@ def export_network_to_format(to_format: str) -> Dict[str, Any]: Converts the loaded network to MATPOWER tables and serializes them with powerio. to_format is a powerio format name: matpower (m), powermodels-json (pm), egret-json (egret), psse (raw), powerworld (aux). - Requires the powerio extra (pip install 'powermcp[powerio]'). + powerio is a core dependency, so this is always available. Args: to_format: Target format name diff --git a/powermcp/registry.py b/powermcp/registry.py index 552c2a2..a2cea82 100644 --- a/powermcp/registry.py +++ b/powermcp/registry.py @@ -131,10 +131,10 @@ def resolve_module_root(self) -> Path: external_solvers=("Julia",), ), Tool( - "powerio", "PowerIO", "open-source", windows_only=False, extra="powerio", + "powerio", "PowerIO", "open-source", windows_only=False, extra=None, server_dir="powerio", run_kind="script", entry_rel="powerio_mcp.py", probe="powerio", - notes="Format-neutral case conversion and matrix builder; the JSON transport is the cross-server exchange format.", + notes="Format-neutral case conversion and matrix builder; the JSON transport is the cross-server exchange format. Core dependency: it is the cross-server exchange substrate the pandapower/Egret/PyPSA/ANDES bridges build on.", ), # ---- CLOSED-SOURCE / VENDOR ---- Tool( @@ -205,7 +205,11 @@ def resolve_module_root(self) -> Path: } # Tools installed by a bare `pip install powermcp` and pre-checked in the wizard. -CORE: tuple[str, ...] = ("pandapower", "pypsa") +# powerio is core because it is the cross-server exchange substrate (the +# pandapower/Egret/PyPSA/ANDES bridges all build on its JSON transport) and is +# cheap: abi3 wheels, zero required runtime deps, extras resolving to numpy +# (core) + scipy (transitive via pandapower). +CORE: tuple[str, ...] = ("pandapower", "pypsa", "powerio") def get_tool(name: str) -> "Tool": diff --git a/pyproject.toml b/pyproject.toml index f9d0504..ffb7852 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,11 @@ dependencies = [ "numpy>=1.24", "pandas>=2.0", "mcp>=1.0", + # powerio is the cross-server exchange substrate (the pandapower/Egret/PyPSA/ + # ANDES bridges build on its JSON transport), and is cheap to require: abi3 + # wheels for five platforms, zero required runtime deps, [mcp,matrix] adding + # only scipy (already transitive via pandapower) on top of mcp+numpy. + "powerio[mcp,matrix]>=0.1.1", # CLI / installer toolkit: "typer>=0.12", "questionary>=2.0", @@ -53,7 +58,8 @@ opendss = ["py_dss_toolkit"] ltspice = ["PyLTSpice", "matplotlib"] surge = ["surge-py>=0.1.5; python_version >= '3.12' and python_version < '3.15'"] hope = ["PyYAML>=6.0"] -powerio = ["powerio[mcp,matrix]>=0.1.1"] +# powerio moved to core `dependencies`; the extra is gone (a bare +# `pip install powermcp` now always provides the conversion server). # --- closed-source / vendor tool engines --- powerworld = ["esa", "numba"] # esa auto-discovers a running Simulator via COM. # numba is required: esa 1.3.5's no-numba code path @@ -65,7 +71,7 @@ psse = [] # vendor `psspy` not on PyPI — pslf = ["pandas"] # vendor `PSLF_PYTHON` not on PyPI — path captured in config.toml powerfactory = ["fastmcp>=2.0", "numpy>=1.26", "matplotlib>=3.8", "pandas>=1.5"] # vendor `powerfactory` not on PyPI # --- convenience groups --- -opensource = ["powermcp[andes]", "powermcp[egret]", "powermcp[opendss]", "powermcp[ltspice]", "powermcp[surge]", "powermcp[hope]", "powermcp[powerio]"] +opensource = ["powermcp[andes]", "powermcp[egret]", "powermcp[opendss]", "powermcp[ltspice]", "powermcp[surge]", "powermcp[hope]"] windows = ["powermcp[pscad-windows]", "powermcp[powerworld]", "powermcp[powerfactory]", "powermcp[psse]", "powermcp[pslf]"] all = ["powermcp[opensource]", "powermcp[powerworld]", "powermcp[powerfactory]", "powermcp[pscad-windows]", "powermcp[psse]", "powermcp[pslf]"] diff --git a/tests/test_powerio_server.py b/tests/test_powerio_server.py index f3754f9..fc451c2 100644 --- a/tests/test_powerio_server.py +++ b/tests/test_powerio_server.py @@ -1,7 +1,8 @@ """Tests for the powerio conversion server, the PyPSA bridge, and the registry/runner wiring. -The whole module skips when powerio is not installed (it is an opt-in extra). +powerio is a core dependency, so it is normally present; the importorskip below +stays as insurance for stripped-down environments. The FastMCP-decorated tools stay ordinary callables, so we exercise them in-process without a transport. The launch test lives here rather than in test_runner.py so it skips with the rest of the module. @@ -236,7 +237,7 @@ def test_pypsa_import_missing_file(tmp_path): def test_registry_entry(): t = TOOLS["powerio"] assert t.kind == "open-source" - assert t.extra == "powerio" + assert t.extra is None # promoted to a core dependency (issue #30) assert t.run_kind == "script" assert t.windows_only is False assert t.probe == "powerio" diff --git a/tests/test_registry.py b/tests/test_registry.py index 001945b..3237346 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -9,7 +9,7 @@ def test_core_tools_present_and_have_no_extra(): - assert set(CORE) == {"pandapower", "pypsa"} + assert set(CORE) == {"pandapower", "pypsa", "powerio"} for name in CORE: assert TOOLS[name].extra is None