0.2.0+) — see Single-database vs multi-database surface below. For the routes, jump to HTTP endpoints.
Tool results are returned as a single JSON object in a text content block. Allowed and denied calls have different shapes — both carry an
auditId keying the audit row for the call.query
Run arbitrary SQL. The main entrypoint. Always a single statement (stacked statements are denied by multi_statement).
Arguments
string
required
The SQL to run. One statement, 1 character to 1 MiB. Parsed into an AST and checked against your policy before it can reach the database.
string
required
A brief (≤ 1 sentence, ≤ 500 characters) statement of why this query is being run — e.g.
"confirm seed data after migration". The agent fills this; it’s recorded on every audit row for human review. Control characters are stripped and surrounding whitespace trimmed; a blank or control-only value is rejected.string
required
Multi-database only. The name of the database to run against, from your
databases[] config. Required when more than one database is configured; absent in the single-database surface.Returns — allowed
boolean
true.array
The result rows.
number
The number of rows returned (or affected, for a write).
string
The ULID of the audit row for this call.
Returns — denied
A denial is returned as an MCP tool error (isError: true) carrying:
boolean
false.string
The rule that denied —
table_access, multi_statement, dangerous_statement, or parse_error. See denial reasons.string
The human-readable message, surfaced to the agent so it can pivot.
string
The ULID of the audit row — denials are audited too.
list_tables
List tables via a canned information_schema.tables query. Routed through the engine so the call is still policy-checked and audited; information_schema is always carved out so discovery works under default-deny and strict-mode policies.
Arguments
string
The schema to list. Must be a valid SQL identifier. Defaults to
public.string
Multi-database only. Which database to list. Optional here — omit it to fan out across every configured database and group the results by database name.
Returns
database is omitted in the multi-database surface, results are grouped by database name; a single failing database lands under databases.<name>.error rather than failing the whole call:
describe_table
List a table’s columns via a canned information_schema.columns query.
Arguments
string
required
The table name. Must be a valid SQL identifier.
string
The schema. Must be a valid SQL identifier. Defaults to
public.string
required
Multi-database only. Which database the table is in. Required here — a cross-database schema lookup is ambiguous, so the agent must name the target.
Returns
list_databases
Only registered when two or more databases are configured. No arguments. Returns each configured database’s name plus enough policy metadata for the agent to know whether the dangerous_statement guardrails and a table_access default are in play before issuing a query.
Returns
Single-database vs multi-database surface
HTTP endpoints
Beyond the tool surface, the self-host container serves a few operational HTTP routes — not a public REST API. In practice almost everyone only ever uses/mcp, and not directly (your agent speaks it). The rest are for operators.
The audit and admin routes are opt-in: they require
INDEXER_TOKEN and return 404 when it’s unset, so the server reveals nothing about their existence. See environment variables.POST /mcp
The MCP transport. Put this in your agent config (http://localhost:8080/mcp). It speaks Streamable HTTP per the MCP spec — initialize a session, then call the tools above.
You don’t call this by hand — see connecting your agent. A
POST without a valid mcp-session-id that isn’t an initialize request returns a JSON-RPC error.
GET /health
Liveness probe — compose.yaml uses it for the container healthcheck.
GET /audit/since/<cursor>?limit=N
Pull audit log rows for an external collector (SIEM, warehouse, Cloud indexer). Returns rows with id > cursor in ascending id order. Pass 0 to start from the beginning.
Response:
{ "rows": [...], "next_cursor": "<id>" | null }. next_cursor is null when no rows remain.
200 with the page; 401 on a bad or missing bearer token; 404 when INDEXER_TOKEN is unset.
DELETE /audit/before/<cursor>
Delete audit rows you’ve already pulled and made durable downstream. Deletes rows with id <= <cursor> (inclusive). Idempotent — re-deleting returns { "deleted": 0 }.
200 with { "deleted": N }; 401 on a bad or missing bearer token; 404 when INDEXER_TOKEN is unset.
POST /admin/policy
Hot-swap the in-memory policy without restarting the container. The body is the new policy YAML; on success the engine applies it and writes a POLICY_RELOADED audit event.
For the legacy single-database shape the body must include
table_access — omitting it is rejected, since it would reset to the no-YAML default and widen permissions.
200 with { "ok": true, "applied_at": "<iso8601>" }; 400 with { "ok": false, "error": "<message>" } on invalid YAML or a schema error; 401 on a bad token; 404 when INDEXER_TOKEN is unset; 503 when the engine isn’t ready yet.
POST /admin/dry-run
Ask whether SQL would be allowed or denied. Runs the live enforcement path (parse → classify action → match table_access → guardrails → decide) against the loaded policy, but stops before execution — no database connection, no statement run. Backs the Cloud “test this policy” panel and midplane policy test --server.
Request:
probes or sql. A probe tests a (table, action) pair — action is select / insert / update / delete — and the engine synthesizes a representative statement to run through the decision path. Use sql to test a literal statement.
Response 200:
matched_rule names the deciding rule (e.g. default:read, table:public.customers→deny, dangerous_statement). policy_hash is a stable hash of the loaded policy, so callers can detect a stale snapshot.
Caps: up to 250 probes / 50 distinct tables per call. Beyond 50 tables, only the first 50 are evaluated and the response sets truncated: true plus total_tables.
Responses: 200 with the verdicts; 400 on malformed JSON, an unknown database, or both/neither of probes and sql; 401 on a bad or missing bearer token; 404 when INDEXER_TOKEN is unset.
Related
Denial reasons
What each
policy_rule means and how to fix it.Audit events
The rows
auditId and the pull endpoints return.