MCP tool — db.run_sql
db.run_sql MCP tool
The locked contract for any downstream caller — chat orchestrator, BI widgets, agents — that wants to execute SQL against a tenant connection.
Never call drivers directly from a route handler. All execution goes through this tool so AuthContext + validator + audit are uniform.
Contract (locked)
tool: db.run_sqlrequired cap: db.readminted scope: db:connection:{connection_id}:readParameters
{ "connection_id": "uuid", "sql": "SELECT ...", "row_cap": 1000}row_cap is advisory — the validator AND-injects a LIMIT clause
when SQL doesn’t have one or has a larger one.
Response
{ "columns": [{"name": "payer", "type": "varchar"}, ...], "rows": [["Aetna", 145], ...], "row_count": 12, "total": null, "truncated": false}rows— list of column-value arrays incolumnsorder.total— the un-capped row count when known (Postgres returns this cheaply for unbounded scans); null when unavailable.truncated— true when row_count == row_cap (more rows existed).
Error response
{ "error": "validation_failed", "detail": "DELETE statements are not permitted"}Error codes: validation_failed (SQL didn’t pass sqlglot),
explain_blocked (preflight blocked), timeout,
driver_error, connection_not_found, not_granted.
AuthContext required
Calls must come with a minted AuthContext token whose capabilities
include db.read. The tool re-checks at execution time — a stale
token (≥30s TTL) is rejected even if it looks otherwise valid.
The token is single-use. Once db.run_sql consumes it, the audit
trail records the consumption and any replay is rejected.
Where it’s used
- Workbench (
/db) — every user query goes through this tool. - BI widgets —
bi.refresh_widgetcallsdb.run_sqlunder the hood with the widget’s saved connection_id + sql. - Chat orchestrator — when the workbench’s chat surface routes a “data” question through the DB skill, the skill mints a token and calls this tool.
- Health satellite RCM analytics — surfaces that query the tenant’s warehouse for “what changed in the last 30 days” use this contract.
Don’t bypass
If you’re tempted to call get_driver(conn.kind).execute(...)
directly, stop. Bypassing the tool bypasses:
- Audit trail.
- AuthContext consumption + minting.
- Why-trace stamping.
- EXPLAIN preflight.
- Scope predicate injection.
The driver is a private implementation detail. The contract is the tool.