BigQuery MCP write access, granted per statement type.
Giving an agent SQL access to a warehouse today is an all-or-nothing choice: a read-only tool, or a tool that will run whatever the model writes. Most teams want the thing in between — “this agent can INSERT into the warehouse” and “this agent can DROP tables” as different permissions, not one scary checkbox.
The gap
BigQuery’s MCP surface splits SQL into two tools: execute_sql_readonly, which Google enforces as read-only server-side, and execute_sql, which runs anything — backfills, schema changes, DROP TABLE, all behind the same switch. The read-only tool is genuinely safe, so it should be free. But the moment an agent needs to write one row, the naive setup hands it the entire write surface of your warehouse. GCP IAM itself doesn’t work that way — bigquery.tables.updateData and bigquery.tables.deleteare different permissions — and agent access shouldn’t either.
Typed grants, classified by Google’s own parser
Gentkey’s BigQuery connector types the write surface the way GCP IAM does. Every execute_sqlcall is classified first with a free, slot-less dry-run job: BigQuery’s own parser returns the statement type, and the policy gate matches it against your grants. INSERT, UPDATE, DELETE, MERGE, and TRUNCATE resolve to mcp-bigquery.tables.updateData; the CREATE, ALTER, and DROP families each resolve to their own grant, mirroring bigquery.tables.create, .update, and .delete.
Two details make this honest rather than cosmetic. First, Gentkey never parses SQL itself — regex classification is a leaky boundary, and the parser that decides what the statement isis the same one that will execute it. Second, the classified text and the executed text are byte-identical: the dry run reads the SQL straight from the call’s arguments, under the caller’s own token, against the same project the real query bills to. There is no window where the classified statement and the executed statement can differ.
Everything unclassifiable fails closed
The interesting design decisions are the deliberate absences. Multi-statement scripts and EXECUTE IMMEDIATE classify as SCRIPT, and dry runs don’t classify the inner statements — so scripts always require the full mcp-bigquery.write grant. CALLis a black box (the procedure body could do anything), so it’s deliberately unmapped. CREATE FUNCTION and friends can create UDFs and remote functions that reach the network, so they stay under the full grant too. Even SELECT through execute_sql needs the full grant — the no-grant read path is execute_sql_readonly, where Google also blocks the UDF egress the unrestricted tool would allow. And any statement type Google ships later is unknown by definition, which means full write grant — the default, not an enumerated case.
What it looks like
Grant: mcp-bigquery.tables.updateData · max writes/hour = 5
“Backfill yesterday’s daily_metrics” → execute_sql, dry run says statementType=INSERT → allowed, executed, logged. “Clean up the old staging table” → same tool, dry run says statementType=DROP_TABLE → denied — no mcp-bigquery.tables.delete grant exists, nothing ran, and the denial is in the audit log with the statement type that produced it. Same tool, same agent, different statements, different verdicts.
What this doesn’t do
Honest limits: statement typing bounds the class of change, not its correctness. An agent granted tables.updateData can still write a bad WHEREclause — the blast radius inside a granted statement class is your query review, the per-grant write-rate limits, and the per-agent audit trail, not the classifier. And classification costs one dry-run round trip per write; that’s the price of asking the real parser instead of guessing.
The full access model — every grant and what it governs — is on the BigQuery connector page.
Grant statements, not the warehouse.
Connect BigQuery and give your first agent INSERT without DROP in under a minute.