Answer
The server turns the WDA REST API of each WAGO PLC into 29 MCP tools, so an AI agent can read and change system settings. It checks each write in code before the write reaches a PLC.
- The agent sees named tools. The server handles the HTTP calls, the paging, and the login for each PLC.1
- At startup, the server stores the parameter and method IDs of each PLC, so a search does not contact the PLC.2
- The agent cannot remove a safety gate or add a PLC. A person changes these settings and restarts the container.3
Applies toServer 2.4.4, commit 015769b. Example output is from a read on a PFC300 with firmware 04.09.01. For readers who are new to this server.
Which words do you need first?
| Word | Meaning | Example |
|---|---|---|
| MCP | A protocol that lets an AI assistant call a fixed set of tools | get_parameter |
| WDA | The WAGO REST API for system and diagnostic management | GET /wda/parameters/{id} |
| Parameter | One named system value | 0-0-ntpclient-enabled |
| Method | A remote action on the PLC | NTP sync, reboot |
| Watchlist | A list of parameters that the PLC keeps open for repeated reads | Service health every 30 s |
A parameter ID has a fixed form: <device>-<instance>-<feature>-<name>. So 0-0-ntpclient-enabled is device 0, instance 0, feature ntpclient, name enabled.3
What are the parts of the server?
Diagram structure
- MCP client
client - 29 MCP tools
tools - PLC manager cache
cache - Safety gates
gates - WDA client
wda - Audit log
audit - Config repository
repo - WAGO PLC fleet
plc client→tools· Bearer keytools→cache· looks uptools→gates· writestools→wda· readsgates→wda· passedgates→audit· recordsgates→repo· GitOps YAMLwda→plc· HTTPS
| File | Job |
|---|---|
src/main.py | Defines the tools and starts the server |
src/auth.py | Checks the Bearer key and the rate limit |
src/safety.py | Identifies dangerous methods and read-only PLCs |
src/plc_manager.py | Registers PLCs and stores their IDs |
src/wda_client.py | Sends the WDA requests for one PLC |
src/enricher.py | Adds labels and status text to raw values |
src/audit.py | Writes the hash-chained audit log |
src/gitops.py | Makes YAML proposals for the config repository |
src/config.py | Reads the fleet list, secrets, and TLS settings |
What happens when the server starts?
Diagram structure
- Check security profile
profile - Seed audit chain
seed - Read fleet config
fleet - Register each PLC
register - Resolve API key
key - Serve the tools
serve fleet→registerkey→serveprofile→seedregister→keyseed→fleet
- Check security profile
With
SECURITY_PROFILE=hardened, the server stops if TLS is not complete. - Seed audit chain
The server reads the last log line, so the hash chain continues after a restart.
- Read fleet config
The list comes from
WAGO_PLC_HOSTSandWAGO_PLC_HOSTS_FILE. Passwords come from Docker secrets or settings. - Register each PLC
The server does a login test, then stores the IDs. It registers 5 PLCs at a time by default.
- Resolve API key
The order is Docker secret,
MCP_API_KEY, saved file, new key. The stdio transport does not use a key. - Serve the tools
The default is port 6042 at
/mcp. The/healthroute does not need a key.
If a PLC fails the login test, or its parameters or methods do not load, the server skips that PLC and continues.42
To change the fleet, edit the list and recreate the container. All PLCs then register again.5
Which tools does the agent get?
| Group | Tools | Count |
|---|---|---|
| Fleet and device | list_plcs, describe_plc, get_device, get_feature | 4 |
| Read parameters | find_parameters, get_parameter, get_parameters_bulk, get_parameter_definition, get_enum_definition, get_parameter_referenced_instances, list_parameter_instances, get_parameter_instance | 8 |
| Write parameters | set_parameters, set_parameter | Gated2 |
| Methods | find_methods, get_method, invoke_method, get_method_run, list_method_runs, delete_method_run | Gated6 |
| Watchlists | create_watchlist, read_watchlist, list_watchlists, delete_watchlist | 4 |
| Files | create_file, upload_file, download_file, get_file_metadata | Gated4 |
| Audit | get_plc_audit_log | 1 |
| Total | All groups | 29 |
In the Methods and Files groups, the gates apply only to invoke_method, create_file, and upload_file. The server also gives the agent the wago://catalog resource and the wago_assistant prompt.6
The usual order is list_plcs, describe_plc, find_parameters or find_methods, get_parameter or get_method, then set_parameters or invoke_method.
What happens during a read and a write?
Diagram structure
- AI agent
agent - MCP tools
tools - PLC manager cache
cache - Safety gates
gates - WAGO PLC
plc agent→tools· find_parameterstools→cache· search IDstools→agent· matching IDsagent→tools· get_parametertools→plc· GET /wda/parameters/{id}plc→tools· raw valuetools→agent· value and labelsagent→tools· set_parameterstools→gates· read-only PLC?gates→plc· PATCH /wda/parametersplc→agent· 204, then status ok
json{
"dataRank": "scalar",
"dataType": "boolean",
"path": "NTPClient/Enabled",
"value": true,
"status": "Activated",
"writeable": true
}WDA returns the raw value. The server adds status, path, and writeable. For an enum, it also adds the text label.7
Before it sends a write, the server checks the cache. It refuses an unknown ID and a parameter that is not writeable. So a bad write does not reach the PLC.8
When does the server allow a write?
Diagram structure
- invoke_method
call - PLC read-only?
readonly - GitOps mode?
gitops - Dangerous method?
dangerous - Refused
refused - YAML proposal
proposal - Runs on the PLC
run call→readonlydangerous→refused· yesdangerous→run· nogitops→dangerous· nogitops→proposal· yesreadonly→gitops· noreadonly→refused· yes
| Condition | Read | set_parameters | Safe method | Dangerous method | File upload |
|---|---|---|---|---|---|
| Read-only PLC, all modes | Allowed | Refused | Refused | Refused | Refused |
| Live mode (default) | Allowed | Allowed if writeable | Allowed | Refused, unless in WAGO_ALLOW_METHODS | Allowed |
| GitOps mode | Allowed | YAML proposal | YAML proposal | Person approvesYAML proposal with requires_human: CRITICAL | Refused |
A dangerous method has a segment that starts with reboot, restart, factory, firmware, or format. All 0-0-update-* methods are also dangerous.3
A read-only PLC comes from WAGO_ALLOW_WRITES, WAGO_READONLY_HOSTS, or # readonly in the fleet file. Any WAGO_ALLOW_WRITES value other than true blocks writes.9
In GitOps mode, the tool returns YAML. The agent commits it to the config repository and opens a pull request. CI applies the change after the merge.10
Which WDA details cause problems?
The WDA client handles these details, so the agent never sees them. You need them only for a change to src/wda_client.py or for a test with curl.
| Task | Correct call | Trap |
|---|---|---|
| Connect | https://<PLC_IP>/wda | HTTP returns 426 |
| Log in | Basic auth, then the WAGO-WDX-Auth-Token as a Bearer token | After a 401, the client gets a new token one time |
| Write | PATCH /wda/parameters | Success is 204 with an empty body. POST returns 405. |
| Call a method | POST /wda/methods/{id}/runs?result-behavior=sync | Wrap each argument: {"value": ...} |
| Read many | Follow links.next | WDA sends at most 255 entries in a page |
| Read parameters | Add parameter-errors-as-data-attributes=true | Without it, one unreadable parameter can fail the full page |
How does the server protect itself and record changes?
| Connection or control | Default | Stronger setting |
|---|---|---|
| Client to server | Bearer key, 60 requests a minute for each IP address, no TLS | Set MCP_TLS_CERT and MCP_TLS_KEY |
| Server to PLC | Not verifiedHTTPS without a certificate check | Set WAGO_TLS_CA, or a plc_cert_<ip> Docker secret |
| Startup | Warnings only | SECURITY_PROFILE=hardened refuses to start without TLS |
| Local stdio | No network connection, so no key | Not necessary |
| Audit log | Local hash chain in /app/data/audit.log | Set AUDIT_SYSLOG for a second copy on another host |
Each audit line stores the SHA-256 hash of the line before it. If someone changes an old line, the check fails at the next line. The server removes passwords before it calculates the hash.17
textline 1: {"action": "set_parameters", "result": "ok", "prev": "<start value>"}
line 2: {"action": "invoke_method", "result": "denied: dangerous method", "prev": "<SHA-256 of line 1>"}
line 3: {"action": "set_parameters", "result": "ok", "prev": "<SHA-256 of line 2>"}bashdocker exec wmcp python /app/src/audit_verify.py --log /app/data/audit.logWhy can the same request need different parameters?
WAGO devices use two firmware generations. A parameter ID from one generation can be missing on the other.18
| Item | PTXdist | Yocto |
|---|---|---|
| Example device | CC100 751-9401 | CC100-IEC62443 751-9412 |
| Firmware format | 04.09.01 (build 31) | 02.00.13 |
| Parameter count on the same hardware line | About 360 | About 1056 |
| DNS servers | One flat parameter: 0-0-networking-dns-customdnsservers | One for each bridge: 0-0-networking-bridges-<N>-nameservers-dns |
| I/O channel role | Fixed | Configurable: 0-0-io-channelcompositions-<N>-channels |
Before you write to an unfamiliar PLC, read 0-0-version-firmwareversion. Then search for the feature with find_parameters. Do not reuse a parameter ID from another generation without this check.
What does the server not do?
- It does not read or write control-program I/O tags, process values, or PLC memory.
- It supports WAGO devices only. It is not an HMI or a SCADA system.
- It does not let an agent add or remove a PLC.
- It does not flash firmware. A person or CI runs the separate
fwupdatetool. Seedocs/firmware-update-overview.html.19
How can you try it safely?
Use a test PLC. These requests read data only.
| Ask the agent | Tool | Expected result |
|---|---|---|
| List my PLCs. | list_plcs | The IP addresses of the registered PLCs |
| Describe 192.168.1.10. | describe_plc | Device class, parameter count, feature names |
| What firmware runs on 192.168.1.10? | find_parameters, get_parameter | A value such as 04.09.01 |
| Is NTP on for 192.168.1.10? | get_parameter | status: Activated or Deactivated |
| Show the last audit entries. | get_plc_audit_log | Recent writes, method calls, and refusals |