PCM MCP
# PCM MCP
An MCP server for **ABB / Hitachi Energy PCM600** (Protection and Control IED Manager).
No such server exists publicly — this is a from-scratch build. Status: **Phases 1-4 complete**,
24 tools working end-to-end over stdio against PCM600 2.14.
## Tier 0 — offline analysis (no PCM600 install needed)
Reads and reasons about IEC 61850 SCL files (`.scd`, `.icd`, `.iid`, `.cid`, `.ssd`) already
exported from PCM600, and `.apcmp` project archives.
| Tool | Purpose |
|---|---|
| `scl_summary` | Header, substation/voltage-level/bay tree, inventory counts |
| `scl_list_ieds` | Every IED with manufacturer, type, config version, IP addressing |
| `scl_get_ied` | One IED in detail: logical devices, logical nodes, datasets, control blocks |
| `scl_list_datasets` | Datasets and their FCDA members |
| `scl_list_goose` | GOOSE publishers + addressing; **flags publishers with no GSE address** |
| `scl_list_reports` | Report control blocks: buffering, trigger options, timing |
| `scl_list_addresses` | IP addressing per access point; **flags duplicate IPs / APPIDs / MACs** |
| `scl_check_goose_bindings` | Traces publishers → subscribers; catches unsubscribed publishers, unknown IEDs/control blocks, and **subscriptions to signals the dataset doesn't carry** |
| `scl_validate` | Referential integrity — dangling dataset refs, FCDAs pointing at missing logical nodes, Communication/IED mismatches, duplicates. Optional `xsd_path` for schema validation |
| `scl_diff` | Structural, model-level diff of two SCL files across 11 categories |
| `apcmp_summary` | Inspect a `.apcmp` project archive **without importing it** — project name, IED inventory, and whether the required connectivity packages are installed |
| `apcmp_logic` | Recover ACT application logic from a `.apcmp` **without importing it** — worksheets, signals, blocks; flags naming clashes and **device groups where one member lacks what its siblings have** |
| `pcm_probe_archive` | Characterise any PCM600 archive without assuming its layout — for file kinds not yet verified against a real sample (`.apcmi`, `.apcmt`) |
`scl_diff` compares the parsed model, not the text. PCM600 reorders elements and rewrites
attributes on every export, so a text diff of two SCDs is unusable — a one-setting change can look
like a thousand-line rewrite.
`scl_validate`'s XSD support is opt-in: the IEC 61850-6 schema is a licensed IEC deliverable and
isn't bundled here (nor shipped with PCM600), so pass your own copy via `xsd_path` if you want it.
The rule-based checks are the primary value — XSD only catches syntax.
`apcmp_logic` reads the ACT logic sheets, which PCM600 stores as XML inside the project database
and which survive as recoverable text. That means application logic can be reviewed even when the
connectivity package needed for an import is missing.
Its asymmetry checks are the useful part, because they find the defects that are invisible when
reading sheet by sheet — each sheet looks self-consistent:
* **Device groups** — a concept present for some members and not others (on a real 3-breaker
scheme: B3 lacked the fail-to-open/close and breaker-failure supervision B1 and B2 both had).
* **Source pairs** — an `A`-side signal with no `B`-side counterpart, and vice versa. In a
two-source transfer scheme this is where inconsistent failure handling hides.
* **Transfer direction** — concepts tagged for one direction only. Where one direction carries an
explicit `_BA`/`_AB` suffix and the other reuses untagged names, the second direction was
typically added as a parallel copy rather than the original logic being generalised.
* **Letter-transposition typos** — `SRCA_AVAIL` vs `SCRB_AVAIL`, where `SRC` silently became
`SCR`. Deliberately narrow: it will not flag `FTC`/`FTO` (different concepts) or `BA1`/`BA4`
(legitimate numbering), because a noisy detector is worse than none in a safety review. Limits are reported in every result — block-to-block wiring
and setting values are **not** recoverable, so absence in the output is not proof of absence in the
project.
`apcmp_summary` is the import pre-flight. PCM600's import silently ignores IEDs whose object type
it does not recognise, so importing a project whose connectivity package is missing can discard
IEDs with no error. Run it first. Note the archive's *project name* (what `-p` takes) is **not**
the database name — the tool reports both.
Resource `pcm600://config` reports resolved config and which tiers are available.
### Tier 1 — drives the PCM600 CLI (needs PCM600 installed)
All read-only with respect to the PCM600 project database.
| Tool | Purpose |
|---|---|
| `pcm_cli_info` | Detected install, SQL version, connectivity packages, and a live reachability check |
| `pcm_export_scd` | Export a project's SCD |
| `pcm_export_parameters` | One IED's parameters to `.csv` / `.xrio` (by technical key) |
| `pcm_export_bulk_parameters` | Every IED's `.xrio` into a directory — **needs no technical key** |
| `pcm_export_ied` | One IED as `.apcmi` |
| `pcm_export_project` | Whole project as `.apcmp` |
| `pcm_validate_parameters` | List deviations a parameter import *would* make. Validates only |
Tier 1 degrades cleanly: if PCM600 isn't detected, these report why and the offline tools keep working.
**It will never talk to a physical relay.** `PCM600Cmd` *does* expose eight commands that open a
connection to a live IED — `write-ied`, `read-ied`, and their `-under-bay` /
`-under-voltage-level` / `-under-substation` variants. All eight are on a hard denylist in
`cli.py`: the wrapper refuses to build the argv, and no setting enables them. `--allow-unsigned-file`
is refused too. See [DESIGN.md §6](docs/DESIGN.md#6-safety-design).
**Success is never inferred from the exit code.** `PCM600Cmd` returns 0 even on failure. Outcome
comes from parsing its log output *and* verifying the output artifact exists and is non-empty.
Existing export files are never silently overwritten — pass `overwrite=true` deliberately.
### Tier 1 — mutating (off by default)
| Tool | Purpose |
|---|---|
| `pcm_import_scd` | Import an SCD. Counts IEDs before, re-exports after, and **reports any that vanished** |
| `pcm_import_parameters` | Import one IED's settings. Runs `validate-parameters` first and aborts if it fails |
| `pcm_import_bulk_parameters` | Bulk `.xrio` import. States that no per-file pre-flight exists |
| `pcm_import_ied` | Import an `.apcmi`/`.apcmt` into a bay, voltage level or substation |
All four require **`PCM_MCP_ALLOW_WRITES=1`** *and* **`confirm=true`** — two independent gates — and
snapshot the project before touching it.
> **The snapshot is not rollback.** PCM600 has no CLI command to restore a project; an `.apcmp` can
> only be re-imported by hand in the GUI. Every mutating tool says so in its own output.
The IED-loss verification on `pcm_import_scd` exists because PCM600's import **silently ignores
IEDs whose object type it does not recognise**. A missing connectivity package makes IEDs disappear
with no error, so the tool round-trips the SCD back out and compares rather than trusting silence.
## Setup
Requires Python ≥3.12 (built and tested on 3.14).
```powershell
cd C:\theWork\pcm-mcp
python -m venv .venv
.\.venv\Scripts\python.exe -m pip install -e ".[dev]"
```
`PCM_MCP_WORKSPACE` is **required** — it is the allowlist root for every path argument, so there
is no safe default and the server refuses to start without it.
| Var | Meaning | Default |
|---|---|---|
| `PCM_MCP_WORKSPACE` | Allowlisted root for all file access | **required** |
| `PCM600_BIN` | PCM600 `bin` dir containing `PCM600Cmd.exe` | auto-detected |
| `PCM_MCP_ALLOW_WRITES` | `1` enables mutating tools (none exist yet — Phase 4) | `0` |
| `PCM_MCP_CLI_TIMEOUT` | Seconds per `PCM600Cmd` call | `600` |
| `PCM_MCP_AUDIT_LOG` | JSONL audit path | `$WORKSPACE/.pcm-mcp-audit.jsonl` |
### Register with Claude Code
```json
{
"mcpServers": {
"pcm600": {
"command": "C:\\theWork\\pcm-mcp\\.venv\\Scripts\\python.exe",
"args": ["-m", "pcm600_mcp.server"],
"env": { "PCM_MCP_WORKSPACE": "C:\\theWork\\pcm-mcp" }
}
}
}
```
## Tests
```powershell
.\.venv\Scripts\python.exe -m pytest -q # 203 passed, 13 skipped
.\.venv\Scripts\python.exe scripts\smoke_stdio.py # live stdio handshake
```
Most skips are the integration suite (below) plus the symlink-escape test, which needs Windows
Developer Mode. The smoke script drives the real transport: `initialize` → `tools/list` →
`tools/call` → path-confinement refusal.
### Integration tests against a real PCM600
`tests/test_integration_cli.py` drives the installed `PCM600Cmd.exe` for real. It skips cleanly
when prerequisites are absent, so it is safe to run anywhere.
Three tests need only PCM600 installed and run automatically — CLI reachability, the
**IED-communication denylist against the real binary**, and the regression guard for
exit-code-0-on-failure. The rest need a project:
```powershell
$env:PCM_MCP_TEST_PROJECT = "SCRATCH_MCP_TEST" # a SCRATCH project, never production
$env:PCM_MCP_TEST_TECHNICAL_KEY = "AA1J1Q01A1" # optional; unlocks per-IED tests
.\.venv\Scripts\python.exe -m pytest tests/test_integration_cli.py -v -rs
```
Mutating tests additionally require `PCM_MCP_ALLOW_WRITES=1`. **Point these at a scratch project
only** — a bad import can only be undone by a manual GUI import of the snapshot.
`test_export_ied_then_probe_the_format` is the one to run first once a project exists: it produces
a real `.apcmi` and prints its structure, which settles whether the `.apcmp` readers can be reused
for IED exports and templates.
## Layout
```
src/pcm600_mcp/
server.py MCP wiring, the twenty-four tools
cli.py PCM600Cmd wrapper: denylist, log parsing, artifact verification
config.py env resolution, PCM600 + connectivity-pack detection, tier availability
safety.py path confinement, write gating, audit log
apcmp.py .apcmp project-archive reader (ZIP + metadata sidecars)
act.py ACT logic recovery from the project database inside a .apcmp
scl/model.py dataclass model of an SCL document
scl/parse.py namespace-agnostic lxml reader
scl/checks.py binding tracing + referential-integrity rules
scl/diff.py structural diff
tests/
fixtures/synthetic.scd clean-ish 2-IED substation
fixtures/synthetic_modified.scd one change per diff category
fixtures/problems.scd duplicate IPs/APPIDs/MACs, orphan address
fixtures/bindings.scd all four GOOSE binding failure modes
fixtures/invalid.scd eleven referential-integrity failures
test_integration_cli.py live PCM600 round-trips; skips without a project
scripts/smoke_stdio.py end-to-end transport check
docs/ DESIGN.md, PLAN.md, DISCOVERY.md, cli-help.txt
```
## Documents
| File | What |
|---|---|
| [docs/DESIGN.md](docs/DESIGN.md) | Design, tool surface, safety model, ~30 cited references |
| [docs/PLAN.md](docs/PLAN.md) | Phased plan and status |
| [docs/DISCOVERY.md](docs/DISCOVERY.md) | Phase 0 results: real CLI contract, `.apcmp` format, traps |
| [docs/cli-help.txt](docs/cli-help.txt) | Verbatim `PCM600Cmd` help, captured from the install |
## Current state
```
Phase 0 Discovery ............ MOSTLY DONE - CLI captured; scratch project needs GUI
Phase 1 Scaffold + SCL core .. DONE
Phase 2 Analysis / diff ...... SCL half DONE; parameter CSV/XRIO readers deferred (no sample)
Phase 3 CLI wrapper .......... DONE - cli_info verified live; 6 tools await a project to test
Phase 4 Mutating tools ....... DONE - 4 import tools, two gates, snapshot + verify
Phase 5 Packaging ............ partial (pyproject + .mcp.json snippet done)
Phase 6 Test & harden ........ partial (203 tests + stdio smoke + live integration suite)
```
## Next action
Two things are blocked on data, not code:
1. **A project in the local PCM600 database.** Six of the seven Tier 1 tools need one to be tested
end-to-end (`pcm_cli_info` is already verified live, since `export-system-info` needs no
project). There is no `create-project` CLI command, so this needs the GUI.
2. **A parameter `.csv` / `.xrio` sample** to build the deferred `params_*` tools against. One
read-only command supplies it and needs no technical key:
`export-bulk-parameters -p "<project>" -d "<dir>"`.
Also still untested: whether `PCM600Cmd` works with the PCM600 GUI open (DISCOVERY.md §0.7).
TDQS
Scored across 23 tools
Every tool targets a distinct resource and action. scl_* tools are differentiated by operation (get, list, validate, diff, check), and pcm_* tools by export/import/validate. No two tools appear to do the same thing, even within similar areas like parameter export/import.
Naming follows a clear prefix convention: scl_ for SCL operations, pcm_ for PCM600 operations, apcmp_ for archive inspection. Within each prefix, actions are mostly verb-first (export_*, import_*, list_*). Minor deviations like pcm_cli_info and apcmp_summary are not fully verb-noun but remain predictable.
With 23 tools, the count is above the typical 3-15 range, but the server covers a broad domain: SCL analysis, project import/export, parameter management, validation, and diffing. Each tool has a distinct purpose, so the size feels justified rather than bloated.
The tool surface covers core workflows: SCL inspection, validation, diff, GOOSE binding analysis, project export/import, and parameter validation/import/export. Minor gaps exist, such as no direct SCL editing, no SMV control block tools, and no project listing, but these are workable and do not create dead ends.