
How to Build a Secure Database MCP Server for PostgreSQL and SQLite in Python
Quick Start (Minimal Read-Only Database MCP Server)
Connecting an AI agent directly to a database without strict read-only execution boundaries risks accidental DROP TABLE executions or runaway full-table scans.
To build a secure, zero-boilerplate database MCP server right now, install fastmcp and psycopg:
pip install fastmcp psycopg[binary]
Save this script as db_mcp.py:
import sqlite3
from fastmcp import FastMCP
mcp = FastMCP("Database-Inspector")
@mcp.tool()
def query_sqlite(db_path: str, sql_query: str) -> list[dict]:
"""Execute a read-only SQL query against a local SQLite database."""
# Enforce immutable read-only mode at the connection level
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
try:
cursor.execute(sql_query)
rows = cursor.fetchmany(100) # Hard limit on returned rows
return [dict(row) for row in rows]
finally:
conn.close()
if __name__ == "__main__":
mcp.run()
Security Architecture: Agent Database Access Matrix
Never give language models raw write access or administrative credentials. Always enforce safety constraints at the driver and network layers rather than relying on prompt instructions:
| Defense Layer | Recommended Configuration | Risk Prevented |
|---|---|---|
| Transaction State | SET TRANSACTION READ ONLY |
Accidental UPDATE, DELETE, DROP mutations |
| Row Limits | Hardcoded LIMIT 100 in cursor driver |
Context window exhaustion & high token costs |
| Statement Timeout | statement_timeout = 3000 (3 seconds) |
DoS via unindexed Cartesian joins |
| Schema Isolation | Dedicated ai_agent_readonly user role |
Privilege escalation & data leaks |
| Connection Mode | SQLite URI ?mode=ro |
Direct file-level writes bypassing SQLite engine |
Step 1: PostgreSQL Implementation with Connection Pooling
For PostgreSQL, use a connection pool and execute queries inside a read-only transaction block with an enforced statement timeout.
import psycopg
from psycopg.rows import dict_row
from fastmcp import FastMCP
mcp = FastMCP("Postgres-Inspector")
PG_CONN_STRING = "postgresql://readonly_user:secret_pass@localhost:5432/production_replica"
@mcp.tool()
def describe_table_schema(table_name: str) -> list[dict]:
"""Inspect column names, data types, and nullability for a PostgreSQL table."""
query = """
SELECT column_name, data_type, is_nullable
FROM information_schema.columns
WHERE table_name = %s
ORDER BY ordinal_position;
"""
with psycopg.connect(PG_CONN_STRING, row_factory=dict_row) as conn:
with conn.cursor() as cur:
cur.execute(query, (table_name,))
return cur.fetchall()
@mcp.tool()
def execute_safe_select(sql_query: str) -> list[dict]:
"""Execute a SELECT query with strict timeout and read-only enforcement."""
with psycopg.connect(PG_CONN_STRING, row_factory=dict_row) as conn:
# Enforce read-only at session level
conn.read_only = True
with conn.cursor() as cur:
# Set 3 second statement timeout to stop runaway queries
cur.execute("SET statement_timeout = 3000;")
cur.execute(sql_query)
return cur.fetchmany(50)
if __name__ == "__main__":
mcp.run()
Step 2: Database User Provisioning in PostgreSQL
Execute these commands in PostgreSQL (psql) to create a dedicated user with minimal permissions:
-- 1. Create dedicated read-only role
CREATE ROLE ai_reader WITH LOGIN PASSWORD 'strong_random_password';
-- 2. Grant connection rights to target database
GRANT CONNECT ON DATABASE analytics_db TO ai_reader;
-- 3. Grant schema usage and select privileges
GRANT USAGE ON SCHEMA public TO ai_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_reader;
-- 4. Ensure future tables are automatically readable
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ai_reader;
-- 5. Revoke write privileges explicitly
REVOKE CREATE ON SCHEMA public FROM ai_reader;
Step 3: Registering the Server in Claude Code, Cursor, and Antigravity
Add the database server configuration to your AI client’s MCP configuration file.
For Claude Desktop / Claude Code (claude_desktop_config.json):
{
"mcpServers": {
"postgres-inspector": {
"command": "python3",
"args": ["/absolute/path/to/db_mcp.py"],
"env": {
"PG_CONN_STRING": "postgresql://ai_reader:pass@127.0.0.1:5432/analytics_db"
}
}
}
}
For Cursor (.cursor/mcp.json):
{
"mcpServers": {
"db-tools": {
"command": "python3",
"args": ["/absolute/path/to/db_mcp.py"]
}
}
}
Verification: Test Prompt for Your AI Assistant
Once configured, restart your AI assistant and issue this prompt to verify tool discovery and schema retrieval:
Inspect the tables in our analytics database.
Find the top 5 customers by order count in 2026 using describe_table_schema and execute_safe_select.
Do not modify any tables.
The agent will execute describe_table_schema("orders"), analyze the foreign keys, formulate an optimized SQL query, and return structured tabular insights without manual copy-pasting.