DEV Community

Mohammed Arshad Ansari
Mohammed Arshad Ansari

Posted on Originally published at hikmahtechnologies.com

Postgres MCP server: which one to run, and how to make it read-only

A Postgres MCP server is a small program that lets an AI client such as Claude Code or Cursor list your tables and run SQL against your database. Search for "postgres mcp" and you will find a dozen of them. The one most tutorials still point at is archived, and its read-only mode could be escaped with one word: COMMIT.

So the useful question is not "which Postgres MCP server?" It is where does the read-only guarantee actually live? My answer, after building an MCP server of my own: in the database role. The server is a convenience on top.

Is there an official Postgres MCP server?

There used to be. The Model Context Protocol project shipped a reference Postgres server as @modelcontextprotocol/server-postgres. It is now in modelcontextprotocol/servers-archived, a repository whose own description is "Reference MCP servers that are no longer maintained". The active modelcontextprotocol/servers repository keeps seven reference servers today (everything, fetch, filesystem, git, memory, sequentialthinking, time). Postgres is not one of them.

Its read-only mode worked like this, from its source:

await client.query("BEGIN TRANSACTION READ ONLY");
const result = await client.query(sql);   // the model's SQL, as one string
// ...
await client.query("ROLLBACK");
Enter fullscreen mode Exit fullscreen mode

The model's SQL went in as a single string, and the Node client accepts several statements separated by semicolons. Datadog Security Labs showed in August 2025 that COMMIT; DROP SCHEMA public CASCADE; ends the read-only transaction and then runs the drop with whatever rights the connection has. The same write-up says the server was deprecated on 10 July 2025 and archived on GitHub, npm and Docker Hub, and that the npm package still had about 21,000 weekly downloads.

That is the whole lesson in one bug. A read-only flag in the server is a promise the server makes. A role without write grants is a fact the database enforces.

Which Postgres MCP server to run instead

The maintained option I would start with is Postgres MCP Pro by Crystal DBA. It is MIT-licensed. I checked the repository on 1 October 2026: the last tagged release is v0.3.0 (May 2025), and the last commit to main was on 16 August 2026.

It gives the model nine tools, among them list_schemas, list_objects, get_object_details, execute_sql, explain_query, get_top_queries (from pg_stat_statements) and analyze_db_health. Two details matter more than the tool list:

  • It has two access modes, and the default is unrestricted, which allows writes and schema changes. You have to pass --access-mode=restricted yourself.
  • Restricted mode parses each statement with pglast and allows only safe statement types, and wraps execution in a 30-second timeout (in server.py, SafeSqlDriver(sql_driver=base_driver, timeout=30)). That timeout runs in the client. I would still set one in Postgres, below, so the database stops the query even if the client goes away.

The README is candid about the limits. It says restricted mode is "conservative" and that "we may add measures in the future to make sure that restricted mode is safe to use with production databases". Read that as: use it with a read-only role, not instead of one.

Other servers exist, including Supabase's own and several "read-only by default" projects. I have not run them, so I won't rank them. The test I would apply to any of them is the same: does it still hold if the server's own check is bypassed?


This is the first part. The full post — including the rest of the working details — is on my site: Postgres MCP server: which one to run, and how to make it read-only

Top comments (1)

Collapse
 
freedailypro profile image
FreeDailyPro •

Putting the guarantee in the database role is the right call, and I'd add two small things that have saved me. First, set default_transaction_read_only = on on the role itself (ALTER ROLE ... SET), so a stray BEGIN or COMMIT from the model can't quietly flip it into write mode. Second, set a statement_timeout and a low connection limit for that role, because a read-only agent can still run a runaway cross join and hurt production. I'd also point the MCP server at a read replica when you have one, so the worst case is a slow replica instead of a slow primary. Good write-up on the COMMIT escape, that one surprises a lot of people.