Repository navigation
Add MCP tool annotations so read-only tools aren't shown as destructive - #34
Merged
Merged
Conversation
Clients (ChatGPT etc.) were showing read-only tools like get_candid as "PUBLIC WRITE / DESTRUCTIVE" because we set no MCP tool annotations, and the hints default to the unsafe interpretation when omitted (readOnlyHint false, destructiveHint true, openWorldHint true). Add annotations() to all 19 #[tool]s, classified against what each tool actually does: - Read-only (read_only_hint=true): get_candid, discover_canisters, find_canister, lookup_canister, list_ic_skills, get_ic_skill, list_accounts, cycles_balance, get_principal, canister_status. - Additive/reversible writes (read_only=false, destructive=false): create_canister, top_up_canister, start_canister, stop_canister. - Destructive/irreversible writes (destructive=true): install_code (reinstall wipes), update_canister_settings (controller replacement can lock out), uninstall_code, delete_canister. - call_canister is dual-mode (is_query) and calls arbitrary methods, so it is conservatively read_only=false, destructive=true. Notes: - canister_status reads (changes nothing) so read_only=true, but on the IC it is an update call gated by require_write; its description now says it needs a full (non-read-only) session. - open_world_hint=true for tools that reach the live IC / arbitrary canisters / web registries; false for the two skills tools (one curated registry). Add a test asserting every tool carries annotations and spot-checking the read/write classification via ToolRouter::list_all(). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
There was a problem hiding this comment.
Pull request overview
Adds MCP tool annotations(...) metadata to the IC MCP server’s tool definitions so clients can correctly label tools as read-only vs. write/destructive, rather than defaulting to unsafe interpretations when hints are omitted.
Changes:
- Annotates all 19
#[tool]endpoints with MCP tool annotation hints (read-only / destructive / idempotent / open-world, plus titles). - Updates several tool descriptions to clarify behavior/requirements (notably around read-only vs. update-call semantics).
- Adds a unit test asserting all tools have annotations and spot-checking the intended read/write/destructive classification.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Per MCP spec, destructiveHint is ignored when readOnlyHint=true, so omitting it is spec-correct. But a naive client that reads destructiveHint without first checking readOnlyHint would fall back to the true default and still mislabel a read-only tool as destructive — the exact cross-client robustness this change is about. Set destructive_hint=false explicitly on all ten read-only tools as belt-and-suspenders, and assert it in the test. Addresses the Copilot review on #34. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Clients like ChatGPT were labeling read-only tools (e.g.
get_candid, a pure metadata read) asPUBLIC WRITE / DESTRUCTIVE / OPEN WORLD. That's because we set no MCP tool annotations, and the hints default to the unsafe interpretation when omitted:readOnlyHintdestructiveHintopenWorldHintSo every tool rendered as write/destructive. This PR adds
annotations(...)to all 19#[tool]s (rmcp 1.7.0's macro attribute — no logic changes), classified against what each tool actually does (query vs update call, which method, HTTP read, etc.).Classification
Read-only (
read_only_hint = true):get_candid,discover_canisters,find_canister,lookup_canister,list_ic_skills,get_ic_skill,list_accounts,cycles_balance,get_principal,canister_status.Additive / reversible writes (
read_only = false,destructive = false):create_canister,top_up_canister,start_canister,stop_canister(last twoidempotent = true).Destructive / irreversible writes (
destructive = true):install_code(reinstall wipes state),update_canister_settings(controller replacement can lock you out),uninstall_code,delete_canister.call_canisteris dual-mode (is_query) and calls arbitrary methods on arbitrary canisters, so it's conservativelyread_only = false, destructive = true— correctly not read-only.open_world_hintistruefor tools that reach the live IC / arbitrary canisters / web registries;falsefor the two skills tools (one curated registry).Notes / judgment calls
canister_statusreads status (changes nothing →read_only = true, so it's recognized as safe), but on the IC it's anupdatecall gated byrequire_write. Its description now states it needs a full (non-read-only) II session, so a read-only session's rejection isn't surprising.get_principal/cycles_balanceareread_only = true; on a cache miss they triggermcp_prepare_delegation(an update to II) under the hood, but that only mints an ephemeral, self-scoped credential — not user/canister state — so read-only is correct. (Unlikecanister_status, these are notrequire_write-gated.)outputSchema(the separate "recommended" badge). Our tools return free text, so an output schema isn't applicable without converting tools to structured output; deferred.Classification was produced by reading the code and adversarially verified (the only correction that pass made:
update_canister_settings→destructive = true, since controller replacement is an irreversible lock-out).Testing
cargo buildclean (pre-existing macro warning only);cargo test— 44/44.every_tool_has_correct_read_write_annotationsasserts (viaToolRouter::list_all()) that all 19 tools carry annotations and spot-checks the read/write/destructive classification, so the values are verified to serialize into the tool list — not just compile.main(after Comply with peer design-review outcomes (H3 Consent-Bound Completion, H2 read-only, H1 derivation, M2 drop device grant) #32/Bump ic-agent/ic-transport-types to 0.48.0 and forward read-only delegation permissions #33/fix(monitoring): drop the perpetually-warning OAuth live-discovery check #23 merged); the diff is only this change.🤖 Generated with Claude Code
https://claude.ai/code/session_014T9N8USDfNK5yzznPGg7Ym
Generated by Claude Code