BoundBench

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

Minimal

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.

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

Minimal 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

Moderate 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

Moderate 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

Minimal 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

Minimal 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

Minimal 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

Minimal 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

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