Skip to content

Say what each tool does to the machine before a client runs it - #27

Open
melbinjp wants to merge 4 commits into
mainfrom
tool-annotations
Open

Say what each tool does to the machine before a client runs it#27
melbinjp wants to merge 4 commits into
mainfrom
tool-annotations

Conversation

@melbinjp

@melbinjp melbinjp commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Rigout advertised fifteen tools identically. Reading a CPU count and running an arbitrary command as root arrived at a client looking the same — so anything wanting to warn before the second had nothing to go on but the name.

MCP has carried the answer since well before the version Rigout pins: Tool.title and ToolAnnotations with readOnlyHint, destructiveHint, idempotentHint and openWorldHint. Rigout declared none of them.

What this adds

All four hints and a human-readable title on every tool, as one table rather than fifteen scattered arguments — a missing argument in a definition is invisible, a missing row is not, and these judgements are arguable so they should sit somewhere they can be argued with.

tools
read-only get_hardware_info, get_server_activity, system_monitoring, list_terminal_sessions
destructive execute_command, execute_in_terminal, install_software, file_operations, docker_operations, bulk_file_transfer, environment_setup, manage_tunnels, close_terminal_session

The conservative reading is the honest one: anything that runs a caller's command is destructive and not idempotent, because what it does is decided by the caller and cannot be known here. close_terminal_session is destructive and idempotent, which is not a contradiction — it can kill running work, and closing an already-closed session changes nothing further.

Tests hold the contract, not the values

  • every advertised tool must be classified
  • the table must contain nothing that is not advertised, so a rename touching one side fails instead of silently leaving a tool unclassified
  • read-only and destructive can never both be true
  • nothing that runs a caller's command may claim idempotence

602 tests pass; ruff, format and mypy clean. Verified on the wire: execute_command serialises with readOnlyHint: false, destructiveHint: true.

One thing found on the way

This repository's development environment had mcp 1.15 while users install 1.29 — fourteen minor versions apart, including Tool.execution, which exists in what users run and not in what the tests ran against. The environment is now on 1.29 and the suite passes there.

Rigout advertised fifteen tools identically. Reading a CPU count and running an
arbitrary command as root arrived at a client looking the same, so anything
wanting to warn before the second had nothing to go on and had to guess from the
name.

MCP has carried the answer since well before the version Rigout pins: Tool.title
and ToolAnnotations, with readOnlyHint, destructiveHint, idempotentHint and
openWorldHint. Rigout declared none of them. This adds all four to every tool,
plus a human-readable title.

The classification is one table rather than fifteen scattered arguments,
because a missing argument in a definition is invisible while a missing row is
not, and because the judgements are arguable and should be somewhere they can be
argued with. The conservative reading is the honest one: anything that runs a
caller's command is destructive and not idempotent, since what it does is
decided by the caller and cannot be known here.

Four tools are read-only - hardware information, server activity, system
metrics, listing sessions. Eight are destructive. close_terminal_session is
destructive and idempotent at once, which is not a contradiction: it can kill
running work, and closing an already-closed session changes nothing further.

Tests hold the contract rather than the values. Every advertised tool must be
classified, the table must contain nothing that is not advertised - so a rename
touching one side fails rather than silently leaving a tool unclassified -
read-only and destructive can never both be true, and nothing that runs a
caller's command may claim idempotence.

Found while checking what the current mcp actually offers, which also turned up
that this repository's development environment had mcp 1.15 while users install
1.29. Fourteen minor versions of difference, including Tool.execution, which
exists in what users run and not in what the tests ran against. The environment
is now on 1.29 and the suite passes there.
@github-actions
github-actions Bot enabled auto-merge (squash) August 1, 2026 19:35
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown

Jules Review

COVERAGE: 17a0963 6 files

Summary

The PR adds tool annotations indicating whether each tool is read-only, destructive, idempotent, or communicates with the outside world, following the MCP ToolAnnotations schema. It also widens the MCP package bound to <3 to support MCP 2.x and replaces the deprecated list_tools/call_tool decorators with direct add_request_handler calls when using 2.x, falling back correctly on 1.x. The logic is solid and the tests are thorough.

Findings

[NIT]

