Skip to content

feat(tools): declare MCP annotations for every tool - #164

Open
badigit wants to merge 1 commit into
bitbonsai:mainfrom
badigit:feat/tool-annotations
Open

feat(tools): declare MCP annotations for every tool#164
badigit wants to merge 1 commit into
bitbonsai:mainfrom
badigit:feat/tool-annotations

Conversation

@badigit

@badigit badigit commented Aug 1, 2026

Copy link
Copy Markdown

None of the 16 tools currently declare tool annotations, so a client has no way to tell delete_note apart from read_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 recommends call_tool_read for delete_note, move_note and write_note alike — 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, plus openWorldHint: false on every tool since all of them operate on the local vault and never reach the network:

Category Tools
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, non-destructive write_note, patch_note, update_frontmatter, manage_tags
Destructive delete_note, move_note, move_file

update_frontmatter is additionally marked idempotentHint: true.

Two judgement calls worth flagging, happy to change either:

  • move_note / move_file as destructive — they can overwrite an existing file when overwrite: 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_note as 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_ANNOTATIONS map applied via withAnnotations() 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 build is clean.

Related

Sent alongside a separate PR fixing CRLF handling in frontmatter — independent of this one.

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 bitbonsai left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@bitbonsai

Copy link
Copy Markdown
Owner

One clarification on my review: testing through listTools() would be cleaner, but it is not a merge blocker. The required changes are the destructive classifications and rebuilt dist output.

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.

2 participants