wago-plc-mcp-server
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@wago-plc-mcp-serverRead the firmware version from all PLCs"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
wago-plc-mcp-server
Talk to your WAGO PLC fleet the way you'd talk to a colleague. Ask an AI assistant to read, configure, and monitor your controllers in plain English - no scripts, no parameter IDs to memorize.
Choose your path
I am a... | I want to... | Start here |
Claude Desktop / Claude Code user | Connect my AI assistant to WAGO PLCs and start asking questions | Part 1 → Quick Start → What can I ask it? |
Automation / OT engineer | Understand what this does to my PLCs and whether it's safe | Part 2 → What this does and doesn't do |
Software / DevOps engineer | Deploy this in production with GitOps, TLS, and audit logging | Part 3 → Production deployment → GitOps write-gate |
Each part is self-contained top to bottom - a Claude Desktop user never needs to read the GitOps internals, and a DevOps engineer never needs the chat examples. Reference at the end covers cross-cutting material (FAQ, raw curl access, CRA compliance) that applies regardless of persona.
Related MCP server: tiacommander-mcp
Architecture
%%{init: {'theme':'base', 'themeVariables': {
'primaryColor':'#1F2837',
'primaryTextColor':'#ffffff',
'primaryBorderColor':'#6EC800',
'lineColor':'#6EC800',
'secondaryColor':'#EFF0F1',
'secondaryTextColor':'#1F2837',
'secondaryBorderColor':'#A5A8AF',
'tertiaryColor':'#FFFFFF',
'tertiaryTextColor':'#1F2837',
'tertiaryBorderColor':'#DEDFE1',
'fontFamily':'Segoe UI, Helvetica, Arial, sans-serif',
'clusterBkg':'#EFF0F1',
'clusterBorder':'#A5A8AF',
'edgeLabelBackground':'#1F2837'
}}}%%
flowchart TB
subgraph Clients["AI clients (any MCP client works)"]
direction LR
CD("Claude Desktop<br/>(stdio, via wago_proxy.py)")
CC("Claude Code<br/>(direct HTTP)")
OC("OpenClaw<br/>(direct HTTP)")
end
CD & CC & OC -- "Bearer token" --> MCP
subgraph Server["wago-plc-mcp-server - Docker, port 6042"]
direction LR
MCP("14 MCP tools<br/>find_parameters · get_parameter<br/>set_parameters · invoke_method<br/>create/read_watchlist · get_plc_audit_log · …")
Guard("Bearer auth · rate limiting<br/>hash-chained audit log")
MCP --- Guard
end
MCP --> FAN("WDA Bearer token + TLS<br/>parallel, semaphore-bounded<br/>fans out to every registered PLC")
FAN --> P1 & P2 & P3 & P4 & P5 & P6 & Pn
subgraph Fleet["WAGO PLC fleet"]
direction LR
P1("CC100")
P2("PFC100 Gen 2")
P3("PFC200 Gen 2")
P4("PFC300")
P5("Edge Controller")
P6("WP400")
Pn("TP600")
endDemoed end to end with 16 PLCs of mixed device class on a single rack. The parallel fan-out model has no architectural ceiling below 100+.
Part 1 - For Claude Desktop / Claude Code users
Everything to get connected and start asking questions in plain English.
Demo
These are short screen recordings of the server driving real WAGO controllers from Claude Desktop, start to finish - no edited-out steps.
Overview - connecting Claude Desktop and a first interaction

Asks the agent to reconcile a "health report" across the fleet - listing all PLCs, bulk-fetching firmware versions, and probing device types to figure out what's actually running where before trusting any conclusions.

Asks the agent to set up a watchlist monitoring CPU/service health and LED
diagnostic state on the Edge Controllers, then read it back - including the
agent pushing back to clarify ambiguous requirements before touching
anything, and discovering the actual parameter IDs via find_parameters
rather than guessing.

The same health-watchlist workflow as above, run against a PFC300 instead - shows the same parameter-discovery process landing on different actual parameter names for an equivalent capability.

Asks the agent to sync NTP time on any PLC that's drifted. The agent checks NTP status across the entire fleet first, identifies which PLCs are actually affected (stuck clocks, wrong timezone offsets), and only then invokes the time-sync method on the specific units that need it.

