# Connect Claude to your database with MCP

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.

Add a read-only server, ask a plain-English question, and watch the SELECT Claude ran. Illustrative session.

If MCP is new to you, read [MCP fundamentals](/guides/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.

```bash
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;
SQL
```

The `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.Read-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 `--`.

```bash
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:

- `--readonly` tells DBHub to refuse anything that is not a read. This is your second lock, on top of the database role.
- `--dsn` is 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.The 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.

```bash
claude mcp add --transport stdio db \
  --env DATABASE_URL="postgresql://claude_ro:change-me@db.internal:5432/analytics" \
  -- npx -y @bytebase/dbhub --readonly
```

DBHub 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.

```bash
claude mcp list
```

To see the stored config for one server, including its transport and scope:

```bash
claude mcp get db
```

Inside an interactive Claude Code session, run `/mcp` to view connected servers and their tools. Once `db` shows as connected, ask a question:

What are our five highest-revenue customers this quarter? Use the db server and show me the SQL you ran before the results.

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 `orders` table, 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.

```bash
claude mcp add --transport http team-db https://db-mcp.internal/mcp
```

In `.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:

```bash
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.

```bash
claude mcp remove db
```

That 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](/guides/connect-claude-to-your-notes) or [your GitHub repos](/guides/connect-claude-to-github). For the config formats and scopes in depth, read [MCP fundamentals](/guides/mcp-fundamentals), and for deciding what to allow and what to block on any server, [MCP security basics](/guides/mcp-security-basics).