tests/unit/test_server_metadata.py, line 109 - Missing type hint in test helper
The helper function hints(tool) is missing a type hint for the tool parameter and return type, which goes slightly against the project's typing practices. Adding (tool: Any) -> dict[str, Any] would make it clearer.

Verdict

VERDICT: approve


This review never edits code or force-blocks a merge. No blocking issues were found, so this PR was auto-approved.

Chasing what the current mcp actually offers turned up the Tasks API -
Tool.execution with taskSupport, the Task type, tasks/get and its siblings, and
mcp.server.experimental.task_support. It addresses Rigout's oldest limitation
directly: a command that outlives its timeout fails, and builds, installs and
downloads all can.

Declined, and the reason is worth recording so it is not rediscovered by
somebody reading mcp.types and finding it apparently available. The API
deprecates itself: tasks (SEP-1686) were removed from the MCP specification and
the interface is removed in mcp 2.0. Checked rather than taken on the warning's
word - mcp.server.experimental.task_support raises ModuleNotFoundError on 2.0.0,
while Tool.execution, ServerCapabilities.tasks and Task all still exist there.
The types outliving the implementation is exactly what would make this look safe
to adopt.

Building on it would ship a feature on an interface deprecated in the version
Rigout pins and absent from the version it must move to next. That is the same
trade as 0.2.0's unbounded mcp>=1.0.0: it works until someone else's release
day. When tasks return as an extension it is worth revisiting, and the reason is
now written down.
Checked against 2.0.0 rather than assumed. Every tool definition ports as
written: 2.x renamed Tool's fields to snake_case but kept the camelCase
spellings as aliases, and stdio, streamable_http_manager, models, types, Server
and Server.run all survive.

The whole of the migration is the registration. The list_tools and call_tool
decorators are gone, replaced by add_request_handler(method, params_type,
handler) with tools/list taking PaginatedRequestParams and tools/call taking
CallToolRequestParams; the handlers then return results directly instead of the
bare list and content the decorators wrapped, which also removes the wrinkle
where an error result must be raised as RuntimeError for the SDK to rebuild it.

Two decisions are recorded rather than made, because both are judgement rather
than discovery: whether to support both majors behind one hasattr branch - the
incompatibility is narrow enough that the cap could widen to <3 without forcing
anyone onto a five-day-old major - and whether it is time at all, given that
nothing Rigout needs is exclusive to 2.x now that tasks are gone from both, and
the caps mean nobody is broken while it waits.
The bound widens to mcp>=1.0.0,<3. Rigout now works on 1.x and 2.x, verified by
running against both rather than reasoned about.

The whole incompatibility is registration. 1.x uses the list_tools and call_tool
decorators; 2.x removed them for add_request_handler(method, params_type,
handler), with tools/list taking PaginatedRequestParams and tools/call taking
CallToolRequestParams, and handlers returning results directly instead of the
bare list and content the decorators wrapped. Every tool definition constructs
unchanged, because 2.x renamed Tool's fields while keeping the camelCase
spellings as construction aliases.

That last point has a trap in it, and running the code found it where reading
had not. The aliases cover construction, not attribute access. Building
CallToolResult(isError=True) works on both; reading result.isError raises on
2.x, where the field is is_error. The same is true of every annotation hint:
readOnlyHint constructs, .readOnlyHint does not read. One read in the product
and every read in the annotation tests were affected, and all now go through
helpers that answer on either major - result_is_error, and a dump by alias which
gives the protocol's own names on both.

Supporting both is deliberate rather than a step on the way to dropping 1.x. The
fork is four lines wide, so it costs little, and it means nobody is pushed onto
a major that is five days old while nobody is stranded on the older one either.
Which is in use is decided by what is installed, so there is nothing to
configure wrong.

Verified on mcp 2.0.0: fifteen tools listed with annotations intact, an unknown
tool answering is_error true with its name in the message, and a real tool
answering is_error false with its output. Verified on 1.29.0 by the suite, 602
tests.

0.3.1 rather than 0.4.0: nothing here is breaking, and the policy in
VERSIONING.md puts a compatible change at a patch below 1.0.0.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated approval: Jules found no blocking issues (verdict: approve). See the review comment above.

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.

1 participant