Conversation
`resolve_solver_case` resolves one file or one serialized PowerIO IR document into a typed module, selects a collection entry by `time_index` or `scenario_id`, and hands a writer the balanced network an operating point states. `OperatingPoint.network` supplies that network directly, so no MATPOWER round trip narrows the state to a representable subset. `pio-ir` and `pio` are the format tokens PowerIO accepts; a document carrying neither is refused on PowerIO's own terms. Diagnostics follow PowerIO's severity vocabulary: `error` fails validation, `remark` and `note` stay out of the user-facing warning list. `check_diagnostics`, `operating_point_module` and `select_entry` are public because a second server module calls them. `value_type_name` isolates the one structural-type read PowerIO publishes no accessor for. `powermcp.sandbox` re-exports `staged_file_write` alongside the rest of the path policy and states powerio's containment default. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pandapower, PyPSA, ANDES, Egret and surge accept `powerio_ir` as the primary argument, keep `network_json` as its alias, and splice the shared response tail: `value_type`, `selection`, `diagnostics`, `warnings`, plus the emission fidelity and the typed-edit report for the adapters that convert into their own model. PowerIO 0.11.2 hands a directory writer a path that does not exist yet, so the PyPSA CSV export and the surge table export create the staging directory before writing into it. ANDES and Egret install their one emitted case file through `staged_file_write`, so a write that fails part way leaves whatever stands at the destination untouched. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fourteen declarations now read `powerio[mcp,matrix]>=0.11.2,<0.12`. The floor is 0.11.2 because 0.11.1 added `OperatingPoint.network` and matrix `row_ids`, and 0.11.2 sets the path-containment default this repository documents. `GenX/requirements.txt` gains a pin: `GenX/server.py` imports `powermcp.sandbox`, which imports powerio. Version 0.4.0. The sdist excludes local virtual environments and uv build caches, and `.gitignore` covers `.venv-*/`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
PowerIO 0.11.2 confines MCP paths to the directory the process started in when no root variable names one. A session-scoped autouse fixture names the repository checkout, pytest's temporary base and the system temporary directory, so a test writing under `tmp_path` is admitted. A test that sets its own roots still overrides it, because `allowed_roots()` reads the environment on every call. `_containment_status` distinguishes an explicit root list from the implicit default, reports the default as a setting to make rather than a policy already chosen, and reports the refusal PowerIO raises for a root variable naming no directory. The doctor's user-visible strings carry no decorative characters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The powerio and solver-boundary tests require powerio 0.11.2, so the matrix tools name their axes unconditionally and a retired Package document is asserted for what it is: not PowerIO IR. `tests/test_transport.py` states why it exists. The SDK parses a string argument whose text is JSON before validation, so only a real stdio session proves a `powerio_ir` argument reaches the tool intact. `tests/test_sdk_imports.py` covers PLEXOSDB, the one server directory it missed. `tests/test_publish_workflow.py` asserts the sdist exclude list keeps local virtual environments and build caches out of a release. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The PowerIO section describes the server shipped in 0.11.2, names the arguments every adapter takes, and states the containment default: with no root variable set, paths stay beneath the directory the server process started in. Per-server READMEs describe what the import tools accept, which is serialized PowerIO IR or one selected entry of a collection. The retired `Package`, `model-json`, `package_json` and package `study_commit` formats keep their migration note: re-parse the original case and pass its `powerio_ir`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
stdout carries JSON-RPC once a server runs, so the ANDES startup notes go to its logger, which writes to stderr, and the two PowerWorld error reports go to stderr explicitly. `GenX/server.py` keeps the repository root on `sys.path` for the process lifetime and says why: GenX carries no `__init__.py`, so its namespace `__path__` is recomputed from `sys.path` on every attribute lookup and the lazy `GenX.tool_logic` imports would fail without it. PLEXOSDB's notes on its shared import name say what the module is: a re-export of the `plexosdb-mcp` server object under that same name. The install picker's instruction line and the `powermcp` package docstring carry no decorative characters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
powerio 0.11.2 ships a cp39-abi3 manylinux wheel serving Python 3.9 through 3.14, and the only other Rust dependency, surge-py, lives in an opt-in extra CI never installs. Without the toolchain step a future wheel gap fails loudly instead of building from an sdist. The suite spawns roughly forty subprocesses per job with 60 second timeouts, so stacked pushes on a busy runner turn a timeout into a failure. The concurrency group cancels superseded pull request runs and leaves pushes to main alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The tree is that of the eight commits ending at d1a8d91, which modernize every bridge for powerio 0.11.2 and return the suite to green. The earlier commits on this branch stay reachable through the second parent and contribute nothing to the tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
qian-harvard
left a comment
There was a problem hiding this comment.
Reviewed at depth. The substance here is sound and unusually well tested — solver_case.py is a real improvement over the hand-rolled projection it replaces, and the typed-edit boundary is the right shape. I want this in. Three things first, plus a decision about this PR versus #71 that I think needs settling before either moves.
This PR, not #71
I checked whether #64 is still needed given #71, because on the surface #71 looks like a strict superset. It is:
$ git diff pr-71...pr-64 --stat
(empty)
$ comm -12 <(git diff --name-only main...pr-64|sort) <(git diff --name-only main...pr-71|sort) | wc -l
52 # all 52 of #64's files
So #71 contains every line of this PR. But it also bundles #67 (the Tellegen server) and #70 (the tool-result-shape change) — two drafts that have not been reviewed — and merging #71 today would land all four PRs in one click. That is not a trade I want to make for a 2,000-line dependency migration.
It also carries a test that fails. PLEXOSDB/plexosdb_mcp/main.py:183 returns tool_success(...) on #71, and PLEXOSDB/tests/test_tools.py:129 still asserts the old shape:
>>> tool_success(output_path="/tmp/out/system.json", model_name="Base", component_types={"Bus": 2})
{'status': 'success', 'output_path': '/tmp/out/system.json', 'model_name': 'Base', 'component_types': {'Bus': 2}}
>>> result["ok"]
KeyError: 'ok'CI cannot see it: .github/workflows/test.yml:33 runs pytest tests/ only, so PLEXOSDB/tests/ is never collected. That belongs to #70 and I will raise it there — noting it here only because it is the reason I am not taking the superset.
So: this PR is the one to merge. #67, #69, #70 and #71 should then rebase onto the result, at which point their real diffs are small (#69 is 4 files, #71 is 23 of which 16 are one-token version bumps).
You will need a rebase, and that one is on me
I merged #72 an hour ago to cap powerio at <0.10. main had been red since 6 September — the <1 ceiling was resolving to 0.11.x against 0.9-era code, so pip install powermcp==0.3.0 was broken for everyone and CI was running 61 failed / 283 passed. That is now v0.3.1, 344 passed.
It collides with this PR in exactly the 15 files it touched — the 13 powerio pin sites plus powermcp/__init__.py and tests/test_registry.py. Every hunk is a one-line version string where both sides edited the same line, so the resolution is "take yours" throughout: your >=0.11.2,<0.12 supersedes my <0.10 cap, and your 0.4.0 supersedes 0.3.1. Sorry for the churn — the alternative was leaving a broken package on PyPI for however long this PR takes.
Please also re-run CI after rebasing. The last green run installed powerio 0.11.2, and 0.11.3 went up on 17 September, so the current pin now resolves to a version this branch has never been tested against. I checked the 0.11.2→0.11.3 delta myself and it is additive everywhere PowerMCP touches — allowed_roots() is byte-identical, the sandbox diff is comment-only — so I expect it to be clean. Worth the click anyway.
Findings
1. The multiconductor guard is unreachable for four of the five types it covers — powermcp/solver_case.py:478-483
if isinstance(value, _MULTICONDUCTOR): # 5 types (:399-405)
if not to_balanced or not isinstance(value, powerio.MulticonductorNetwork):
raise ValueError("... or pass to_balanced=True")_MULTICONDUCTOR is (MulticonductorNetwork, McAcPfInstance, McAcOpfInstance, McAcPfSolution, McAcOpfSolution). For the four that are not MulticonductorNetwork, the second clause is true regardless of to_balanced, so the raise fires unconditionally — and tells the caller to pass to_balanced=True, which is what they just did. A serialized McAcPfInstance is a dead end with advice that cannot work.
If lowering genuinely only applies to a network and not to a calculation instance, the message should say that instead of naming a flag. If instances should be lowerable, the guard is wrong.
2. powermcp doctor names its own startup directory as the servers' sandbox — powermcp/doctor.py:145-158
The policy description is right and the docstring is honest about it. The problem is {listed}: allowed_roots() runs in the doctor process, and powerio captures the startup directory at import, so the reported root is wherever powermcp doctor was invoked from. Servers launch as separate processes from the MCP client's working directory, which is usually somewhere else. An operator who runs the doctor from the repo, sees a plausible-looking path, and moves on has been told something that is not true of the thing they are about to run. Either name the directory the servers will actually start in, or drop the specific path and describe the rule.
3. The containment change needs a migration note, and wizard.py is where it should land
powermcp/sandbox.py:14-15 — powerio 0.11.2 flips unset roots from "nothing is constrained" to "confined to the server's startup directory". Every existing 0.3.x deployment that never set POWERIO_MCP_ALLOWED_ROOTS silently narrows on upgrade, and will surface as PathNotAllowed on paths that worked yesterday. The README prose covers it, but wizard.py's diff is 10 lines of pure punctuation — it still never prompts for roots, and the generated client entries carry no env block. At minimum this needs a line in the release notes; ideally the wizard asks.
Minor: the PR body says "GenX stops leaking the repository root onto sys.path", but GenX/server.py:22-24 still does sys.path.insert(0, _REPO_ROOT) exactly as main does — the change is three comment lines explaining why it stays. Worth fixing the description so a reviewer does not go looking for a change that is not there.
Explicitly dropping
I chased two concerns to the powerio source and they do not hold — flagging so nobody re-raises them:
- The private
_inner._type_nameread is not at risk from 0.11.3. The 0.11.2→0.11.3 diff ofpython/powerio/__init__.pyremoves exactly one line and it is a docstring;_type_nameis still declared at_powerio.pyi:879. IR_VERSIONbeing unused is real but thedocument["version"]KeyError it implies is unreachable:StoredModuledeclaresversion: u64withdeny_unknown_fieldsand noserde(default), so a document without it fails to deserialize before that subscript runs. Dead constant, not a bug.
Also worth saying: tests/test_publish_workflow.py plus the new sdist excludes for .venv* and .uv-build-cache are a genuine packaging fix — hatchling would otherwise ship a developer virtualenv to PyPI — and it is not mentioned in the description at all. It deserves a line.
Rebase, re-run CI, and answer 1. Then I will merge this and we can work through the stack on top of it.
PowerIO removed the 0.9
PackageAPI in 0.11.0 and changed its default MCP pathpolicy in 0.11.2. This moves every PowerMCP bridge onto the typed PowerIO IR module
model, pins
powerio[mcp,matrix]>=0.11.2,<0.12in all fourteen places, and returnsthe suite to green.
Solver boundary
powermcp/solver_case.pyresolves one file or one serialized PowerIO IR documentinto a typed module, selects a collection entry by
time_indexorscenario_id,applies typed what-if edits through PowerIO's
OperatingPointUpdate,NetworkUpdateand
apply_bus_load_active_power, and hands the solver a balanced network. Itre-parses and re-validates nothing PowerIO already states. Every adapter response
carries the same tail:
value_type,selection,diagnostics,warnings, and theemission
fidelitywhere the adapter converts.pandapower,PyPSA,ANDESandEgrettakepowerio_ir(withnetwork_jsonasan alias),
edits,to_balanced,base_mva,time_indexandscenario_id.pandapower's hand-rolled PYPOWER-to-MATPOWER text projection is gone; PowerIO's
native writers do the conversion.
Path containment
powerio 0.11.2 confines MCP paths to the directory the server process started in when
no root variable names one, where earlier versions left every path allowed. The
doctor now distinguishes an explicit root list from that implicit default, and the
test suite names the repository and pytest's temporary tree as its roots. This is the
root cause of the 41 failures on this branch's previous head.
Also
surgeandPyPSAcreate the staging directory that powerio 0.11.2 now hands to adirectory writer unset.
ANDESandEgretinstall their emitted case file through powerio's staged filewrite, so a failed write leaves no truncated file behind.
time_indexor
scenario_idis refused with the available entries listed.warningslist.printcalls that wrote to the MCP stdio channel move to stderr and logging(
ANDES,PowerWorld).GenXstops leaking the repository root ontosys.pathand declares its poweriodependency.
PLEXOSDBstops citing a server file this repository deleted, and joins the SDKimport sweep.
pull request runs.
History
The change is the eight commits from
86dd105tod1a8d91, rebuilt on currentmain. The merge commit064c0bcadopts that tree; the branch's earlier commitsstay reachable through its second parent and contribute nothing to the diff.
Review the eight commits or the combined diff against
main.Validation
pytest tests -qon Python 3.12 against powerio 0.11.2:358 passed, 19 skipped, 0 failed.
tests/test_server_entrypoints.pylaunches every registered server as asubprocess and lists its tools.
tests/test_transport.pydrives the powerio server over real stdio throughpowermcp run powerio.python -m buildplustwine checkon the sdist and wheel.🤖 Generated with Claude Code