Repository navigation
feat(mcp): add a read-only D1 query tool and Code Mode endpoint - #285
Open
demosjarco wants to merge 2 commits into
Open
demosjarco wants to merge 2 commits into
demosjarco wants to merge 2 commits into
Conversation
With ?codemode=false, list d1_read_query next to the generated endpoint
tools, annotated readOnlyHint: true so clients can approve it without
asking. It runs SQL through POST /accounts/{account_id}/d1/database/
{database_id}/query only when every statement is SELECT, VALUES,
WITH ... SELECT, EXPLAIN of one of those, or an allowlisted reporting
PRAGMA. Other SQL is refused before any API call.
The check is a small tokenizer that follows SQLite's rules for comments,
strings and quoted identifiers. In SQLite the statement head decides
whether a statement can write, because subqueries and CTE bodies can only
be SELECTs. The WITH case finds the main statement keyword outside
parentheses. sql-parser-cst is GPL-2.0-or-later, and node-sql-parser's
SQLite grammar rejects EXPLAIN QUERY PLAN, PRAGMA, VALUES, WINDOW, FILTER
and NULLS LAST, so neither is used.
post_accounts_d1_database_query is unchanged and still runs any SQL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kuoB4UxMwTp2hrqUCyv63
Code Mode reaches D1 through execute, so add the read-only check there as
an endpoint: POST /accounts/{account_id}/d1/database/{database_id}/read_query.
The scheduled handler adds it to spec.json, copied from the real query
endpoint, after building mcp-tools.json, so search finds it but no
endpoint tool targets a path the API doesn't have. GlobalOutbound refuses
SQL that could write, checking every query in a batch and refusing bodies
with keys it doesn't read, and sends the rest unchanged to .../query.
execute's description points to it.
The real query endpoint accepts a D1 Read token, so this check is what
separates reads from writes. execute stays one destructive tool and can
still call .../query.
Limit the reporting PRAGMAs to the ones D1 supports: table_list,
table_info, table_xinfo, index_list, index_info, index_xinfo, quick_check,
foreign_key_check and foreign_key_list.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019kuoB4UxMwTp2hrqUCyv63
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
POST /accounts/{account_id}/d1/database/{database_id}/queryaccepts a token with either D1 Read or D1 Write, so Cloudflare's permissions don't separate reads from writes on it. Today every D1 query an agent runs through this server goes through a tool that can also drop tables or any other destructive SQL statement . This adds a read-only path that refuses SQL that could write before it reaches D1 (actual D1 query command is identical), so an agent or MCP client can use a separate tool for reads and keep the existing one for everything else.What
Non-Code-Mode (
?codemode=false)d1_read_querytool (src/tools/d1-read-query.ts), listed next to the generated endpoint tools withreadOnlyHint: true, destructiveHint: false, idempotentHint: true, so clients can approve it without prompting.database_id,sql, optionalparamsand optionalaccount_id, and it rejects unknown keys. Refused SQL never reaches the API.post_accounts_d1_database_queryis unchanged and still runs any SQL.Code Mode
POST /accounts/{account_id}/d1/database/{database_id}/read_query(src/tools/d1-read-route.ts). The scheduled handler (andseed-r2.ts) adds it tospec.jsonas a copy of the real query endpoint, aftermcp-tools.jsonis built, sosearchfinds it but no endpoint tool targets a path the API doesn't have.GlobalOutboundrefuses SQL that could write and sends everything else, body unchanged, to the real.../query. It checks every query in a{ batch }body and refuses bodies with keys it doesn't check (for examplesqlandbatchtogether) and methods other than POST.execute's description points to it.executestays destructive and can still call.../query.The check (
src/tools/d1-sql.ts)SELECTs. So this is a small tokenizer, not a parser.tokenize.crules for--//* */comments,'…'strings with''escapes and no backslash escapes, and"…",`…`and[…]identifiers, so text it treats as a literal or a comment is what SQLite treats that way.;-separated statement must beSELECT,VALUES,WITH … SELECT/VALUES,EXPLAIN [QUERY PLAN]of one of those, or one of the reporting PRAGMAs D1 supports:table_list,table_info,table_xinfo,index_list,index_info,index_xinfo,quick_check,foreign_key_check,foreign_key_list.WITH, the main statement keyword is the firstSELECT/VALUES/INSERT/UPDATE/DELETE/REPLACEoutside parentheses, which catchesWITH x AS (…) DELETE ….Dependencies
None added. Two parsers were considered:
sql-parser-cstis GPL-2.0-or-later, which doesn't fit this Apache-2.0 repo.node-sql-parser's SQLite grammar rejects valid reads such asEXPLAIN QUERY PLAN,PRAGMA table_info(t),VALUES,WINDOW,FILTERandNULLS LAST.Notes for review
searchafter the next six-hourly sync ornpm run seed:*.Tests
npm run checkpasses: 544 tests. They cover:d1_read_querythrough the real worker, including a refusal with no outbound request (MSW fails on unhandled requests)read_querythrough a realexecuteisolate andGlobalOutboundspec.jsonbut not tomcp-tools.jsonExisting tests that pin the non-Code-Mode tool list were updated.
🤖 Generated with Claude Code