Enterprise Data Access for AI Agents via MCP
VP asked for full API access to ERP. The ERP has no API. MCP bridge pattern: read-only access, audit logging, approval gates. Architecture for IBM i, SAP, and legacy systems.
A post on r/sysadmin with 660 upvotes described a familiar scenario: a VP asked for "full API access to our ERP for the AI integration." The ERP runs on IBM i (formerly AS/400) and communicates via JDBC. It has no REST API. It has no GraphQL endpoint. It barely has a web interface. The gap between what executives expect from AI integration and what enterprise systems actually support is enormous. MCP bridges that gap with a pattern that works without rewriting the backend.
The Enterprise System Reality
Most enterprise systems in production today were not built for AI agent access. The stack looks like this:
- ERP: SAP (BAPI/RFC), Oracle E-Business (PL/SQL), IBM i (JDBC/ODBC)
- CRM: Salesforce (REST API exists), legacy systems (SOAP or direct DB)
- Financial: mainframe batch processing, FTP file drops, EDI
- Manufacturing: OPC-UA, proprietary protocols, PLC interfaces
None of these were designed for an AI agent to query in real time. The executive vision of "just let the AI read our ERP" ignores decades of access control, data governance, and system architecture that exist for good reasons.
The MCP Bridge Pattern
Model Context Protocol provides a standardized way for AI agents to interact with external systems through tool definitions. The MCP bridge pattern wraps enterprise system access in a controlled interface: the agent calls MCP tools, the MCP server translates those calls into the native system protocol, and returns structured results.
[AI Agent] --> [MCP Server] --> [Enterprise Bridge] --> [ERP/CRM/DB]
| | | |
Calls Validates Translates Executes
tools permissions to JDBC/SOAP query
| | | |
Gets Logs audit Applies filters Returns
JSON trail and limits raw dataThe critical architectural decision: the MCP server is the security boundary. It controls what the agent can access, logs every request, and enforces rate limits. The agent never gets direct database access.
Read-Only Access Is Non-Negotiable
The first rule of connecting AI agents to enterprise systems: read-only access. Period. An AI agent that can modify ERP data is a production incident waiting to happen. The agent might misinterpret a query and update 10,000 inventory records. It might hallucinate a valid-looking order number and modify real orders.
The MCP server enforces this by only exposing read operations as tools. There is no "update_inventory" tool. There is no "create_order" tool. If write operations are needed, they go through an approval gate (described below), not through direct agent access.
# MCP Server for IBM i ERP access
# Read-only tools only
import jaydebeapi # JDBC bridge for Python
class ErpMcpServer:
def __init__(self, jdbc_url, jdbc_driver, credentials):
self.conn = jaydebeapi.connect(
jdbc_driver,
jdbc_url,
credentials,
)
self.cursor = self.conn.cursor()
def get_inventory_level(self, product_code: str) -> dict:
"""Read current inventory for a product."""
self.cursor.execute(
"SELECT PRDCD, PRDNM, QTYOH, QTYAV, WHSCD "
"FROM INVMST WHERE PRDCD = ?",
(product_code,)
)
row = self.cursor.fetchone()
if not row:
return {"error": "Product not found"}
return {
"product_code": row[0].strip(),
"product_name": row[1].strip(),
"quantity_on_hand": int(row[2]),
"quantity_available": int(row[3]),
"warehouse": row[4].strip(),
}
def get_open_orders(self, customer_code: str) -> list:
"""Read open orders for a customer."""
self.cursor.execute(
"SELECT ORDNO, ORDDT, ORDST, ORDTL "
"FROM ORDHDR WHERE CUSTCD = ? AND ORDST IN ('O', 'P') "
"ORDER BY ORDDT DESC",
(customer_code,)
)
rows = self.cursor.fetchall()
return [
{
"order_number": row[0].strip(),
"order_date": str(row[1]),
"status": "Open" if row[2] == "O" else "Processing",
"total": float(row[3]),
}
for row in rows
]Audit Logging
Every query an AI agent makes against enterprise data must be logged. This is not optional. When the CFO asks "who accessed the financial data last Tuesday," the answer cannot be "an AI agent, we are not sure which query."
import json
import datetime
class AuditLogger:
def __init__(self, log_file="agent_audit.jsonl"):
self.log_file = log_file
def log(self, tool_name, params, result, user_id, agent_id):
entry = {
"timestamp": datetime.datetime.utcnow().isoformat(),
"tool": tool_name,
"params": params,
"result_summary": self._summarize(result),
"user_id": user_id,
"agent_id": agent_id,
"rows_returned": len(result) if isinstance(result, list) else 1,
}
with open(self.log_file, "a") as f:
f.write(json.dumps(entry) + "\n")
def _summarize(self, result):
"""Log metadata, not full data, for sensitive systems."""
if isinstance(result, list):
return f"{len(result)} records returned"
if isinstance(result, dict) and "error" in result:
return result["error"]
return "single record returned"Approval Gates for Write Operations
When the business requires AI-initiated changes (updating a shipping address, adjusting an order quantity), the approval gate pattern puts a human in the loop. The agent proposes a change, the system queues it, a human approves or rejects it, and only then does the change execute.
class ApprovalGate:
def __init__(self, notification_channel):
self.pending = {}
self.channel = notification_channel
def propose_change(self, change_type, params, reason, agent_id):
"""Queue a proposed change for human approval."""
change_id = generate_id()
self.pending[change_id] = {
"type": change_type,
"params": params,
"reason": reason,
"agent_id": agent_id,
"proposed_at": datetime.datetime.utcnow().isoformat(),
"status": "pending",
}
# Notify human reviewer
self.channel.send(
f"Agent proposed: {change_type}\n"
f"Params: {json.dumps(params)}\n"
f"Reason: {reason}\n"
f"Approve: /approve {change_id}\n"
f"Reject: /reject {change_id}"
)
return change_id
def approve(self, change_id, approver_id):
"""Execute an approved change."""
change = self.pending.get(change_id)
if not change or change["status"] != "pending":
return {"error": "Invalid or already processed"}
change["status"] = "approved"
change["approved_by"] = approver_id
change["approved_at"] = datetime.datetime.utcnow().isoformat()
# Execute the actual change
return self._execute(change)Input Validation and Query Limits
AI agents generate queries based on natural language, which means they can construct unexpected inputs. The MCP server must validate every parameter:
- Parameter type checking: product codes must match expected patterns, dates must be valid, numeric ranges must be bounded.
- Row limits: every query has a maximum result count. An agent should never be able to dump an entire customer table.
- Rate limiting: cap queries per minute per agent to prevent runaway loops from overwhelming the ERP.
- Data masking: sensitive fields (SSN, credit card, salary) are masked or excluded from results.
The Architecture Applies Beyond ERP
This pattern works for any enterprise system that was not built for AI access. The MCP bridge pattern has been used for:
- Mainframe financial systems (CICS transactions via MCP)
- Manufacturing execution systems (OPC-UA data via MCP)
- Legacy CRM systems (SOAP/XML via MCP)
- Data warehouses (JDBC/ODBC via MCP with query governance)
In each case, the pattern is identical: MCP server as the security boundary, read-only default, audit logging on every request, approval gates for write operations, and strict input validation.
What to Tell the VP
When the executive asks for "full API access for the AI," the answer is: "We can give the AI read access to specific data through a controlled interface. Every query is logged. Write operations require human approval. The ERP itself is never directly exposed."
This is not a compromise. It is the only responsible way to connect AI agents to systems that run the business. The 660-upvote thread on r/sysadmin exists because sysadmins understand what happens when untested code gets write access to production ERP. AI agents are untested code that generates its own queries at runtime. The guardrails are not optional.