Skip to content

feat: add update_rows and delete_rows tools - #40

Merged
rahulcrl merged 1 commit into
mainfrom
add_row_mutation_tools
Jul 10, 2026
Merged

rahulcrl merged 1 commit into
mainfrom
add_row_mutation_tools

Conversation

@rahulcrl

@rahulcrl rahulcrl commented Jul 7, 2026 •

Copy link
Copy Markdown
Contributor

Adds update_rows and delete_rows tools so agents can modify and remove existing rows, completing the DML surface alongside insert_rows.

Design

Follows the ecosystem pattern (MongoDB MCP, Postgres MCP both gate all writes behind a single read-only/write switch): both tools register under the existing CRDB_MCP_ENABLE_WRITE_QUERIES=true gate. No new env vars.

Guardrails, all knob-free:

  • Parser-validated: exactly one statement, correct statement type, length cap (same parseSingleStatement path as insert_rows).
  • WHERE clause mandatory - bare UPDATE/DELETE is rejected before reaching the database.
  • Nested writes hidden in a CTE, joined table expression (UPDATE ... FROM, DELETE ... USING), or expression subquery are rejected via a shared ensureNoHiddenDML helper; read-only ones pass.
  • destructiveHint: true on both tools, plus idempotentHint: true on delete_rows, so MCP clients can surface accurate prompts.

RETURNING

Supported across all three DML tools (insert_rows included): a statement with a RETURNING clause routes through Query instead of Exec so the returned rows reach the agent, which Exec otherwise drops. Matters most for DELETE, where the removed rows cannot be read back afterwards.

Testing

  • Rejection-table tests mirroring insert_rows_test.go: wrong statement type, missing WHERE, multi-statement, CTE-hidden DML, and bracket-subquery DML in WHERE / FROM / USING; plus valid execution, RETURNING-through-Query, and legitimate SELECT subqueries in SET/WHERE.
  • Verified end-to-end against a local single-node cluster via the MCP tools: insert/update/delete each plain and with RETURNING, and the WHERE-less / hidden-DML cases correctly rejected.
  • go test ./... passes.

@rahulcrl
rahulcrl force-pushed the add_row_mutation_tools branch 6 times, most recently from dc805c8 to 1f3d607 Compare July 8, 2026 07:22
@rahulcrl
rahulcrl marked this pull request as ready for review July 8, 2026 07:25
@rahulcrl
rahulcrl requested review from arjunmahishi and jrxna July 8, 2026 07:25
@arjunmahishi

Copy link
Copy Markdown

Should DELETE sit behind the same gate as other writes?

delete_rows currently registers under CRDB_MCP_ENABLE_WRITE_QUERIES alongside inserts and updates. That follows the ecosystem pattern, but the blast radius differs: an unintended DELETE is irreversible row loss, while INSERT/UPDATE mistakes are often more recoverable. Some operators may want append/mutate without delete.

Worth noting for any future revisit: CRDB_MCP_ENABLE_WRITE_QUERIES does double duty — it gates tool registration (tools.go) and flips the pool out of read-only mode (db/manager.go → ReadOnly: !cfg.EnableWriteQueries). So a delete-specific gate could only be enforced at the registration layer, not the DB layer.

Not blocking — the mandatory-WHERE guard covers the worst case. Just flagging so the single-flag design is a deliberate choice.

Register row-mutation tools under the existing
CRDB_MCP_ENABLE_WRITE_QUERIES gate, matching the single read-only/write
switch used by other database MCP servers. Both tools parse via
cockroachdb-parser, accept exactly one statement, require a WHERE
clause, and block DML hidden in CTEs, joined table expressions
(UPDATE ... FROM, DELETE ... USING), or expression subqueries. Both
carry a destructiveHint annotation, and delete_rows an idempotentHint,
so MCP clients can surface accurate prompts.

RETURNING is now supported across all three DML tools: statements with
a RETURNING clause route through Query instead of Exec so the returned
rows reach the agent (Exec silently drops them). This matters most for
DELETE, where the removed rows cannot be read back afterwards.
@rahulcrl
rahulcrl force-pushed the add_row_mutation_tools branch from 1f3d607 to 271fed2 Compare July 8, 2026 10:46
@rahulcrl

rahulcrl commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Should DELETE sit behind the same gate as other writes?

Thanks for the review @arjunmahishi!

Fair point, the single flag is intentional.
The CRDB_MCP_ENABLE_WRITE_QUERIES just answers "can this server write at all".
"Write but not delete" is a privilege question, so it belongs in SQL RBAC. An operator who wants append/mutate without delete grants the role SELECT/INSERT/UPDATE and no DELETE, and the database enforces that no matter which tools are registered. This follows our design recommendation to delegate authorization to the database wherever possible, and it aligns with the mongo/postgres MCPs.
For the accidental case, the mandatory WHERE and the destructiveHint is added.

I have added a note in the README (Tools section) making the RBAC path explicit, so the single-flag plus RBAC split is documented.

@rahulcrl
rahulcrl merged commit 902636c into main Jul 10, 2026
5 checks passed
@rahulcrl
rahulcrl deleted the add_row_mutation_tools branch July 10, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

⌂ 换网址