feat(tools): declare MCP annotations for every tool - #164
Conversation
None of the 16 tools carried annotations, so a client had no way to tell
delete_note apart from read_note. MCP proxies derive the call variant from
these hints: with them missing, mcpproxy-go recommended call_tool_read for
delete_note, write_note and move_note alike.
Declares readOnlyHint / destructiveHint per tool and openWorldHint: false
across the board, since every operation is local to the vault:
read-only read_note, list_directory, search_notes, read_multiple_notes,
get_notes_info, get_frontmatter, get_vault_stats,
list_all_tags, wiki_link
mutating write_note, patch_note, update_frontmatter, manage_tags
destructive delete_note, move_note, move_file (the move tools can
overwrite an existing file when overwrite: true)
bitbonsai
left a comment
There was a problem hiding this comment.
Hey @badigit, tool annotations are a useful addition, and I agree that every tool should be explicitly closed-world.
The destructive classifications need one safety correction. MCP defines destructiveHint: false as an operation that performs only additive updates. write_note can overwrite a note, patch_note can remove or replace text, update_frontmatter can replace metadata, and manage_tags can remove tags, so those four cannot be marked non-destructive. Please mark them destructive or omit the hint and accept the spec's default of true.
Please also test through the public connected client listTools() path rather than private SDK _requestHandlers, and commit the dist output produced by the build. The suite and build otherwise pass locally.
|
One clarification on my review: testing through |
None of the 16 tools currently declare tool annotations, so a client has no way to tell
delete_noteapart fromread_note.Why it matters in practice
MCP proxies and clients route calls by these hints. I run mcpvault behind mcpproxy-go, which exposes three call variants (
call_tool_read/call_tool_write/call_tool_destructive) and picks one per tool from its annotations. With none present it falls back to its safe default and recommendscall_tool_readfordelete_note,move_noteandwrite_notealike — the destructive tools look exactly as harmless as the read-only ones to the agent.The same hints drive discovery filters (
readOnlyOnly,excludeDestructive), so unannotated tools also get filtered inconsistently: a strict client has to assume the worst for all 16, a permissive one assumes the best.What this adds
Per-tool
readOnlyHint/destructiveHint, plusopenWorldHint: falseon every tool since all of them operate on the local vault and never reach the network:read_note,list_directory,search_notes,read_multiple_notes,get_notes_info,get_frontmatter,get_vault_stats,list_all_tags,wiki_linkwrite_note,patch_note,update_frontmatter,manage_tagsdelete_note,move_note,move_fileupdate_frontmatteris additionally markedidempotentHint: true.Two judgement calls worth flagging, happy to change either:
move_note/move_fileas destructive — they can overwrite an existing file whenoverwrite: true, and a rename is not reversible from the client side. If you would rather treat them as plain writes, it is a one-line change.patch_noteas non-destructive — it replaces a string the caller supplied and fails on ambiguous matches, so it felt closer to an edit than a destructive op.Implementation
Annotations live in a small
TOOL_ANNOTATIONSmap applied viawithAnnotations()when the tool list is returned, rather than being inlined into all 16 literals — keeps the diff small and the defaults in one place.Two tests in
src/createServer.test.ts: every tool carries annotations and is closed-world, and the destructive/read-only ones are marked correctly.npm run buildis clean.Related
Sent alongside a separate PR fixing CRLF handling in frontmatter — independent of this one.