> ## Documentation Index
> Fetch the complete documentation index at: https://midplane.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# The gateway's MCP tools

> What an agent can call through the gateway: query, write, list_tables, describe_table and check_approval, their inputs and results, and how claude.ai and ChatGPT gate them.

Every agent reaches Postgres through five MCP tools. Two run statements, one
kind each: `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](#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: a `SELECT`, `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.

| Input | |
| - | - |
| `sql` | Exactly one statement |
| `intent` | Optional. Why, in a sentence; it goes in the audit log |
| `database` | The database id. Optional when the gateway serves one. The argument's description names the databases your access covers, so the agent needn't guess |

## write

One SQL write: an `INSERT`, `UPDATE`, `DELETE`, or a schema change Midplane
supports (`CREATE TABLE`, `CREATE INDEX`, `ALTER TABLE`, `DROP TABLE`;
[the policy](https://midplane.ai/docs/gateway/configuration.md#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](https://midplane.ai/docs/policies/approvals-and-taint.md#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:

```text theme={null}
public.customers (table, read only)
public.orders (table, read and write)
public.order_totals (view, read only)
```

Takes `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.

```text theme={null}
public.orders (table, read and write)
  -- One row per checkout
  id bigint  primary key
  customer_id bigint  not null  -> public.customers(id)
  email text  [masked]
  note text  -- Free text from the agent
```

Takes `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 held `write` 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](https://midplane.ai/docs/gateway/local-mode.md), it says approvals need a linked gateway.

## Results

An allowed statement returns JSON text, and the same object as the result's
`structuredContent`:

```json theme={null}
{
  "columns": ["id", "email"],
  "rows": [[1, "***"]],
  "row_count": 1,
  "truncated": false
}
```

* `rows` are arrays in column order. Bigints come as strings, `bytea` as
  `\x…` hex, and dates, times and timestamps as Postgres prints them.
* A read stops at `limits.max_rows` (1,000 by default) or `limits.max_bytes`
  (1 MiB) ([configuration](https://midplane.ai/docs/gateway/configuration.md)): `truncated` is `true`,
  `row_count` is `null`, and a line says so. A write's `row_count` is exact.
* Masked columns hold masked values. The result doesn't say which columns;
  `describe_table` does.
* 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](https://midplane.ai/docs/policies/approvals-and-taint.md#taint)).
* A write that ran after approval says who approved it and the request id.

### When nothing runs

| Outcome | What comes back |
| - | - |
| Denied | `isError`, and why: the rule in words, and what the agent can do instead |
| Held | `isError`, the request id, the review link, how many rows it would change and when the request expires; `structuredContent.approval` holds `id`, `status`, `preview`, `expires_at` and `review_url` |
| Failed in Postgres | `isError`, `Postgres returned an error (SQLSTATE): message`. On a database with masks the message's detail and hint are dropped, and on a statement that reads a table with untrusted columns only the SQLSTATE comes back: either could quote a row |
| Unavailable | `isError`, and why nothing runs now: the project is paused, the gateway has no policy or halted, the database can't be reached, or the audit log can't be written (every statement is recorded before it 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:

| Tool | `readOnlyHint` | `destructiveHint` | `idempotentHint` | `openWorldHint` |
| - | - | - | - | - |
| `query` | `true` | `false` | `true` | `false` |
| `write` | `false` | `true` | `false` | `false` |
| `list_tables`, `describe_table`, `check_approval` | `true` | `false` | `true` | `false` |

* **claude.ai** groups a connector's tools into read-only and write/delete,
  each set to Always allow, Needs approval or Blocked. `write` is the one
  tool in the write/delete group; blocking it leaves reads working
  ([claude.ai](https://midplane.ai/docs/agents/claude-ai.md#permissions)).
* **ChatGPT** treats a tool without `readOnlyHint` as a write action and asks
  before each call, remembering your answer for the chat. Reads run; `write`
  asks ([ChatGPT](https://midplane.ai/docs/agents/chatgpt.md#permissions)).
* **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.

The two checks are different people's. The client's prompt is yours, as the
agent's user. A held write waits for an owner or admin in Midplane, and the
policy decides whether a write runs at all, whatever the client allowed. If
the policy already holds the writes that matter, Always allow on `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](https://midplane.ai/docs/gateway/upgrading.md#changes-that-need-attention)).

*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).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.