To let Claude query a Postgres database, run a database MCP server pointed at a connection string, add it to Claude Code, and set it to read-only. Then you ask questions in plain English and Claude runs the SELECT queries for you. Safety comes from two layers you control. One is a database user that can only read; the other is a server flag that refuses anything but reads.
If MCP is new to you, read MCP fundamentals first. This guide assumes you know what a host, a client, and a server are.
The short version
You wire one local server between Claude Code and Postgres. Claude never holds your credentials and never opens a raw connection. Instead, it calls a tool on the server. The server runs the query inside a read-only transaction, and the rows come back as context.
The server runs as a local stdio subprocess on your machine. No network port is opened. The connection string stays in your local config.
Step 1: create a read-only database user
Do this before you touch Claude. The first and most important lock is at the database, not the AI tool. Create a role that can read and nothing else, so even a bug or a bad query cannot change data.
psql "$ADMIN_DATABASE_URL" <<'SQL'
CREATE ROLE claude_ro LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE analytics 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;
SQLThe ALTER DEFAULT PRIVILEGES line matters: it grants SELECT on tables created later, so a new table next month is readable without re-running grants. This role has no INSERT, UPDATE, DELETE, or DROP. The database itself rejects writes, regardless of what any tool sends.
For a database with sensitive columns, grant SELECT on specific tables or views instead of ALL TABLES, and point Claude at a view that omits the columns you do not want it to see.Common gotchaRead-only is not the same as safe to read. A perfectly valid SELECT still pulls real PII (emails, card hints, password hashes) straight into Claude's context, where it may be logged or sent upstream.Grant on a view that drops or masks those columns, so the raw rows never leave the database.
Step 2: add the server to Claude Code
The official Claude Code path uses DBHub. It is a database MCP server that speaks to Postgres, MySQL, SQL Server, MariaDB, and SQLite. Add it with claude mcp add and pass the read-only connection string after the --.
claude mcp add --transport stdio db -- \
npx -y @bytebase/dbhub \
--readonly \
--dsn "postgresql://claude_ro:change-me@db.internal:5432/analytics"Two flags do the work here:
--readonlytells DBHub to refuse anything that is not a read. This is your second lock, on top of the database role.--dsnis the Postgres connection string for the read-only user you just made.
The -- is load-bearing. Everything after it is passed to the server untouched, so --transport, --scope, and --env are Claude's own options and --readonly and --dsn belong to DBHub. Drop the -- and Claude tries to read --readonly as one of its own flags, and fails.
Scope defaults to local, meaning the server is available only to you in the current project. The three choices:
| Scope | Stored in | Who sees it |
|---|---|---|
local (default) | your local user config | only you, this project |
project | .mcp.json in the repo | everyone on the project |
user | your global config | you, every project |
Never use project scope for a database server with a live connection string. That writes the DSN, password and all, into .mcp.json, which is committed to the repo. Keep it local. Or use user if you query the same database across projects.Common gotchaThe trap is reaching for --scope project so a teammate gets the server too, which silently bakes the plaintext password into .mcp.json and commits it. Once it lands in git history, deleting the line is not enough: the secret is in every clone and you must rotate the database password.Keep the DSN in --env at local or user scope, and share a sanitized .mcp.json that reads the secret from an environment variable instead.
Step 3: keep the password out of the config
A connection string with an inline password sits in plain text in your config file. Pass it through an environment variable instead. The secret then lives in your shell or a secret manager, and the config holds only a reference.
claude mcp add --transport stdio db \
--env DATABASE_URL="postgresql://claude_ro:change-me@db.internal:5432/analytics" \
-- npx -y @bytebase/dbhub --readonlyDBHub reads the DSN from the DSN or DATABASE_URL environment variable when --dsn is absent, so the password never appears in the command Claude stores. One detail trips people up. When you use --env, put at least one other option between --env and the server name. The CLI reads the value right after --env as another KEY=value pair, so a bare server name there is rejected. Putting --transport stdio first, as above, avoids that.
Step 4: confirm it connected
List your servers and check the database one is healthy.
claude mcp listTo see the stored config for one server, including its transport and scope:
claude mcp get dbInside an interactive Claude Code session, run /mcp to view connected servers and their tools. Once db shows as connected, ask a question:
Asking Claude to show the SQL is a habit worth keeping. You see exactly what ran, you catch a wrong join early, and you build trust in the answers.
What can I ask Claude once the database is connected?
A read-only database connection is most useful for questions you would otherwise answer by hand-writing SQL or exporting a spreadsheet. Claude reads the schema first, then writes the query.
- "List the columns and types on the
orderstable, then count rows per status." - "Find the ten users who signed up last week and have not placed an order."
- "Our revenue dashboard looks off. Compare last month to this month by product category and tell me which one moved."
- "Write the query for monthly active users, then explain the date math you used."
Because the role is read-only, you can let Claude explore freely. It cannot delete a row or drop a table. The cost of a wrong query is a wrong answer you can spot, not lost data. Treat the first answer to any new question as a draft: read the SQL, sanity-check the row counts, and ask Claude to refine if a join looks wrong.
Why won't the database server connect?
If claude mcp get db shows the server as failed, work through these in order.
| Symptom | Likely cause | Fix |
|---|---|---|
| Server fails immediately | npx cannot reach the package | Run the npx -y @bytebase/dbhub command on its own to see the error |
| Authentication failed | Wrong password or user in the DSN | Test the DSN with psql first, then re-add |
| Connection refused | Host or port unreachable from your machine | Confirm network access to the database, including any VPN |
| Server starts but every query is rejected | --readonly is on, query is a write | Expected behaviour; rephrase as a read, or it is a permissions gap |
| Timeout on startup | Server is slow to launch | Raise the limit, for example MCP_TIMEOUT=15000 claude |
The fastest way to isolate a problem is to run the server command directly in your shell. If npx -y @bytebase/dbhub --readonly --dsn "..." connects on its own, the issue is in how Claude stored the config, not in the database or the server.
Should I use a local stdio server or a remote one?
This guide uses a local stdio server, which Claude launches as a subprocess on your machine. That keeps the connection string on your laptop and opens no network port. A team that wants one shared, audited database connection has another option. Run a remote server over HTTP and add it with --transport http, where the config holds a URL rather than a DSN.
claude mcp add --transport http team-db https://db-mcp.internal/mcpIn .mcp.json, the type field accepts streamable-http as an alias for http, so a config copied from a server's own docs works without edits. A remote server is the right choice when you want one place to manage credentials, rotate them, and log every query. For a single developer querying a replica, though, local stdio is simpler and just as safe.
What does read-only block?
DBHub's --readonly flag restricts execution to read operations: SELECT, SHOW, DESCRIBE, and the like. A statement that writes is rejected by the server before it ever reaches Postgres. Combine that with a role that has only SELECT and you have defence in depth. The server says no, and if something slips past, the database says no too.
Neither layer alone is enough. The server flag is config you could fat-finger. The database role is the durable guarantee. Set both.
A note on the deprecated reference server
You will find guides that use @modelcontextprotocol/server-postgres, the original reference server. It moved to an archived repository in 2025 and is no longer maintained. True, it ran queries in a read-only transaction, which is a reasonable default. But it is not receiving fixes. Prefer a maintained server such as DBHub, and rely on the database role as your primary control rather than trusting any single package.
What should I lock down before production?
Read-only is the start, not the finish. Run this checklist before you point Claude at anything that holds real data.
| Control | Why it matters |
|---|---|
| Read-only database role | The query cannot mutate data even if the server flag is wrong |
--readonly on the server | A second gate that rejects writes before they reach the database |
DSN in --env, not inline | The password stays out of stored config and out of the repo |
local or user scope, never project | A live DSN must not be committed to .mcp.json |
| A query view that hides sensitive columns | Claude reads the view, never the raw customers table |
| A connection on a low-privilege network path | The read-only user reaches only the database it needs |
| Statement timeout on the role | A heavy exploratory query cannot stall the database |
You can set a statement timeout on the role so one slow scan cannot tie up the database:
psql "$ADMIN_DATABASE_URL" -c \
"ALTER ROLE claude_ro SET statement_timeout = '15s';"Point at a replica, not the primary
For a production database, give Claude a read replica or an analytics copy rather than the primary that serves your application. A read replica is a separate copy that receives the same data, so an expensive analytical query Claude runs there cannot compete with live traffic. The read-only role and --readonly flag still apply; the replica is one more boundary between exploration and the system your users depend on.
Removing the server
When you are done, or before handing your machine to someone else, remove it.
claude mcp remove dbThat deletes the stored config, including any inline DSN. If you used --env, also clear the variable from your shell or secret manager so the password does not linger.
Where to go next
You now have a pattern that generalises: a read-only role, a maintained server, secrets in environment variables, and the narrowest scope that works. The same shape applies to any data source you connect, such as a folder of notes or docs or your GitHub repos. For the config formats and scopes in depth, read MCP fundamentals, and for deciding what to allow and what to block on any server, MCP security basics.