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.