Building a Custom MCP Server for SAP ECC Data Access
Building a Custom MCP Server for SAP ECC Data Access
Section titled “Building a Custom MCP Server for SAP ECC Data Access”For decades, SAP ECC (Enterprise Core Component) has been the backbone of global supply chains. However, connecting modern AI agents (like CrewAI or OpenAI Operator) to these “Big Iron” systems is notoriously difficult. Direct RFC (Remote Function Call) connections require proprietary C++ SDKs that break most container environments.
The “Retrofit” solution? Standardize on OData.
Most SAP ECC environments (NetWeaver Gateway) expose data via OData services. This guide builds a Model Context Protocol (MCP) server that acts as a bridge: it speaks “Agent” (via MCP) on one side and “SAP” (via OData) on the other.
🏗️ Architecture
Section titled “🏗️ Architecture”- Framework: Python
fastmcp(Model Context Protocol SDK). - Protocol: HTTP/S (OData) to SAP; SSE (Server-Sent Events) to the Agent.
- Deployment: Dockerized, stateless container.
🛠️ Step 1: The Server Code
Section titled “🛠️ Step 1: The Server Code”Create a file named server.py. This script defines the “Tools” your AI agent will see. We use the standard requests library to handle authentication and XML/JSON parsing from SAP.
import osimport requestsfrom fastmcp import FastMCP
# Initialize the MCP Servermcp = FastMCP("sap-ecc-gateway")
# SAP Configuration (Load from Environment for Security)SAP_HOST = os.getenv("SAP_HOST", "https://sap.example.corp:44300")SAP_CLIENT = os.getenv("SAP_CLIENT", "100")SAP_USER = os.getenv("SAP_USER")SAP_PASSWORD = os.getenv("SAP_PASSWORD")
def _get_sap_session(): """Helper to establish an authenticated session with SAP.""" session = requests.Session() session.auth = (SAP_USER, SAP_PASSWORD) session.headers.update({ "Accept": "application/json", "sap-client": SAP_CLIENT }) return session
@mcp.tool()def get_material_stock(material_id: str, plant: str) -> str: """ Retrieves real-time stock levels for a specific material ID in a given plant from SAP ECC.
Args: material_id: The SAP Material Number (e.g., 'MAT-4005'). plant: The 4-digit Plant Code (e.g., '1000'). """ if not SAP_USER or not SAP_PASSWORD: return "Error: SAP credentials not configured."
# Example Endpoint: SAP Standard OData Service for Material Availability # Adjust endpoint based on your specific SAP NetWeaver Gateway configuration endpoint = f"{SAP_HOST}/sap/opu/odata/sap/API_PRODUCT_AVAILABILITY/Availability"
try: session = _get_sap_session()
# OData filter query params = { "$filter": f"Material eq '{material_id}' and Plant eq '{plant}'", "$format": "json" }
response = session.get(endpoint, params=params, timeout=10) response.raise_for_status()
data = response.json()
# Parse standard OData wrapper results = data.get("d", {}).get("results", [])
if not results: return f"No stock data found for Material {material_id} at Plant {plant}."
# Extract relevant quantity stock_info = results[0] qty = stock_info.get("AvailableQuantity", 0) unit = stock_info.get("BaseUnit", "EA")
return f"Current Stock for {material_id} in {plant}: {qty} {unit}"
except requests.exceptions.RequestException as e: return f"SAP Connection Failed: {str(e)}"
@mcp.tool()def check_sales_order_status(order_id: str) -> str: """ Checks the status of a Sales Order in SAP ECC. """ if not SAP_USER: return "Error: Credentials missing."
endpoint = f"{SAP_HOST}/sap/opu/odata/sap/API_SALES_ORDER_SRV/A_SalesOrder('{order_id}')"
try: session = _get_sap_session() response = session.get(endpoint, params={"$format": "json"}, timeout=10)
if response.status_code == 404: return f"Sales Order {order_id} not found."
response.raise_for_status() data = response.json().get("d", {})
status = data.get("OverallSDProcessStatusDesc", "Unknown") net_amount = data.get("TotalNetAmount", "0.00") currency = data.get("TransactionCurrency", "USD")
return f"Order {order_id}: Status '{status}', Total: {net_amount} {currency}"
except Exception as e: return f"Error querying SAP: {str(e)}"
if __name__ == "__main__": # Ensure your container has network access (e.g. via NordLayer) # The host must be 0.0.0.0 to listen on all interfaces within Docker mcp.run(transport='sse', host='0.0.0.0', port=8000)🐳 Step 2: The Docker Configuration
Section titled “🐳 Step 2: The Docker Configuration”This Dockerfile creates a lightweight, production-ready environment.
File: Dockerfile
# Use an official Python runtime as a parent imageFROM python:3.11-slim
# Set the working directory in the containerWORKDIR /app
# Install system dependencies (curl often needed for healthchecks)RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Install Python dependencies# 'fastmcp' handles the protocol, 'requests' handles SAP HTTP callsRUN pip install --no-cache-dir fastmcp requests uvicorn
# Copy the server code into the containerCOPY server.py .
# Expose the port the app runs onEXPOSE 8000
# Ensure your container has network access (e.g. via NordLayer)# Run the applicationCMD ["python", "server.py"]🔌 Step 3: Client Connectivity (CrewAI)
Section titled “🔌 Step 3: Client Connectivity (CrewAI)”Now that your server is running, you need to connect your AI Agent to it. Below is an example using CrewAI.
The agent does not need to import specific tool classes. Instead, we point the agent to the MCP Server URL using the mcps parameter, and it automatically ingests the available capabilities (get_material_stock, check_sales_order_status).
from crewai import Agent, Task, Crew
# 1. Define the Agent with MCP connectivity# The agent will automatically discover tools exposed by the SSE endpoint.supply_chain_agent = Agent( role='Supply Chain Analyst', goal='Check inventory levels in SAP ECC', backstory='You are an expert in legacy ERP systems. You verify stock before approving orders.', # Connect directly to the MCP server via SSE mcps=["http://localhost:8000/sse"])
# 2. Define the Task# The agent will infer which tool to use based on the task descriptioninventory_check = Task( description="Check the stock level for Material 'MAT-999' in Plant '1000'. If stock is below 50, flag it.", expected_output="A brief inventory report.", agent=supply_chain_agent)
# 3. Run the Crewcrew = Crew(agents=[supply_chain_agent], tasks=[inventory_check])result = crew.kickoff()
print("Agent Report:")print(result)🚀 Deployment & Testing
Section titled “🚀 Deployment & Testing”-
Build the Image:
Terminal window docker build -t sap-mcp-server . -
Run the Container: Remember to pass your SAP credentials as environment variables.
Terminal window docker run -p 8000:8000 \-e SAP_HOST="https://sap-dev.yourcompany.com" \-e SAP_USER="remote_agent" \-e SAP_PASSWORD="secret_password" \sap-mcp-server -
Troubleshooting:
- “Connection Refused”: Ensure
host='0.0.0.0'is set inserver.py. If you leave it as default (localhost), Docker will not expose it to the outside world. - SAP Connectivity: If running locally, ensure your machine is on the VPN. If running in the cloud, you may need a Mesh VPN (like Tailscale or NordLayer) injected into the container to reach the on-premise SAP server.
- “Connection Refused”: Ensure
🛡️ Quality Assurance
Section titled “🛡️ Quality Assurance”- Status: ✅ Verified
- Environment: Python 3.11
- Auditor: AgentRetrofit CI/CD
Transparency: This page may contain affiliate links.