When a bug only makes sense once you see the actual data, a database server pays for itself. Instead of describing your schema to Claude Code and hoping it guesses right, you connect Postgres through MCP and let it inspect the real tables, run the query, and reason from what’s actually there. The catch is that database access is exactly the kind of reach you want to scope carefully — which is most of what this guide is about.
The short version: connect as a read-only role, against a non-production database, with the connection string in an environment variable. Do that and you get the upside — Claude reasoning from real structure — without the nightmare of an agent holding write access to production. Here’s the full setup and the workflows it’s good for.
New to MCP setup? Start with the Windows and WSL guide.
Connect the server
Add a Postgres MCP server with claude mcp add, passing a connection string. The exact package name can vary and move, so confirm the current one against the MCP docs before relying on it:
claude mcp add postgres -- npx -y @modelcontextprotocol/server-postgres "$DATABASE_URL"
Notice the connection string is an environment variable, not a literal. Set it in your shell:
export DATABASE_URL="postgresql://claude_ro:password@localhost:5432/app_dev"
On native Windows, remember the cmd /c wrapper for npx-based servers and set the variable with PowerShell’s $env: or setx. The connection itself is the same; only the shell mechanics differ.
Create the read-only role
A few lines of SQL give you a role that can look but not touch. Run this as an admin against the database you’ll connect:
CREATE ROLE claude_ro LOGIN PASSWORD 'a-strong-password';
GRANT CONNECT ON DATABASE app_dev TO claude_ro;
GRANT USAGE ON SCHEMA public TO claude_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO claude_ro;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO claude_ro;
That grants SELECT and nothing else — no INSERT, UPDATE, DELETE, or DDL. The PostgreSQL roles documentation covers the privilege model if you need finer control. Point DATABASE_URL at this role and the server physically cannot change data.
Target a non-production database
Read-only is the first guardrail; a non-production target is the second. Even read queries can be heavy, and you don’t want an agent running an unindexed scan against your production database during business hours. Point it at:
Where to point the connection
| Local dev database | Best for most work — a copy of the schema, safe to hammer |
|---|---|
| Read replica | When you need real-shaped data without touching the primary |
| Staging | A realistic environment that isn't customer-facing |
| Production primary | Avoid — at most a read replica, never the write node |
The workflows it’s good for
With a safe connection in place, the value shows up in debugging and exploration:
Inspect a schema you don’t remember. “List the tables related to billing and show me the columns on the invoices table.” Faster than opening a SQL client and reconnecting.
Debug against real data shape. “This query returns duplicates — look at the orders and order_items tables and tell me why.” Claude reads the actual structure and data instead of working from your paraphrase.
Understand a slow query. “Here’s a query that’s slow. Check the table sizes and indexes and suggest what’s missing.” It can read the schema and reason about the plan.
Validate a bug. “The report shows the wrong total for March — query the underlying data and check whether the bug is in the data or the calculation.” It distinguishes a data problem from a code problem by looking.
In every case the agent runs SELECTs you can see and reasons from real results. Because the role is read-only, you can let these run without worrying.
Keep the connection string out of git
The connection string is a credential. If you use project scope so the team shares the server, the .mcp.json gets committed — and a literal connection string in it is a leaked password.
{
"mcpServers": {
"postgres": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-postgres", "$DATABASE_URL"]
}
}
}
Reference the environment variable and document which one teammates set locally. That way the shared config tells everyone how to connect without shipping anyone’s credentials. The same principle and more is in MCP security best practices.
Safe Postgres setup checklist
- Create a dedicated read-only role with SELECT only
- Point the connection at dev, a replica, or staging — not the prod primary
- Put the connection string in an environment variable
- Reference the env var in .mcp.json, never the literal string
- Use cmd /c for the npx server on native Windows
- Prefer synthetic/anonymized data to avoid sensitive rows in the session
Wrapping up
A Postgres MCP server turns “let me describe my schema” into “go look at it,” which is a real upgrade for debugging. The whole game is scoping: a read-only role so the database enforces the boundary, a non-production target so heavy queries can’t hurt anything, and the connection string in an environment variable so credentials stay out of git. Set those three up and you can let Claude explore the database freely, because there’s nothing it can break.
For the broader safety picture across all MCP servers, see MCP security risks and best practices; for the other servers worth running, the best MCP servers list.