Asks the agent to sweep the entire fleet, check reachability, and report
firmware versions - all in one shot. The agent calls list_plcs, then
describe_plc in parallel across every registered controller, and returns
a clean table of what's alive, what model it is, and which firmware build it
carries.

Asks the agent to audit NTP configuration across the fleet and flag any controller still pointing at the factory-default time server. The kind of compliance sweep that would otherwise require manual access to each device.

Quick Start
1. Clone and configure
git clone https://github.com/WagoAlex/wago-plc-mcp-server.git
cd wago-plc-mcp-server
cp _env .envEdit .env:
WAGO_PLC_HOSTS=192.168.1.10,192.168.1.11,192.168.1.12
DEFAULT_PLC_USERNAME=admin
PORT=6042
WAGO_TIMEOUT_SECONDS=45For large fleets, useWAGO_PLC_HOSTS_FILE=/app/data/fleet.txt - one IP
per line, # comments supported. Both can be set together; IPs are merged.
2. Set the PLC password
mkdir -p secrets
echo "your-plc-password" > secrets/plc_default_password.txt
chmod 600 secrets/plc_default_password.txt3. Start
docker compose up -d
docker logs wmcp -fOn first boot the server generates an API key and announces its fingerprint (the key itself is never written to container logs). Retrieve it with:
docker exec wmcp cat /app/data/mcp_api_keyOnce you're past initial testing, provision the key as a Docker Secret
instead (seeAPI key management) - then retrieve it
with cat secrets/mcp_api_key.txt directly on the host, no docker exec
required.
════════════════════════════════════════════════════════════════════════
NEW MCP API KEY GENERATED (fingerprint: 7290f42b…)
Stored in ./data/mcp_api_key - retrieve it with:
docker exec wmcp cat /app/data/mcp_api_key
.mcp.json:
"headers": {"Authorization": "Bearer <key>"}
Regenerate: docker exec wmcp python src/mcp_keygen.py
════════════════════════════════════════════════════════════════════════
Registration: 3/3 ready
MCP server listening on http://0.0.0.0:6042/mcp (Streamable HTTP)4. Connect your AI client
Claude Code (one command):
claude mcp add --transport http --header "Authorization: Bearer <key>" wago-plc http://localhost:6042/mcpClaude Desktop - add to %APPDATA%\Claude\claude_desktop_config.json:
{
"mcpServers": {
"wago-plc": {
"type": "http",
"url": "http://localhost:6042/mcp",
"headers": { "Authorization": "Bearer <your-api-key>" }
}
}
}Fully quit and relaunch Claude Desktop. You should see a hammer icon with 14 tools:

