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.