Skip to content

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_sql
required cap: db.read
minted scope: db:connection:{connection_id}:read

Parameters

{
"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 in columns order.
  • 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 widgetsbi.refresh_widget calls db.run_sql under 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.