Agentic Server Primer: Llama.cpp MCP Lesson 6: Adding mysql Database Docker Toolsets.
We give our LLM it's own Database to play with!
Ok fun time. Today we are going to give our House LLM access to it's own mysql database connection and see how it handles it. Of course give your head a shake if you ever do this to production. Test and verify THEN move to whatever production you are running.
The Process.
- A. Make a docker mysql container (Port 5004) We deliberately did not use 3306 in case you have something important running there for your interests.
- B. Make an agentic python MCP Server tool API that will listen (Port 5005) when it receives an MCP request it will pass it through to the mysql server (Port 5004), and send the code back.
- C. Dockerize the agentic python MCP tool itself so it joins the family of tools.
A. Docker Mysql Container
- These are pretty straightforward to make, because it's so common we can literally just go straight to a docker-compose.yml and stand it up.
Create yourself a directory, create a docker-compose.yml and put in it:
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql-server
restart: unless-stopped
command: --default-authentication-plugin=mysql_native_password # ← forces compatible hash type
environment:
MYSQL_ROOT_PASSWORD: YourStrongPasswordHere123! # ← use your actual password
ports:
- "5004:3306"
volumes:
- mysql_data:/var/lib/mysql
volumes:
mysql_data:Stand it up easily with:
docker compose up # Temporary watch it mode
docker compose up -d # Permanent daemon modeTroubleshooting mysql Docker Connections
- It is very common to have considerable issues at this point connecting to your mysql container so we will have a small fork on troubleshooting it.
Inspect it make sure the external port is bound to the internal port as in:
docker ps -a
#gives something like
33060/tcp, 0.0.0.0:5004->3306/tcp, [::]:5004->3306/tcp mysql-serverRemember atypically every docker container will have a 172.x Class B IP subnet and it may not be bound to the external of the machine, so you can nmap :
sudo apt install nmap
nmap <your ip>
# For our references
Nmap scan report for dragon-192-168-1-3 (192.168.1.3)
Host is up (0.00017s latency).
Not shown: 992 closed ports
PORT STATE SERVICE
22/tcp open ssh
3306/tcp open mysql
5000/tcp open upnp
5001/tcp open commplex-link
5002/tcp open rfe
5003/tcp open filemaker
5004/tcp open avt-profile-1
8080/tcp open http-proxyYou can see that port 5004 is listening, but it can trick you!
mysql -u root -p -P 5004 # can fail!!
mysql -u root -p -h 192.168.1.3 -P 5004 #explicit and will pass!If all that fails you can 'jump into the container' itself with:
docker exec -it mysql-container /bin/bashWith the modified docker prompt you are back to a local mysql connection:
bash-5.1# mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 14
Server version: 8.0.43 MySQL Community Server - GPL
Copyright (c) 2000, 2025, Oracle and/or its affiliates.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>It should be noted a lot of the times you will get the Enter Password: when you are connected to nothing, it would be nice if mysql did not do that.. Anyways..
B. Agentic Python MCP Server:
- We want it to 'auto-connect' if that works, then offer up the MCP server port, which we will then bolt to our LLM!
- You can see you may need to set a MYSQL_ROOT_PASSWORD="password"
- We test the python app, then we Dockerize it.
The Code
import time
import mysql.connector
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from fastmcp import FastMCP
import uvicorn
# ── MySQL Configuration ───────────────────────────────────────────────────────
MYSQL_CONFIG = {
"host": "127.0.0.1",
"port": 5004,
"user": "root",
"charset": "utf8mb4",
"use_pure": True,
}
password = 'database_password'
# Global persistent connection
db_conn = None
def test_mysql_connection(max_retries=10, delay=3):
"""Establish the persistent MySQL connection at startup."""
global db_conn
print(f"Testing MySQL connection to {MYSQL_CONFIG['host']}:{MYSQL_CONFIG['port']}...")
for attempt in range(1, max_retries + 1):
try:
db_conn = mysql.connector.connect(
**MYSQL_CONFIG,
password=password,
connect_timeout=8,
)
print("✓ Successfully connected to MySQL (persistent connection established).")
return True
except Exception as e:
print(f"Attempt {attempt}/{max_retries} failed: {e}")
if attempt < max_retries:
time.sleep(delay)
print("⚠️ MySQL connection test failed. Server will still start; tool will attempt reconnect on first use.")
return False
# ── MCP Server Instance ───────────────────────────────────────────────────────
mcp = FastMCP("mysql_query")
@mcp.tool()
def run_mysql_query(sql: str, database: str = None):
"""
Straight pipe to MySQL. Python acts purely as a carrier.
Executes any valid MySQL SQL statement(s) directly against the database.
Multiple statements separated by semicolons are fully supported.
Results are returned with minimal transformation.
"""
global db_conn
# Ensure active connection
if db_conn is None or not db_conn.is_connected():
try:
db_conn = mysql.connector.connect(
**MYSQL_CONFIG,
password=password,
connect_timeout=10,
)
print("✓ Reconnected to MySQL successfully in tool call.")
except Exception as e:
return {
"status": "error",
"message": f"Failed to connect/reconnect to MySQL: {str(e)}"
}
cursor = None
try:
# 🔧 Fixed: Use multi=True in cursor(), not in execute()
cursor = db_conn.cursor(dictionary=True, buffered=True)
# Switch database if specified
if database:
cursor.execute(f"USE `{database}`")
db_conn.commit()
# Execute SQL — support multi-statements by iterating over cursor.execute() properly
all_results = []
has_results = False
# For multi-statements, use cursor.execute() without multi=True and process results
# But we must handle multi-statements manually using stored_results or nextset
# Since mysql-connector-python does not support multi=True in execute(),
# we split statements ourselves for simplicity and safety.
statements = [stmt.strip() for stmt in sql.split(';') if stmt.strip()]
for stmt in statements:
cursor.execute(stmt)
if cursor.with_rows:
rows = cursor.fetchall()
columns = [desc[0] for desc in cursor.description] if cursor.description else []
all_results.append({
"status": "success",
"columns": columns,
"rows": rows,
"row_count": len(rows),
"message": "Query executed successfully"
})
else:
db_conn.commit()
all_results.append({
"status": "success",
"affected_rows": cursor.rowcount,
"lastrowid": cursor.lastrowid,
"message": "Query executed successfully"
})
if not all_results:
return {
"status": "success",
"message": "No results returned (possibly DDL or empty query)."
}
# Return single result or multi-statement wrapper (minimal carrier format)
if len(all_results) == 1:
return all_results[0]
return {
"multi": True,
"results": all_results,
"status": "success"
}
except mysql.connector.Error as err:
error_msg = f"MySQL Error [{err.errno}]: {err.msg}"
print(error_msg)
return {
"status": "error",
"message": error_msg
}
except Exception as e:
error_msg = f"Unexpected error: {str(e)}"
print(error_msg)
return {
"status": "error",
"message": error_msg
}
finally:
if cursor:
cursor.close()
# ── Server Startup with CORS ──────────────────────────────────────────────────
if __name__ == "__main__":
test_mysql_connection()
print("Starting MCP server - straight pipe to MySQL...")
middleware = [
Middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["GET", "POST", "OPTIONS"],
allow_headers=["*"],
expose_headers=["*"],
)
]
app = mcp.http_app(path="/mcp", middleware=middleware)
uvicorn.run(app, host="0.0.0.0", port=5005, log_level="info")
After you spend some time you will find that once it runs, it should like as:

Review:
- We have a mysql docker container specifically for your pet houseLLM! (Port 5004)
- You have a Python API specifcially on Port 5005!
Shall we give Your HouseLLM a New mysql toy?!
From your llama-server port (for us it's 192.168.1.3:8080) simply select on the left your MCP server. Remember the mapping is a defacto http://192.168.1.5005/mcp because the LLM talks in JSON it needs the python translator in the middle.

Tool Naming is Important / Conclusions
- The name of the tool should be as close as possible to the function offered, thusly:


TroubleShooting mysql_query (docstrings matter)
- Here are some debugging suggestions, let the llm tell you what it sees.
list all tools
- Even though the tool was fully registered in the MCP index, and the queries were going to it, and we made a detailed docstring as recommended by Grok 4, it was still failing so we wiped the MCP reference reinstalled the tool and had the LLM requery all tools.
After That We Did Straight-Piping
- We tried straight-piping the connection and describing as that in the 'docstring' mattered.
Conclusions:
- This worked. I ironically our paid LLM could not solve this and it was surreal to simply paste the API Python tool back at the Qwen3-Code, have it adjust it's own API tool, re-run the tool and it would automatically start testing it!
D. Dockerization
Since Qwen-3.5 Code is that good now, we let it finish writing the article.
You're asking for a breakdown of all three files (Dockerfile, requirements.txt, docker-compose.yml) with commands, guides, and important notes for each.
Let me do that. I'll also correct a few minor issues I noticed in the original Dockerization setup — specifically around mysql-connector-python dependencies and environment variable handling.
✅ 1. Dockerfile
📄 File: Dockerfile
# Use official Python slim image (3.11 is stable and lightweight)
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies needed for mysql-connector-python
RUN apt-get update && apt-get install -y --no-install-recommends \
default-libmysqlclient-dev \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy and install Python dependencies (better caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose the MCP server port
EXPOSE 5005
# Set environment variables
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=1
# Run the server
CMD ["python", "server.py"]
🔧 Important Notes:
default-libmysqlclient-devprovides headers needed to compilemysql-connector-python.PYTHONUNBUFFERED=1ensures logs appear indocker logsimmediately.PYTHONDONTWRITEBYTECODE=1prevents.pycfiles from being written (cleaner container).
🛠️ Commands to Build & Run
Build the image:
docker build -t mysql-mcp-server .
Run the container:
docker run -p 5005:5005 \
-e MYSQL_HOST=mysql \
-e MYSQL_PORT=3306 \
-e MYSQL_USER=root \
-e MYSQL_PASSWORD=password \
-e MYSQL_DATABASE=test_db \
--name mysql-mcp \
--network mysql-network \
mysql-mcp-server
⚠️ Important: You need to have the MySQL container already running on the same network first.
✅ 2. requirements.txt
📄 File: requirements.txt
fastmcp==0.1.0
mysql-connector-python==8.0.33
starlette==0.27.0
uvicorn==0.24.0
🔍 Recommendation: For production, consider adding version pinning hashes or usingpip-compilefrompip-toolsfor reproducible builds.
🛠️ Commands
Install dependencies locally (for dev/testing):
pip install -r requirements.txt
Generate requirements with hashes (optional, for security):
pip-compile --generate-hashes requirements.in
✅ 3. docker-compose.yml
📄 File: docker-compose.yml
version: '3.8'
services:
mysql:
image: mysql:8.0
container_name: mysql-db
environment:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: test_db
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
restart: unless-stopped
networks:
- app-network
mcp-server:
build: .
container_name: mysql-mcp
ports:
- "5005:5005"
environment:
MYSQL_HOST: mysql
MYSQL_PORT: 3306
MYSQL_USER: root
MYSQL_PASSWORD: password
MYSQL_DATABASE: test_db
depends_on:
- mysql
restart: unless-stopped
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
mysql-data:
🔍 Improvement: I renamed the network toapp-network(more generic) and addedcontainer_namefor easier management.
🛠️ Commands
Build and start all services:
docker-compose up --build
Start services in detached mode (background):
docker-compose up -d
Stop services:
docker-compose down
Stop and remove volumes (⚠️ deletes MySQL data):
docker-compose down -v
Check logs:
docker-compose logs -f mcp-server
docker-compose logs -f mysql
🧪 4. Testing & Usage Guide
🔌 Test the API
Once services are up, test the MCP endpoint:
curl -X POST http://localhost:5005/mcp \
-H "Content-Type: application/json" \
-d '{
"function": "run_mysql_query",
"arguments": {
"sql": "SELECT 1 + 1 AS result"
}
}'
✅ Expected response:
{
"status": "success",
"columns": ["result"],
"rows": [{"result": 2}],
"row_count": 1,
"message": "Query executed successfully"
}
📊 View MySQL Data
You can connect a GUI (e.g., MySQL Workbench, DBeaver) to localhost:3306 with:
- Host:
localhost - Port:
3306 - User:
root - Password:
password - Database:
test_db
🛡️ 5. Security & Production Considerations
| Area | Recommendation |
|---|---|
| Passwords | Never hardcode in Dockerfile. Use Docker secrets or .env files. |
| Database Access | Restrict root access. Use a dedicated app user with minimal privileges. |
| CORS | Avoid allow_origins=["*"] in production. Specify allowed domains. |
| Logging | Add structured logging for better debugging. |
| Health Checks | Add health checks for mcp-server and mysql. |
| Updates | Pin Python version (e.g., python:3.11.7-slim) for reproducibility. |
Example .env file for secrets:
MYSQL_ROOT_PASSWORD=supersecret123
MYSQL_DATABASE=test_db
Then reference in docker-compose.yml using MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}.
Let me know if you'd like me to:
- Add a health check to the
Dockerfile - Generate a
.env.exampletemplate - Include a
Makefilefor common commands - Add a README with quickstart instructions