How does the WAGO PLC MCP server work?

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.

  1. The agent sees named tools. The server handles the HTTP calls, the paging, and the login for each PLC.1
  2. At startup, the server stores the parameter and method IDs of each PLC, so a search does not contact the PLC.2
  3. 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.

1

Which words do you need first?

WordMeaningExample
MCPA protocol that lets an AI assistant call a fixed set of toolsget_parameter
WDAThe WAGO REST API for system and diagnostic managementGET /wda/parameters/{id}
ParameterOne named system value0-0-ntpclient-enabled
MethodA remote action on the PLCNTP sync, reboot
WatchlistA list of parameters that the PLC keeps open for repeated readsService 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

2

What are the parts of the server?

From the AI agent to the PLC
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 key
  • tools → cache · looks up
  • tools → gates · writes
  • tools → wda · reads
  • gates → wda · passed
  • gates → audit · records
  • gates → repo · GitOps YAML
  • wda → plc · HTTPS
FileJob
src/main.pyDefines the tools and starts the server
src/auth.pyChecks the Bearer key and the rate limit
src/safety.pyIdentifies dangerous methods and read-only PLCs
src/plc_manager.pyRegisters PLCs and stores their IDs
src/wda_client.pySends the WDA requests for one PLC
src/enricher.pyAdds labels and status text to raw values
src/audit.pyWrites the hash-chained audit log
src/gitops.pyMakes YAML proposals for the config repository
src/config.pyReads the fleet list, secrets, and TLS settings
3

What happens when the server starts?

Server startup
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 → register
  • key → serve
  • profile → seed
  • register → key
  • seed → fleet
  1. Check security profile

    With SECURITY_PROFILE=hardened, the server stops if TLS is not complete.

  2. Seed audit chain

    The server reads the last log line, so the hash chain continues after a restart.

  3. Read fleet config

    The list comes from WAGO_PLC_HOSTS and WAGO_PLC_HOSTS_FILE. Passwords come from Docker secrets or settings.

  4. Register each PLC

    The server does a login test, then stores the IDs. It registers 5 PLCs at a time by default.

  5. Resolve API key

    The order is Docker secret, MCP_API_KEY, saved file, new key. The stdio transport does not use a key.

  6. Serve the tools

    The default is port 6042 at /mcp. The /health route 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

4

Which tools does the agent get?

GroupToolsCount
Fleet and devicelist_plcs, describe_plc, get_device, get_feature4
Read parametersfind_parameters, get_parameter, get_parameters_bulk, get_parameter_definition, get_enum_definition, get_parameter_referenced_instances, list_parameter_instances, get_parameter_instance8
Write parametersset_parameters, set_parameterGated2
Methodsfind_methods, get_method, invoke_method, get_method_run, list_method_runs, delete_method_runGated6
Watchlistscreate_watchlist, read_watchlist, list_watchlists, delete_watchlist4
Filescreate_file, upload_file, download_file, get_file_metadataGated4
Auditget_plc_audit_log1
TotalAll groups29

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.

5

What happens during a read and a write?

Find, read, and write one parameter
Diagram structure
  • AI agent agent
  • MCP tools tools
  • PLC manager cache cache
  • Safety gates gates
  • WAGO PLC plc
  • agent → tools · find_parameters
  • tools → cache · search IDs
  • tools → agent · matching IDs
  • agent → tools · get_parameter
  • tools → plc · GET /wda/parameters/{id}
  • plc → tools · raw value
  • tools → agent · value and labels
  • agent → tools · set_parameters
  • tools → gates · read-only PLC?
  • gates → plc · PATCH /wda/parameters
  • plc → agent · 204, then status ok
Real get_parameter resultjson
{
  "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

6

When does the server allow a write?

Gates of invoke_method
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 → readonly
  • dangerous → refused · yes
  • dangerous → run · no
  • gitops → dangerous · no
  • gitops → proposal · yes
  • readonly → gitops · no
  • readonly → refused · yes
ConditionReadset_parametersSafe methodDangerous methodFile upload
Read-only PLC, all modesAllowedRefusedRefusedRefusedRefused
Live mode (default)AllowedAllowed if writeableAllowedRefused, unless in WAGO_ALLOW_METHODSAllowed
GitOps modeAllowedYAML proposalYAML proposalPerson approvesYAML proposal with requires_human: CRITICALRefused

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

7

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.

TaskCorrect callTrap
Connecthttps://<PLC_IP>/wdaHTTP returns 426
Log inBasic auth, then the WAGO-WDX-Auth-Token as a Bearer tokenAfter a 401, the client gets a new token one time
WritePATCH /wda/parametersSuccess is 204 with an empty body. POST returns 405.
Call a methodPOST /wda/methods/{id}/runs?result-behavior=syncWrap each argument: {"value": ...}
Read manyFollow links.nextWDA sends at most 255 entries in a page
Read parametersAdd parameter-errors-as-data-attributes=trueWithout it, one unreadable parameter can fail the full page
8

How does the server protect itself and record changes?

Connection or controlDefaultStronger setting
Client to serverBearer key, 60 requests a minute for each IP address, no TLSSet MCP_TLS_CERT and MCP_TLS_KEY
Server to PLCNot verifiedHTTPS without a certificate checkSet WAGO_TLS_CA, or a plc_cert_<ip> Docker secret
StartupWarnings onlySECURITY_PROFILE=hardened refuses to start without TLS
Local stdioNo network connection, so no keyNot necessary
Audit logLocal hash chain in /app/data/audit.logSet 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

How the chain connectstext
line 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>"}
Verify the audit logbash
docker exec wmcp python /app/src/audit_verify.py --log /app/data/audit.log
9

Why 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

ItemPTXdistYocto
Example deviceCC100 751-9401CC100-IEC62443 751-9412
Firmware format04.09.01 (build 31)02.00.13
Parameter count on the same hardware lineAbout 360About 1056
DNS serversOne flat parameter: 0-0-networking-dns-customdnsserversOne for each bridge: 0-0-networking-bridges-<N>-nameservers-dns
I/O channel roleFixedConfigurable: 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.

10

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 fwupdate tool. See docs/firmware-update-overview.html.19
11

How can you try it safely?

Use a test PLC. These requests read data only.

Ask the agentToolExpected result
List my PLCs.list_plcsThe IP addresses of the registered PLCs
Describe 192.168.1.10.describe_plcDevice class, parameter count, feature names
What firmware runs on 192.168.1.10?find_parameters, get_parameterA value such as 04.09.01
Is NTP on for 192.168.1.10?get_parameterstatus: Activated or Deactivated
Show the last audit entries.get_plc_audit_logRecent writes, method calls, and refusals
12

Can you explain these points now?

Opening diagram…