SafetyCard Request a review

Postgres MCP Pro

Postgres MCP with configurable read/write access, index tuning and health analysis

github.com/crystaldba/postgres-mcp · 2026-10-03 · 15c8e33

Defense-in-depth score

4.2 / 10

As shipped, Postgres MCP Pro runs in unrestricted mode: the model can execute any SQL, including multi-statement DROP/DELETE batches, with the operator's database login, and every statement is committed immediately with no timeout, record or undo. Risk annotations on some read-only-labelled tools are not accurate. The opt-in restricted mode is a genuinely strong parser allowlist plus read-only transaction with a 30-second timeout; deployers should always use it and a read-only database role.

Safest setup

Postgres MCP Pro keeps no memory, loads no plugins and runs over stdio by default, but as shipped it runs any SQL the model writes with your login and commits it at once. The safest setup gives it a read-only database role, turns on its restricted mode and adds limits and logging on the database side.

SetupHowWhat it gives you
A read-only database rolePut a login in DATABASE_URI that can only read the schemas the task needs and is neither owner nor superuserPostgres itself refuses writes and host-level features, whatever SQL the model sends C1
Restricted mode--access-mode=restricted on the server command lineEvery tool runs SQL through a parser allowlist in a read-only transaction with a 30-second timeout C3
The local stdio transportKeep the default --transport of stdioOnly the host that launched the server can talk to it C1
A role-level timeoutSet a statement timeout and connection limit on the database rolePostgres ends runaway queries even when the server sets no limit of its own C10
Index tuning kept localStart the server with no OpenAI API key in its environmentQuery text and plans stay on your network, since the 'llm' tuning method can't reach OpenAI C5
A database-side recordTurn on Postgres statement logging or auditing for the role, written where the agent can't reachA record of every statement the agent ran, which the server itself doesn't keep C9

Approving with confidence

  • Read each prompt and approve calls one at a time; in the default mode a single execute_sql call can run several statements and commit them immediately.
  • Restricted mode makes every tool's read-only label accurate; in the default mode, rely on reading each call rather than on the labels.

Handling data

  • Failed queries are logged with their full SQL text and errors return to the model as-is; treat the server's stderr like the data it queries.
  • The connection string carries a long-lived password; keep it in your host's secret store rather than in shared config files.

Key gaps (1)

  1. Default unrestricted mode: a hijacked host session can read any data the login can see and run irreversible SQL that the server commits immediately, with no server-side gate. C5 · Untrusted input blast radius

Criteria

C1 Identity & least privilege

0.17 / 1.00

The server connects to Postgres with the single connection string the operator supplies in DATABASE_URI and uses that one credential, through one connection pool, for every tool, reads and writes alike. Nothing in the server narrows the database role or checks who is asking: there is no per-caller authorization. Least privilege therefore depends entirely on the operator creating a restricted database role; the README's own examples use a generic user/password login with unrestricted mode.

C2 Approval gates

0.50 / 1.00

As a tool server, Postgres MCP Pro cannot show approval prompts itself; it gives the host risk annotations. In the default unrestricted mode, execute_sql is correctly flagged destructive, but the read-only labels on some tools are not accurate. The opt-in restricted mode is much better: every tool goes through a SQL parser allowlist and a read-only transaction, so the read-only labels become true, but it is not the default.

C3 Tool & action scoping

0.50 / 1.00

In the default unrestricted mode the main tool takes any SQL string and runs it as-is, multi-statement batches included, against the whole database. A few helper tools are narrow and parameterised (schema listing, object details, top-queries ranking), but the general execute_sql tool is raw passthrough. The opt-in restricted mode adds real validation: SQL is parsed with Postgres's own parser and checked against allowlists of statement types, node types and functions, inside a read-only transaction with a 30-second timeout.

C4 Code-execution isolation

0.05 / 1.00

The code-execution surface here is SQL: execute_sql and other SQL-taking tools send model-written statements straight to the database. In the default unrestricted mode there is no boundary at all: no statement filter, no read-only transaction, no timeout, and multi-statement batches are executed and committed. What that SQL can reach is everything the configured login can do; with a superuser login, Postgres features such as COPY ... TO PROGRAM or untrusted procedural languages extend that to the database host. The opt-in restricted mode's parser allowlist is credited under tool scoping rather than here.

C5 Untrusted input blast radius

0.07 / 1.00

The server returns database rows, catalog data and pg_stat_statements query texts to the model as plain stringified Python lists, with no marking of what came from the database versus the server. Any of that content can be attacker-written (user-submitted rows, logged queries). Nothing in the server limits what a hijacked host can then do: in the default mode the same session can read any data the login can see and run irreversible SQL, and the model-selectable index-tuning method 'llm' sends query text and plans to OpenAI. Restricted mode removes the write leg but not the read-everything leg, and it is opt-in.

C6 Memory, context & configuration integrity

N/A · full credit 1.00 / 1.00

The server keeps no memory, vector store or conversation state, and it auto-loads no workspace files: configuration comes only from the DATABASE_URI environment variable or a command-line argument set by the operator. There is no .env loading. The database itself is persistent and writable in unrestricted mode, but that is the system being operated on (covered under the other criteria), not agent memory.

C7 Third-party extensions

N/A · full credit 1.00 / 1.00

The server loads no plugins, MCP servers, downloaded tools or model files, and installs nothing at runtime. Its only runtime third-party call is the optional OpenAI API request in the 'llm' index-tuning method, which is a fixed dependency, not loaded code. Database-side extensions created via SQL are scored as SQL execution under C4.

C8 Secrets & sensitive-data protection

0.25 / 1.00

The database password lives in the DATABASE_URI environment variable or command line. A password-obfuscation helper exists but is only applied to the startup connection-failure warning. Elsewhere, failed SQL is logged with its full text and raw error strings are returned to the model; the Docker entrypoint's credential handling is not locked down. There is no telemetry. The credential is a long-lived database password whose scope is whatever the operator chose.

C9 Audit & traceability

0.20 / 1.00

The server keeps no record of what it did. Successful tool calls, including every write executed through execute_sql, are not logged at all; only failures are logged, as free-text error lines that include the failing SQL. The repository configures no log handler, so these lines go wherever the MCP library or Python's default handler sends them (stderr), and nothing is flushed per action or protected against loss.

C10 Limits & kill switch

0.40 / 1.00

In the default unrestricted mode, execute_sql and explain_query have no timeout and no row or output cap: a query runs until Postgres finishes and all rows are buffered. The index-tuning advisor stops itself after 30 seconds and accepts at most 10 queries, and the LLM optimizer stops after 5 attempts without progress. The opt-in restricted mode adds a 30-second timeout to every query. Shutdown closes the connection pool on SIGTERM/SIGINT.