Rightshold™ Sign up

Answers · MCP security

How do I restrict an MCP database tool to read-only queries?

Short answer: Enforce read-only in the database, not in the MCP server's prompt or its query wrapper. Connect the server with a database role that can only SELECT (in PostgreSQL, GRANT SELECT on the schemas the agent needs, or the predefined pg_read_all_data role, and nothing else), and point it at a read replica when you have one: on a PostgreSQL hot standby every transaction is read-only and can't be switched to read-write. A wrapper that runs the agent's SQL inside BEGIN TRANSACTION READ ONLY is not a boundary on its own: Datadog Security Labs showed in August 2025 that Anthropic's archived reference Postgres MCP server could be bypassed with COMMIT; DROP SCHEMA public CASCADE;, because it sent stacked statements in one string. Then trim what the agent sees: turn on the server's read-only mode if it has one, and remove the write tools from the client, so an agent that needs one query tool isn't handed twenty.

Checklist

  1. Create a dedicated role for the agent with SELECT only: GRANT USAGE on the schema and GRANT SELECT on the tables or views it needs (PostgreSQL 14 and later also offer the predefined pg_read_all_data role). Don't let the role own tables, and don't give it INSERT, UPDATE, DELETE, TRUNCATE or CREATE.
  2. Prefer a read replica: on a PostgreSQL hot standby, transaction_read_only is always on and BEGIN READ WRITE fails, even for a superuser, so a write can't land on your data whatever SQL the agent sends.
  3. Treat session settings as a second fence, not the wall: default_transaction_read_only = on can be overridden by the session itself (BEGIN READ WRITE), and a read-only transaction wrapper can be closed with a stacked COMMIT.
  4. Use a server that sends one statement per query through PostgreSQL's extended protocol (prepared statements), which rejects stacked statements. Avoid the archived @modelcontextprotocol/server-postgres package where writes must be prevented.
  5. Turn on the server's own read-only mode where it exists, and check it took effect: Supabase's hosted MCP server, for example, takes ?read_only=true (with an equals sign) to run SQL as a read-only Postgres user and disable mutating tools such as apply_migration, plus ?project_ref= to scope it to one project and ?features= to load only the tool groups you need.
  6. Remove the tools the task doesn't need in the client. In Claude Code, a deny rule with a bare MCP tool name, such as mcp__supabase__apply_migration, removes that tool from the model's context; other clients let you switch individual tools off per server.
  7. Limit what reads can reach: expose views instead of raw tables, leave out columns with secrets or personal data, use row-level security for tenant data, and don't grant EXECUTE on SECURITY DEFINER functions that write, since a SELECT can call a function.
  8. Bound every query: a statement_timeout and a row limit on the role or in the server, so a careless SELECT can't lock up the database or pull a whole table into the agent's context.

Why the query wrapper isn't enough

Read-only hints and annotations

Ask Rightshold (free, no key)

Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “Before I connect this database MCP server to my agent, is it a listed server, what tools does it expose, and have they changed since they were recorded?”

One-click setup

{
  "tool": "precall_check",
  "arguments": {
    "url": "<the database MCP server's https URL>",
    "tool": "<optional tool name, for example execute_sql>"
  }
}

Rightshold does not connect to your database, create roles or enforce permissions in your MCP client; read-only access is enforced by your database role, replica and client settings.

Sources

Related

Not legal advice. Updated 2026-10-29. JSON · Markdown · All answers

Get a free API key in one call

No card and no email needed. 100 free credits; the license and policy checks stay free without a key.

curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_...   # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
  -H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'