How I Built an MCP Integration for LLMs in an On-Premise MES
How I built an MCP to connect an on-premise MES to GPT, Claude and Gemini
When we started bringing AI into our MES, the idea seemed simple: allow someone to ask in Portuguese, “what was the OEE of line 3 yesterday?” and get an answer with the factory's real data.
The problem is that this MES does not run in a central cloud. It runs inside each customer's infrastructure, often on a server that cannot be updated at any time and whose access to the internet is tightly controlled.
So the question stopped being “which model should we use?” and became something else:
How do we make an AI query industrial data without giving it direct access to the database, while respecting existing permissions and still allowing the customer to choose the model?
The solution ended up involving MCP, a separate AI service, and several architectural decisions that only appeared once the project started running for real.
Before MCP: one giant JSON
Before MCP existed, we already had a first version of the AI integration.
The idea was relatively simple: the MES screen knew which data the user was viewing and sent that content to the AI service in a large JSON containing the data from that context.
For questions about that screen, it worked.
If the user was looking at a production report and asked something about those data, the model could answer.
But there was an important limitation: the model only knew what the screen had sent.
If the person asked:
“And what were the main downtime reasons for this machine?”
but the current screen did not contain that information, the AI had no way to find out.
It had no access to the database.
It also had no tool to ask the MES for additional information.
In practice, the flow was:
User
│
▼
MES screen
│
│ JSON with visible data
▼
AI Service
│
▼
ModelIt was a safe approach because the model only received data that was already in the screen's context.
But it was also quite limited.
The AI could interpret the data it received, but it could not fetch new data.
That limitation is exactly what led to MCP.
MCP as the layer between AI and the MES
Instead of continuing to increase the size of the JSON sent by each screen, the idea became allowing the model to request additional information in a controlled way.
The MES then started exposing specific capabilities as tools:
The model does not need to know how the data is stored.
To answer about OEE, it can request something like:
get_daily_oee
line = 3
date = 2026-09-21Execution happens inside the MES itself.
This creates an important separation:
AI model
│
│ tool call
▼
MCP
│
│ business rules
▼
MES
│
▼
DatabaseThe model does not access the database or execute arbitrary SQL.
The rule that became a principle
There is one rule I consider especially important:
the AI cannot disagree with the screen.
The MES already contains the logic used by its reports. If a downtime report shows 42 minutes, there is no reason to implement another calculation for the AI and risk it answering 39.
The tools reuse the existing rules.
AI should be a new interface to the system's source of truth, not a second implementation of it.
Where should the AI live?
The next problem was architectural.
The MES is a mature Java application. Putting the Anthropic, OpenAI and Google SDKs directly inside it would work, but it would create unnecessary coupling.
Models and SDKs change quickly. An industrial system needs a different pace.
So the AI got its own service, installed inside the customer's infrastructure.
The architecture became roughly:
┌─────────────────────────────────────────────────────┐
│ customer's infrastructure │
│ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ MES │ │ AI Service │ │
│ │ │◀─MCP──│ │───┼──▶ GPT
│ │ /mcp │ │ orchestrator │───┼──▶ Claude
│ │ │ │ │───┼──▶ Gemini
│ └──────┬───────┘ └──────────────────────┘ │
│ │ │
│ ▼ │
│ Database │
└─────────────────────────────────────────────────────┘The MES remains responsible for data and rules.
The AI service talks to the models and executes the tools through MCP.
This keeps the database isolated, and only the component that needs to talk to the internet makes external calls.
One service, three providers
Another requirement was not to be locked into a single model.
The customer may already have a corporate contract with a provider, or simply prefer a different model.
So provider, model and key became customer configuration.
Under the hood there are the official SDKs from the three providers, but the service works through a common abstraction.
This brought an important benefit: changing models does not require changing or reinstalling the MES.
The tool-calling loop
A question can also require more than one query.
For example:
“What were the main losses on line 3 yesterday and what was their impact on OEE?”
The model can request downtime reasons, receive the data, realize it needs OEE, and make another call.
The service then runs a loop:
question
│
▼
model
│
├── needs data? ── yes ──▶ tool
│ │
│ ▼
│ result
│ │
└────────────────────────────┘
│
▼
final answerBut there is an iteration limit.
Normal questions use only a few calls. The limit exists to prevent the model from entering a loop that unnecessarily increases latency and cost.
Another problem also appeared: you cannot simply send the raw result of every query to the model.
A month-long report, with dozens of machines, can generate thousands of rows.
So some results are compacted before being sent back to the model. The goal is to keep the information needed for reasoning without turning the context into a copy of the database.
Fewer tools can also be better
Another decision was to make the tool catalog depend on context.
If the user is inside a downtime report, there is no reason to present the model with every tool available in the MES.
It receives only those related to that context.
This reduces the number of decisions the model has to make.
The same logic applies to charts: the tool that allows requesting a visualization is only made available when the current interface can render it.
Instead of telling the model “you can do everything”, the architecture tries to tell it:
in this context, these are the things you can do.
Security: mostly what the AI cannot do
In an industrial system, giving a model too much freedom is difficult to justify.
So several restrictions were built directly into the architecture.
The model:
- does not access the database;
- does not execute arbitrary SQL;
- has no write tools;
- does not automatically receive the service's permissions;
- cannot query resources the user could not access in the MES.
The MCP endpoint has its own authentication. A missing token does not mean open access: it means access denied.
There is also an important separation between the AI service session and the user's authorization.
The connection between services can be shared, but every tool call carries the identity of the person who asked the question. The MES then applies its existing access rules.
This prevents AI from becoming a shortcut to data the user would not normally be allowed to see.
What about the data leaving the factory?
Because the AI service can communicate with external providers, another question appeared: what if the customer does not want certain names sent to the model?
An anonymization option was implemented.
Before the conversation leaves, machine names, items and reasons can be replaced with identifiers.
CNC-04
↓
MACHINE_001
Product ABC-123
↓
ITEM_014The identifiers can then be reconstructed on the way back.
MCP became bigger than the chatbot
Perhaps the most interesting consequence of the project appeared later.
Once the MES started exposing its capabilities through MCP, it no longer depended on our own interface to be consumed by an AI.
The same endpoint can be used by other clients compatible with the protocol.
The architecture went from:
MES → our chatbotto:
┌── our chat
│
MES ── MCP ──────┼── external MCP client
│
└── future AIsThat's when MCP stopped looking like just a technology for connecting a chatbot to the system.
It became a standardized interface to the MES capabilities.
What I learned
At first, the problem looked like integrating an LLM with an industrial system.
It wasn't.
The real problem was creating a safe boundary between a probabilistic model and a system that needs to remain predictable.
The model can change.
The provider can change.
The SDK can change.
But the MES business rules need to remain the same.
So perhaps the most important architectural decision was not choosing between GPT, Claude or Gemini.
It was deciding what none of them would be allowed to do.
The AI does not need to know the entire factory.
It needs to know exactly what the system decided to show it.
And in industrial software, perhaps that is the difference between simply adding AI to a product and actually building an AI architecture.
Stack: Java/Spring · MCP over HTTP and stdio · official Anthropic, OpenAI and Google SDKs · Angular · Docker Compose · PostgreSQL · on-premise MES