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โ
- Read-only by default. A write-capable tool exists only if 06-permissions explicitly enables it for the calling identity.
- Every tool accepts
server+database(exceptlist_servers/list_databases). The pair fully scopes the call. reasonis 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.- Results are size-capped. All result-returning tools take a
limitparameter; the gateway clamps it to a per-database max. - 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>" } }.
| Code | HTTP | When |
|---|---|---|
unauthenticated | 401 | Token missing/expired โ agent triggers re-login |
forbidden | 403 | Authenticated but permission denied for this server/db/action |
forbidden_sql | 403 | SQL 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_required | 400 | Policy requires a reason for this call; none provided |
timeout | 408 | Statement timeout fired (30 s ceiling) |
row_limit_exceeded | 200 | Result truncated at configured cap (flag in response, not an error response) |
syntax_error | 400 | DB rejected the SQL |
unavailable | 503 | DB unreachable or pool exhausted |
rate_limited | 429 | Calling identity has too many concurrent in-flight requests (per-identity cap). Retry-After: 1 header is set. |
service_overloaded | 503 | Gateway-wide concurrency ceiling reached. Retry-After: 1 header is set. |
internal | 500 | Bug. 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:
| Limit | Default | Response when exceeded |
|---|---|---|
| Global (process-wide) | 512 concurrent requests | 503 service_overloaded |
Per-identity (per SSO sub) | 16 concurrent requests | 429 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 throughrun_querywith the role having write grants; the audit log captures the SQL. - No raw
pg_dump/mysqldumpstyle export. Bulk export is a different product. - No "run on all databases" โ every call is scoped to one DB.