From 97bbc61089a35d9363c5433bfc3e0b3cc15c82b1 Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 1 Nov 2026 15:01:01 +0700 Subject: [PATCH 1/2] docs(developers): document what Read Only allows for external callers --- developers/applescript.mdx | 9 +++++---- developers/index.mdx | 27 +++++++++++++++++++++++++++ developers/mcp-tools.mdx | 6 +++--- developers/tokens.mdx | 2 +- features/ai-assistant.mdx | 2 ++ features/mcp.mdx | 2 +- features/safe-mode.mdx | 4 +++- 7 files changed, 42 insertions(+), 10 deletions(-) diff --git a/developers/applescript.mdx b/developers/applescript.mdx index c607ee06..38c481c5 100644 --- a/developers/applescript.mdx +++ b/developers/applescript.mdx @@ -28,7 +28,8 @@ end tell Read Only is the default and the right level for most connections: reporting and export scripts need nothing more. Raise a connection to Read & Write with **External Clients** on its **Options** -tab only when a script has to write to it. +tab only when a script has to write to it. [What Read Only allows](/developers#what-read-only-allows) +has what counts as a read and what is refused at every level. Read & Write is not a way past Safe Mode. A statement that drops or truncates always raises a confirmation naming the app that sent it, whatever the level says, and Safe Mode's authentication @@ -139,7 +140,7 @@ fails with error -10000. `row limit` defaults to 500 and caps at 10000. `timeout` defaults to 30 seconds and caps at 600. Several statements in one call are refused, as are statements that read files or run server-side -code. +code and the requests TablePro builds for its own grid. SQL Server is the exception to one statement per call: send the whole script, and it runs batch by batch at its `GO` lines, as the query editor runs it. The record is the first result set, @@ -148,7 +149,7 @@ than one. `row limit` caps each result set on its own. ```applescript tell application "TablePro" - set r to run query "EXEC sp_help 'dbo.orders'" in connection "Warehouse" + set r to run query "SELECT TOP 5 * FROM dbo.orders; SELECT COUNT(*) FROM dbo.orders" in connection "Warehouse" set sets to results of r repeat with one in sets columns of one @@ -227,7 +228,7 @@ end try |---|---| | `-1728` | No connection or tab by that name or id | | `-50` | The request is malformed: several statements in one call, or no database to run against | -| `-10000` | Refused by Safe Mode or by **External Clients**, or cancelled at the confirmation | +| `-10000` | Refused by Safe Mode, by **External Clients** or by a [check that holds at every level](/developers#what-read-only-allows), or cancelled at the confirmation | ## Related diff --git a/developers/index.mdx b/developers/index.mdx index 7c955976..017891e4 100644 --- a/developers/index.mdx +++ b/developers/index.mdx @@ -97,6 +97,33 @@ Each request lands in the activity log with the token behind it, and a statement SHA-256 digest rather than as text. Open **Settings > MCP** and click **View Activity** to read it. +### What Read Only allows + +**Read Only** is a check on the statement's text, made before the statement reaches the driver. A +statement from an MCP client, a script or a shortcut runs only when TablePro can tell for certain +that it reads, by the rules in [What counts as a read](/features/safe-mode#what-counts-as-a-read). +Anything else fails with "This connection is read only for external clients." + +These fail at every level, **Read & Write** included: + +- Text holding more than one statement, or ending inside a block it opened, with "Send one + statement at a time." A SQL Server script is the exception: it runs batch by batch at its `GO` + lines. +- A statement that reads or writes files, or runs code on the server, such as `COPY`, `ATTACH`, + `LOAD`, `VACUUM INTO`, DuckDB's file readers like `read_csv`, ClickHouse's `file`, `url` and `s3`, + and T-SQL that runs a string through `EXEC` or `sp_executesql`. +- A request in the form TablePro builds for its own grid on engines such as BigQuery, + Elasticsearch and Typesense. Send the statement it stands for. + +The AI assistant's `execute_query` makes the same three refusals. The assistant answers to the +connection's AI policy and Safe Mode rather than to **External Clients**; see +[Tool calling](/features/ai-assistant#tool-calling). + +A statement that clears the check runs as the connection's login, with every privilege that login +holds. Those privileges are the final limit. For a connection where an unwanted write matters, save +it with a login that can only read, or set **External Clients** to **Blocked** and **AI Policy** to +**Never**. + ## Publish it Listed integrations appear at [tablepro.app/integrations](https://tablepro.app/integrations), and diff --git a/developers/mcp-tools.mdx b/developers/mcp-tools.mdx index 7e4b7dd0..bb30f298 100644 --- a/developers/mcp-tools.mdx +++ b/developers/mcp-tools.mdx @@ -43,7 +43,7 @@ A call that has to open a connection with a pre-connect script also waits for th `confirm_destructive_operation` needs `admin` as well, so it takes a Full Access token, and **Read Only** refuses it outright whatever the token carries. No token skips the user's approval. -Statements that read or write files, or that run server-side code, are refused on every tool. So is more than one statement in a single call, and a statement a `GO 5` line would run five times, except in a SQL Server script sent to [`execute_query`](#execute_query). +**Read Only** refuses any statement that is not a read, with `denied`. At every level, every tool refuses a statement that reads or writes files or runs server-side code, and a request in the form TablePro builds for its own grid, also with `denied`. More than one statement in a call fails with `invalid_argument`, and so does a statement a `GO 5` line would run five times, except in a SQL Server script sent to [`execute_query`](#execute_query). Examples of each, and the limit the connection's login sets, are in [What Read Only allows](/developers#what-read-only-allows). ## Connections @@ -155,9 +155,9 @@ Without filters this returns the engine's fast estimate unless `exact` is set. W ### `execute_query` -One statement, 100 KB at most. `DROP` and `TRUNCATE` are refused here; use `confirm_destructive_operation`. With `read_only: true`, a statement that is not a read gets `-33007` before the user is asked anything, even on a connection set to **Read & Write** and with a token that carries `tools:write`. On MongoDB, a statement is a read only when it is one call with literal arguments, as [Safe Mode](/features/safe-mode#what-counts-as-a-read) describes; any other JavaScript is a write and needs `tools:write`. When the request carries a `progressToken`, the tool emits progress at 0.0 (resolving), 0.3 (executing) and 1.0 (done). +One statement, 100 KB at most. `DROP` and `TRUNCATE` are refused here; use `confirm_destructive_operation`. With `read_only: true`, a statement that is not a read fails with `denied` before the user is asked anything, even on a connection set to **Read & Write** and with a token that carries `tools:write`. On MongoDB, a statement is a read only when it is one call with literal arguments, as [Safe Mode](/features/safe-mode#what-counts-as-a-read) describes; any other JavaScript is a write and needs `tools:write`. When the request carries a `progressToken`, the tool emits progress at 0.0 (resolving), 0.3 (executing) and 1.0 (done). -On SQL Server, send the whole script instead. A variable exists only in the batch that declares it, so a `DECLARE` and the `SELECT`s that read it belong in one call. The script is cut into batches at its `GO` lines, as the query editor cuts it, and `GO 5` runs a batch five times. The gates judge the script by its worst statement: one write anywhere needs `tools:write` and Safe Mode, and one `DROP` refuses the call. `DECLARE` counts as a write. +On SQL Server, send the whole script instead. A variable exists only in the batch that declares it, so a `DECLARE` and the `SELECT`s that read it belong in one call. The script is cut into batches at its `GO` lines, as the query editor cuts it, and `GO 5` runs a batch five times. The gates judge the script by its worst statement: one write anywhere needs `tools:write` and Safe Mode, and one `DROP` refuses the call. `DECLARE` and `EXEC` of a procedure count as writes, `sp_help` included. `EXEC` of a string and `sp_executesql` run server-side code and are refused. The top-level fields describe the first result set, with `rows_affected` counting the whole script. When the script returns more than one result set, `result_sets[]` lists every one in order, each with `columns`, `rows`, `row_count` and `is_truncated`. `max_rows` caps each result set on its own. `status_message` says when the script left a transaction open. diff --git a/developers/tokens.mdx b/developers/tokens.mdx index f8c83283..ebb356f7 100644 --- a/developers/tokens.mdx +++ b/developers/tokens.mdx @@ -129,4 +129,4 @@ Request limits are keyed by token id, and `subscriptions/listen` is exempt. Fail | Create, change, or read other tokens | no | | Create or edit connection records | no | -Statements that reach the filesystem or run server-side code are refused before they reach the driver, on every tool. The reachable surface is the [tool catalog](/developers/mcp-tools), the [resources](/developers/mcp-resources), and the [prompts](/developers/mcp-prompts). Nothing outside those lists is exposed. +Statements that reach the filesystem or run server-side code are refused before they reach the driver, on every tool, along with the rest of [what no external caller may send](/developers#what-read-only-allows). The reachable surface is the [tool catalog](/developers/mcp-tools), the [resources](/developers/mcp-resources), and the [prompts](/developers/mcp-prompts). Nothing outside those lists is exposed. diff --git a/features/ai-assistant.mdx b/features/ai-assistant.mdx index ab389c7b..db576364 100644 --- a/features/ai-assistant.mdx +++ b/features/ai-assistant.mdx @@ -100,6 +100,8 @@ In Edit and Agent modes each tool call appears as a card in the reply. Read-only Safe Mode **Silent** auto-approves write tools and **Read-Only** auto-denies them. Destructive operations are the exception: `confirm_destructive_operation` always needs a click, Silent does not auto-approve it, **Always for this connection** is refused for it, and the model must pass the verbatim phrase `I understand this is irreversible`. +`execute_query` refuses what [external clients cannot send at any level](/developers#what-read-only-allows), several statements in one call among them. Text stored in the database can steer the model, so on a connection where an unwanted write matters, connect with a login that can only read: the database holds that limit whatever the model sends. + Every provider can call tools. Claude Agent reaches them through the MCP server, so that has to be on, and local models depend on the model. A reply pauses after 25 tool calls, keeps everything it has done, and offers **Continue** for a fresh budget or **Adjust Limit** to change the number, 5 to 200, under **Agent** in **Settings > AI**. Every call is another request with the schema attached, so a higher limit costs tokens. diff --git a/features/mcp.mdx b/features/mcp.mdx index 79f5f7e4..151f8b14 100644 --- a/features/mcp.mdx +++ b/features/mcp.mdx @@ -97,7 +97,7 @@ Every request needs a token, including one from your own Mac. The bundled bridge - AI access is set per connection, including blocking a connection from external clients entirely. A blocked connection is not even listed. - Tokens carry scopes and can be limited to named connections. An unauthenticated local caller is read-only and can never be more. - Writes go through the connection's [Safe Mode](/features/safe-mode#external-clients). `DROP` and `TRUNCATE` need a full-access token and the user's approval every time. -- Statements that read or write files, or run server-side code, are refused before they reach the driver. +- Statements that read or write files, or run server-side code, are refused before they reach the driver. [What Read Only allows](/developers#what-read-only-allows) lists the rest. - The reachable surface is the [tool catalog](/developers/mcp-tools), the [resources](/developers/mcp-resources) and the [prompts](/developers/mcp-prompts). Nothing else. - Reaching the server from another machine means forwarding the port over SSH yourself, and owning what that exposes. diff --git a/features/safe-mode.mdx b/features/safe-mode.mdx index 1c90ed41..0ef94e69 100644 --- a/features/safe-mode.mdx +++ b/features/safe-mode.mdx @@ -52,6 +52,8 @@ It does not sit in front of reading metadata. Loading the sidebar, opening a tab A statement runs as a read only when TablePro can tell for certain that it reads. Anything it cannot tell counts as a write: confirmed at the Alert and Safe Mode levels, blocked at Read-Only. +`EXPLAIN` and `DESCRIBE` are reads until an option runs the statement. With `ANALYZE` they count as the statement they explain, so `EXPLAIN ANALYZE` of a `SELECT` is a read and of a `DELETE` is a write. An option TablePro does not recognize, or one in quotes, counts as running the statement, and an `EXPLAIN` whose statement cannot be found counts as a write. `ANALYZE` of a table stores statistics and is a write. + On MongoDB, a read is one call on a collection or database with literal arguments, such as `db.orders.find({status: "open"}).sort({at: -1}).limit(50)`, an `aggregate` pipeline without `$out` or `$merge`, `countDocuments`, `distinct`, `explain`, `show collections` and `use`. Any other JavaScript counts as a write: a variable, a function, `forEach`, `runCommand`. Run several reads at once and each one is checked. Shell state lasts as long as the connection, so a function you redefine in one statement runs redefined in every later one. A read still cannot write through it: the MongoDB driver refuses any write while it runs a statement as a read, once the plugin in **Settings > Extensions** is updated for this release. Redis reads come from a fixed list of read commands. etcd reads are `get`, `watch`, and the commands that only list or report, such as `member list` and `endpoint health`. SAP HANA is read like any other SQL database. @@ -120,7 +122,7 @@ This is a floor on TablePro's own behavior, not on the database. It stops the ap A write from an external client (Raycast, Cursor, Claude Desktop, any MCP client) clears three locks in order: -1. **External Clients** (per connection: **Blocked** / **Read Only** / **Read & Write**), next to **Safe Mode** under **Access** on the connection form's **Options** tab. A Read Only connection rejects a write before the request reaches the database. +1. **External Clients** (per connection: **Blocked** / **Read Only** / **Read & Write**), next to **Safe Mode** under **Access** on the connection form's **Options** tab. A Read Only connection rejects a write before the request reaches the database. The database login still decides what a statement that passes may do; see [What Read Only allows](/developers#what-read-only-allows). 2. **Token scope** (per integration: `readOnly` / `readWrite` / `fullAccess`), issued by the [pairing flow](/developers/pairing). The effective permission is `MIN(token.scope, connection.externalAccess)`. 3. **Safe Mode**, per query. Everything on this page then applies, Touch ID prompts and confirmation dialogs included. From 2396e349f5eca348d259e23d4b39d7d62d7ab68c Mon Sep 17 00:00:00 2001 From: Ngo Quoc Dat Date: Sun, 1 Nov 2026 15:02:33 +0700 Subject: [PATCH 2/2] docs(features): say a tokenless MCP request gets 401 --- features/mcp.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/mcp.mdx b/features/mcp.mdx index 151f8b14..09dfe5d3 100644 --- a/features/mcp.mdx +++ b/features/mcp.mdx @@ -95,7 +95,7 @@ Every request needs a token, including one from your own Mac. The bundled bridge - Clients work through your saved connections. Passwords never leave the Keychain and are not readable over MCP. - AI access is set per connection, including blocking a connection from external clients entirely. A blocked connection is not even listed. -- Tokens carry scopes and can be limited to named connections. An unauthenticated local caller is read-only and can never be more. +- Tokens carry scopes and can be limited to named connections. A request without one gets `401`, from your own Mac too. See [No token, no access](/developers/tokens#no-token-no-access). - Writes go through the connection's [Safe Mode](/features/safe-mode#external-clients). `DROP` and `TRUNCATE` need a full-access token and the user's approval every time. - Statements that read or write files, or run server-side code, are refused before they reach the driver. [What Read Only allows](/developers#what-read-only-allows) lists the rest. - The reachable surface is the [tool catalog](/developers/mcp-tools), the [resources](/developers/mcp-resources) and the [prompts](/developers/mcp-prompts). Nothing else.