AI agents become much more useful when they can work with real data instead of only generating text. A database is one of the most common sources of structured information developers may want to connect to an AI application.
The Model Context Protocol (MCP) provides a standardized way for AI applications to interact with external tools and data sources.
In this tutorial, we’ll build a lightweight SQLite MCP server in Python using FastMCP. The server will expose the database schema as a readable resource and provide controlled, read-only tools for querying inventory data.
The goal isn’t simply to connect an AI model to a database. We also want to place clear security boundaries around what the model can do.
What We’ll Build
Our setup will look like this:
AI Application
|
v
MCP Client
|
v
SQLite MCP Server
|
+----------------+
| |
v v
Schema Resource Query Tools
| |
+--------+-------+
|
v
SQLite DatabaseThe sample database will contain product information such as SKUs, categories, stock levels, and prices.
What You Need
Before starting, make sure you have:
- Python 3.10 or newer
- FastMCP
- SQLite
- Basic familiarity with Python and SQL
- An MCP-compatible client for testing
SQLite is included with Python, so you don’t need to install a separate database server.
Why Security Matters
Giving an AI agent database access is different from simply giving it text to read.
If a database tool is poorly designed, the model could potentially request sensitive information or attempt operations that the application never intended to allow.
For a read-only assistant, a safer design is to:
- Give the agent only the access it needs.
- Open the database in read-only mode.
- Use parameterized queries for structured operations.
- Limit the amount of data returned.
- Keep write and destructive operations separate.
We’ll apply these principles throughout the tutorial.
Step 1: Create the Python Environment
Create a new project directory:
mkdir sqlite-mcp-server
cd sqlite-mcp-serverCreate a virtual environment:
python3 -m venv .venvOn macOS or Linux, activate it with:
source .venv/bin/activateOn Windows:
.venv\Scripts\activateNow install FastMCP:
pip install fastmcpVerify the installation:
python -c "import fastmcp; print(fastmcp.__version__)"Step 2: Create the SQLite Database
Create a file named seed_db.py.
Add the following code:
import sqlite3
def init_db():
conn = sqlite3.connect("inventory.db")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sku TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
category TEXT NOT NULL,
stock_level INTEGER NOT NULL,
unit_price REAL NOT NULL
)
""")
sample_products = [
("SKU-1001", "Developer Mechanical Keyboard", "Hardware", 45, 129.99),
("SKU-1002", "USB-C Dual 4K Dock", "Accessories", 18, 179.50),
("SKU-1003", "Ergonomic Mesh Chair", "Furniture", 8, 349.00),
("SKU-1004", "Noise-Cancelling Headphones", "Audio", 22, 199.95)
]
cursor.executemany("""
INSERT OR IGNORE INTO products
(sku, name, category, stock_level, unit_price)
VALUES (?, ?, ?, ?, ?)
""", sample_products)
conn.commit()
conn.close()
print("Database seeded successfully.")
if __name__ == "__main__":
init_db()Run it:
python seed_db.pyThis creates an inventory.db SQLite database in your project.
Step 3: Create the FastMCP Server
Create another file named server.py.
Start with:
import sqlite3
import json
from pathlib import Path
from fastmcp import FastMCP
mcp = FastMCP("Inventory SQLite Service")
BASE_DIR = Path(__file__).resolve().parent
DB_PATH = BASE_DIR / "inventory.db"
def get_connection():
"""Return a read-only SQLite connection."""
if not DB_PATH.exists():
raise FileNotFoundError(
f"Database file not found: {DB_PATH}"
)
conn = sqlite3.connect(
f"file:{DB_PATH}?mode=ro",
uri=True
)
conn.row_factory = sqlite3.Row
return connThere are two important decisions here.
First, the database location is calculated relative to server.py. This avoids problems when an MCP client starts the server from another working directory.
Second, the SQLite connection uses:
mode=roThis opens the database in read-only mode.
Step 4: Expose the Database Schema
An MCP resource can provide readable context to an MCP client.
We can expose the SQLite schema so the client understands which tables and columns exist.
Add this to server.py:
@mcp.resource("schema://inventory")
def get_database_schema() -> str:
"""Return the SQLite schema."""
conn = get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT name, sql
FROM sqlite_master
WHERE type = 'table'
AND name NOT LIKE 'sqlite_%'
""")
tables = cursor.fetchall()
conn.close()
schema_dump = []
for table_name, create_sql in tables:
schema_dump.append(
f"-- Table: {table_name}\n{create_sql};\n"
)
return "\n".join(schema_dump)The client can now retrieve information about the structure of the database without receiving unrestricted database access.
Step 5: Create a Read-Only Query Tool
Next, create a tool that accepts read-only SQL queries.
@mcp.tool()
def query_inventory(sql_query: str) -> str:
"""Execute a read-only SELECT query."""
clean_query = sql_query.strip()
if not clean_query.lower().startswith("select"):
return json.dumps({
"error": "Only SELECT queries are permitted."
})
forbidden_keywords = [
"insert",
"update",
"delete",
"drop",
"alter",
"attach",
"detach",
"replace"
]
tokens = clean_query.lower().split()
for forbidden in forbidden_keywords:
if forbidden in tokens:
return json.dumps({
"error": f"{forbidden.upper()} is not permitted."
})
conn = get_connection()
cursor = conn.cursor()
try:
cursor.execute(clean_query)
rows = cursor.fetchmany(50)
results = [dict(row) for row in rows]
return json.dumps({
"count": len(results),
"rows": results
}, indent=2)
except sqlite3.Error as error:
return json.dumps({
"error": f"SQLite error: {error}"
})
finally:
conn.close()Notice that we're not relying on the keyword checks as the only security mechanism.
The underlying SQLite connection is also read-only.
That gives us multiple layers of protection.
Step 6: Create a Parameterized Tool
For common operations, a narrowly defined tool is preferable to allowing the model to construct arbitrary SQL.
For example, let's create a tool for finding low-stock products:
@mcp.tool()
def get_low_stock_products(threshold: int = 15) -> str:
"""Return products at or below a stock threshold."""
conn = get_connection()
cursor = conn.cursor()
query = """
SELECT
sku,
name,
category,
stock_level,
unit_price
FROM products
WHERE stock_level <= ?
ORDER BY stock_level ASC
"""
try:
cursor.execute(query, (threshold,))
rows = cursor.fetchall()
return json.dumps(
[dict(row) for row in rows],
indent=2
)
finally:
conn.close()The important part is:
WHERE stock_level <= ?combined with:
cursor.execute(query, (threshold,))The value is supplied separately rather than being inserted directly into the SQL string.
Why Narrow Tools Are Useful
Compare:
query_database(sql_query)with:
get_low_stock_products(threshold)The first gives the model considerable freedom over the SQL it generates.
The second gives it one clearly defined operation.
When possible, purpose-built tools are easier to validate, monitor, and restrict.
Step 7: Limit Query Results
Our general query tool uses:
cursor.fetchmany(50)so a single query doesn't automatically return thousands of rows.
For larger applications, consider additional controls such as:
Pagination
SQL
LIMITMaximum response sizes
Column allowlists
Query timeouts
Maximum result counts
This is especially important when database results are going directly into an AI model's context.
Step 8: Test the MCP Server
FastMCP provides development tooling for inspecting and testing your server.
Run:
fastmcp dev server.pyVerify that your server exposes:
query_inventory
get_low_stock_products
schema://inventoryTest the tools with simple inventory queries before connecting the server to a larger AI application.
Step 9: Connect It to an MCP Client
Once the server works locally, you can connect it to an MCP-compatible client.
A command-based configuration may look similar to:
{
"mcpServers": {
"inventory-database": {
"command": "fastmcp",
"args": [
"run",
"/absolute/path/to/server.py"
]
}
}
}Replace:
/absolute/path/to/server.pywith the real path to your server.py file.
The exact configuration and file location depend on the MCP client you're using, so check that client's current documentation.
Common Errors and Troubleshooting
SQLite: Unable to Open Database File
You may encounter:
sqlite3.OperationalError: unable to open database fileOne possible cause is the MCP client launching your server from a different working directory.
That's why this tutorial uses:
BASE_DIR = Path(__file__).resolve().parent
DB_PATH = BASE_DIR / "inventory.db"instead of relying on:
Path("inventory.db")Problems With STDIO
If your MCP server uses standard input/output transport, arbitrary print() debugging can interfere with communication.
For server debugging, use Python's logging facilities and direct diagnostic logs to standard error or another appropriate destination.
Write Queries Don't Work
That's intentional.
Our database connection uses:
mode=robecause this tutorial is designed around read-only access.
If your application genuinely needs to modify data, don't simply expose unrestricted write SQL.
Create specific write tools with validation, authorization, logging, and appropriate user confirmation.
Security Best Practices
Use Read-Only Connections
If an AI agent only needs to retrieve information, don't give it write access.
Our SQLite connection uses:
file:inventory.db?mode=roThis creates a database-level restriction rather than depending only on instructions given to the AI model.
Use Parameterized Queries
For model-controlled or user-controlled values, use SQL parameters:
cursor.execute(
"SELECT * FROM products WHERE stock_level <= ?",
(threshold,)
)Avoid directly concatenating untrusted values into SQL.
Limit Returned Data
Don't allow a simple query to dump an entire large database into the model context.
Set sensible limits and use pagination when necessary.
Prefer Purpose-Built Tools
Instead of one powerful database tool, consider specific operations such as:
get_product(sku)
get_low_stock_products(threshold)
search_products(category)
get_inventory_summary()Each tool can have its own validation rules and permissions.
Keep Sensitive Data Out of Model Context
If your database contains passwords, API keys, payment information, private customer information, or other sensitive data, don't assume the AI model needs access to it.
Restrict which tables and columns are exposed.
Log Important Tool Activity
For production environments, consider recording:
Which tool was called
When it was called
Whether it succeeded
Which client initiated the operation
Relevant non-sensitive parameters
Avoid unnecessarily storing secrets or sensitive data in logs.
Why AI Agent Database Security Is Different
A traditional application generally performs database operations through predefined application code.
An AI agent introduces another decision-making layer:
User Request
|
v
AI Model
|
v
Tool Selection
|
v
Tool Arguments
|
v
Database OperationBecause the model participates in deciding which tools to call, security shouldn't depend on the model always making the correct decision.
The tool implementation and database permissions should enforce the important restrictions independently.
Final Result
At this point, you've created an SQLite MCP server that:
Uses FastMCP.
Exposes the database schema as an MCP resource.
Provides controlled read-only database access.
Uses SQLite's read-only mode.
Uses parameterized SQL for structured lookups.
Limits returned records.
Can be tested before connecting it to an AI application.
From here, you can gradually add purpose-built tools as your application requires them.
Conclusion
Building an MCP server for SQLite with Python and FastMCP is relatively straightforward.
The more important question is how much database access your AI agent actually needs.
For a read-only database assistant, a sensible starting architecture combines:
Least Privilege
+
Read-Only Database Access
+
Parameterized Queries
+
Input Validation
+
Limited Results
+
LoggingThe key principle is simple: don't rely on the AI model itself to enforce your security boundaries.
Your tools, database permissions, and infrastructure should enforce them.
Sources & Documentation
FastMCP Documentation:
https://gofastmcp.com/
FastMCP GitHub Repository:
https://github.com/PrefectHQ/fastmcp
Model Context Protocol Documentation:
https://modelcontextprotocol.io/
Python SQLite Documentation:
https://docs.python.org/3/library/sqlite3.html
