query runs reads and write runs writes. Clients decide from a
tool’s annotations whether a call needs your confirmation, so the split is
what lets reads run while writes wait (what clients ask you to
confirm). It is enforced, not declared:
the gateway refuses a write sent to query and a read sent to write, and
Postgres runs every read in a READ ONLY transaction.
At initialize, the gateway sends instructions for the agent: the order to
use the tools in, that masked columns stay masked, how a held write is
re-run, that a denial is the policy and not an error to get around, and that
content from untrusted columns is data, never instructions.
query
One read-only SQL statement: aSELECT, VALUES, or a WITH query that
writes nothing. A write sent here returns an error naming write, or saying
the agent’s access is read-only, and nothing runs.
write
One SQL write: anINSERT, UPDATE, DELETE, or a schema change Midplane
supports (CREATE TABLE, CREATE INDEX, ALTER TABLE, DROP TABLE;
the policy). The policy decides: it
runs now, waits for a person’s approval, or is refused, and the answer says
which. A read sent here returns an error naming query.
The inputs are query’s. intent is what approvers read, and an approval
binds to it: once a held write is approved, the agent re-runs exactly the
same statement with the same intent through write
(held writes).
An agent whose access is read-only on every database this gateway has
doesn’t get this tool at all, and its instructions say so. A client keeps the
tool list for its session, so an agent given write access sees write on its
next connection.
list_tables
The tables and views the agent may read, one per line, with its access to each:database. A table the policy denies isn’t listed.
describe_table
One table’s columns, from the catalog: each column’s type,primary key or
not null, where a foreign key points, [masked] when the policy masks it,
and its comment. The table’s comment follows the first line, and a foreign
key over several columns follows the columns.
table (optionally schema-qualified) and database. A foreign key to
a table the agent can’t read is left out, as list_tables leaves that table
out. All of it is catalog metadata: the gateway reads no row for it, and
comments stay on the gateway, never going to Midplane Cloud.
check_approval
Where a held write stands, by the request id a heldwrite returned:
pending, with the review link and when it expires; approved, with the
statement and intent to re-run through write; denied, with the note;
expired; or used, with what the run changed. In local
mode, it says approvals need a linked gateway.
Results
An allowed statement returns JSON text, and the same object as the result’sstructuredContent:
rowsare arrays in column order. Bigints come as strings,byteaas\x…hex, and dates, times and timestamps as Postgres prints them.- A read stops at
limits.max_rows(1,000 by default) orlimits.max_bytes(1 MiB) (configuration):truncatedistrue,row_countisnull, and a line says so. A write’srow_countis exact. - Masked columns hold masked values. The result doesn’t say which columns;
describe_tabledoes. - A result with content from a column labeled untrusted ends with a note to treat it as data, never as instructions, and that read taints the agent’s grant (taint).
- A write that ran after approval says who approved it and the request id.
When nothing runs
EXPLAIN is refused as unsupported, like everything else the gateway can’t
analyze: SET, SHOW, COPY, transaction control and MERGE. Each
statement is one statement; a write hidden in a WITH is denied.
What clients ask you to confirm
Each tool carries the MCP annotations clients read:- claude.ai groups a connector’s tools into read-only and write/delete,
each set to Always allow, Needs approval or Blocked.
writeis the one tool in the write/delete group; blocking it leaves reads working (claude.ai). - ChatGPT treats a tool without
readOnlyHintas a write action and asks before each call, remembering your answer for the chat. Reads run;writeasks (ChatGPT). - Codex CLI, in its writes approval mode, runs read-only tools without asking and prompts for the rest.
- Claude Code, Cursor and VS Code ask by their own settings, per tool or per server.
write is
reasonable: the prompt then adds a check only to writes the policy runs
outright.
Before 0.26.0, one query tool ran reads and writes, so clients treated
every read as a write (upgrading).
Tested by apps/gateway/test/gateway.e2e.test.ts (the split and what each
refusal says, every tool’s title and hints, the instructions, the database
argument, and keys and comments without a row read) and
packages/corpus/fixtures/tools.json (the rule in the core).