Skip to content

feat(mcp): McpQueryService, read-only SQL core for MCP tools - #1139

Merged
ZhuchkaTriplesix merged 3 commits into
devfrom
issue/1134-mcp-query-service
Oct 8, 2026
Merged

ZhuchkaTriplesix merged 3 commits into
devfrom
issue/1134-mcp-query-service

Conversation

@ZhuchkaTriplesix

@ZhuchkaTriplesix ZhuchkaTriplesix commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

First part of the MCP epic #1133: the UI-free core the MCP tools will call (#1135 adds the server and the shim).

What's in it (lib/core/mcp/)

  • McpQueryService: listConnections, listTables, describeTable (columns, types, PK, FK target, indexes), sampleRows, runQuery, explainQuery for PostgreSQL, MySQL, SQLite.
    • Reuses the SQL editor delegates and ErdCatalog; no new execution code.
    • A fresh delegate per call, disposed afterwards (the pools keep the connection warm).
    • Row limit 1000 (samples 100), statement timeout 15 s, cells over 4 KB cut with a marker.
    • Errors become McpToolException with a readable text (the model fixes its query).
    • A missing connection and a connection that is not shared give the same error, so ids cannot be probed.
    • Table names from the model must exist in the catalog before they reach SQL; identifiers are quoted per dialect.
  • McpSqlGuard: one statement; only SELECT / WITH without DML / EXPLAIN without ANALYZE / SHOW / DESCRIBE / VALUES, plus SQLite schema pragmas. Refuses SELECT ... INTO (and INTO OUTFILE), row locks, and server functions that act even in a read-only transaction (pg_terminate_backend, pg_read_file, dblink, set_config, LOAD_FILE, load_extension, ...).
  • createReadOnlyMcpDelegate: the editor delegates with isReadOnly: true, i.e. Postgres default_transaction_read_only, MySQL SET SESSION TRANSACTION READ ONLY, SQLite SQLITE_OPEN_READONLY.
  • McpAccessStore: opt-in per connection (ids in app_settings), nothing shared by default. The settings UI comes in feat(mcp): Settings page, per-connection access and activity log #1136.
  • listConnections returns id, name, type, environment and database only; for SQLite only the file name.

Tests (test/core/mcp/)

  • mcp_sql_guard_test.dart: allowed / refused statements incl. comments, strings, CTEs, multi-statements, EXPLAIN ANALYZE, SELECT INTO, dangerous functions, pragmas.
  • mcp_query_service_test.dart: no credentials in output, opt-in access, refused SQL never reaches the delegate, limits and timeout, truncation, errors, schema tools, quoting, explain.
  • mcp_sqlite_readonly_test.dart: real SQLite file through the production delegate; schema, sample, join, plan; an INSERT on the MCP session is refused by the database itself.

Bug fix found on the way

ErdCatalog selected m.name and p.name without aliases. The SQLite driver returns rows as maps keyed by column name, so both collapsed, every catalog row lost a field and was dropped: the ERD diagram for SQLite connections was always empty. All catalog columns now have distinct aliases (also the Postgres FK query, for safety).

Not done here: views are not listed yet (the catalog is base tables only).

Written without running locally; CI is the first run.

Closes #1134

@github-actions github-actions Bot added core Core library logic and services backend Backend database driver execution and queries P2 Medium priority / Parity & Refactoring labels Oct 8, 2026
@ZhuchkaTriplesix
ZhuchkaTriplesix merged commit d5ca4f8 into dev Oct 8, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Backend database driver execution and queries core Core library logic and services P2 Medium priority / Parity & Refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant