Build a PostgreSQL MCP Server Safely

A database MCP server can be powerful, but “let the model run arbitrary SQL” is usually the wrong first design.

Prefer task tools

Expose narrow tools such as find_customer, get_recent_orders, or explain_slow_query. These let you parameterize SQL and enforce domain rules.

@mcp.tool()
async def recent_orders(customer_id: str, limit: int = 20) -> list[dict]:
    """Return recent orders for one customer."""
    limit = min(max(limit, 1), 100)
    rows = await pool.fetch(
        "SELECT id,total,status,created_at FROM orders WHERE customer_id=$1 ORDER BY created_at DESC LIMIT $2",
        customer_id, limit,
    )
    return [dict(r) for r in rows]

Use a read-only database role

If the use case is analysis, provision credentials that cannot mutate data. Database permissions are a stronger control than prompt instructions.

Bound result size

Always cap rows and columns. Returning 100,000 rows wastes tokens, hurts latency, and may leak unnecessary data.

Schema as a resource

You can expose sanitized table/column documentation as MCP resources while keeping actual query operations as tools.

Never rely on “the model won't do that” as a database security mechanism.