Overview

The read-only database role, not the MCP server’s own flags, is what stops Claude from writing. This guide creates that role and registers DBHub (@bytebase/dbhub), the Postgres server used in Claude Code’s own MCP docs, over stdio. The old @modelcontextprotocol/server-postgres package is archived and deprecated, and an unpatched SQL injection let it bypass its read-only mode, so do not use it.

Prerequisites

Steps

1. Create a read-only role

CREATE ROLE mcp_readonly LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE mydb TO mcp_readonly;
GRANT USAGE ON SCHEMA public TO mcp_readonly;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO mcp_readonly;

Never connect an agent as a superuser or as the application owner. Point it at a replica or a staging copy when the data is sensitive.

2. Keep the DSN out of committed files

export PG_MCP_DSN="postgres://mcp_readonly:change-me@localhost:5432/mydb"

3. Register DBHub

claude mcp add --transport stdio --scope project db -- \
  npx -y @bytebase/dbhub --transport stdio --dsn "$PG_MCP_DSN"

This writes the DSN into .mcp.json, which you may commit. To keep it out, put "--dsn", "${PG_MCP_DSN}" in the args array by hand; ${VAR} expands at launch. DBHub exposes execute_sql and search_objects by default; explain_sql is opt-in.

4. Allow the low-risk tool

{ "permissions": { "allow": ["mcp__db__search_objects"] } }

Leave mcp__db__execute_sql on prompt so you read each statement before it runs.

Verify it worked

psql "$PG_MCP_DSN" -c "INSERT INTO users DEFAULT VALUES"   # must fail: permission denied

In a session, /mcp should list db as connected. Ask “List the tables and describe users”, then ask it to delete a row; the database must refuse even if you approve the call.

Common errors

  • db is not connected. PG_MCP_DSN was not exported in the shell that launched claude.
  • permission denied for table. The grant ran before the table existed; re-run the GRANT, or rely on ALTER DEFAULT PRIVILEGES for future tables.
  • Password appears in .mcp.json. Switch to ${PG_MCP_DSN}, rotate the password, and check git history.
  • Statements still change data. The role owns the tables or has write grants; check with \dp.

FAQ

Which Postgres MCP server should I use with Claude Code?

Use DBHub, the Postgres server used in Claude Code’s own MCP docs. Avoid @modelcontextprotocol/server-postgres, which is archived and had an unpatched SQL injection that bypassed its read-only mode.

How do I stop Claude from writing to my Postgres database?

Connect it as a read-only database role. The role, not the MCP server’s own flags, is what blocks writes, so never use a superuser or the application owner.

How do I keep the database password out of .mcp.json?

Put "--dsn", "${PG_MCP_DSN}" in the args array by hand so the value expands at launch. If the password already landed in the file, rotate it and check git history.

Should I auto-approve the execute_sql tool?

No. Leave mcp__db__execute_sql on prompt so you read each statement before it runs.