
MCP Server + Postgres: Production Architecture Patterns (2026)
We built a Postgres MCP server that lets AI agents query a production database in plain English. The naive approach — give the agent raw SQL access — is a security disaster. This is the architecture we landed on after getting burned, and the one we ship to every client now.
TL;DR
- Never give LLMs raw SQL — it is a security and cost disaster waiting to happen.
- Generate typed MCP tools from your schema — one tool per table or semantic entity.
- Four security layers: read-only Postgres role, parameterized queries, hard LIMIT, RLS passthrough.
- Production gotchas: connection pool exhaustion, query timeouts, missing indexes on LLM-picked filter columns.
Why Raw SQL Access Is a Trap
Every team starts by thinking: we will let the LLM write SQL and just review the queries. This breaks down within the first week of production.
- Prompt injection writes SQL. A user message can influence the query the LLM generates. We have seen cases where a cleverly-worded support ticket caused the LLM to write a query that leaked customer data.
- Expensive queries happen accidentally. An LLM-generated JOIN across five tables without appropriate filters can scan hundreds of gigabytes.
- Destructive queries are one prompt away. Any DELETE, UPDATE, or DROP command the model emits is one role misconfiguration away from executing.
- Debuggability is terrible. When the LLM generates a bad query at 3 AM, you are reading post-hoc logs trying to reconstruct intent.
The Three-Layer Architecture
Our Postgres MCP server has three layers. Each addresses a specific class of risk.
Layer 1 — Schema-Aware Tool Generation
On startup, the server introspects Postgres using information_schema and generates one MCP tool per table (or per semantic entity defined in configuration). Each tool has:
- A description derived from the table's Postgres COMMENT.
- Parameters derived from indexed columns — equality filters, range filters, text search.
- A typed return schema derived from the column types.
The LLM never writes SQL. It calls get_orders_by_customer(customer_id, limit) with typed arguments. The tool description tells the LLM when this query makes sense; the signature constrains what can be passed.
Layer 2 — Parameterized Execution
Every tool invocation is a parameterized query. The tool accepts typed filters; the server builds the SQL internally using $1, $2 placeholders. There is no code path that interpolates user input into a SQL string. No injection surface.
Column names from filter arguments are whitelisted against the schema before being inserted into the query. Only columns the server explicitly allows as filterable can appear.
Layer 3 — Database-Enforced Authorization
The MCP server is not the authorization layer. The database is. Three controls:
- Read-only role. The connection pool uses a Postgres role with SELECT privileges only. If the tool layer has a bug that somehow issues a write, Postgres refuses it.
- Row-level security passthrough. The server sets request.user_id as a session variable. RLS policies on each table enforce per-user data access. The database, not the tool, decides what rows this user can see.
- Statement timeout. SET statement_timeout = 5000 at the start of every session. No query runs longer than 5 seconds.
Connection Pooling — Do Not Skip This
Our first production deployment ran fine for 4 hours, then everything fell over. Every tool call was opening a new Postgres connection. At about 100 queries/minute, we exhausted the connection limit.
The fix: one asyncpg pool created at server startup, shared across all tool invocations. Pool size tuned for the Postgres connection limit and expected concurrency. We default to min_size=5, max_size=20 for a single MCP server instance.
If you are running managed Postgres (RDS, Supabase, Neon), put PgBouncer between the MCP server and the database. Your connection count will otherwise become the bottleneck.
Tool Descriptions Decide Everything
The generated tool descriptions are the single largest factor in whether the LLM uses your tools correctly. A vague description like "Query the orders table" will be called 10 times more than needed. A specific description sets expectations for when and how to call.
Compare:
- Bad: "Query the orders table."
- Good: "Retrieve orders for a specific customer. Use when the user asks about a customer's order history. Filter by customer_id (required) and status (optional). Returns up to 50 recent orders. Do not call repeatedly for the same customer in one session — cache the result."
Generate descriptions from the Postgres COMMENT on each table, and treat those comments as prompt engineering. Invest in writing them well.
Schema Change Workflow
Schema changes — new tables, dropped columns, renamed fields — need to flow into the MCP server without stale tool definitions causing failures.
- Tool generation runs on server startup. New deploys pick up schema changes automatically.
- On a long-running server, re-introspect every 15 minutes and diff. Changes trigger tool re-registration.
- Deprecated columns get a short grace period before removal, logged loudly.
- All schema changes go through migrations that update table COMMENTs where the description logic would change.
Observability — What to Log
You need to reconstruct why the LLM made the decisions it made. Our logging, per tool invocation:
- Tool name, input arguments, user_id, request_id.
- Generated parameterized SQL (not the values, for PII).
- Execution time, row count returned, Postgres query plan for slow queries.
- Any error with full stack trace.
- Link back to the originating LLM trace (LangSmith, Langfuse, OpenTelemetry span).
Track p99 latency per tool. When a tool's p99 creeps up, it usually means the LLM started using a filter that needs a new index.
When to Skip MCP and Just Build an API
MCP is not always the right answer. If the agent only needs 3-5 specific queries, a normal REST or GraphQL API with function-calling tools is simpler and gives you more control. MCP wins when:
- You want multiple clients (Claude Desktop, internal agents, third-party tools) to use the same capabilities.
- Your schema is large enough that auto-generating tools beats hand-writing them.
- You benefit from the full MCP ecosystem (resources, prompts, sampling).
For a narrow integration owned by one team, a handful of typed function-calling tools backed by an internal API is often the faster path.
FAQ
What is an MCP server for Postgres and why do I need one?
A Postgres MCP server is a Model Context Protocol server that exposes your Postgres database as a set of typed tools that AI agents can call. Instead of giving an agent raw SQL access (a security disaster), you expose specific operations like get_customer_by_id or list_recent_orders. The MCP server generates these tools automatically from your schema and enforces parameterized execution, read-only access, and row-level security.
Why not just give the LLM raw SQL access?
Several reasons. First, SQL injection: any LLM can be prompt-injected into writing malicious SQL if it has raw SQL access. Second, cost: an LLM writing ad-hoc queries will eventually write a query that scans 500GB and bills $400. Third, authorization: SQL access gives the LLM everything, when most queries only need a narrow slice. Fourth, observability: debugging LLM-generated SQL after the fact is a nightmare.
How does schema-aware tool generation work?
On startup, the MCP server introspects the Postgres schema — tables, columns, types, foreign keys, comments — and generates one MCP tool per table. Each tool has a description derived from the table comment, parameters derived from the indexed columns, and typed return values. The LLM picks tools by reading descriptions, not by writing SQL.
What are the critical security controls for a production Postgres MCP server?
Four non-negotiables. Connect as a read-only Postgres role — the database itself refuses writes. Use parameterized statements exclusively — no string interpolation. Enforce LIMIT clauses on every query (1000 rows default) to prevent accidental full-table scans. Pass the user's identity to Postgres so row-level security policies apply — the MCP server is not the authorization layer; the database is.
What goes wrong in production MCP server deployments?
Connection pool exhaustion — each tool call opened a new connection, fix is asyncpg connection pooling. Long-running queries — a query that scans 10M rows holds up the worker, fix is a hard 5-second statement timeout. Over-broad tool descriptions causing unnecessary calls. Missing indexes on columns the LLM filters by — monitor p99 query time per tool and add indexes reactively.
Ready to Start Your Project?
Let's discuss how we can bring your vision to life with AI-powered solutions.
Let's Talk