a2ally™ Sign up

Answers · MCP security

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

Last updated

TL;DR: 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.

What should I check, step by step?

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. 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 isn't the query wrapper enough?

The archived reference server wrapped each query in BEGIN TRANSACTION READ ONLY and rolled back afterwards. The Node.js Postgres client accepts several semicolon-separated statements in one string, so COMMIT ended the read-only transaction and the next statement ran with the role's full rights; the final ROLLBACK had nothing left to undo.

Do read-only hints and annotations make a tool safe?

MCP tool annotations such as readOnlyHint describe what a server says a tool does. The MCP specification says clients must treat annotations as untrusted unless they come from trusted servers, so they help a client decide what to confirm but are not a permission.

How do I ask a2ally about this (free, no key)?

Add the MCP server https://www.a2ally.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>"
  }
}

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

What are the sources?

Related

Not legal advice. 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.a2ally.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export A2ALLY_API_KEY=rs_...   # the api_key from the reply (shown once)
curl -s https://www.a2ally.com/v1/licenses/check -H "Authorization: Bearer $A2ALLY_API_KEY" \
  -H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'