Skip to content

Declare MCP tool annotations, including readOnlyHint - #63

Open
spawnia wants to merge 1 commit into
MariaDB:mainfrom
spawnia:annotate-tools-with-read-only-hint
Open

Declare MCP tool annotations, including readOnlyHint#63
spawnia wants to merge 1 commit into
MariaDB:mainfrom
spawnia:annotate-tools-with-read-only-hint

Conversation

@spawnia

@spawnia spawnia commented Aug 27, 2026

Copy link
Copy Markdown

Declares ToolAnnotations for every tool registered in register_tools, most importantly readOnlyHint.

Why

A client receiving tools/list cannot tell a reading tool from a writing one. Names do not carry that information, and execute_sql in particular looks write-capable regardless of the server's mode. Some clients gate on this: Claude Code's plan mode, for example, prompts for confirmation on every call to a tool it classifies as write-capable, so schema questions during planning cost one prompt per query even for a server started with MCP_READ_ONLY=true.

ToolAnnotations is the spec's answer (readOnlyHint, destructiveHint; MCP revision 2025-03-26), and FastMCP passes it through the @mcp.tool decorator. This server just was not declaring it.

What

Tool Annotations
list_databases readOnlyHint=True
list_tables readOnlyHint=True
get_table_schema readOnlyHint=True
get_table_schema_with_relations readOnlyHint=True
list_vector_stores readOnlyHint=True
search_vector_store readOnlyHint=True
execute_sql readOnlyHint=self.is_read_only
create_database readOnlyHint=False, destructiveHint=False
create_vector_store readOnlyHint=False, destructiveHint=False
insert_docs_vector_store readOnlyHint=False, destructiveHint=False
delete_vector_store readOnlyHint=False, destructiveHint=True

destructiveHint is only meaningful when readOnlyHint is false, so it is left unset on the read-only rows.

execute_sql is the substantive one. self.is_read_only is in scope at registration and is the same flag that enforces the read-only statement allowlist in execute_sql, so the annotation reports behaviour this server already enforces rather than promising good behaviour. That is what makes the hint worth acting on: the spec warns clients not to trust annotations from untrusted servers, and a hint backed by enforcement is the strongest thing a server can offer.

Tests

Four cases added next to the existing ones in src/tests/test_mcp_server.py, asserting the annotations on the tools/list response. Both execute_sql shapes are covered — read-only mode giving readOnlyHint=True and non-read-only mode giving False — since that conditional is the point of the change.

Verified against a local MariaDB 11.4:

  • the four new tests pass
  • the pre-existing failures are unchanged: 6 errors before and after this branch (vector store tests, which need an EMBEDDING_PROVIDER, plus one SHOW-related case)
  • tools/list over stdio with MCP_READ_ONLY=true reports execute_sql with {"readOnlyHint": true}, so the annotation reaches the client and is not merely present in the source

Clients cannot tell a reading tool from a writing one by name. The MCP
spec covers this with ToolAnnotations (revision 2025-03-26), which
FastMCP passes through, so declare them for every registered tool.

execute_sql reports readOnlyHint=self.is_read_only — the same flag that
enforces the read-only statement allowlist — so the hint states enforced
behaviour rather than promising it.

destructiveHint is only meaningful when readOnlyHint is false, so it is
set only on the writing tools.
@spawnia
spawnia marked this pull request as ready for review August 27, 2026 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant