All writing
AI3 min read

Build an MCP server that answers questions about your CNCs

An AI agent is only as useful as the data you connect it to. Here's how to build a read-only MCP server over your machine data — and the guardrails that keep it from doing anything it shouldn't.

The rear of a networked server rack with cabling

The previous post covered what an MCP server is and why manufacturing is full of exactly the disconnected data it's built for. This one is the build: a small, read-only MCP server that lets an AI answer real questions about your machines — and, just as importantly, the guardrails that keep it safe on a shop network.

The architecture

It's three layers. Your data source (FOCAS, MTConnect, the ERP database). An MCP server that exposes a few tools — plain functions like get_machine_status or list_late_jobs. And an LLM host (a desktop assistant, an IDE, or your own agent) that connects to the server and calls those tools when a question needs them. The AI never touches your machine directly; it only ever calls the functions you wrote.

from mcp.server.fastmcp import FastMCP
from focas_reader import read_status   # your FOCAS wrapper

mcp = FastMCP('shop-floor')

@mcp.tool()
def get_machine_status(machine: str) -> dict:
    """Return live status for one machine: running/idle, feed, rpm, alarm."""
    ip = MACHINES[machine]           # a fixed, allow-listed lookup
    return read_status(ip)           # read-only FOCAS call

@mcp.tool()
def list_late_jobs() -> list[dict]:
    """Jobs whose due date has passed and aren't complete."""
    return db.query('SELECT * FROM jobs WHERE due < NOW() AND status != "done"')

if __name__ == '__main__':
    mcp.run()

That's a real, if minimal, server. Each @mcp.tool() function becomes something the AI can call — with a docstring the model reads to know when to call it. Ask "is machine 3 running?" and the host calls get_machine_status('3') and answers from the live result.

Read-only, and mean it

Every tool you expose is a capability you're handing an AI. Start with read-only tools and keep it there until you have a very good reason not to. Never expose a tool that can start a cycle, change an offset, or write a macro without a human confirmation step and a hard allow-list. The failure mode of a chatty read-only agent is a wrong answer; the failure mode of a write-capable one is a crash.

Grounding the agent in more than status

Live status is the start. Add resources — a machine list, the job schedule, the tool library — so the agent has context, not just sensors. And for the tribal-knowledge questions ("why does this part chatter on op 20"), point a retrieval tool at your manuals, setup notes, and past job records so the model can cite real documents instead of guessing.

Running it safely on a shop network

  • Keep it local. A local or air-gapped LLM means machine data never leaves the building — often non-negotiable for a real shop.
  • Allow-list everything. Machines, tables, and queries the server can touch should be an explicit list, not whatever the AI asks for.
  • Audit every call. Log which tool ran, with what arguments, and what it returned. You want a paper trail the day someone asks what the agent did.
  • Scope the reads. The server's database user should have read-only permissions on only the tables it needs.
The model is the easy part — you can swap it in an afternoon. The durable work is the boring, careful layer underneath: the tools, the allow-lists, and the audit log.

This is the natural payoff of already being able to read your machine data in code — the agent is just a friendlier front door onto data you can already reach. If you want a read-only shop-floor agent built and secured properly, that's exactly the kind of project I take on.

Muerus Rodrigues

Applications Engineer

Get in touch

Keep reading

Home
Blog
Email
LinkedIn
Résumé