Postgres MCP: the database user stops writes, not the server

Anthropic’s reference Postgres MCP server promised read-only access, and a request that started with COMMIT; could still drop every table. So whichever server you pick, give it a database user that can only read. Here’s the SQL to make one, and how to put that user in Claude or ChatGPT right next to your ad accounts.

The read-only promise that didn’t hold

Anthropic’s server ran every query inside a read-only transaction. Anthropic archived it in May 2025, and that August Datadog showed that it also accepted several statements at once, so a request could close that transaction with COMMIT; and run anything after it. Their example was COMMIT; DROP SCHEMA public CASCADE;. Datadog’s advice holds for any server: use “a Postgres user with restricted privileges. You should definitely do this. This would prevent any unauthorized operation, even outside the transaction.”

Read-only modes in today’s servers are settings you can get wrong. Every setup example in the Postgres MCP Pro README uses --access-mode=unrestricted. Microsoft’s postgres-mcp says its profiles “permit write tools unless access_mode: ro is set.”

We built SyncWith’s server to run one SELECT at a time and refuse anything else. Even so, our connect form recommends a read-only user.

What can stop a writeThree safeguards, from top to bottom. The server’s read-only mode: a COMMIT sent with the query got past the old reference server’s. The default_transaction_read_only setting: only a default for new transactions, and a SET statement can change it. The user’s grants, SELECT only: Postgres refuses the write whatever the server sends. Only the third holds on its own.What can stop a writeOnly the last one holds on its ownThe server’s read-only modeA COMMIT; sent with the query got past the oldreference server’sCan be bypasseddefault_transaction_read_onlyOnly a default for new transactions;a SET statement can change itCan be bypassedThe user’s grants: SELECT onlyPostgres refuses the write, whateverthe server sendsHolds

Make a user that can only read

If a developer runs your database, send them this. It makes a login called ai_reader that can read the tables in the public schema.

-- 1. A login for the assistant (pick your own password)
CREATE ROLE ai_reader LOGIN PASSWORD 'change-me';

-- 2. Let it see the schema and read its tables
GRANT USAGE ON SCHEMA public TO ai_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_reader;

-- 3. Tables created later by app_owner are readable too
ALTER DEFAULT PRIVILEGES FOR ROLE app_owner IN SCHEMA public
  GRANT SELECT ON TABLES TO ai_reader;

-- 4. A second layer: new transactions start read-only
ALTER ROLE ai_reader SET default_transaction_read_only = on;
  • The two GRANT lines decide what the assistant can see. If some tables hold customer details it doesn’t need, name only the tables it does need (GRANT SELECT ON leads, orders TO ai_reader;) in place of ALL TABLES.
  • ALTER DEFAULT PRIVILEGES only covers tables that app_owner creates from now on. Replace app_owner with the user your app uses to create tables, or new tables won’t be readable.

Checked against the PostgreSQL 18 docs: CREATE ROLE, GRANT, ALTER DEFAULT PRIVILEGES, ALTER ROLE, default_transaction_read_only.

Connect the read-only user to Claude or ChatGPT

If you query Postgres from Cursor, VS Code or Claude Desktop, a server on your own machine works fine. Claude on the web and ChatGPT can’t use one directly: ChatGPT “connects to remote MCP servers” and Claude’s local servers “aren’t available” on claude.ai. Ours is remote, so there’s nothing to install.

  1. Add the server to your assistant. Paste this URL as a custom connector:

    https://mcp.syncwith.com/mcp

    Every Claude plan can add one, though Free allows a single custom connector. In ChatGPT, an admin of a Business, Enterprise or Edu workspace can add it, and Pro accounts can only read and fetch. Steps for Claude and ChatGPT.

  2. Sign in with Google. You don’t need a SyncWith account first. We’ll create a free one for you.
  3. Ask it to connect PostgreSQL. It replies with a link to a form. Paste the connection string for ai_reader, or enter the details one by one. We test the connection before saving it.
  4. Allow our IP addresses in your firewall. If your database has a firewall or IP allowlist, allow SyncWith’s two addresses: 3.227.95.35 and 34.199.138.158. Those are the only addresses we connect from.

Ask about leads and ad spend in one chat

One SyncWith customer keeps every lead in Postgres, with the UTM tags each lead arrived with, and checks them against what they spend on Google Ads and Meta. With the database and the ad accounts on one connection, that’s one question. Start each question with “Using SyncWith.” A few to try:

  • “Using SyncWith, what tables are in my PostgreSQL database?” Ask this one first. If it lists your tables, the connection works.
  • “Using SyncWith, count last month’s leads in my leads table by utm_campaign, and put Google Ads spend by campaign next to them.” It only lines up if your utm_campaign values match your Google Ads campaign names. Check a few rows first.
  • “Using SyncWith, show new leads by month from my leads table for the last 6 months.”

There’s no separate charge for the MCP server today. Each database query uses one refresh from your SyncWith plan, and the free Hobby plan includes 35 refreshes a month. Listing tables doesn’t use any.

Want the same numbers in a sheet that updates itself? Start with Postgres to Google Sheets. For every tool the server has, see the SyncWith MCP docs.