Skip to main content

03 โ€” MCP Tool Surface

The set of tools the gateway exposes to agents. This is the agent-facing contract. Keep it small and boring.

Design rulesโ€‹

  1. Read-only by default. A write-capable tool exists only if 06-permissions explicitly enables it for the calling identity.
  2. Every tool accepts server + database (except list_servers / list_databases). The pair fully scopes the call.
  3. reason is optional in the protocol but may be required by policy. If policy requires it and it's absent, the gateway returns a structured error telling the agent to ask the user for one.
  4. Results are size-capped. All result-returning tools take a limit parameter; the gateway clamps it to a per-database max.
  5. No tool exposes credentials, connection strings, or hostnames. Servers and databases are referenced by their config-defined logical name.

Toolsโ€‹

list_serversโ€‹

Returns the servers the caller can see โ€” logical name, kind (postgres, mysql, mssql, โ€ฆ), human description. No connection info.

list_databasesโ€‹

Args: server. Returns databases on that server visible to the caller, with description and tags.

describe_schemaโ€‹

Args: server, database, optional schema, optional table. Returns tables/columns/types/PK/FK/indexes. Cached aggressively โ€” schema doesn't change per query.

sample_tableโ€‹

Args: server, database, table, optional limit (default 10, capped). Returns a small sample. Useful for "what does this data look like" without writing SQL.

run_queryโ€‹

Args: server, database, sql, optional limit, optional reason. Executes under the caller's grant with statement timeout. Returns rows + truncation flag + execution stats. The primary tool.

Read vs. write is per-grant. With a query_read grant the sql guard accepts only read-only statements (SELECT / EXPLAIN). A query_write grant on the target (server, database) additionally lets a single top-level INSERT / UPDATE / DELETE through โ€” data writes only. Schema modification (CREATE / ALTER / DROP / TRUNCATE), GRANT / REVOKE, COPY, transaction control, and multi-statement bodies are rejected in both modes; the gateway never issues DDL. Writes also require the target-DB role to actually hold write privileges โ€” the gateway does not provision them (see 06-permissions and CLAUDE.md non-negotiable #3). Writes commit synchronously before the response returns, and every write is audited exactly like a read. Mongo targets remain read-only regardless of grant.

Statement-timeout ceiling: every query is subject to a hard 30 s ceiling regardless of the per-grant statement_timeout_ms value. A grant may set a shorter timeout; it may not exceed 30 s โ€” the gateway clamps it. The timeout is enforced both DB-side (SET LOCAL statement_timeout) and by a Tokio guard as belt-and-suspenders. A query that exceeds it returns timeout.

EXPLAIN ANALYZE is rejected. EXPLAIN ANALYZE executes the query and can therefore run write-containing CTEs on a read-only role, defeating the read-only guarantee. The sql guard rejects it before the query reaches the DB; the caller receives forbidden_sql.

explainโ€‹

Args: server, database, sql. Returns EXPLAIN (or vendor equivalent) without executing. Lets the agent estimate cost before running expensive queries.

EXPLAIN ANALYZE is rejected โ€” same reason as in run_query. Use plain EXPLAIN instead.

get_query_historyโ€‹

Args: optional database, optional since, optional limit. Returns the caller's own recent queries (SQL + timestamp + duration + row count). Lets the agent recover context across sessions without exposing other users' queries.

Errorsโ€‹

Errors are structured JSON, not free-text strings. Shape: { "error": { "category": "<code>", "code": "<detail>" } }.

CodeHTTPWhen
unauthenticated401Token missing/expired โ€” agent triggers re-login
forbidden403Authenticated but permission denied for this server/db/action
forbidden_sql403SQL rejected before reaching the DB: statement not covered by the grant (a write without query_write, or a schema mod / COPY / multi-statement in any mode), EXPLAIN ANALYZE, or dangerous function (pg_read_file, lo_export, โ€ฆ)
reason_required400Policy requires a reason for this call; none provided
timeout408Statement timeout fired (30 s ceiling)
row_limit_exceeded200Result truncated at configured cap (flag in response, not an error response)
syntax_error400DB rejected the SQL
unavailable503DB unreachable or pool exhausted
rate_limited429Calling identity has too many concurrent in-flight requests (per-identity cap). Retry-After: 1 header is set.
service_overloaded503Gateway-wide concurrency ceiling reached. Retry-After: 1 header is set.
internal500Bug. Has a request ID that matches a server-side log line

Every error includes a request_id the user can paste back to ops.

Resource safetyโ€‹

Two independent concurrency caps protect the query path:

LimitDefaultResponse when exceeded
Global (process-wide)512 concurrent requests503 service_overloaded
Per-identity (per SSO sub)16 concurrent requests429 rate_limited

Both caps are checked on the bearer-gated router after authentication. A per-identity permit is held for the full lifetime of the request. The global cap is checked first to keep the per-identity map lookup cheap on a saturated gateway.

The 30 s statement-timeout ceiling (see run_query above) is the complementary per-query bound: it limits how long one request can hold its permits.

What we don't exposeโ€‹

  • No DDL tools (create_table, drop_table, โ€ฆ) โ€” those don't belong in a debugging gateway.
  • No DML tools (insert, update, delete) at the protocol layer. If writes are enabled by policy, they go through run_query with the role having write grants; the audit log captures the SQL.
  • No raw pg_dump / mysqldump style export. Bulk export is a different product.
  • No "run on all databases" โ€” every call is scoped to one DB.