industrial-mcp
An MCP server that gives AI hosts read access to industrial grain-facility data and safety-gated motor control.
List facilities and assets —
list_plants,list_silos,list_motors(optionally filtered by kind).Inspect sensor health —
get_silo_thermometryreturns fill, cable count, and min/avg/max grain temperature in °C.Check alerts —
get_active_alertslists active alerts, optionally filtered by severity.Control motors safely —
trigger_motor_actionstarts/stops motors; dry-run by default, requiresdry_run=False,operator_id,reason, and server-levelINDUSTRIAL_MCP_ALLOW_WRITES=trueto execute.Audit executed actions — successful motor commands are appended to an append-only JSONL audit log; dry runs are not logged.
Use different data sources — ships with a deterministic mock adapter and supports a live ESP32 thermometry adapter; unknown adapters cause startup failure rather than silent fallback.
Click on "Deploy 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., "@industrial-mcp What are the active alerts in plant 2?"
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.
industrial-mcp
An MCP server that gives Claude (or any MCP-compatible AI host) read access to industrial sensor data and safety-gated control over motors and actuators.
The hard part of Physical AI isn't getting a language model to talk about a factory — it's getting it to act on one without anyone losing sleep. That requires three boring things working together:
Tool schemas the model can understand and call correctly.
Safety preconditions that block dangerous actions even when the model is confident.
An audit trail that survives the next post-mortem.
This repo is a working implementation of all three, in under 500 lines of Python, using FastMCP. Default mode is read-only and ships with a deterministic mock adapter modeling a 7-silo grain storage facility, so it runs in CI with no infrastructure.
The read path is verified end to end against real hardware: an
ESP32-S3 with DS18B20 probes on a OneWire mux, answering a model's
questions about grain temperature over the LAN. Warming one probe by
hand moved max_temp_c from 28.0 to 31.0 — and dropped the average,
because the other probe was in cold water. That measurement is why
these tools return min, max, and faulted_sensors instead of one
reassuring number. Evidence, including what is not verified:
docs/hardware-verification.md
(español).
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ MCP host (Claude Desktop, Claude Code, Cursor, etc.) │
│ user prompt ──► model decides which tool to call │
└──────────────────────────────┬──────────────────────────────────────┘
│ stdio (JSON-RPC over MCP)
▼
┌─────────────────────────────────────────────────────────────────────┐
│ industrial-mcp (this repo) │
│ │
│ server.py ── wires tools, env config, audit log │
│ │ │
│ ├── tools.py ── 5 read tools + 1 write tool (dry-run default) │
│ │ │
│ ├── safety.py ── preconditions, advisory warnings │
│ │ │
│ ├── audit.py ── append-only JSONL of executed actions │
│ │ │
│ └── adapters/ │
│ ├── base.py ← PlantAdapter protocol (the contract) │
│ ├── mock.py ← default, deterministic demo data │
│ └── esp32.py ← HTTP to a live SiloScan module │
└──────────────────────────────┬──────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
[MQTT broker] [internal REST API] [historian / TSDB]Related MCP server: MCP PLC Core
Quick start
Run the server, talk to it from Claude Desktop, see it work.
# 1. Run the server with the mock adapter (no infra required)
uvx industrial-mcp@latest
# 2. Add this to ~/Library/Application Support/Claude/claude_desktop_config.json
# (macOS) — see examples/claude-desktop-config.jsonThen in Claude Desktop:
¿Qué silos tienen alerta crítica hoy?
Claude calls list_plants → get_active_alerts → answers with the
data. See examples/demo-transcript.md
for a full session.
To enable live (non-dry-run) execution against the mock adapter:
INDUSTRIAL_MCP_ALLOW_WRITES=true uvx industrial-mcp@latestThis flag does not affect dry runs — those always work. It only unlocks the path where a tool call would actually mutate state.
Tools exposed over MCP
Tool | Type | Purpose |
| read | List facilities the server can see. |
| read | Silos at a plant with capacity in tons. |
| read | Latest thermometry snapshot (min/avg/max °C). |
| read | Motors at a plant, optionally filtered by kind. |
| read | Active alerts, filterable by severity. |
| read | Find modules on the LAN when one stopped answering. Never writes config. |
| write | Start/stop a motor — dry-run by default, safety-gated, audited. |
Tool schemas live in src/industrial_mcp/tools.py.
Keep their docstrings short and exact — they become the model's tool
descriptions.
Why three layers of safety, not one
trigger_motor_action will only execute when all of the following
hold:
The LLM explicitly sets
dry_run=False.The call includes an
operator_idand areason.The server itself was started with
INDUSTRIAL_MCP_ALLOW_WRITES=true.Every precondition in
safety.evaluate_motor_actionpasses.
Step 3 is the one that matters most. The model can hallucinate
dry_run=False; the operator field can be spoofed by a clever prompt;
the safety check can have a bug. But if the server was started
read-only, none of that touches a motor. The deploy posture is the
last word, not the prompt.
Every executed call is appended to an append-only JSONL audit log:
{"ts": 1779600000.12, "actor": "op-42", "action": "motor.start",
"target": "fan-7-1", "outcome": "applied",
"details": {"reason": "silo-7 at 32.1°C, manual fan-on"}}Dry runs are not logged. They're not interesting and they'd dilute the signal.
Adapters
The contract every adapter must satisfy lives in
src/industrial_mcp/adapters/base.py as a PlantAdapter Protocol —
list_plants, list_silos, get_silo_thermometry, list_motors,
get_motor, get_plant_context, get_active_alerts,
apply_motor_action. Motor records must carry plant_id; the tool
layer resolves plant context from the motor rather than from a
constant, so a motor without one is refused instead of evaluated
against the wrong plant.
INDUSTRIAL_MCP_ADAPTER selects which one runs. It knows mock
(deterministic, shipped, used by CI) and esp32 (HTTP to a live
SiloScan thermometry module). An unrecognized name raises at startup —
the server never falls back to the mock, because answering questions
about a plant you are not connected to is worse than refusing to start.
INDUSTRIAL_MCP_ADAPTER=esp32 \
INDUSTRIAL_MCP_ESP32_HOST=banco-silo3.local \
INDUSTRIAL_MCP_ESP32_DEVICE_ID=banco-silo3 \
INDUSTRIAL_MCP_ESP32_CABLES=0,8 \
uv run industrial-mcpINDUSTRIAL_MCP_ESP32_CABLES defaults to channel 0 alone — each
channel costs a OneWire conversion, so the default is cheap rather than
complete. Leaving it out silently drops every other probe.
Use a name, not an address. Firmware 0.5.0+ publishes
<device_id>.local over mDNS. DHCP will move the address out from under
any file you write it into — the bench module took three different
leases in three reboots, and every stale config in this repo's history
traces back to that.
INDUSTRIAL_MCP_ESP32_DEVICE_ID is optional and turns on self-healing:
when the configured host stops answering, the adapter looks the module
up by name, then by subnet sweep, and adopts the new address only if
the device identifies itself as that id. Without it the adapter
reports device_unreachable instead of guessing — a sweep cannot tell
"my module moved" from "some other module answered", and silo 3's tools
reading silo 7's sensors would produce numbers that look perfectly
reasonable and come from the wrong bin.
The scan_devices tool exposes the same sweep to the model, read-only:
it reports what answered and never edits configuration. Scanning is
restricted to private address space and to ranges of /22 or smaller.
Two things the esp32 adapter does that are worth copying into your
own adapter, both of them about not laundering a sensor fault into a
plausible number:
A reading counts only when the firmware classified it
validand the temperature is actually present. Averaging a missing reading as 0 °C is how a hot silo reads cool.An unreachable module returns
{"error": "device_unreachable"}, never an exception and never a stale number wearing a fresh timestamp. The timestamp comes from the machine doing the read, because the module's clock is only right after an NTP sync.
To talk to a different plant, drop a sibling module implementing the
Protocol and add its name to build_adapter in server.py.
Development
git clone https://github.com/brayangcastro/mcp-industrial-agent
cd mcp-industrial-agent
uv sync --extra dev
uv run pytest -v
uv run ruff check src testsCI runs the same two commands on Python 3.11 and 3.12 — see
.github/workflows/ci.yml.
FAQ
Why not just give the model raw API keys? Because then every prompt injection is one HTTP call away from a production motor. The MCP surface is narrow on purpose: the model sees six functions, not your AWS console.
Why a separate dry_run flag instead of a confirmation step?
Confirmation steps add latency and break flow. The dry run returns
the outcome the model would have caused — preconditions, warnings,
state delta — as data the model can keep reasoning over. Live
execution is then a one-line escalation, not a five-prompt dance.
Is the audit log enough for compliance? No. It's enough to reconstruct what happened. It is not enough on its own for IEC 62443, IATF 16949, or similar. Pair it with your plant's existing change-management system; this repo is a starting point, not a finished compliance story.
Is this only a mock? Where's MQTT / OPC UA?
No longer only a mock — the esp32 adapter reads a live device over
HTTP, and the read path is verified against it. MQTT and OPC UA are
still absent, and they are usually plant-specific anyway: the broker
URLs, topic structures, and ACLs you can publish publicly are usually
zero. The PlantAdapter Protocol is the extension point.
So the safety gate is proven against real equipment?
Against a real pin, yes. Firmware 0.4.0 exposes /api/relay, and
the four-step escalation was run against it: dry run left the pin low,
a call without operator_id was rejected with the pin low, a read-only
server rejected it with the pin low, and only the fourth call drove it
high. Be precise about the limit, though — that is a logic-level GPIO
with an LED on it, not a three-phase fan with inrush current and
interlocks. What was proven is that the gate stops a real actuator from
moving; motor sequencing is a different problem.
How do you know the relay actually switched?
Because the module is asked, not trusted. GET /api/relay reports the
level read back off the pad next to the command it was given, and
apply_motor_action decides success from the reading. A 200 response
with the pin still low comes back as a fault. An actuator that only
repeats the order it was given is not telemetry, it is an echo.
Status
Read and write paths both verified against hardware. Not 1.0 — the shapes are stable, but expect breaking changes in non-public APIs.
Area | State |
| Shipped, deterministic, used by CI |
| Verified against a live module — evidence |
Safety gate + audit log | Verified against a physical pin — dry run and both rejections left it low |
Actuator fault detection ( | Triggered on hardware — feedback wire pulled; safety refused with every gate open |
Real motor (inrush, interlocks) | Not attempted — the verified actuator is a GPIO pair with an LED |
MQTT / OPC UA adapters | Not started |
Author
Built and maintained by Brayan Castro — Ing.
Mecatrónica (ITESM Sonora Norte), operating BC Ingeniería from Guasave,
Sinaloa. Background: 4+ years building IoT systems for agroindustrial
grain handling (firmware ESP32 + thermocouple MUX + cloud), backend
services for property management and POS, and conversational AI agents
on top of Claude / GPT-4o. Reach out: info@ingebc.com.
License
MIT — see LICENSE.
Available Tools
6 toolsget_active_alertsA
List active alerts at a plant, optionally filtered by minimum severity.
Inputs: plant_id (required), min_severity. Returns: result
get_silo_thermometryA
Return the latest thermometry snapshot for one silo.
Includes fill percent, cable count, and min/max/avg grain temperature in Celsius. Use this when the operator asks about a specific silo's health.
Inputs: silo_id (required). Returns: object
list_motorsA
List motors at a plant. Optionally filter by kind.
Inputs: plant_id (required), kind. Returns: result
list_plantsA
List industrial facilities the server can see.
Inputs: no named parameters. Returns: result
list_silosB
List grain silos at a plant, with capacity in metric tons.
Inputs: plant_id (required). Returns: result
trigger_motor_actionA
Request a start or stop of an industrial motor.
Defaults to a DRY RUN: returns what would happen, evaluates safety preconditions, and lists warnings — without sending any command to the field. To actually execute:
Pass
dry_run=False.Pass
operator_id(recorded in the audit log).Pass
reason(short free-text justification).The server itself must be started with
INDUSTRIAL_MCP_ALLOW_WRITES=true; otherwise the call is rejected even when the LLM sets the flags.
Every executed command is appended to an audit log; dry-run calls are not logged.
Inputs: action (required), motor_id (required), reason, dry_run, +1 more. Returns: object
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 6 tools
Each tool targets a distinct resource and action: plants, silos, thermometry, motors, alerts, and motor actions. The only possible overlap is between list_silos and get_silo_thermometry, but one lists silos and the other retrieves data for a specific silo, so there is no ambiguity.
All tool names follow a consistent verb_noun pattern: list_plants, list_silos, list_motors, get_active_alerts, get_silo_thermometry, trigger_motor_action. The verbs are clear and the style is uniformly snake_case, making the set predictable and easy to navigate.
With 6 tools, the server is well-scoped for industrial monitoring and control. Each tool serves a clear purpose without excessive overlap or unnecessary additions, and the count is neither too thin nor too heavy for the domain.
The tool set covers the core workflows: listing assets, retrieving detailed sensor data, checking alerts, and issuing motor control commands with safety checks. Minor gaps exist, such as no dedicated tool for getting a single motor's detailed status or acknowledging alerts, but these do not critically hinder agent operations.
Maintenance
Related MCP Connectors
MCP server unifying ERPs, CRMs, APIs and knowledge base for Claude, ChatGPT and Gemini.
An MCP server that provides an API to LLMs to manage their JumpCloud resources.
Cloud-hosted MCP server for durable AI memory
Hosted MCP server for live public-data APIs and Skills for AI agents.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA local MCP server that connects Claude Code to your work environment through auditable tools for file operations, API calls, and command execution, with safety gates and configuration.-
- AlicenseBqualityCmaintenanceUniversal MCP server for industrial PLC communication, enabling AI agents to read sensors, alarms, status, setpoints, and write setpoints via adapters for Modbus, S7, or custom PLCs.6MIT
- AlicenseAqualityDmaintenanceMCP server for industrial PLC integration, enabling AI to read tags, monitor alarms, and interact with Allen-Bradley ControlLogix PLCs via natural language.9MIT
- AlicenseNot gradedqualityDmaintenanceThe first and only MCP server for PLC (Programmable Logic Controller) intelligence. Give any AI agent direct access to industrial automation data — ladder logic, tag databases, cross-references, fault root cause analysis, and sequence blockersMIT