PostgreSQL MCP Server with a Dedicated Read-Only Role

Create a Postgres role that can only SELECT and use it as the connection-string user for the postgres MCP server. Defence-in-depth even though the server itself is read-only.

Security

Detailed Explanation

Why a Dedicated Role

The postgres MCP server enforces "no writes" at the protocol level — it only exposes a query tool that wraps a read-only transaction. But protocol-level filters can have bugs. The robust pattern is to give the connection string a database user that the server physically cannot misuse.

One-time database setup

-- Create the role
CREATE ROLE mcp_readonly WITH LOGIN PASSWORD 'use-a-strong-password';

-- Allow connecting to the database
GRANT CONNECT ON DATABASE myapp TO mcp_readonly;

-- Allow listing schemas
GRANT USAGE ON SCHEMA public TO mcp_readonly;

-- Grant SELECT on existing tables
GRANT SELECT ON ALL TABLES IN SCHEMA public TO mcp_readonly;

-- Auto-grant SELECT on future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO mcp_readonly;

-- Allow reading sequences (for SERIAL columns in introspection)
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO mcp_readonly;

Config

{
  "mcpServers": {
    "postgres": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-postgres",
        "postgresql://mcp_readonly:STRONG-PASSWORD@localhost:5432/myapp"
      ]
    }
  }
}

What this protects against

  • A bug in the MCP server that accidentally allows DELETE.
  • A model "hallucinating" a tool name that bypasses the read-only check.
  • A future server upgrade that adds write tools you didn't enable.

Hardening the connection itself

Append ?sslmode=require to the connection string for any non-localhost database. For production-adjacent reads, use a separate read replica rather than your primary; the model's queries can be expensive and you don't want them competing with user traffic.

Multi-database setups

For projects with multiple databases, run multiple postgres MCP entries with different names:

"postgres-prod-replica": { ... },
"postgres-staging": { ... }

Use Case

Production systems where 'the model is read-only because the server says so' isn't sufficient — you want defence in depth so even a buggy or compromised server can't write to the database.

Try It — MCP Server Config Generator

Open full tool