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