5. Install the WAGO skill (recommended)
The bundled skill teaches the assistant the WAGO parameter names, how to operate safely, and how the tools actually behave. It's the difference between a vague answer and one that lands on the right parameter first try.
mkdir -p ~/.claude/skills
cp -r wago-plc-skill ~/.claude/skills/What can I ask it?
You don't need to know any parameter IDs or anything about the WDA API. Just ask for what you want; the assistant works out which tools to call and deals with the REST plumbing for you.
Fleet-wide checks
What you type | What happens |
"Which PLCs are running firmware older than build 31?" | Reads firmware version from every controller in parallel and lists the ones behind |
"Are NTP and Docker running on all Edge Controllers?" | Reads service running-flags across the fleet, highlights stopped services |
"Show the diagnostic LED states on all PLCs" | Reads SYS, RUN, and fieldbus LED text strings from every unit |
"Is any controller showing a fault or error state?" | Cross-checks LED strings and error parameters fleet-wide |
Single-controller diagnostics
What you type | What happens |
"What firmware version is running on 192.168.1.14?" | Reads the firmware version parameter |
"List all network settings on Edge Controller .19" | Searches parameters by keyword, returns names + current values |
"Is the CODESYS program loaded and running on PFC300 .22?" | Reads the CODESYS runtime state parameter |
"What NTP server is configured on PLC .10?" | Reads NTP client configuration |
Configuration changes and remote actions
What you type | What happens |
"Set the NTP server to 192.168.0.1 on all PLCs in building A" | Writes NTP address after your confirmation; every write is recorded in the audit log |
"Trigger an NTP time sync on the three controllers that showed clock drift" | Invokes the NTP sync action only on affected units |
"Enable SSH on controller .14 for remote maintenance access" | Finds the SSH enable parameter and writes it after confirmation |
Ongoing monitoring
What you type | What happens |
"Set up a health monitor for the packaging line PLCs" | Creates a server-side watchlist combining LED states, service flags, and cloud status - one HTTP request per poll cycle |
"Track the firmware update progress on all 12 PLCs" | Polls update status and progress across the fleet |
The assistant asks for confirmation before writing any value to a controller.
Part 2 - For automation / OT engineers
What this actually touches on your PLCs, in terms you already know, and exactly when a write or method call is allowed versus refused.
What this does and doesn't do
For automation and OT engineers
You know PLCs - TIA Portal, Studio 5000, EcoStruxure, ladder logic. Here's the 60-second translation.
What WDA/WDx is: Every WAGO controller exposes a REST API called WDA (WAGO Device Access) for system and diagnostic management - firmware version, network config, service health, status LEDs, reboot and firmware-update control. Think of it as the machine-readable equivalent of TIA Portal's Online & Diagnostics view or Studio 5000's Controller Properties - not a fieldbus, not OPC-UA, and not access to your control program's I/O data.
What MCP is: A standard protocol that lets an AI assistant call a fixed set of defined tools against a system, instead of you writing custom integration code for every request. This server turns the WDA REST API into 14 tools an AI assistant can call directly.
Term | Plain meaning | Closest thing you already know |
WDA / WDx | WAGO's REST API for system/diagnostic management | TIA Portal Online & Diagnostics, Studio 5000 Controller Properties |
MCP | Protocol letting an AI assistant call a fixed set of "tools" | A structured API contract invoked by an LLM instead of your own code |
Parameter | A single named system value (firmware version, LED state, service flag) | A diagnostic/status tag - not a control-program I/O tag |
Method | A remote action you can trigger (NTP sync, reboot, firmware update) | An RPC / "execute" command, similar to an online action in TIA/Studio 5000 |
Watchlist | A server-side list of parameters the PLC keeps open for cheap repeated reads | Closest analog: a Watch Table (TIA) or Trend window (Studio 5000) - polled by an agent |
What this does NOT do:
It is WAGO-only - no Siemens S7, Rockwell Logix, or Schneider Modicon.
It does not read or write your control program's I/O tags, real-time process values, or PLC memory. Field I/O still goes through OPC-UA, Modbus TCP, or WAGO I/O-Check.
It is not an HMI/SCADA replacement - no graphical front end, just tool calls an AI assistant makes on your behalf.
What values can actually be monitored
WDA exposes the system management layer, not the real-time process image. What it does expose as live, poll-worthy values:
Category | Example parameters | Typical use |
Service health |
| Detect silently stopped services |
LED & fault state |
| Mirror physical status LEDs; surface diagnostic text without physical access |
Firmware update |
| Track OTA update progress across a fleet |
CODESYS runtime |
| Confirm a PLC program is loaded and running |
Cloud connectivity |
| Monitor MQTT broker reachability and queue depth |
System time |
| Verify clock synchronisation after NTP updates |
Supported hardware
Device | Article Numbers | Notes |
CC100 |
| Slow ARM CPU - set |
PFC100 Gen 2 |
| |
PFC200 Gen 2 |
| |
PFC300 |
| |
Edge Controller |
| Exposes CODESYS runtime state via |
WP400 |
| Web panel only - 189 WDA params, no CODESYS. HMI params: display brightness/orientation/screensaver, integrated browser startpage, touch cleaning mode |
TP600 |
| Full PLC+HMI - 410 WDA params. CODESYS3, BACnet, cloud, serial, all WP400 HMI params plus front LED and acoustic feedback |
Requires firmware build ≥ 28 (FW28). Tested up to 04.09.01 (FW31).
How reads and writes work
Every operation an agent can perform falls into exactly one of three classes. These are mutually exclusive and cover everything the server can do - there is no fourth kind of action.
Class | Tools | Changes the PLC? |
Read |
| No |
Write a parameter |
| Yes - changes a stored config value |
Invoke a method |
| Yes - triggers an action (NTP sync, reboot, firmware update, ...) |
Standard behavior (default config: live mode, no read-only hosts)
Reads are always allowed. They have no side effects and are never gated.
Parameter writes are allowed when the parameter is writeable. The server pre-checks writeability from its cache and refuses values the firmware marks read-only for that device/firmware, before any HTTP call reaches the PLC.
Safe method calls are allowed. Anything that is not on the dangerous list below runs directly.
Dangerous methods are denied. Method IDs whose segments start with
reboot,restart,factory,firmware, orformatare refused unless you explicitly allowlist the exact ID.
When a write or method call is allowed
The outcome is decided by three independent conditions. Read-only status takes precedence over everything else; otherwise the server mode decides.
Condition | Read |
| Safe | Dangerous |
Read-only PLC ( | Allowed | Refused | Refused | Refused |
Live mode ( | Allowed | Allowed if writeable | Allowed | Denied unless ID in |
GitOps mode ( | Allowed | Returns a PR YAML fragment (no direct write) | Returns a PR YAML fragment | Returns a PR YAML flagged |
Read it top-down: if the PLC is read-only, stop there - nothing is written. If not, the active mode determines whether a write happens directly (live) or becomes a reviewed pull request (GitOps).
Every write and every method call - allowed, refused, or denied - is recorded in the tamper-evident audit log. For the rationale behind the dangerous-method and read-only gates, see Safety gates.
Part 3 - For software / DevOps engineers
Deploying this in production, wiring up GitOps, securing the endpoint, and the full tool/config reference.
Production deployment
Deployment options
Path | Best for | Requires |
Plant server, shared multi-user fleet | Docker host on the OT network | |
OT engineer laptop, air-gapped Windows | Nothing - zero dependencies | |
Developer machine, any OS |
| |
Cursor, VS Code + Copilot |
| |
ChatGPT, OpenAI API, n8n | Running server reachable over network |
Config file examples for every path: deploy/configs/
Docker (recommended)
One server, many clients. PLCs register once at startup and stay connected.
cp _env .env # edit PLC IPs, password, API key
docker compose up -dConnect any client to http://<host>:6042/mcp with Authorization: Bearer <key>.
Large fleet - host file:
# data/fleet.txt
# Production floor A
192.168.1.10
192.168.1.11
# Production floor B
192.168.2.10
# 192.168.2.11 decommissionedWAGO_PLC_HOSTS_FILE=/app/data/fleet.txtFleet changes require editing the file and restarting the container. The
audit log persists across restarts on the ./data volume.
Windows .exe
Self-contained bundle - no Python, no package manager.
deploy\windows\build.bat # build once on any Windows machine with Python 3.11+
deploy\windows\setup.bat # configure .env and get the Claude Desktop JSON snippet%APPDATA%\Claude\claude_desktop_config.json:
{
"mcpServers": {
"wago-plc": {
"command": "C:\\wago-mcp\\wago-proxy.exe",
"env": {
"WAGO_MCP_URL": "http://localhost:6042/mcp",
"WAGO_MCP_API_KEY": "your-api-key"
}
}
}
}
uvx / PyPI
Runs the full server locally in stdio mode - no Docker, no proxy, no persistent process. Starts fresh each Claude session (PLCs re-register on connect, adds a few seconds).
Requires: uv
%APPDATA%\Claude\claude_desktop_config.json:
{
"mcpServers": {
"wago-plc": {
"command": "uvx",
"args": ["wago-plc-mcp-server"],
"env": {
"TRANSPORT": "stdio",
"WAGO_PLC_HOSTS": "192.168.1.10,192.168.1.11",
"DEFAULT_PLC_USERNAME": "admin",
"DEFAULT_PLC_PASSWORD": "wago",
"WAGO_TIMEOUT_SECONDS": "45",
"LOG_LEVEL": "WARNING"
}
}
}
}Prefer Docker for fleets > 20 PLCs to avoid per-session re-registration.
IDE (Cursor, VS Code)
Cursor - .cursor/mcp.json in the project root:
{
"servers": {
"wago-plc": {
"command": "uvx",
"args": ["wago-plc-mcp-server"],
"env": {
"TRANSPORT": "stdio",
"WAGO_PLC_HOSTS": "192.168.1.10",
"DEFAULT_PLC_USERNAME": "admin",
"DEFAULT_PLC_PASSWORD": "wago"
}
}
}
}VS Code + Copilot - .vscode/mcp.json, same structure.
HTTP remote (ChatGPT API, n8n, OpenAI)
Any client that supports MCP over HTTP connects to http://<host>:6042/mcp
with Authorization: Bearer <key>. For legacy SSE clients set
TRANSPORT=sse in .env and point at /sse.
OpenAI Responses API:
response = client.responses.create(
model="gpt-4o",
tools=[{
"type": "mcp",
"server_url": "http://plc-gateway.plant.internal:6042/mcp",
"server_label": "wago-plc",
"headers": {"Authorization": "Bearer <your-api-key>"}
}],
input="List all PLCs and their firmware versions."
)Skills - install the right one
Three skills ship with this repo - install the one that matches your use case:
Skill | For | Install |
Claude Desktop / Claude Code end users - plain-English interaction, safety guidance, troubleshooting |
| |
Autonomous agents / pipelines - tool I/O contracts, error shapes, retry rules, watchlist lifecycle |
| |
Contributors to this repo - raw WDA HTTP behaviour, pagination encoding, payload shapes |
|
GitOps write-gate
For production environments where every PLC configuration change needs a human-reviewed audit trail before it reaches hardware - an ArgoCD-style pattern applied to PLCs.
How it works
Agent proposes a config change
│
▼ (GITOPS_MODE=1)
set_parameters / invoke_method returns YAML instead of writing
│
▼
Agent commits YAML to wago-plc-config repo and opens a PR
│
▼
Engineer reviews and approves the PR
│
▼
CI runs: python scripts/apply.py plcs/192.168.1.10.yaml --execute
│
▼
Live PLC updated - ops files self-delete on successThe CI step is a GitHub Actions workflow living in the config repo, not here -
it decides dry-run vs. execute purely from the triggering event, never from a
flag you set: a pull request always dry-runs (prints drift, touches nothing),
and only a push to main (i.e. a merge) executes. It runs on a self-hosted
runner because GitHub-hosted runners have no route to the PLC subnet, and it
borrows scripts/apply.py from this repo via sparse-checkout on every run - a
fix here is picked up there without a version bump. Full mechanics (trigger
table, checkout steps, secrets, why contents: write is needed for the
ops-file-cleanup commit): wago-plc-config README - How the GitHub Actions
workflow works.
Enable
GITOPS_MODE=1 # intercept writes; return YAML fragments for PR
GITOPS_MODE=0 # default: write directly (still fully audit-logged)
# Only needed if your config repo isn't named/owned wago-plc-config -
# every returned YAML fragment's next_step points the agent at this repo.
WAGO_GITOPS_REPO=wago-plc-configWithoutWAGO_GITOPS_REPO set correctly, the agent has no other way to know
where to commit the YAML fragment - the repo name comes from this variable,
not from any auto-discovery. If you fork or rename the config repo, set this
or the returned next_step instructions will point at the wrong (or a
nonexistent) repo.
Config YAML - two file types
plcs/<ip>.yaml - desired steady state
plc_ip: 192.168.1.10
managed_parameters:
0-0-ntpclient-enabled: true
0-0-ntpclient-configuredtimeservers:
- 192.168.1.1
0-0-snmp-enable: true
0-0-snmp-communities-1-name: ops-team
0-0-snmp-location: Building-A-Panel-3apply.py reads the live PLC, diffs it against this file, and patches only
parameters that have drifted. Idempotent - safe to run in CI on every merge.
ops/<id>.yaml - one-shot action (self-deletes on success)
id: b7d3e1f9
proposed_at: 2026-06-21T10:00:00+00:00
proposed_by: agent-claude-code
plc_ip: 192.168.1.10
action: invoke_method
method_id: 0-0-ntpclient-updatetime
arguments: {}Apply manually
# Show what would change - no writes
python scripts/apply.py plcs/192.168.1.10.yaml
# Apply drift to live PLC
python scripts/apply.py plcs/192.168.1.10.yaml --execute
# Invoke a one-shot method
python scripts/apply.py ops/b7d3e1f9.yaml --executeSupported subsystems (parameter IDs)
Subsystem | Key parameters | Helper |
Cloud / MQTT |
|
|
NTP |
|
|
SNMP |
|
|
Serial port |
|
|
OpenVPN |
|
|
HMI browser |
|
|
FTP / FTPS |
| direct |
SSH |
| direct |
Docker |
| direct |
CODESYS 3 webserver |
| direct |
Full parameter ID reference with YAML examples for every subsystem:
docs/gitops/README.md
The config repo that receives these YAML fragments and runs apply.py via CI:
github.com/WagoAlex/wago-plc-config
Safety gates - guarding against a rogue agent
The risk with giving an AI agent write access to industrial controllers is not just "it might delete something" - it's that an agent can go off-script (hallucination, prompt injection, a bug) and take a high-consequence action you never wanted. On a production line, a config change with side effects or a badly-timed reboot can mean equipment damage or worse. These gates are enforced in code and cannot be overridden by the agent:
Gate | What it does | Configure |
Read-only PLCs | Listed controllers reject all writes and method calls, in every mode |
|
Dangerous-method denylist | Reboot / restart / factory-reset / firmware / format are denied in live mode unless explicitly allowlisted |
|
Human approval for dangerous ops | In GitOps mode these become a PR flagged | set |
The intended path for any high-consequence action is therefore a human-reviewed
PR plus an audit-log entry - not an autonomous tool call. A denial is the
system working as designed. Full details and a dry-run walkthrough:
docs/gitops/README.md → Safety model
Step-by-step guide for reviewers (GitHub UI and CLI): github.com/WagoAlex/wago-plc-config
Security
API key management
The server resolves the MCP API key in priority order:
Docker Secret
/run/secrets/mcp_api_key- recommended for productionEnv var
MCP_API_KEYPersisted file
./data/mcp_api_key- auto-generated on first boot, survives container recreationsAuto-generate - new key if none of the above exist
Retrieving the key differs by source. With a Docker Secret (path 1), readsecrets/mcp_api_key.txt directly on the host - no docker exec needed, so
the key never crosses the container boundary or touches any container-side
log path:
cat secrets/mcp_api_key.txtWith the auto-generated key (path 3/4), it only exists inside the
container's /app/data volume:
docker exec wmcp cat /app/data/mcp_api_keyPrefer the Docker Secret path once you've moved past initial testing - it's both more auditable (key lifecycle lives in a file you control, not a volume the server writes to) and keeps the key out of any container-exec trail entirely.
# Regenerate (only affects the auto-generated/persisted key - has no effect
# if a Docker Secret or MCP_API_KEY env var is set, since those outrank it)
docker exec wmcp python src/mcp_keygen.py
docker restart wmcpTLS configuration
Both TLS legs are opt-in. The server starts without TLS and logs a startup warning for each disabled leg.
WDA connections (server → PLC) - three options:
# Option A: Per-PLC cert pinning (recommended for self-signed certs)
openssl s_client -connect 192.168.1.10:443 </dev/null 2>/dev/null \
| openssl x509 > secrets/plc_cert_192_168_1_10
# Declare the secret in docker-compose.yml and restart
# Option B: Private CA bundle
WAGO_TLS_CA=/run/secrets/wago_ca.pem
# Option C: System trust store (only if PLC certs are CA-signed)
WAGO_TLS_CA=trueMCP endpoint (client → server):
openssl req -x509 -newkey rsa:4096 \
-keyout secrets/mcp_tls_key.pem \
-out secrets/mcp_tls_cert.pem \
-days 365 -nodes -subj "/CN=wago-mcp"MCP_TLS_CERT=/run/secrets/mcp_tls_cert
MCP_TLS_KEY=/run/secrets/mcp_tls_keyAudit log
Every set_parameters and invoke_method call is appended to a
tamper-evident hash-chained JSON-lines log:
Entry 1 {"ts":"…","action":"set_parameters",…,"prev":"0000…0000"} ← genesis
Entry 2 {"ts":"…","action":"invoke_method",…,"prev":"a3f1…c2d8"}
Entry 3 {"ts":"…","action":"set_parameters",…,"prev":"7b2e…91fa"}Each entry includes timestamp, PLC IP, parameter IDs + values, and
key-<first 8 chars of API key> for per-engineer traceability.
# Tail live log
docker exec wmcp tail -f /app/audit.log
# Verify chain integrity
docker exec wmcp python src/audit_verify.py
# → [PASS] Chain intact - 42 entries verified (/app/audit.log)Security feature summary
Feature | Status |
Bearer auth on | ✅ Auto-generated key; Docker Secret + env override; |
Rate limiting | ✅ 60 req/60 s per source IP; |
Auth failure alerts | ✅ WARNING per failure; ERROR at 10 consecutive from same IP |
WDA Bearer token auth | ✅ Credentials sent once; cached token refreshed on 401 |
Hash-chained audit log | ✅ Tamper-evident JSON-lines on |
Default password warning | ✅ Startup WARNING if factory default password detected |
TLS - WDA connections | ⚙️ Off by default; enable with |
TLS - MCP endpoint | ⚙️ Off by default; enable with |
CycloneDX SBOM | ✅ Published alongside every release image |
Docker Secrets | ✅ PLC passwords, MCP key, TLS certs all mountable as secrets |
CVE scanning | ✅ Weekly grype scan on SBOM; HIGH/CRITICAL fails CI |
For the vulnerability disclosure policy, patch SLA, and support lifetime see SECURITY.md.
Tool reference
Discovery
Tool | Description |
| List all registered PLC IPs |
| Capability counts + feature names + |
| Read recent tamper-evident audit log entries; filter by PLC and/or action, newest first (max 500) |
Parameters
Tool | Description |
| Search by keyword (up to 100 results) |
| Read one value, enum labels resolved |
| Read one param from N PLCs in parallel |
| Write one or more parameters (bulk PATCH) |
Methods
Tool | Description |
| Search by keyword |
| Fetch inArgs/outArgs schema |
| Execute sync or async |
| Poll async run status |
Watchlists
Tool | Description |
| Register a server-side monitoring list on the PLC |
| Return current values for all watched parameters (one HTTP request) |
| Release the watchlist immediately |
Why watchlists exist: Every get_parameter call opens a new HTTPS connection. For repeated polling of a fixed set across a fleet, the overhead compounds: 10 parameters × 15 PLCs every 30 seconds = 150 HTTPS round-trips per cycle. Watchlists solve this - one read_watchlist returns all current values in a single request.
Example workflows
Read firmware version from all PLCs in one call:
get_parameters_bulk([
{"plc_ip": "192.168.1.10", "parameter_id": "0-0-version-firmwareversion"},
{"plc_ip": "192.168.1.11", "parameter_id": "0-0-version-firmwareversion"}
])Sync NTP time on a PLC:
find_methods("192.168.1.10", "ntp")
→ ["0-0-ntpclient-updatetime"]
invoke_method("192.168.1.10", "0-0-ntpclient-updatetime", wait=True)
→ {"status": "done", "run_id": "1", "out_args": {}}Poll operational health with a watchlist:
create_watchlist("192.168.1.10", [
"0-0-ledstates-1-diagnosticinformation",
"0-0-ledstates-4-diagnosticinformation",
"0-0-ntpclient-isrunning",
"0-0-docker-isrunning",
"0-0-cloudconnections-1-status-connected"
], timeout_seconds=300)
read_watchlist("192.168.1.10", "1") # call every 30 s
delete_watchlist("192.168.1.10", "1") # explicit cleanup when doneConfiguration reference
Variable | Default | Description |
| - | Comma-separated PLC IPs |
| - | Path to host file (one IP per line) |
|
| Shared username |
|
| Shared password (use Docker Secret instead) |
| - | Per-PLC password override |
| - | Bearer token for |
|
|
|
|
| Config repo name/path shown in the returned YAML's |
| - | Comma-separated PLC IPs that refuse |
| - | Comma-separated exact method IDs to re-allow from the dangerous-method denylist in live mode |
| - | WDA TLS: |
| - | Path to TLS cert for MCP endpoint |
| - | Path to TLS private key for MCP endpoint |
| - | Password for encrypted TLS private key (optional) |
|
| Audit log path inside container |
| - | Syslog/SIEM receiver hostname; enables audit forwarding |
|
| Syslog receiver port |
|
|
|
|
|
|
|
| Bind address |
|
| Listen port |
|
| Per-PLC HTTP timeout (CC100 needs 45+) |
|
| WDA pagination page size |
|
| Parallel PLC init limit |
|
| Parallel PLC request limit inside |
|
|
|
|
| Debug log path inside container |
Reference
Cross-cutting material that isn't specific to any one persona - come back to these as needed.
Frequently asked questions
Can the AI modify my control program or process I/O values?
No. The WDA REST API has no access to the CODESYS runtime, PLC variables, fieldbus I/O, or anything in your control program. Field I/O still goes through OPC-UA, Modbus TCP, or WAGO I/O-Check.
What if the AI writes a wrong value?
Every write is recorded in the tamper-evident audit log with timestamp, parameter ID, value written, and which API key made the change. For most WDA parameters a wrong value is correctable by writing the correct value again. For disruptive actions like firmware update or reboot, the assistant asks for explicit confirmation before executing.
Does the server need internet access after initial setup?
No. All traffic is local: AI client → MCP server (port 6042) → PLCs (port 443 HTTPS). No cloud calls, no telemetry. Suitable for air-gapped OT networks once the Docker image has been transferred to the host.
Our PLCs have different passwords. How do we configure that?
DEFAULT_PLC_PASSWORD=wago # applied to all PLCs unless overridden
PLC_PASSWORDS_192_168_1_11=secret # override for this unit (IP with underscores)What firewall rules does IT need to open?
Direction | Source | Destination | Port | Protocol |
Inbound | Engineer workstations | MCP server host | 6042 | TCP |
Outbound | MCP server host | WAGO PLC IPs | 443 | TCP (HTTPS) |
Can multiple engineers share one server?
Yes. Deploy one container on a host reachable from the OT network. Each
engineer connects their own client to http://<server>:6042/mcp. Use a
shared API key, or provision individual keys per engineer for per-person
traceability in the audit log.
Which firmware version is required?
Firmware build ≥ 28 (04.xx.xx(28) or later). Check the build number in
the controller's web interface under Device Information, or ask the
assistant: "What firmware version is PLC 192.168.x.x running?"
Fetching raw parameter data (curl)
For bulk exports, debugging, or building contract-test cassettes, bypass the
MCP layer and query WDA directly. WDA hard-caps at 255 entries per page -
most device classes need two pages. Always include
parameter-errors-as-data-attributes=true.
IP=192.168.1.10
OUT=wda-parameters-${IP}.json
{
curl -sk -u "admin:wago" -H "Accept: application/vnd.api+json" --max-time 90 \
-G --data-urlencode "parameter-errors-as-data-attributes=true" \
--data-urlencode "page[limit]=255" \
--data-urlencode "page[offset]=0" \
"https://${IP}/wda/parameters"
curl -sk -u "admin:wago" -H "Accept: application/vnd.api+json" --max-time 90 \
-G --data-urlencode "parameter-errors-as-data-attributes=true" \
--data-urlencode "page[limit]=255" \
--data-urlencode "page[offset]=255" \
"https://${IP}/wda/parameters"
} | jq -s '{data: (map(.data) | add)}' > "$OUT"
echo "Saved $(jq '.data | length' "$OUT") parameters to $OUT"page[limit] and page[offset] must be passed via --data-urlencode - embedding literal brackets in the URL string is silently ignored and causes an infinite page-0 loop.
Requirements
Docker 24+ with Compose v2
WAGO PLC with WDx/WDA REST API enabled (firmware build ≥ 28)
Network route from Docker host to PLC subnets
For Claude Desktop proxy: Python 3.11+ and fastmcp on the client machine.
Security & CRA compliance
This project targets compliance with the EU Cyber Resilience Act (Regulation 2024/2847).
Document | Purpose |
Vulnerability reporting, patch SLA, support lifetime | |
STRIDE risk assessment | |
Annex I requirements → evidence mapping | |
CRA Article 28 self-declaration | |
CRA Article 31 technical file index |
Releases
Pre-built images on Docker Hub. A CycloneDX SBOM is published alongside every release. docker compose up -d pulls the latest automatically.
License
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Latest Blog Posts
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/WagoAlex/wago-plc-mcp-server'
If you have feedback or need assistance with the MCP directory API, please join our Discord server