diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2d1194a..598188f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,13 +24,9 @@ jobs: - uses: dtolnay/rust-toolchain@stable - - name: Install companion PowerIO draft - run: | - python -m pip install --upgrade pip - python -m pip install "powerio[mcp,matrix] @ git+https://github.com/eigenergy/powerio.git@feat/mcp-dist-display-tools" - - name: Install package run: | + python -m pip install --upgrade pip python -m pip install -e . pytest - name: Run tests diff --git a/README.md b/README.md index 9d383cd..8fe2572 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ These tools wrap commercial or locally-installed software, so PowerMCP stores th PowerMCP ships a compiler server backed by [powerio](https://github.com/eigenergy/powerio) as a **core dependency** (no extra needed). It parses transmission and distribution formats into canonical JSON transports, converts between target artifacts 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 save runtime artifacts only when a backend needs a file. +Its JSON transport is the exchange format between PowerMCP servers: parse a case once, pass the returned `json` string between tool calls, and save runtime artifacts only when a backend needs a file. Existing `json` transport workflows remain supported. ``` parse(path="case9.raw") # powerio server -> {"json": ..., "summary": ...} @@ -139,6 +139,17 @@ matrix(kind="ptdf", json=...) # powerio server builds matri save(to_format="psse", out_path="case9.raw", json=...) # stage a file for path only servers ``` +PowerIO 0.4.0 also supports the `.pio.json` package transport, which carries the model plus package metadata and structured diagnostics: + +``` +parsed = parse(path="case9.raw", transport="package") +pkg = parsed["package_json"] +summary(package_json=pkg) +matrix(kind="ptdf", package_json=pkg) +save(to_format="psse", out_path="case9.raw", package_json=pkg) +diagnostics(package_json=pkg) # package diagnostics summary and structured findings +``` + `summary` returns the canonical nested shape used by PowerIO and PowerMCP: counts live under `elements` (`elements.buses`, `elements.branches`, `elements.generators`) and topology metadata lives under `topology` (`topology.connected_components`, `topology.reference_buses`). `save` covers the servers without a bridge: write the converted case to disk and point their load tools at the file. For OpenDSS, save a distribution transport as DSS, then compile that DSS file: diff --git a/powerio/powerio_mcp.py b/powerio/powerio_mcp.py index 3e21c68..2d97e5a 100644 --- a/powerio/powerio_mcp.py +++ b/powerio/powerio_mcp.py @@ -5,11 +5,12 @@ this server keeps no copy of the conversion/summary/matrix tools. PowerIO is the cross-server compiler layer for transmission and distribution cases: -- ``parse``: source format to canonical JSON transport. +- ``parse``: source format to canonical JSON or ``.pio.json`` package transport. - ``convert`` / ``save``: canonical transport or source format to target artifact. - ``summary``: canonical network summary. - ``normalize``: normalized transmission transport. - ``matrix``: sparse transmission matrix outputs. +- ``diagnostics``: structured diagnostics for ``.pio.json`` packages. - ``display``: display artifacts such as PowerWorld ``.pwd`` geometry. PowerMCP deliberately re-exports only those canonical tools. GridFM and PyPSA @@ -30,6 +31,7 @@ parse = _server.parse normalize = _server.normalize matrix = _server.matrix +diagnostics = _server.diagnostics display = _server.display __all__ = [ @@ -40,6 +42,7 @@ "parse", "normalize", "matrix", + "diagnostics", "display", ] diff --git a/pyproject.toml b/pyproject.toml index 8ef659e..89c014c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ build-backend = "hatchling.build" [project] name = "powermcp" -version = "0.2.0" +version = "0.2.1" description = "MCP servers for power-system software (PowerWorld, OpenDSS, PSS/E, pandapower, PyPSA, and more)" readme = "README.md" license = { file = "LICENSE" } @@ -40,10 +40,10 @@ dependencies = [ # 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. - # >=0.3.3: the MCP tool surface is the seven canonical verbs: - # convert, save, summary, parse, normalize, matrix, display. + # >=0.4.0: the MCP tool surface is: + # convert, save, summary, parse, normalize, matrix, diagnostics, display. # powerio_mcp.py re-exports exactly that surface with no local overlay. - "powerio[mcp,matrix]>=0.3.3", + "powerio[mcp,matrix]>=0.4.0", # CLI / installer toolkit: "typer>=0.12", "questionary>=2.0", diff --git a/tests/test_powerio_server.py b/tests/test_powerio_server.py index f1db6c2..81c99cd 100644 --- a/tests/test_powerio_server.py +++ b/tests/test_powerio_server.py @@ -24,7 +24,7 @@ import pytest -pytest.importorskip("powerio", minversion="0.3.3") +pytest.importorskip("powerio", minversion="0.4.0") import powerio # noqa: E402 @@ -91,19 +91,26 @@ def test_tool_surface_is_canonical(): "parse", "normalize", "matrix", + "diagnostics", "display", } for name in ("parse", "summary", "normalize", "matrix", "display"): props = tools[name].inputSchema["properties"] assert "from_format" in props assert "format" not in props + parse_props = tools["parse"].inputSchema["properties"] + assert "transport" in parse_props convert_props = tools["convert"].inputSchema["properties"] assert "to_format" in convert_props and "from_format" in convert_props + assert "package_json" in convert_props assert "to" not in convert_props and "format" not in convert_props + for name in ("summary", "normalize", "matrix"): + assert "package_json" in tools[name].inputSchema["properties"] save_schema = tools["save"].inputSchema assert save_schema["required"] == ["out_path"] save_props = save_schema["properties"] assert "to_format" in save_props and "from_format" in save_props + assert "package_json" in save_props assert "to" not in save_props and "format" not in save_props @@ -225,6 +232,38 @@ def test_save_accepts_json_transport(tmp_path): assert powerio.parse_file(out).n_buses == 9 +def test_package_transport_flows_through_core_tools(tmp_path): + parsed = powerio_mcp.parse(path=str(CASE9), transport="package") + assert parsed["schema"] == "powerio.parse" + assert parsed["transport"] == "package" + assert parsed["json_format"] == "package" + assert parsed["domain"] == "transmission" + assert parsed["model"] == "balanced" + assert "package_json" in parsed + + package = json.loads(parsed["package_json"]) + assert package["model_kind"] == "balanced" + assert package["model"]["kind"] == "balanced" + + package_json = parsed["package_json"] + assert powerio_mcp.summary(package_json=package_json)["elements"]["buses"] == 9 + + matrix = powerio_mcp.matrix("bprime", package_json=package_json) + assert matrix["kind"] == "bprime" + assert matrix["shape"] == [9, 9] + + out = tmp_path / "case9.m" + powerio_mcp.save(out_path=str(out), package_json=package_json) + assert powerio.parse_file(out).n_buses == 9 + + diag = powerio_mcp.diagnostics(package_json) + assert diag["schema"] == "powerio.diagnostics" + assert diag["model_kind"] == "balanced" + assert diag["summary"]["status"] in {"ok", "info", "warning", "error", "fatal"} + assert isinstance(diag["summary"]["text"], str) + assert isinstance(diag["diagnostics"], list) + + def test_save_exactly_one_input(tmp_path): out = tmp_path / "x.m" with pytest.raises(ValueError):