You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c37042c
Browse filesBrowse the repository at this point in the historyBrowse files
fix(mcp): list a seed a load-time rule refuses at every open as unavailable (#2252, part 2) (#2255)
* feat(db): record the engines whose open refuses a handle with file access denied
MCP must know before any provider exists that no open of a connection can succeed for its caller, and no provider declares it: SQLite's connect() refuses a handle opened with file access denied, because its drivers cannot confine a statement's file access, while DuckDB opens one with external access off.
REFUSES_DENIED_FILE_ACCESS records it beside READS_FILE_ACCESS_POSTURE, true for SQLite alone, and joins the registries.
A census builds each engine that reads the posture under the context each MCP profile gives a denied caller: SQLite rejects with its exact error before touching a path that does not exist, and DuckDB opens a real file.
* fix(mcp): list a connection no MCP open can serve for its caller as unavailable
A SQLite seed offered to every role was listed with run_read_query although every open of it is refused, for an admin's token too.
mcpConnectionUnavailable asks editorExecutionContext for the posture of the caller's own copy, the call every MCP open makes, so the listing and the open agree.
Such a connection now lists read_tool: null and unavailable, a sentence that says why and what an operator changes, and the description says so.
Every other row lists as before, with one audit event per call.
* fix(mcp): refuse an unavailable connection in inspect_schema before acquiring it
inspect_schema built a provider for such a connection and relayed SQLite's refusal behind the untrusted-data notice as the database's error.
It now refuses it once the connection resolves, with the sentence list_connections gives it and a decision event recorded as mcp_execution_failed, as it refuses an engine with no tables, and builds nothing.
Statement tools are unchanged and still answer the provider's refusal at open.
* docs(mcp): define unavailable and state the SQLite seed requirement
The guide said read_tool is null where inspect_schema is the only read and named no SQLite seed requirement, though a SQLite seed opens only for an admin's token on a seed offered to admins alone.
It now defines unavailable beside read_tool and states that requirement beside the PostgreSQL and SQL Server ones, the SQLite provider doc says how MCP lists and refuses such a seed, and tests pin each line.
* docs(backlog): close MCP7, now that list_connections says why a SQLite seed cannot be read
MCP7's done-when holds: list_connections lists such a seed with unavailable, docs/MCP.md states the SQLite seed requirement, and tests pin both.
D460 also names the new census among the tests that pin SQLite's refusal.
* test(mcp): set aside LIBREDB_ENABLED_PROVIDERS in the unavailable-connection agreement test
The agreement test acquires real SQLite providers, and acquisition checks
LIBREDB_ENABLED_PROVIDERS first, so a developer's setting decided it.
* docs(mcp): say Studio opens a SQLite seed only for an admin where no other role can use it
The sentence spoke of every SQLite connection, but an admin's own inline
SQLite connection opens with full file access. D460 now also names the
agreement test that pins the SQLite refusal.
@@ -4523,7 +4523,7 @@ The DuckDB refusal names that case: `FILE_ACCESS_DENIED_REASON` in `src/lib/db/p
4523
4523
Measured 2026-11-05 through `POST /api/db/query` with the real driver and an admin session: SQLite seeds offered to `["admin", "user"]` and to `["*"]` each answered 400 with that sentence, and one offered to `["admin"]` answered 200.
4524
4524
Found 2026-11-05 by the review of the change that reads a shared seed's file-access audience from its operator roles.
4525
4525
4526
-
**Done when:** the SQLite refusal names a connection another role can use the way the DuckDB one does, for example "SQLite connections open only for an administrator on a connection no other role can use, because this driver cannot confine statement-level file access", every test that pins the sentence (in `tests/integration/mcp/context.test.ts`, `tests/integration/db/reserved-storage-paths.test.ts`, `tests/isolated/factory.test.ts` and `e2e/embedded-samples.spec.ts`) asserts the new wording, one of them for an administrator on a seed offered to `["admin", "user"]`, and any document that quotes the sentence follows it.
4526
+
**Done when:** the SQLite refusal names a connection another role can use the way the DuckDB one does, for example "SQLite connections open only for an administrator on a connection no other role can use, because this driver cannot confine statement-level file access", every test that pins the sentence (in `tests/integration/mcp/context.test.ts`, `tests/integration/db/reserved-storage-paths.test.ts`, `tests/isolated/factory.test.ts`, `tests/unit/db/refuses-denied-file-access.test.ts`, `tests/integration/mcp/list-connections.test.ts` and `e2e/embedded-samples.spec.ts`) asserts the new wording, one of them for an administrator on a seed offered to `["admin", "user"]`, and any document that quotes the sentence follows it.
4527
4527
4528
4528
## Value interpolation
4529
4529
@@ -9611,15 +9611,6 @@ Found 2026-11-01 while verifying `run_read_query` on ClickHouse; pre-existing an
9611
9611
9612
9612
**Done when:** every statement tool's row refusal states the limit the row actually exceeds, or the cap is measured on one copy of the rows, with a test on a row between 16 and 32 KiB on each served arm, and `docs/MCP.md` says which bytes the 32 KiB counts.
9613
9613
9614
-
### MCP7. `list_connections` names `run_read_query` for a SQLite seed every MCP call refuses
9615
-
9616
-
A SQLite seed whose `roles` admit a non-admin role is opened with external file access denied, so every `run_read_query` on it answers "SQLite connections require an administrator because this driver cannot confine statement-level file access" (`connect()` in `src/lib/db/providers/sql/sqlite.ts`), whoever holds the token; `docs/providers/sqlite.md` says so, and `docs/MCP.md` does not.
9617
-
`list_connections` still gives that connection `read_tool: "run_read_query"`.
9618
-
Measured 2026-11-01 with a seed set to `roles: ["*"]` and an admin's token.
9619
-
Found 2026-11-01 while verifying `run_read_query` on ClickHouse.
9620
-
9621
-
**Done when:** `list_connections` gives such a seed a `read_tool` that can run, or says why none can, `docs/MCP.md` states the SQLite seed requirement beside the PostgreSQL, SQL Server and ClickHouse ones, and a test pins both.
9622
-
9623
9614
### MCP8. An InfluxDB 3 refusal through `run_read_query` names `SHOW` and `DESCRIBE`, which the tool refuses
9624
9615
9625
9616
`run_read_query`'s statement fence admits a `TABLE` statement on an InfluxDB 3 (SQL) connection, and the provider's read policy then refuses it with `notARead` of `INFLUX_SQL_POLICY_SENTENCES` in `src/lib/db/providers/timeseries/influxdb/sql-policy.ts`: "Only SELECT, WITH, VALUES, SHOW, EXPLAIN and DESCRIBE run on an InfluxDB 3 connection in Studio".
Copy file name to clipboardExpand all lines: docs/MCP.md
+4-1Lines changed: 4 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -18,7 +18,9 @@ In the rest of this guide, `agent-read-only` is Studio's least-privilege read pr
18
18
- The tools, each annotated read-only and closed-world:
19
19
-`list_connections` lists the connections opted in for MCP that your token's role may use, without credentials.
20
20
It works for every engine but Oxia, Firestore, Modbus and NETCONF, whose connections the seed file cannot opt in.
21
-
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.
21
+
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`.
22
+
`unavailable` is a sentence that says why no tool can read the connection and what an operator changes, and `inspect_schema` refuses that connection with it as soon as the connection resolves, before anything is acquired.
23
+
A SQLite seed carries it for every token but an admin's, and for an admin's token too where the seed's `roles` name any role but admin ([Opting connections in](#opting-connections-in)).
22
24
A connection's `database` is left out on MQTT and Sparkplug B, where that setting is a stored value, the discovery topic filter or the group id, as the language's facet declares (`withholdsDatabase`).
23
25
-`inspect_schema` lists one connection's tables with their columns and, on request, their indexes.
24
26
It works on the same engines as `list_connections`: it lists every object kind the engine reads rows from, such as views beside tables, MongoDB collections, Redis keyspaces and search indexes, and `kind` names which one each entry is.
@@ -321,6 +323,7 @@ An empty `list_connections` answer means no connection is opted in for your toke
321
323
Where an engine's line under "Read-only is the database's own enforcement" above says it refuses a login that could do more than read, an opted-in seed for that engine needs a least-privilege principal.
322
324
On PostgreSQL the seed's role must not be a superuser and must not hold `pg_read_server_files`, `pg_write_server_files` or `pg_execute_server_program`.
323
325
On SQL Server the login must hold no fixed server role, neither `CONTROL SERVER` nor `ADMINISTER BULK OPERATIONS`, and none of `db_owner`, `db_accessadmin`, `db_securityadmin`, `db_ddladmin`, `db_backupoperator` or `db_datawriter`, and it must be granted `SHOWPLAN`.
326
+
A SQLite file has no user, so a SQLite seed proves no principal, but MCP opens it only for an admin's token on a seed offered to admins alone, `roles: ["admin"]`: SQLite's drivers cannot confine a statement's file access, so Studio opens a SQLite seed only for an admin, and only where no other role can use it ([sqlite.md](./providers/sqlite.md)), and `list_connections` lists every other SQLite seed a token can see, such as one offered to every role, `roles: ["*"]`, with `read_tool: null` and `unavailable`.
324
327
On libSQL the seed's token must be a read-only token (a JWT whose claims carry `"a": "ro"` and nothing beyond the standard claims) on a server that checks it, as the open proves for `run_read_query` ([libsql.md](./providers/libsql.md) section 3.6): a seed that names a user signs in with HTTP Basic and is refused, and so is a seed with no token, a writable token, or a server that runs a write the token sends, such as one that checks no credential.
325
328
On InfluxDB 3 (SQL) every Core token is an admin token, so no seed token is least-privilege and none is asked for: `run_read_query`refuses instead a server that is not InfluxDB 3 Core on the 3.12 line, whose planner's write refusals were captured, checking it at the open ([influxdb3.md](./providers/influxdb3.md) section 3.5).
326
329
On Elasticsearch with security on, the open asks the server what the seed's login may do and lists its privileges, and refuses, before any statement runs, a login that holds the cluster privilege `all`, reads the restricted system indices it asks about, or is listed with an index privilege granted with `allow_restricted_indices`, on this cluster or a remote one, so a seed signed in as a superuser is refused; that check cannot name every restricted index, and the server lists no privileges for an API key with role descriptors of its own, so it does not refuse every login that may read one.
Copy file name to clipboardExpand all lines: docs/providers/sqlite.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -1747,6 +1747,7 @@ so hard links cannot alias reserved files. Other user database files beside them
1747
1747
SQLite connections are refused when the server-derived file-access posture is denied,
1748
1748
including agent handles. HTTP and MCP derive that posture from the verified role and
1749
1749
managed connection: non-admin callers and seeds shared with non-admin roles are denied.
1750
+
Where it is denied, MCP's `list_connections` lists the seed with `read_tool: null` and an `unavailable` sentence that says what an operator changes, and `inspect_schema` refuses it before anything is acquired ([MCP.md](../MCP.md#opting-connections-in)).
1750
1751
SQLite's adapters cannot confine statement-level file access, so a denied connection
1751
1752
cannot be opened safely. Administrators retain SQLite access on private connections.
1752
1753
Embedded callers using the cached factory must explicitly pass `{ allowExternalFileAccess: true }`
* Whether any MCP open of a listed connection can succeed for its caller (#2252, part 2).
8
+
*
9
+
* The file-access posture is the one load-time rule decided per caller: the seed schema refuses the others (a read
10
+
* credential where one is required, read roots) when the file loads, so a listed connection already meets them. This
11
+
* asks `editorExecutionContext` for the posture of the caller's own listed copy, the call `McpConnectionContext.acquire`
12
+
* makes, and Vault resolution never rewrites the id, type, roles or `sharedWithNonAdminRoles` it reads, so the listing
13
+
* and the open agree. It builds no provider and reads no Vault reference.
14
+
*
15
+
* The engine's row is read first, so only an engine that refuses a denied handle asks for a posture. The sentence names
16
+
* no engine, as `list_connections` serves every engine.
17
+
*/
18
+
19
+
/** What `list_connections` lists and `inspect_schema` answers for a connection no MCP open can serve for its caller. */
20
+
exportconstMCP_CONNECTION_UNAVAILABLE_TEXT=
21
+
"This connection is not available to MCP: its engine opens only with full file access, which Studio grants only to an admin on a seed limited to the admin role, so an operator limits this seed's roles to admin.";
22
+
23
+
/**
24
+
* The sentence when `connection`'s engine refuses a handle opened with file access denied and `caller`'s posture on
25
+
* that record denies it, otherwise null. `refuses` is injectable for the tests.
@@ -60,15 +65,16 @@ type ListedConnection = z.infer<typeof ListedConnectionSchema>;
60
65
61
66
constLIST_CONNECTIONS_TITLE="List connections";
62
67
exportconstLIST_CONNECTIONS_DESCRIPTION=
63
-
"List the database connections this deployment opted in for MCP that your token's role may use, without credentials. Works for every engine. Each connection's read_tool names the tool that runs a statement on it, or is null where inspect_schema is the only read. An empty list means no connection is opted in: an operator adds mcp: true to a seed connection. Pages with offset; has_more and next_offset say when more connections exist.";
68
+
"List the database connections this deployment opted in for MCP that your token's role may use, without credentials. Works for every engine. Each connection's read_tool names the tool that runs a statement on it, or is null where inspect_schema is the only read. A connection that carries unavailable cannot be read by any tool, and unavailable says why and what an operator changes. An empty list means no connection is opted in: an operator adds mcp: true to a seed connection. Pages with offset; has_more and next_offset say when more connections exist.";
64
69
65
70
/** The file name alone, cut after the last / or \, so no server-side path reaches the client. */
0 commit comments