diff --git a/CHANGELOG.md b/CHANGELOG.md index d0fa2d11..56664387 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **MCP server (#1133)** — Querya can serve shared PostgreSQL / MySQL / SQLite connections to AI clients (Claude Desktop, Cursor, VS Code, Gemini CLI, ...) over MCP: `list_connections`, `list_tables`, `describe_table`, `sample_rows`, `run_query`, `explain_query` and a `schema://` resource. Read-only in three layers (SQL guard, read-only database session, per-connection opt-in), credentials never sent to the model, `querya-mcp` stdio bridge, Preferences → MCP Server with copy-config buttons and an activity log — see docs/mcp-server.md. + +### Fixed + +- **ERD for SQLite (#1134)** — the diagram was always empty for SQLite connections (catalog columns collapsed in the driver's row maps). + ### Planned - Planned 0.5.0 — live Marketplace download and install diff --git a/docs/README.md b/docs/README.md index 010bfa99..e607ba2c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,6 +7,7 @@ Index of Querya Desktop documentation, grouped by audience. - [Getting started](getting-started.md) — prerequisites, install, first run. - [User guide](user-guide.md) — connections, preferences, driver manager. - [Security / local data](security.md) — where metadata and secrets are stored. +- [MCP server](mcp-server.md) — let Claude Desktop, Cursor, VS Code and other AI clients read shared connections (read-only). - [Testing extensions in CI](extension-testing.md) — `querya-ext-tester`, release binaries and the GitHub Action. ## For contributors diff --git a/docs/mcp-server.md b/docs/mcp-server.md new file mode 100644 index 00000000..13edb906 --- /dev/null +++ b/docs/mcp-server.md @@ -0,0 +1,135 @@ +# MCP server + +Querya Desktop can act as an [MCP](https://modelcontextprotocol.io) server. Any +MCP client (Claude Desktop, Claude Code, Cursor, VS Code, Gemini CLI, Continue, +...) and whatever model it runs can then read the schema of the connections you +share and run **read-only** SQL through Querya. Querya has no chat of its own +and needs no AI provider keys: the client brings the model. + +Supported: **PostgreSQL, MySQL, SQLite**. MongoDB, Redis and extension drivers +are not exposed yet. + +## How it works + +``` +MCP client ──stdio──▶ querya-mcp ──127.0.0.1 + token──▶ Querya Desktop (running) + └─ connections, SSH tunnels, keyring +``` + +- The server runs **inside the running app**, so it uses your saved + connections, SSH tunnels and passwords from the OS keyring. The model only + sees each connection's id, name, type, environment tag and database name. +- The client starts `querya-mcp`, a small bridge that forwards the MCP session + to the app over a loopback socket. If Querya is not running (or the server is + off), every request gets the error *"Querya Desktop is not running or its MCP + server is off"*. + +## Enable it + +1. **Preferences → MCP Server → Enable MCP server.** The status line shows the + port and the number of connected clients. +2. Under **Shared connections**, switch on the connections the model may read. + Nothing is shared by default. Connections tagged *Production* show a `PROD` + badge: the model can read everything the connection's database user can. +3. Under **Connect a client**, press **Copy … config** for your client and paste + it where the client keeps MCP servers (see below). + +## `querya-mcp` + +| Build | Where it is | +|---|---| +| Linux zip / AppImage / `.deb` / `.rpm` | next to the app binary, e.g. `/opt/querya-desktop/querya-mcp` | +| Windows zip / setup | next to `querya_desktop.exe` in the install folder | +| macOS, Flatpak | not bundled yet: download `querya-mcp-v-` from the GitHub release | + +The copy buttons insert the bundled path automatically; otherwise replace +`/path/to/querya-mcp` with where you put the downloaded binary. + +## Client configuration + +**Claude Desktop** (`claude_desktop_config.json`), **Cursor** (`~/.cursor/mcp.json`) +and most stdio clients: + +```json +{ + "mcpServers": { + "querya": { "command": "/opt/querya-desktop/querya-mcp" } + } +} +``` + +**VS Code** (`.vscode/mcp.json` or user settings): + +```json +{ + "servers": { + "querya": { "type": "stdio", "command": "/opt/querya-desktop/querya-mcp" } + } +} +``` + +**Claude Code:** `claude mcp add querya -- /opt/querya-desktop/querya-mcp` + +**Gemini CLI** (`~/.gemini/settings.json`): the same `mcpServers` block as above. + +## Tools + +| Tool | Arguments | Returns | +|---|---|---| +| `list_connections` | — | shared connections: `id`, `name`, `type`, `environment`, `database` | +| `list_tables` | `connection_id` | tables with their column count | +| `describe_table` | `connection_id`, `table` | columns, types, primary key, foreign key targets, indexes | +| `sample_rows` | `connection_id`, `table`, `rows` (1-100, default 20) | first rows of the table | +| `run_query` | `connection_id`, `sql` | `columns`, `rows`, `row_count`, `truncated` | +| `explain_query` | `connection_id`, `sql` (without `EXPLAIN`) | the execution plan, without running the query | + +Resource: `schema://` — all tables with columns and keys, for +clients that load context up front. + +SQL errors come back as tool errors with the database's message, so the model +can fix its query and retry. + +## Limits + +- **Read-only.** One statement per call; only `SELECT`, `WITH` without + data-modifying parts, `EXPLAIN` (without `ANALYZE`), `SHOW`, `DESCRIBE`, + `VALUES`, and schema `PRAGMA`s on SQLite. See [security.md](security.md#mcp-server). +- **Rows:** at most 1000 per `run_query` (`truncated: true` when cut), 100 per + `sample_rows`. **Cells** longer than 4 KB are cut with a marker. +- **Time:** 15 s per statement. +- **Tables:** base tables of the current schema / database; views are not + listed yet. + +## Activity log + +**Preferences → MCP Server → Recent calls** lists the last calls (time, client, +tool, connection, SQL, rows and duration, or the error). The app keeps the last +200 in its local database; **Clear** empties it. + +## Troubleshooting + +| Symptom | Fix | +|---|---| +| "Querya Desktop is not running or its MCP server is off" | Start Querya and switch on **Enable MCP server**. | +| `list_connections` is empty | Share at least one PostgreSQL / MySQL / SQLite connection. | +| "Connection N is not available" | The connection was unshared or deleted; call `list_connections` again. | +| "Invalid Querya MCP token" | The token was regenerated or the app restarted with a new one: restart the MCP client. | +| Client cannot find `querya-mcp` | Use the absolute path; on macOS / Flatpak download the release binary and `chmod +x` it. | +| Several Querya instances | Set `QUERYA_MCP_ENDPOINT` to a different file for each instance and for its `querya-mcp`. | + +The endpoint file (port and token) is `$XDG_RUNTIME_DIR/querya/mcp-endpoint.json` +on Linux (fallback `~/.cache/querya`), `~/Library/Caches/Querya/` on macOS and +`%LOCALAPPDATA%\Querya\` on Windows. + +## For contributors + +- Query core: `lib/core/mcp/mcp_query_service.dart`, guard `mcp_sql_guard.dart`, + error redaction `mcp_redaction.dart`, read-only delegates `mcp_sql_delegates.dart`. +- Server and transport: `querya_mcp_server.dart` (`dart_mcp`), + `mcp_socket_host.dart`, `mcp_server_controller.dart`. +- Bridge: `packages/querya_mcp_bridge` — a dependency-free package so + `dart compile exe` produces one static binary (the app package has build + hooks that `dart compile exe` refuses). +- Settings UI: `lib/features/settings/preferences_mcp_section.dart`. +- Tests: `test/core/mcp/` (security suite: `mcp_security_test.dart`). +- Epic: [#1133](https://github.com/QueryaHub/Querya-Desktop/issues/1133). diff --git a/docs/roadmap.md b/docs/roadmap.md index 4b9d3ae3..cc5568cf 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -49,6 +49,11 @@ Living document for planned work. Not a commitment order; adjust as priorities c - **Done:** nested collection subtree in the connections panel (#1023), one-click return to the active Mongo collection / Redis key (#1024), Redis key folder tree by delimiter (#1025), Mongo document viewer in Table / Tree / JSON modes (#1026), Aggregation Pipeline Builder (#1027), visual Index Manager (#1028), interactive Redis CLI console (#1029) and MQL query / shell console (#1030). See the [user guide](user-guide.md#mongodb-and-redis). +## MCP server (AI clients) + +- **Done (epic [#1133](https://github.com/QueryaHub/Querya-Desktop/issues/1133)):** read-only MCP server inside the app for PostgreSQL, MySQL and SQLite — tools `list_connections`, `list_tables`, `describe_table`, `sample_rows`, `run_query`, `explain_query` and a `schema://` resource; `querya-mcp` stdio bridge (bundled on Linux / Windows, release asset for all platforms); per-connection opt-in, activity log, three read-only layers — [mcp-server.md](mcp-server.md), [security.md](security.md#mcp-server). +- **Later:** writes through a `propose_write` tool confirmed in the Querya window; Streamable HTTP transport; MongoDB / Redis tools; extension drivers; views in `list_tables`; bundling `querya-mcp` in the macOS app and Flatpak. + ## SSH and advanced networking - **Done:** SSH tunnels through a bastion / jump host (password, private key or agent auth, optional second hop, host key fingerprint pinning, keep-alive) with a loopback-only forwarded port and a *Test SSH Connection* button. Security model: [security.md](security.md#ssh-tunnels-bastion-hosts). Setup guide: [user-guide.md](user-guide.md#connecting-through-a-bastion-host). diff --git a/docs/security.md b/docs/security.md index e7df21b3..ae760465 100644 --- a/docs/security.md +++ b/docs/security.md @@ -15,7 +15,7 @@ On upgrade from older databases, existing plaintext secrets in SQLite are **migr ## Threat model (practical) - Anyone with **full access to your user session** can usually read app data and may extract secrets depending on OS protections. -- The app does **not** implement team features, audit logging, or network zero-trust controls. +- The app does **not** implement network zero-trust controls. Local audit trails exist for executed data changes and for MCP calls. - **SSH tunnels / jump hosts** are built in (see below); the tunnel does not protect against an attacker who already controls your user session. ## SSH tunnels (bastion hosts) @@ -26,6 +26,22 @@ On upgrade from older databases, existing plaintext secrets in SQLite are **migr - **Host key verification:** with no pinned fingerprint the first key is accepted (trust on first use). Pin the server's SHA-256 fingerprint in the connection to reject a changed key; a mismatch fails the connection and reports the observed fingerprint. - **Team sharing:** *Export Team Profile* writes connections without passwords or SSH secrets; importing re-scrubs the file. +## MCP server + +Querya can serve the connections you share to AI clients over MCP ([mcp-server.md](mcp-server.md)). SQL written by a model is untrusted, so access is limited in three independent layers: + +1. **Guard** (`McpSqlGuard`): one statement per call, only `SELECT` / `WITH` without data-modifying parts / `EXPLAIN` without `ANALYZE` / `SHOW` / `DESCRIBE` / `VALUES` and SQLite schema pragmas. Refused even inside a read: `SELECT ... INTO`, `INTO OUTFILE`, row locks, server functions with side effects (`pg_terminate_backend`, `pg_read_file`, `dblink`, `set_config`, `LOAD_FILE`, `load_extension`, ...) and MySQL executable comments (`/*! ... */`). +2. **Read-only database session:** PostgreSQL `default_transaction_read_only`, MySQL `SET SESSION TRANSACTION READ ONLY`, SQLite opened with `SQLITE_OPEN_READONLY`. A write that slipped past the guard is refused by the database. For defense in depth, also give MCP-shared connections a database user with read-only grants. +3. **Policy:** the server is off by default; each connection is shared individually (off by default); 15 s statement timeout, 1000-row and 4 KB-per-cell limits. + +Credentials never reach the model: tools return only connection id, name, type, environment and database name, and error messages are scrubbed (`McpRedaction`) of the connection's secrets, URI user info, `password=`-style values and private keys. + +**Transport:** the server listens on `127.0.0.1` only, on a random port. Clients connect through `querya-mcp`, which must present a random 256-bit token from an endpoint file readable only by your user (directory `0700`, file `0600`); a wrong token closes the connection. Any process running as your user can read that file, so the MCP server does not protect against an attacker who already controls your session. + +**Prompt injection:** query results are data. Text stored in your tables can contain instructions aimed at the model; the server tells clients not to follow them, but the read-only limits above are what actually bound the damage. + +**Audit:** every call is logged (Preferences → MCP Server → Recent calls). + ## Tests Automated tests use an **in-memory** secrets backend (see `test/flutter_test_config.dart`) so CI does not require a desktop keyring.