Skip to content

feat(mcp): advertise tool annotations on tools/list#2268

Merged
senamakel merged 5 commits into
tinyhumansai:mainfrom
justinhsu1477:feat/mcp-tool-annotations
May 20, 2026
Merged

feat(mcp): advertise tool annotations on tools/list#2268
senamakel merged 5 commits into
tinyhumansai:mainfrom
justinhsu1477:feat/mcp-tool-annotations

Conversation

@justinhsu1477
Copy link
Copy Markdown
Contributor

@justinhsu1477 justinhsu1477 commented May 20, 2026

Summary

Adds MCP ToolAnnotations (spec 2025-03-26+) to every tool advertised by openhuman-core mcp. Clients use readOnlyHint / destructiveHint / idempotentHint / openWorldHint to surface accurate safety affordances — e.g. Claude Desktop's "this tool can take destructive actions" confirmation gate.

Problem

tools/list today returns the curated tool surface but says nothing about each tool's behavior class. The MCP spec defines ToolAnnotations precisely for this — without them:

  • Claude Desktop / Cursor can't render confirmation gates for destructive actions.
  • Humans inspecting the tool list (e.g. via MCP Inspector) can't see at a glance which tools are safe to retry.
  • agent.run_subagent — the one Act-policy surface on the MCP server today — looks identical to memory.search to clients, even though it can call further tools (including potentially destructive ones) through any sub-agent it spawns.

Solution

  • Added annotations: Value to McpToolSpec (always present, serialized into each tool's annotations field in the tools/list response).
  • All read-only tools share a read_only_local_annotations() helper (readOnlyHint: true, openWorldHint: false).
  • agent.run_subagent gets explicit annotations: readOnlyHint: false / destructiveHint: true / idempotentHint: false / openWorldHint: true. Sub-agent execution can call further tools, isn't a no-op on repeat, and reaches into the broader OpenHuman environment.
  • Updated mcp_server/mod.rs docstring to no longer claim the surface is purely read-only — it's curated, with agent.run_subagent as the one Act-policy surface.
  • Per spec, destructiveHint / idempotentHint are only meaningful when readOnlyHint == false, so the read-only helper omits them.

Submission Checklist

  • Tests added — list_tools_emits_annotations_for_every_tool asserts every entry serializes a non-null annotations object; a second test pins the read-only / destructive split to each tool's actual behavior. cargo test --lib mcp_server:: runs 38/38 locally.
  • Diff coverage ≥ 80% — new code is mostly literal annotations + a 6-line helper; both the "every tool has annotations" and "read-only vs destructive" axes are covered.
  • Coverage matrix updated — N/A: extends row 11.1.4 (MCP stdio server) rather than adding a new feature.
  • All affected feature IDs listed under ## Related
  • No new external network dependencies introduced
  • Manual smoke checklist — N/A: extends existing MCP surface, not a release-cut path.
  • Linked issue closed via Closes #NNNN/A: capability extension, no issue tracking this.

Impact

  • Runtime: openhuman-core mcp only. No HTTP RPC, web, or mobile impact.
  • Compatibility: Backward compatible — older MCP clients that don't read annotations ignore the field; newer clients pick up the safety affordances.
  • Performance: Negligible — one extra static Value per tool, serialized on each tools/list call (low frequency).
  • Security: No policy change. agent.run_subagent still goes through enforce_act_policy; the annotation is an advertised hint, not an enforcement point.

Related

Summary by CodeRabbit

  • New Features

    • Tools now include detailed security annotations (readOnly, destructive, idempotent, openWorld hints) so MCP clients can better understand safety and behavior.
    • Most tools are advertised as read-only; the subagent tool is explicitly advertised as act-capable/destructive, and one search tool is advertised as read-only but open-world.
  • Tests

    • Added tests ensuring each tool includes annotations and that read-only vs. destructive/open-world hints are correctly serialized.

Review Change Stack

Set the MCP `ToolAnnotations` block (2025-03-26+ spec) on every tool the
stdio MCP server exposes. Clients use these hints to decide which
confirmation gates to draw — without them, Claude Desktop / Cursor /
Inspector treat all 10 tools as the spec defaults (`readOnlyHint: false`,
`openWorldHint: true`), so even pure memory-tree reads can trigger
destructive-action warnings.

Annotation shape:

- The 9 read-only tools (`core.list_tools`, `core.tool_instructions`,
  `agent.list_subagents`, `memory.search`, `memory.recall`,
  `tree.read_chunk`, `tree.browse`, `tree.top_entities`,
  `tree.list_sources`) advertise `readOnlyHint: true` and
  `openWorldHint: false` (local memory tree / agent registry only).
  Per spec, `destructiveHint` / `idempotentHint` are meaningful only when
  `readOnlyHint == false`, so they are deliberately omitted.
- `agent.run_subagent` is the one Act-policy surface on this server.
  Sub-agents can call further tools (composio, web search, …), so it
  advertises `readOnlyHint: false`, `destructiveHint: true`,
  `idempotentHint: false`, `openWorldHint: true`.

Also drop the stale "read-only" claim from the `mcp_server` module doc —
`agent.run_subagent` made that comment inaccurate when it landed.

Tests:
- `list_tools_emits_annotations_for_every_tool` — every entry in the
  `tools/list` payload has a serialized `annotations` object.
- `read_only_tools_are_marked_read_only_and_closed_world` — the 9
  read-only tools advertise the right two hints AND do not leak the
  destructive/idempotent hints that the spec says are meaningful only on
  non-read-only tools.
- `run_subagent_annotations_signal_act_semantics` — `agent.run_subagent`
  advertises the full Act shape.

Local validation was blocked: no Rust toolchain on this machine
(`rust-toolchain.toml` pins 1.93.0). CI gate is the enforcement source.
@justinhsu1477 justinhsu1477 requested a review from a team May 20, 2026 01:58
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 20, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3127d1d7-a428-464c-a5c4-c639608de4c4

📥 Commits

Reviewing files that changed from the base of the PR and between df44210 and ffb11f1.

📒 Files selected for processing (1)
  • src/openhuman/mcp_server/tools.rs
💤 Files with no reviewable changes (1)
  • src/openhuman/mcp_server/tools.rs

📝 Walkthrough

Walkthrough

Adds an annotations field to MCP tool specs, populates MCP ToolAnnotations for every registered tool (shared read-only preset for most tools; act/destructive/open-world for agent.run_subagent), exposes annotations in list_tools responses, and adds tests validating emitted hints.

Changes

MCP Tool Annotations

Layer / File(s) Summary
Annotations contract and documentation
src/openhuman/mcp_server/mod.rs, src/openhuman/mcp_server/tools.rs
Module docs explain the MCP tool surface semantics. McpToolSpec adds public annotations: Value with inline MCP ToolAnnotations docs and rules for omitting destructive/idempotent hints on read-only tools.
Tool annotation population
src/openhuman/mcp_server/tools.rs
tool_specs() and base_tool_specs() populate annotations for all tools: core/agent-list and most memory/tree tools use a shared read-only preset (readOnlyHint=true, openWorldHint=false); agent.run_subagent is annotated as act-capable (readOnlyHint=false, destructiveHint=true, idempotentHint=false, openWorldHint=true); searxng_search is read-only/open-world and omits destructive/idempotent hints.
Tool descriptor response exposure
src/openhuman/mcp_server/tools.rs
list_tools_result_from_specs() now includes each tool's serialized annotations in the returned tool descriptors.
Annotation validation tests
src/openhuman/mcp_server/tools.rs
Unit tests assert every tool in list_tools_result_for_config includes an annotations object; tests validate read-only tools' hints and agent.run_subagent's act/destructive/non-idempotent/open-world hints.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • senamakel

Poem

🐰 I nibbled through hints, so neat and bright,

Tools now wear labels in the MCP light,
Read-only whispers, quiet and calm,
One bold subagent raises its arm,
Hints hop along to guide each flight.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(mcp): advertise tool annotations on tools/list' clearly and concisely summarizes the main change: adding MCP tool annotations to the tools/list response, which aligns with the PR objectives of advertising annotations for every tool.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/openhuman/mcp_server/tools.rs (1)

976-989: ⚡ Quick win

Strengthen future-proof coverage of read-only annotation semantics.

Line [976]-Line [989] only validates tools present in read_only_names; a newly added read-only tool could bypass semantic checks if the list isn’t updated. Prefer asserting all tools are read-only by default except an explicit act-capable exception list.

Proposed test refactor
-        let read_only_names = [
-            "core.list_tools",
-            "core.tool_instructions",
-            "agent.list_subagents",
-            "memory.search",
-            "memory.recall",
-            "tree.read_chunk",
-            "tree.browse",
-            "tree.top_entities",
-            "tree.list_sources",
-        ];
+        let act_tool_names = ["agent.run_subagent"];
         for spec in tool_specs() {
-            if !read_only_names.contains(&spec.name) {
+            if act_tool_names.contains(&spec.name) {
                 continue;
             }
             let annotations = &spec.annotations;
             assert_eq!(
                 annotations.get("readOnlyHint").and_then(Value::as_bool),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_server/tools.rs` around lines 976 - 989, The current check
iterates read_only_names and skips any tool not listed, which can miss newly
added read-only tools; change the logic to treat tools as read-only by default
and only exempt an explicit act-capable list: replace the read_only_names array
with an act_capable_names (or similar) whitelist, iterate tool_specs() and for
each spec.name assert it's read-only unless spec.name is in act_capable_names,
using the same spec.name symbol and the existing assertion code paths to locate
where to validate semantics (e.g., in the loop that previously referenced
read_only_names and tool_specs()).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/openhuman/mcp_server/tools.rs`:
- Around line 976-989: The current check iterates read_only_names and skips any
tool not listed, which can miss newly added read-only tools; change the logic to
treat tools as read-only by default and only exempt an explicit act-capable
list: replace the read_only_names array with an act_capable_names (or similar)
whitelist, iterate tool_specs() and for each spec.name assert it's read-only
unless spec.name is in act_capable_names, using the same spec.name symbol and
the existing assertion code paths to locate where to validate semantics (e.g.,
in the loop that previously referenced read_only_names and tool_specs()).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 918c41d1-61bb-4ff7-84a6-05ca0fab7314

📥 Commits

Reviewing files that changed from the base of the PR and between 094d482 and 3225012.

📒 Files selected for processing (2)
  • src/openhuman/mcp_server/mod.rs
  • src/openhuman/mcp_server/tools.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 20, 2026
@justinhsu1477 justinhsu1477 force-pushed the feat/mcp-tool-annotations branch from 3225012 to df44210 Compare May 20, 2026 02:11
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/openhuman/mcp_server/tools.rs (1)

970-1017: ⚡ Quick win

Make the read-only annotation test exhaustive instead of allowlist-based.

This test currently skips any tool not in read_only_names, so a newly added read-only tool can accidentally miss validation without failing tests. Prefer asserting by default for all non-agent.run_subagent tools (or asserting full set coverage).

Suggested diff
     #[test]
     fn read_only_tools_are_marked_read_only_and_closed_world() {
-        // Every tool except `agent.run_subagent` reads local OpenHuman state
-        // (memory tree / agent registry). Per MCP spec defaults these would be
-        // `readOnlyHint: false` and `openWorldHint: true`, so we MUST set both
-        // explicitly to communicate accurate safety affordances to clients.
-        let read_only_names = [
-            "core.list_tools",
-            "core.tool_instructions",
-            "agent.list_subagents",
-            "memory.search",
-            "memory.recall",
-            "tree.read_chunk",
-            "tree.browse",
-            "tree.top_entities",
-            "tree.list_sources",
-        ];
         for spec in tool_specs() {
-            if !read_only_names.contains(&spec.name) {
+            if spec.name == "agent.run_subagent" {
                 continue;
             }
             let annotations = &spec.annotations;
             assert_eq!(
                 annotations.get("readOnlyHint").and_then(Value::as_bool),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_server/tools.rs` around lines 970 - 1017, Replace the
allowlist-based check in read_only_tools_are_marked_read_only_and_closed_world
with an exhaustive assertion over tool_specs(): iterate every spec and if
spec.name != "agent.run_subagent" assert
annotations.get("readOnlyHint").and_then(Value::as_bool) == Some(true) and
annotations.get("openWorldHint").and_then(Value::as_bool) == Some(false), and
assert destructiveHint/idempotentHint are None for those read-only tools; for
"agent.run_subagent" assert the inverse expectations (or explicitly skip it) so
newly added tools can't slip past validation—locate this logic in the
read_only_tools_are_marked_read_only_and_closed_world test and replace the
current read_only_names allowlist usage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/openhuman/mcp_server/tools.rs`:
- Around line 970-1017: Replace the allowlist-based check in
read_only_tools_are_marked_read_only_and_closed_world with an exhaustive
assertion over tool_specs(): iterate every spec and if spec.name !=
"agent.run_subagent" assert
annotations.get("readOnlyHint").and_then(Value::as_bool) == Some(true) and
annotations.get("openWorldHint").and_then(Value::as_bool) == Some(false), and
assert destructiveHint/idempotentHint are None for those read-only tools; for
"agent.run_subagent" assert the inverse expectations (or explicitly skip it) so
newly added tools can't slip past validation—locate this logic in the
read_only_tools_are_marked_read_only_and_closed_world test and replace the
current read_only_names allowlist usage.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 336fa482-f246-489a-bfb3-bc770c55c9bd

📥 Commits

Reviewing files that changed from the base of the PR and between 3225012 and df44210.

📒 Files selected for processing (2)
  • src/openhuman/mcp_server/mod.rs
  • src/openhuman/mcp_server/tools.rs
✅ Files skipped from review due to trivial changes (1)
  • src/openhuman/mcp_server/mod.rs

Copy link
Copy Markdown
Contributor

@graycyrus graycyrus left a comment

Choose a reason for hiding this comment

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

Nice work — clean implementation of MCP ToolAnnotations (spec 2025-03-26+).

What I checked:

  • McpToolSpec.annotations field addition + all tool specs updated ✓
  • read_only_local_annotations() correctly omits destructiveHint/idempotentHint per spec (only meaningful when readOnlyHint == false) ✓
  • agent.run_subagent annotations accurately reflect its Act-policy semantics (destructiveHint: true, openWorldHint: true) ✓
  • list_tools_result() serializes annotations into the wire format ✓
  • Three new tests cover: every tool has annotations, read-only split correctness, and run_subagent act semantics ✓
  • Docstring in mod.rs updated to no longer claim purely read-only surface ✓
  • Backward compatible — older clients ignore the field ✓

No issues found. LGTM.

@senamakel senamakel self-assigned this May 20, 2026
senamakel added 2 commits May 20, 2026 15:18
# Conflicts:
#	src/openhuman/mcp_server/tools.rs
Replace the allowlist with an act-capable exclusion list so any newly
added tool that forgets `readOnlyHint` fails the test instead of being
silently skipped. `searxng_search` is read-only but openWorld, so it's
called out separately on the openWorld axis only.

Addresses CodeRabbit nits on PR tinyhumansai#2268.
@senamakel senamakel merged commit 15bdac5 into tinyhumansai:main May 20, 2026
29 checks passed
@senamakel
Copy link
Copy Markdown
Member

huge thanks @justinhsu1477, this one's awesome 🙌 wiring up tool annotations so clients get proper safety hints (like that destructive-action gate) is such a nice quality-of-life win. always love seeing you back in the repo 💚

senamakel added a commit to justinhsu1477/openhuman that referenced this pull request May 22, 2026
- Add `annotations` to the new memory.store / memory.note / tree.tag
  McpToolSpec entries (writeable, idempotent upsert, closed-world). This
  unblocks the `Rust Quality (clippy)` CI failure introduced by the
  `annotations` field added in tinyhumansai#2268 — the new specs were forked from
  pre-tinyhumansai#2268 tinyhumansai#2306 scaffolding and were missing the required field on
  rebase against current main.

- Extend the read-only-by-default test exemption (`act_tool_names`) to
  cover the three new write tools, matching their `readOnlyHint: false`.

- Fix CodeRabbit nit (tools.rs:803): the oversize-tag error said
  "characters" but used `String::len()` (byte length). Reword to
  "bytes" so Unicode tag input gets a self-consistent message.

Defers @graycyrus's three inline threads on lines 686/705/1072
(slug_from collisions, memory.note upsert-vs-append, dispatch_write_tool
hardcoded rpc_method) to tinyhumansai#2306 per the author's review-thread reply —
that's where the underlying functions land and where the fix should
originate so this branch can simply inherit on rebase.
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.

3 participants