Skip to content
Merged
3 changes: 2 additions & 1 deletion docs/API_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2176,7 +2176,8 @@ The answers, in the order they are checked:
| 404 | JSON-RPC `-32601`, `Method not found` | `subscriptions/listen`, which this server does not implement |
| 200, 202 | The SDK's JSON-RPC answer, as JSON or as an event stream; 202 for a notification | Everything else |

The tools it serves are `list_connections`, `inspect_schema` and one statement tool per statement language, listed with their engines in [docs/MCP.md](./MCP.md#what-it-is); `list_connections` names each connection's `read_tool`, and [`docs/MCP.md`](MCP.md) documents each tool's arguments and limits.
The tools it serves are `list_connections`, `inspect_schema` and one statement tool per statement language, listed with their engines in [docs/MCP.md](./MCP.md#what-it-is); a token's `tools/list` holds the metadata tools and only the statement tools that serve the engines of the connections it can read, `list_connections` names each connection's `read_tool`, and [`docs/MCP.md`](MCP.md) documents each tool's arguments, hints and limits.
A seed file that cannot be read or parsed answers `tools/list` with the JSON-RPC error `-32603` on HTTP 200.

#### `GET /api/mcp`, `DELETE /api/mcp`

Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ Full behaviour, the tool set, what bounds a run, the HTTP surface and the honest

### 4.10. MCP Server (`src/lib/mcp/`, off by default)

An MCP endpoint at `/api/mcp` for AI clients of the user's own, served by the official MCP TypeScript SDK: `createMcpHandler` at module scope and one `McpServer` per request, in revision 2026-07-28 and, statelessly, 2025-11-25 and 2025-06-18.
An MCP endpoint at `/api/mcp` for AI clients of the user's own, served by the official MCP TypeScript SDK: two `createMcpHandler` handlers at module scope, one for a token's own `tools/list` and one for every other request, and one server per request, in revision 2026-07-28 and, statelessly, 2025-11-25 and 2025-06-18.
It authenticates with a scoped bearer token each user mints on the settings screen, signed with a key derived from `JWT_SECRET` under a configured label, and never with the session cookie; `src/proxy.ts` and the route both check the Origin, the Host on a loopback bind, and the token.
Its tools reach only seed connections opted in with `mcp: true`, through `acquireExecutionProfileProvider` alone, bound every result to 32 KiB behind an untrusted-content notice, and write `mcp_operation` audit events, the decision before any provider.
It is standalone-only: nothing under `src/lib/mcp/` or `src/app/` is reachable from the package's entry points, which a package-boundary test asserts.
Expand Down
32 changes: 30 additions & 2 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ None of it is a GitHub issue.
- [Row editing](#row-editing) — R1–R3 · 3
- [Studio UI and query execution](#studio-ui-and-query-execution) — X2-X26, U2-U202 · 188
- [Dependencies](#dependencies) — P1-P13 · 11
- [Documentation](#documentation) — DOC3-DOC36 · 30
- [Documentation](#documentation) — DOC3-DOC37 · 31
- [Release pipeline](#release-pipeline) — REL1-REL9 · 9
- [Security Phase 1 deferrals](#security-phase-1-deferrals) — H1–H14 · 4
- [Security Phase 2 deferrals](#security-phase-2-deferrals) — C3–C11 · 6
Expand All @@ -42,7 +42,7 @@ None of it is a GitHub issue.
- [Agent M1 deferrals (#328)](#agent-m1-deferrals-328) — A2–A8 · 5
- [Agent M2 deferrals (#329)](#agent-m2-deferrals-329) — B2-B171 · 82
- [Passkey deferrals (#785)](#passkey-deferrals-785) — PK1-PK9 · 8
- [MCP server deferrals (#246)](#mcp-server-deferrals-246) — MCP1-MCP23 · 22
- [MCP server deferrals (#246)](#mcp-server-deferrals-246) — MCP1-MCP25 · 24
- [Governance follow-ups](#governance-follow-ups) — GOV1-GOV31 · 31

---
Expand Down Expand Up @@ -7585,6 +7585,14 @@ Found 2026-11-05 while designing MCP phase 1 (#2252).

**Done when:** each count names what it counts from the tree or `docs/llms/README.md` rather than a number that moves, `tests/unit/agent-documentation.test.ts` holds it, and B9 states the resume path as it is or leaves the backlog.

### DOC37. ADDING_A_PROVIDER.md says only DuckDB reads the file-access posture

The `readsFileAccessPosture` row of the capability table in `docs/ADDING_A_PROVIDER.md` says only DuckDB opens its editor handle under the server-derived file-access posture.
`SQLiteProvider.getCapabilities` in `src/lib/db/providers/sql/sqlite.ts` declares `readsFileAccessPosture: true` as well, and `READS_FILE_ACCESS_POSTURE` in `src/lib/db/compatibility.ts` holds it true: SQLite refuses a denied handle at `connect()` where DuckDB confines it, as `REFUSES_DENIED_FILE_ACCESS` records.
Found 2026-11-05 in the review of #2255.

**Done when:** the row names both engines and what each does with a denied handle, and a test holds the engines the row names to the true rows of `READS_FILE_ACCESS_POSTURE`.

## Release pipeline

### REL1. No CI job installs the released chart artifact with a Helm 3 client
Expand Down Expand Up @@ -9770,6 +9778,26 @@ Found 2026-11-05 while designing MCP phase 1 (#2252).

**Done when:** both comments say "bounded, read-only statements in each engine's own language" in the next change to the chart, with its #167 version bump and `bun run chart:bump` refreshing the operator copy, and `tests/unit/mcp/copy.test.ts` reads both.

### MCP24. An in-memory SQLite or DuckDB seed lists a read tool that every MCP open refuses

A seed whose `database` is `:memory:` loads on SQLite and on DuckDB, and every MCP open of it is refused: both MCP profiles open read-only, and `connectReadOnly` in `SQLiteProvider` (`src/lib/db/providers/sql/sqlite.ts`) and in `DuckDBProvider` (`src/lib/db/providers/sql/duckdb/index.ts`) refuses an in-memory target with an `ExecutionProfileError`.
`mcpConnectionUnavailable` in `src/lib/mcp/availability.ts` reads only the file-access posture, so `list_connections` lists such a seed with `read_tool: "run_read_query"` and no `unavailable`, a token that reaches it lists `run_read_query` in its `tools/list`, and `inspect_schema` and `run_read_query` both answer the profile's refusal, in the words MCP20 records.
Measured 2026-11-05 on `main` for an admin token on a SQLite and on a DuckDB seed offered to admins alone, and for a user token on a DuckDB seed offered to every role.
`docs/MCP.md` says a SQLite seed's `database` is an absolute path, and the seed schema refuses neither engine's `:memory:`.
Found 2026-11-05 in the review of #2255.

**Done when:** such a seed is refused when the seed file loads, or is listed with `read_tool: null` and an `unavailable` sentence and left out of every token's `tools/list`, decided from a record beside the providers rather than a type-id branch, with a test that lists it and opens it under both MCP profiles.

### MCP25. A statement tool's not-served refusal names `run_read_query` for a SQLite seed no MCP open serves

`notServedText` in `src/lib/mcp/tools/run-statement.ts` names the tool that serves the connection's engine (`mcpStatementToolFor`), or `inspect_schema` where none does, and never asks `mcpConnectionUnavailable`.
So a statement tool that does not serve SQLite, called on a SQLite seed that is unavailable to the caller, answers "this tool does not serve SQLite; use run_read_query there", while `list_connections` lists that seed with `read_tool: null` and `unavailable`, a token whose only seed it is lists no `run_read_query`, and `inspect_schema` refuses the seed too.
`docs/MCP.md` says the tool named in that refusal is the one the connection's `read_tool` names, or `inspect_schema` where it is `null`, which this case contradicts.
Measured 2026-11-05 on `main` with `run_cypher_read` and a user token on a SQLite seed offered to `["user"]`.
Found 2026-11-05 while planning #2255.

**Done when:** a statement tool that does not serve such a connection answers that the connection is not available to MCP, naming no tool, `docs/MCP.md` says so beside the not-served refusal, and a test calls such a tool on such a seed.

---

## Governance follow-ups
Expand Down
15 changes: 14 additions & 1 deletion docs/MCP.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ In the rest of this guide, `agent-read-only` is Studio's least-privilege read pr

- One endpoint, `POST /api/mcp`, served by the official MCP TypeScript SDK.
It speaks revision 2026-07-28 and, without sessions, the 2025 revisions 2025-11-25 and 2025-06-18 that most clients still use.
- The tools, each annotated read-only and closed-world:
- The tools, as a token lists them and with their hints described after this list:
- `list_connections` lists the connections opted in for MCP that your token's role may use, without credentials.
It works for every engine but Oxia, Firestore, Modbus and NETCONF, whose connections the seed file cannot opt in.
Each connection's `read_tool` names the statement tool below that runs a statement on it, and is `null` where `inspect_schema` is the only read or where the connection carries `unavailable`.
Expand All @@ -42,6 +42,7 @@ In the rest of this guide, `agent-read-only` is Studio's least-privilege read pr
On Weaviate it lists each collection's declared properties and vectors and never its tenants or aliases, which `run_weaviate_read` reads as far as the key's grants reach; it reads no stored object, so a vector's dimension, which the schema does not declare, reads `?`, and a call wakes no tenant.
On InfluxDB (InfluxQL) it lists measurements with their tag and field keys, and on InfluxDB 3 (SQL) tables with their columns; the `_internal` database and the `system.*` tables of a 3.x server are never returned.
On Databend it lists tables, views, materialized views and dynamic tables; on a Databend Cloud connection with a Warehouse set, a call resumes a suspended warehouse, which bills while it runs, as any statement does.
On Redis its one bounded `SCAN` walk, on a primary, deletes each expired key it meets and, on a server whose `notify-keyspace-events` holds the expired class, publishes an `expired` event for it, as a read of that key would (measured on Redis 8.10.2 and Valkey 9.1.2 on 2026-11-05), and `inspect_schema` stays annotated read-only there, as `run_redis_read` does.
On Apache Spark it lists the tables and views of one namespace, read with SHOW and DESCRIBE statements that start no Spark job on Studio's own session; a namespace is named in Spark's spelling, its parts joined by dots.
- `run_read_query` runs one read-only SQL statement.
A `SELECT` runs on every engine it serves; a refused statement's answer names what else that engine takes.
Expand Down Expand Up @@ -198,6 +199,17 @@ In the rest of this guide, `agent-read-only` is Studio's least-privilege read pr
`src/lib/mcp/tools/statement-arms.ts` builds the tools from those facets and names no language, so a language that gains an MCP tool adds its facet and edits nothing under `src/lib/mcp/`.
A facet may raise the least `timeout_ms` its tool takes (`leastTimeout`), which tools/list then shows as the field's minimum, and may declare that its provider's warnings are Studio's own sentences an answer must carry (`answersWarnings`), which then reach the client as the answer's `notes`; every other tool's answers carry no warning text.
A facet may also declare that a connection's database setting is a stored value on its engines (`withholdsDatabase`), which `list_connections` then leaves out of that connection's row.
- A token's `tools/list` holds `list_connections`, `inspect_schema` and each statement tool that serves the engine of a connection the token can read: one that `list_connections` lists for it without `unavailable`.
A token that can read no connection, such as one whose only seed is unavailable to it, lists `list_connections` and `inspect_schema` alone.
A call to a statement tool the list leaves out still gets Studio's own answer, recorded in the audit log as every call is, never the protocol's unknown-tool error, so the list only shows what a token can use and every check stays at the call.
Each `tools/list` reads the seed file through the seed cache (`SEED_CACHE_TTL_MS`, 60 seconds unless set) and builds no provider and reads no Vault reference, so a client sees an operator's seed edit the first time it lists the tools after the seed cache's time to live has passed, which a client that lists them only when it connects does once it reconnects.
On revision 2026-07-28 the answer carries `ttlMs: 0` and `cacheScope: "private"`, since it is this token's list and no other's.
A seed file that cannot be read or parsed answers `tools/list` with the JSON-RPC error `-32603` and the sentence every tool then answers, never a shorter or a longer list ([Troubleshooting](#troubleshooting)), while `initialize` and `server/discover` still answer.
- Every tool is annotated closed-world (`openWorldHint: false`) and read-only (`readOnlyHint: true`), except `inspect_schema` for a token that reaches a Databend or LibreDB connection, which it tells `readOnlyHint: false` and `destructiveHint: false`, since a call there changes state the engine keeps, though it writes no data.
On Databend Cloud a call resumes a suspended warehouse, which bills while it runs, and on LibreDB it opens the file and keeps its exclusive lock until that handle has been idle for 30 minutes ([Limits](#limits)).
Those engines are the rows of `MCP_METADATA_READ_CHANGES_STATE` in `src/lib/db/compatibility.ts` that are true, one per engine, so a self-hosted Databend server, which bills nothing, is marked too.
No tool sets `idempotentHint`, which a client reads only beside `readOnlyHint: false`, where a repeated call extends the lock's idle time or the billed run.
A statement tool whose read has an effect of the server's own bookkeeping stays read-only, and its description says so: a Redis read can delete an expired key and publish an event about it, and an MQTT or Sparkplug B read opens broker sessions; `inspect_schema` also stays read-only on Redis and Sparkplug B, with what a call does there stated in its entry above.
- Read-only is the database's own enforcement, not a filter over statement text: every statement tool takes the connection under Studio's least-privilege read profile (`agent-read-only`) and runs through the provider's read-only statement path, and each engine enforces it as follows:
- PostgreSQL: by a read-only transaction, and refuses a login that could do more than read.
- SQLite: by a read-only open.
Expand Down Expand Up @@ -648,6 +660,7 @@ For the first few seconds after that service starts, `opencode mcp list` can say
| 429 | The per-user query budget is spent; retry after `Retry-After` seconds |
| 400 `-32020` | A required `MCP-Protocol-Version`, `Mcp-Method` or `Mcp-Name` header is missing, malformed or disagrees with the body |
| 500 | An unrecognized `LIBREDB_MCP_ENABLED`, an unset server version, or a server fault such as a missing `JWT_SECRET`; the server log names which |
| 200 `-32603` | On `tools/list`: the seed file cannot be read or parsed, so Studio cannot tell which tools the token can use, and every tool call answers that the connection configuration could not be read; the server log names the cause |

A tool call that reaches a database writes two `mcp_operation` audit events: a decision before the database is reached and an outcome after it, under one correlation id, with the token's user and the connection's seed id.
`list_connections` reaches no database, so it writes one event.
Expand Down
1 change: 1 addition & 0 deletions docs/providers/databend.md
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,7 @@ The analyze and vacuum cards are never drawn, and are worded true all the same:
`resumesBilledCompute` is declared when Warehouse is set or the host is Databend Cloud's (section 4.4).
`schemaRefreshPattern` re-reads the tree after a statement that leads with `CREATE`, `DROP`, `ALTER`, `RENAME`, `UNDROP`, `TRUNCATE` or `REPLACE`.
MCP's metadata tools and plan mode work; agent execution and MCP `run_read_query` do not run a statement on this type ([B103](../BACKLOG.md)).
MCP's `inspect_schema` tells a token that reaches a Databend connection `readOnlyHint: false` and `destructiveHint: false`, since on Databend Cloud a call resumes a suspended warehouse, which bills while it runs (section 4.4); the hint is per engine (`MCP_METADATA_READ_CHANGES_STATE`), so a self-hosted server, which bills nothing, is told the same.
Import Data has no target on Databend: Studio draws the IMPORT control for every connection, and its dialog writes only into an existing object of a kind that declares `acceptsRowWrites`, which no Databend kind does (section 6.3), or into a table it creates, which the provider withholds by declaring no `createTable`, so it offers only Close and says:

> Studio's import has no target on this connection: it offers no existing table here to write into and cannot create one.
Expand Down
1 change: 1 addition & 0 deletions docs/providers/libredb.md
Original file line number Diff line number Diff line change
Expand Up @@ -1512,3 +1512,4 @@ Measured on `@libredb/libredb` 0.2.2 by [`tests/integration/mcp/run-libredb-read

A cancel or a timeout is seen only when the read has ended, because the read runs on the server's own thread and cannot be stopped midway: a read that outran the time left of `timeout_ms` is then answered as the timeout, and a cancel delivered during a read arrives after the read's answer was settled.
`run_libredb_read` takes no lock, so it never holds the file from the editor; `inspect_schema` opens the file as the operations profile does, which takes the exclusive lock when no Studio session holds the file open, and opens an existing file only ([`docs/MCP.md`](../MCP.md)).
So MCP's `inspect_schema` tells a token that reaches a LibreDB connection `readOnlyHint: false` and `destructiveHint: false` (`MCP_METADATA_READ_CHANGES_STATE`), while `run_libredb_read`, which takes no lock, stays annotated read-only.
1 change: 1 addition & 0 deletions docs/providers/redis.md
Original file line number Diff line number Diff line change
Expand Up @@ -2345,6 +2345,7 @@ The first is the ACL user's key patterns, which the server enforces: a `~app:* +
The second is Studio's, independent of how the command was written: the closed table sends no command that lists keys or reads a value, and the projection keeps only counts, types and sizes and never repeats the key, so neither a key name the command did not write nor a stored value reaches the answer, whatever the user's patterns allow.
MCP's `inspect_schema` takes the operations profile, which proves no user, so it lists the leading segments of the key groupings (`app:*`) for the seed's user whether or not the proof would pass it: `default`, a full user and a `+@read` reader alike.
Its listing is one bounded `SCAN` walk, which the server does not filter by key patterns, so a `~app:* +@read` user is listed the groupings of keys outside its patterns too (`secret:*`); a user refused `CONFIG GET` is listed its session database alone, and a refusal reaches the client worded by the command it names, never the user.
On a primary that walk deletes each expired key it meets, and on a server whose `notify-keyspace-events` holds the expired class it publishes an `expired` event for each, as the server does for any command that meets an expired key: measured on Redis 8.10.2 and Valkey 9.1.2 on 2026-11-05, a `SCAN` of a database holding one live and one expired key answered the live key alone and left one key, and both servers published `expired` for the other; `inspect_schema` stays annotated read-only, as `run_redis_read` does for its own lazy expiry ([§16.4](#164-metadata-only)).
It lists no function library, since a library is not a relation kind.
That is the operations handle's projection, not this tool's (B156).

Expand Down
Loading
Loading