eplan-mcp-bridge
by paulinholt
README.md
# EPLAN MCP Bridge
Give an LLM real access to the **EPLAN Electric P8 data model** — list pages,
read devices and parts, copy a circuit, rename tags, fix texts — in plain
language, through [MCP](https://modelcontextprotocol.io).
*[Versão em português abaixo.](#português)*
```
"Add motor circuit 04 to panel 02, same typical as 03"
→ model_pages("=PLANT+PANEL-02") picks a free page number
→ model_devices("=PLANT+PANEL-02/0031") reads the template circuit
→ model_replicate_typical(...) copies, renames tags, fixes texts
→ model_devices("=PLANT+PANEL-02/0032") verifies the result
```
---
## Why this exists
EPLAN's API surface splits in two, and only one half is easy to reach.
**Actions** are the command surface — everything the menus do (export a PDF,
update reports, run a check). They're reachable from a script, and most EPLAN
automation stops there.
**The data model** is the other half: pages, devices, placements, properties.
This is what you need to answer "which devices are on this page?" or to copy a
circuit and rename it. No action exposes it.
The obvious approach — a C# script using `Eplan.EplApi.DataModel` — **does not
work on EPLAN 2022/2023**. EPLAN's built-in script compiler does not reference
that assembly, so a script that merely *mentions* `Project` fails to compile.
The failure is silent and misleading: the caller just times out waiting for a
result file that never appears.
This is **not** a licensing problem. The assemblies are loaded in the EPLAN
process; the script compiler simply does not reference them.
The fix is a small DLL compiled *outside* EPLAN with the right references. A
generated script does reflection at exactly one point — load the DLL, call
`Run(command, args)` — and everything inside is statically typed.
```
LLM ──MCP──▶ server.py ──▶ reflection script ──▶ EplanBridge.dll ──▶ DataModel
```
The DLL is loaded from bytes (`Assembly.Load(byte[])`), so the file stays
unlocked and can be rebuilt without closing EPLAN. Every data-model access
inside the bridge is wrapped in a `LockingStep`; without one EPLAN throws
`NoLockingStepException`.
## Requirements
- Windows, EPLAN Electric P8 installed and **running with a project open**
- Python 3.10+
- .NET Framework 4.x C# compiler (`csc.exe`, ships with Windows)
## Install
```powershell
git clone https://github.com/<you>/eplan-mcp-bridge
cd eplan-mcp-bridge
pip install -r requirements.txt
.\bridge\build.ps1 # compiles EplanBridge.dll against your EPLAN
```
`build.ps1` picks the newest EPLAN under `C:\Program Files\EPLAN\Platform`.
Point it elsewhere with `-EplanBin "...\Platform\2022.0.3\Bin"`.
Register the server with your MCP client:
```json
{
"mcpServers": {
"eplan": {
"command": "python",
"args": ["C:/path/to/eplan-mcp-bridge/server/server.py"]
}
}
}
```
Then, from the LLM: `eplan_connect()` → `eplan_model_status()`. If the bridge
is not built, `model_status` says so instead of failing obscurely.
## Tools
All data-model tools are registered with an `eplan_` prefix
(`eplan_model_pages`, …).
**Connection** — `eplan_connect`, `eplan_status`, `eplan_ping`,
`eplan_servers`, `eplan_versions`, `eplan_disconnect`
**Reading** — `model_status`, `model_info`, `model_pages`, `model_devices`,
`model_texts`, `model_page_props`, `model_placements`, `model_dump_placement`,
`model_dump_page`, `model_find_value`, `model_changed_pages`, `model_members`
**Writing** — `model_copy_page`, `model_rename_pattern`, `model_rename_device`,
`model_replace_in_props`, `model_replace_text`, `model_set_prop`,
`model_set_structure`, `model_set_name`, `model_remove_page`
**Graphics** — `model_clone_window`, `model_remove_area`, `model_new_column`,
`model_geometry`
**Whole flows** — `model_replicate_typical`, `model_add_feeder`
`model_run` is an escape hatch to any bridge command.
## Replicating a typical circuit
`model_replicate_typical` is the flow "make me another circuit like that one".
It chains copy → rename tags → replace texts, in the order that works:
```python
model_replicate_typical(
source="=PLANT+PANEL-02/0031",
new_number="0032",
description="CONVEYOR 04 - MAIN MOTOR (M04)",
old="M03", new="M04",
also="MOTOR 03=>MOTOR 04")
```
Two details that are easy to get wrong, and are baked in:
- **The copy uses `NumerationMode.None`.** `Number` renumbers everything
including busbar points (`-L1`/`-L2`/`-L3`), which must keep their names to
stay matched with their potential. Duplicate device tags are resolved by the
rename step instead.
- **Text substitutions run longest-first** (`MOTOR 03` before `M03`), otherwise
the short form consumes part of the long one and the replacement comes out
half-applied. That is what `also` is for: the other spellings of the same
change, as `from=>to` pairs separated by `;`.
What it deliberately does **not** do: **PLC addresses are copied from the source
page.** Whether the new circuit shares that address or needs its own is an
engineering decision, not something to guess. The result reports the devices it
produced so you can check.
## Things learned the hard way
Encoded in the code, collected here because they cost real time:
| Symptom | Cause |
|---|---|
| `Timeout waiting for script results` | A type the script compiler cannot reference. Test with a script that uses no data-model type: if it compiles, the reference is the problem — not a licence, not a hung EPLAN. |
| `NoLockingStepException` | Data-model access outside an action context needs a `LockingStep`. |
| A copied page is born as `0000` | `PAGE_COUNTER` determines the page number. `PAGE_NAME` alone is ignored. |
| Writing a device name silently mangles it | `Function.Name` with a full identifier is misparsed when the location contains `-`. Use `model_set_structure`. |
| `S063113` refusing a tag write | `FUNC_DEVICETAG_FULLNAME` is not writable. Write `Name` instead. |
| A property stores the literal `??_??@value;` | That property is not multi-language (e.g. PLC address 20400). Write a plain string first, fall back to `MultiLangString`. |
| A text replacement comes out half-applied | Overlapping substrings replaced shortest-first. |
| Page description reads empty | It is property **11011**. 11015 exists but is empty in many projects. |
## Safety
The write tools change a real project, and MCP has no undo. `model_remove_page`
and `model_remove_area` are destructive. Work on a copy, or take an EPLAN
backup, before letting an LLM write.
## Tests
```powershell
python -m pytest tests/ -q
```
Offline — `call` is stubbed, so no EPLAN is needed. They cover the step order
of `replicate_typical`, the `NumerationMode` choice, longest-first replacement,
failure handling, and that arguments containing quotes cannot escape the
generated C# string literal.
## License
MIT
---
<a name="português"></a>
# EPLAN MCP Bridge — português
Dá a uma LLM acesso real ao **modelo de dados do EPLAN Electric P8** — listar
páginas, ler dispositivos e peças, copiar um circuito, renomear tags, corrigir
textos — em linguagem natural, via [MCP](https://modelcontextprotocol.io).
```
"Inclua o circuito do motor 04 no painel 02, mesmo típico do 03"
→ model_pages("=PLANTA+PAINEL-02") escolhe um número livre
→ model_devices("=PLANTA+PAINEL-02/0031") lê o circuito modelo
→ model_replicate_typical(...) copia, renomeia tags e textos
→ model_devices("=PLANTA+PAINEL-02/0032") confere o resultado
```
## Por que existe
A API do EPLAN se divide em duas metades, e só uma é fácil de alcançar.
**Ações** são a superfície de comando — tudo que os menus fazem (exportar PDF,
atualizar relatórios, rodar verificação). São acessíveis por script, e é aí que
a maioria das automações do EPLAN para.
**O modelo de dados** é a outra metade: páginas, dispositivos, colocações,
propriedades. É o que você precisa para responder "quais dispositivos estão
nesta página?" ou para copiar um circuito e renomeá-lo. Nenhuma ação expõe isso.
O caminho óbvio — um script C# usando `Eplan.EplApi.DataModel` — **não funciona
no EPLAN 2022/2023**. O compilador de scripts do EPLAN não referencia esse
assembly, então um script que apenas *cita* `Project` falha ao compilar. A falha
é silenciosa e enganosa: quem chamou só vê um timeout esperando um arquivo de
resultado que nunca aparece.
Isto **não** é falta de licença. Os assemblies estão carregados no processo do
EPLAN; o compilador de scripts é que não os referencia.
A solução é uma DLL pequena, compilada *fora* do EPLAN com as referências
corretas. Um script gerado faz reflexão em um único ponto — carrega a DLL e
chama `Run(comando, args)` — e tudo lá dentro é tipado.
```
LLM ──MCP──▶ server.py ──▶ script de reflexão ──▶ EplanBridge.dll ──▶ DataModel
```
A DLL é carregada por bytes (`Assembly.Load(byte[])`), então o arquivo não fica
travado e pode ser recompilado sem fechar o EPLAN. Todo acesso ao modelo dentro
da ponte é envolvido por `LockingStep`; sem ele o EPLAN lança
`NoLockingStepException`.
## Requisitos
- Windows, EPLAN Electric P8 instalado e **aberto com um projeto**
- Python 3.10+
- Compilador C# do .NET Framework 4.x (`csc.exe`, já vem no Windows)
## Instalação
```powershell
git clone https://github.com/<voce>/eplan-mcp-bridge
cd eplan-mcp-bridge
pip install -r requirements.txt
.\bridge\build.ps1 # compila a EplanBridge.dll contra o seu EPLAN
```
O `build.ps1` pega o EPLAN mais novo em `C:\Program Files\EPLAN\Platform`. Para
apontar outro, use `-EplanBin "...\Platform\2022.0.3\Bin"`.
Registre o servidor no seu cliente MCP (JSON igual ao da seção em inglês) e,
pela LLM: `eplan_connect()` → `eplan_model_status()`. Se a ponte não estiver
compilada, o `model_status` diz isso em vez de falhar de forma obscura.
## Replicando um típico
```python
model_replicate_typical(
source="=PLANTA+PAINEL-02/0031",
new_number="0032",
description="TRANSPORTADOR 04 - MOTOR PRINCIPAL (M04)",
old="M03", new="M04",
also="MOTOR 03=>MOTOR 04")
```
Duas armadilhas já embutidas:
- **A cópia usa `NumerationMode.None`.** O modo `Number` renumeraria tudo,
inclusive os pontos de barramento (`-L1`/`-L2`/`-L3`), que precisam manter o
nome para casar com o potencial. Os tags duplicados se resolvem no rename.
- **As substituições de texto vão da mais longa para a mais curta**
(`MOTOR 03` antes de `M03`), senão a forma curta consome parte da longa e a
troca sai pela metade. É para isso que serve o `also`.
O que ela **não** faz: **endereços de CLP vêm copiados da página de origem.** Se
o circuito novo compartilha o endereço ou precisa do próprio é decisão de
engenharia — o resultado lista os dispositivos criados para você conferir.
## Segurança
As ferramentas de escrita alteram um projeto real, e o MCP não tem desfazer.
`model_remove_page` e `model_remove_area` são destrutivas. Trabalhe numa cópia,
ou faça backup pelo EPLAN, antes de deixar uma LLM escrever.
## Licença
MIT
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues