feat(mcp)!: confirm destructive SQL via elicitation and rename costConfirmation option - #408
feat(mcp)!: confirm destructive SQL via elicitation and rename costConfirmation option#408barryroodt wants to merge 10 commits into
Conversation
…tion option Extract the duplicated requestState decode/tool/args/decline/cancel/drift block from create_project and create_branch into checkConfirmationState in tools/confirmation.ts (renamed from cost-confirmation.ts), validate the decoded state with zod safeParse, and rename the server option costConfirmation -> confirmation ahead of non-cost confirmations. BREAKING CHANGE: `createSupabaseMcpServer` option `costConfirmation` is now `confirmation`.
Port Studio's regex-based destructive SQL check (DROP, DELETE, TRUNCATE, ALTER TABLE ... DROP COLUMN incl. EXECUTE-string variants, and UPDATE without WHERE) with comment stripping into tools/destructive-sql.ts, along with the Studio test cases that pin its false-positive and false-negative contract. No new dependencies.
When the `confirmation` option enables them and the client declares form elicitation, execute_sql and apply_migration detect destructive SQL (DROP, DELETE, TRUNCATE, ALTER TABLE ... DROP COLUMN, UPDATE without WHERE, ported from Studio) and require an accepted action-only elicitation, bound by a signed requestState to the project and a hash of the query, before running. Read-only servers, capability-free clients and non-destructive SQL are unchanged.
Coverage Report for CI Build 34487004981Warning Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes. Coverage increased (+0.5%) to 96.925%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Brings in #411 (ctx-aware tools(), hidden get_cost/confirm_cost and confirm_cost_id omission for form-capable clients) and the 0.12.0 release. Conflicts resolved so #411's per-request tool shaping runs under the renamed `confirmation` option; #408's tests migrated from setupFormCapable to setupModern with explicit FORM_CAPABLE capabilities.
|
Where do you see controls for turning elicitations on/off fitting in to this? To me, destructive SQL confirmations get into territory where I could see elicitations getting in the way for users who want agents to auto approve safe actions. Wondering if we should be adding options to control elicitation behavior before / alongside this change. |
that's a good question We have two main options imo:
Open to suggestions of course - and more than happy to add on to either this PR or as a second, stacked PR. |
|
Query param sounds reasonable. At first I assumed this would just be a boolean flag, but you bring up a good point that we could make it more fine-grained so users can decide what level of safety they want. |
|
It could be a passthrough of the tools where confirmations are enabled e.g. The risk with an allowlist is if we add a new tool in the future that uses elicitations for approval, the user won't realize they opted out of its safety net. A deny-list would be safer, so they're only taking in risk in areas they've listed explicitly: |
Agreed 💯 , a per-tool deny-list is exactly what I was thinking. Since the package already exposes the per-tool mechanism, this doesn't need an MCP package change; the Platform just needs to parse the user's preference and derive the effective tool set. We'll pick that up on the Platform side shortly. |
Rodriguespn
left a comment
There was a problem hiding this comment.
Asking for changes as I identified some edge cases on the parsing that I think would need further discussion.
For example, a two-line SQL statement that starts with a comment (--) isn't sanitized correctly today:
SELECT '--';
DROP TABLE films;I tried this locally against the current regex and the table gets dropped with no confirmation prompt. The -- inside the string literal on line 1 is treated as a real comment and silently eats the rest of that line, including the ; that would otherwise separate it from the DROP.
Any reason why we're doing our own parsing instead of using the official Postgres parser libpg-query-node?
We're already using it on the backend to classify execute_sql queries: sql-classification.worker.ts.
Same scenario through the real parser:
import { parse } from '@libpg-query/parser'
const { stmts } = await parse("SELECT '--';\nDROP TABLE films;")
// stmts.length === 2
// stmts[1].stmt -> { DropStmt: { removeType: 'OBJECT_TABLE', objects: [...films] } }Two real statements come out and the DROP is fully visible, no regex needed, because the parser understands string literals natively.
I found a few other cases the current regex misses the same way:
UPDATEwithout aWHEREwhen a comment sits nearby (leading, inline, or trailing),ALTER TABLE t DROP colwithout the optionalCOLUMNkeyword,- destructive statements wrapped in
WITH ...orDO $$ ... $$.
All of these come for free with a real parser (checking the whereClause field, matching on the AlterTableCmd subtype instead of literal text, or just walking the statement list) instead of patching the regex edge case by edge case.
I've prepared some test cases that we can reuse here when I was working on the sql parsing.
Lmk what you think.
I've been hesitant to add another runtime dependency to the existing package. Happy to revisit this of course - what are your thoughts on adding this dependency? |
commit: |
Why
Adds confirmation before destructive SQL, such as dropping a table (AI-1186).
What changed
Breaking public option rename:
costConfirmation→confirmation.enabledToolssupports existing cost confirmations (create_project,create_branch) and SQL confirmations (execute_sql,apply_migration); cost discovery is unchanged. Local HTTP opts into both SQL tools. Signed request-state is reusable within its TTL, without exactly-once guarantees. A bounded fix detects directALTER TABLE DROP [COLUMN]. No new dependencies.How to test
With pnpm 10, dependencies installed and
packages/mcp-server-supabase/.env.localpresent (create empty only if absent), run from the repo root:Expect passing SQL prompt/accept and no-form cases; declining bare-column DROP preserves the column and data. These are isolated loopback/MSW/PGlite fixtures, not real operations.
Trade-offs
Best-effort regex is not a security barrier: literal-comment, UPDATE-comment and WITH/DO misses remain. Without per-request form capability, SQL can execute unprompted. Hosted-platform SQL opt-in is separate (draft #38211), default off. Real GREEN behavior is untested.