Skip to content

feat(mcp): list each token's own tools, with hints that state their effects (#2252, part 3) - #2257

Merged
cevheri merged 8 commits into
mainfrom
feat/mcp-per-token-tool-list
Nov 5, 2026
Merged

cevheri merged 8 commits into
mainfrom
feat/mcp-per-token-tool-list

Conversation

@cevheri

@cevheri cevheri commented Nov 5, 2026

Copy link
Copy Markdown
Member

Every token got the same tools/list, every statement tool of the build, and every tool said readOnlyHint: true, though an inspect_schema call can resume a billed Databend Cloud warehouse or hold a LibreDB file's lock for 30 minutes.

A tools/list now goes to a list-only handler registering the metadata tools and only the statement tools serving engines the token can read, by list_connections' availability rule, building no provider and reading no Vault reference. Every other method keeps the full handler, so a call to an unlisted tool still gets Studio's audited refusal. An unreadable or invalid seed file answers tools/list with -32603. A new record makes inspect_schema say readOnlyHint: false and destructiveHint: false to tokens reaching Databend or LibreDB, and run_redis_read's description states a read's server-side effects.

Through the route's POST in process, with an admin token and a seed of every served engine, tools/list took a median of 1.4 ms with the seed cache emptied first and 1.1 ms warm, over 200 calls each; main's took 0.97 ms.

New tests cover each listing case, in both protocol eras where both apply; MCP24, MCP25 and DOC37 are filed. The local gate passed with 100% line coverage.

Part of #2252.

…ey keep

Every MCP tool says readOnlyHint: true, but an inspect_schema call can resume a billed Databend Cloud warehouse, or open a LibreDB file and hold its exclusive lock for 30 minutes.

MCP_METADATA_READ_CHANGES_STATE records those engines per engine, beside the other MCP records and in the registries. It is not derived from resumesBilledCompute, which Snowflake declares though it browses with no warehouse and Databend declares per connection, while the list is decided before any provider exists.

metadataReadAnnotations answers readOnlyHint: false and destructiveHint: false for a reach holding such an engine, and the read-only hints otherwise. McpReach names a token's reach as the listing's own connection type, as the availability helper does.
…hints from it

listedStatementArms keeps the serving arms that serve a reached engine, a registry read. The registrations take an optional reach, and without one register exactly today's set and hints, which the full handler keeps.

listedMcpToolNames gives the tests one token's names from the same records.
… file with -32603

Every token got every statement tool of the build, whatever engines its connections use. A tools/list now goes to a list-only handler whose factory reads the seed file through its cache, keeps the engines the caller can read by the rule list_connections uses, and registers the metadata tools and the statement tools that serve them; it builds no provider and reads no Vault reference.

Every other method keeps the full handler, so a call to a tool the list leaves out still meets Studio's checks and audit, never the SDK's unknown-tool answer.

A seed file that cannot be read or parsed answers tools/list with -32603 and the sentence every tool answers, never a shorter or a fuller list. Discovery never throws and opts nothing in, so a failing export changes no answer.

The harness takes the production dispatcher, and the tests that pinned the build's list now pin the token's.
…e server

A Redis read deletes a key whose time to live has passed, and where notify-keyspace-events asks it publishes a keymiss event for a missing key. The tool keeps readOnlyHint: true, so its description now states both effects, as docs/MCP.md does.
The guide said every tool is annotated read-only and every token lists every tool. It now says what a token lists, when a seed edit reaches the list, what an unreadable seed file answers, which engines make inspect_schema change state, and that a Redis keyspace walk can delete an expired key and publish an event, its hint unchanged.

The Databend and LibreDB docs state the hint, and tests hold the engines the docs name to the record.
Three defects found while building the per-token list and outside it: an in-memory SQLite or DuckDB seed lists a read tool every open refuses, a not-served refusal names run_read_query for a SQLite seed no open serves, and ADDING_A_PROVIDER.md says only DuckDB reads the file-access posture.
…ls do

A seed edit reaches tools/list once the seed cache's time to live has passed, the bookkeeping sentence covers the statement tools and points inspect_schema at its own entry, and docs/ARCHITECTURE.md names both module-scope handlers.
…sed parameter

The mocks in the test of an arm that serves no engine named a parameter only to type their calls, which ESLint reports as unused; they now pass the signature to mock as other tests do.
@codecov

codecov Bot commented Nov 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@sonarqubecloud

sonarqubecloud Bot commented Nov 5, 2026

Copy link
Copy Markdown

@cevheri
cevheri merged commit 9806714 into main Nov 5, 2026
39 checks passed
@cevheri
cevheri deleted the feat/mcp-per-token-tool-list branch November 5, 2026 19:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant