Add tool annotations (readOnlyHint, destructiveHint, etc.) #14

Closed
opened 2026-05-17 15:56:09 +00:00 by claude-noether · 1 comment
Collaborator

Add tool annotations — declare each tool's destructiveness/idempotence/read-only nature so the client can gate the dangerous ones behind extra confirmation.

Goal

shell, shell_bg, write_file, edit_file, remote_* are obviously destructive; read_file, list_dir, search_files, fetch, web_search are obviously not. The client cannot tell. Clients that respect annotations (Claude Desktop, Claude Code) display a banner / require explicit per-call confirmation for destructive tools — but only if the tool declares itself as such. Today none of lmcp's tools do.

This is the smallest possible spec-conformance win — one keyword-table per tool registration.

Annotation keys (per spec)

Key Meaning
title Display name, distinct from name (which is the call-id)
readOnlyHint true → does not modify environment
destructiveHint true → may make irreversible changes
idempotentHint true → same args produce same effect
openWorldHint true → may interact with external systems (network/disk/processes)

Hints are advisory — clients decide whether to enforce. The spec says they SHOULD be set "based on the best understanding of the tool's behaviour."

API for lmcp

Extend the server:tool signature:

server:tool("write_file", "Write content to a file.", {
    type = "object",
    properties = { path = {...}, content = {...} },
    required = { "path", "content" },
}, function(a)  end, {
    annotations = {
        title           = "Write File",
        readOnlyHint    = false,
        destructiveHint = true,
        idempotentHint  = true,
        openWorldHint   = true,
    },
})

5th positional arg (optional opts table) keeps backwards compat.

Suggested annotations for current lmcp tools

Tool readOnly destructive idempotent openWorld
shell false true false true
shell_bg false true false true
read_file true false true false
write_file false true true true
edit_file false true false true
list_dir true false true false
search_files true false true false
systeminfo true false true false
fetch true false false true
web_search true false false true

All hub remote_* tools mirror the corresponding non-remote tool, except openWorldHint is always true (network).

Scope (v1)

  • Extend server:tool to accept a 5th opts arg with annotations.
  • Emit annotations on tools/list responses (spec field name: annotations).
  • Backfill annotations for every existing tool in server.lua and hub.lua.

Out of scope

  • Server-side enforcement (e.g. refuse shell unless an env flag is set). Annotations are advisory.

Priority

High for value-per-effort. Half a day total. Immediate safety win across every client that respects annotations. Should ship before further tool additions so new tools default to declaring their stance.

Add **tool annotations** — declare each tool's destructiveness/idempotence/read-only nature so the client can gate the dangerous ones behind extra confirmation. ## Goal `shell`, `shell_bg`, `write_file`, `edit_file`, `remote_*` are obviously destructive; `read_file`, `list_dir`, `search_files`, `fetch`, `web_search` are obviously not. The client cannot tell. Clients that respect annotations (Claude Desktop, Claude Code) display a banner / require explicit per-call confirmation for destructive tools — but only if the tool declares itself as such. Today none of lmcp's tools do. This is the **smallest possible spec-conformance win** — one keyword-table per tool registration. ## Annotation keys (per spec) | Key | Meaning | |---|---| | `title` | Display name, distinct from `name` (which is the call-id) | | `readOnlyHint` | true → does not modify environment | | `destructiveHint` | true → may make irreversible changes | | `idempotentHint` | true → same args produce same effect | | `openWorldHint` | true → may interact with external systems (network/disk/processes) | Hints are advisory — clients decide whether to enforce. The spec says they SHOULD be set "based on the best understanding of the tool's behaviour." ## API for lmcp Extend the `server:tool` signature: ```lua server:tool("write_file", "Write content to a file.", { type = "object", properties = { path = {...}, content = {...} }, required = { "path", "content" }, }, function(a) … end, { annotations = { title = "Write File", readOnlyHint = false, destructiveHint = true, idempotentHint = true, openWorldHint = true, }, }) ``` 5th positional arg (optional `opts` table) keeps backwards compat. ## Suggested annotations for current lmcp tools | Tool | readOnly | destructive | idempotent | openWorld | |---|---|---|---|---| | `shell` | false | true | false | true | | `shell_bg` | false | true | false | true | | `read_file` | true | false | true | false | | `write_file` | false | true | true | true | | `edit_file` | false | true | false | true | | `list_dir` | true | false | true | false | | `search_files` | true | false | true | false | | `systeminfo` | true | false | true | false | | `fetch` | true | false | false | true | | `web_search` | true | false | false | true | All `hub` `remote_*` tools mirror the corresponding non-remote tool, except `openWorldHint` is always true (network). ## Scope (v1) - Extend `server:tool` to accept a 5th `opts` arg with `annotations`. - Emit annotations on `tools/list` responses (spec field name: `annotations`). - Backfill annotations for every existing tool in `server.lua` and `hub.lua`. ## Out of scope - Server-side enforcement (e.g. refuse `shell` unless an env flag is set). Annotations are advisory. ## Priority **High** for value-per-effort. Half a day total. Immediate safety win across every client that respects annotations. Should ship before further tool additions so new tools default to declaring their stance.
Author
Collaborator

Implemented across lmcp.lua, server.lua, hub.lua, example_server.lua. server:tool() accepts an optional 5th opts arg with annotations = { title?, readOnlyHint?, destructiveHint?, idempotentHint?, openWorldHint? }. tools/list emits annotations only when registered (backwards compat: legacy 4-arg calls keep working).

Backfilled annotations across all 21 tool registrations: 10 in server.lua, 8 in hub.lua, 4 in example_server.lua. Phase 5 reviewer caught a spec-interpretation error in my first table: fetch/web_search were originally marked idempotent=false because remote responses vary, but spec defines idempotent as effect on the tool's own environment (server-side) — flipped to idempotent=true (the world-side variability is conveyed by openWorldHint=true).

Memory: project_mcp_idempotent_semantics.md captures the trap.

Left windows/pkg/ tree unsynced (April-2026 stale snapshot, pre-existing scope debt). Filed as follow-up #18.

Implemented across lmcp.lua, server.lua, hub.lua, example_server.lua. `server:tool()` accepts an optional 5th `opts` arg with `annotations = { title?, readOnlyHint?, destructiveHint?, idempotentHint?, openWorldHint? }`. `tools/list` emits `annotations` only when registered (backwards compat: legacy 4-arg calls keep working). Backfilled annotations across all 21 tool registrations: 10 in server.lua, 8 in hub.lua, 4 in example_server.lua. Phase 5 reviewer caught a spec-interpretation error in my first table: `fetch`/`web_search` were originally marked `idempotent=false` because remote responses vary, but spec defines idempotent as effect on the tool's own environment (server-side) — flipped to `idempotent=true` (the world-side variability is conveyed by `openWorldHint=true`). Memory: project_mcp_idempotent_semantics.md captures the trap. Left `windows/pkg/` tree unsynced (April-2026 stale snapshot, pre-existing scope debt). Filed as follow-up #18.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marfrit/lmcp#14