From 01c274fab3235a9ca9546b97f17287bb70892c6b Mon Sep 17 00:00:00 2001 From: cliffhall Date: Sat, 10 Oct 2026 13:33:02 -0400 Subject: [PATCH] fix(fetch): annotate the fetch tool The fetch tool set no tool annotations, unlike every tool in the git and time servers, and AGENTS.md requires them on every tool that is added or changed. It now declares readOnlyHint: true, destructiveHint: false, idempotentHint: true and openWorldHint: true (the values proposed on #3572), so clients can apply allow-reads/gate-sends policies to it. Protocol test pins them in tools/list; README lists them. Closes #3572 Co-Authored-By: Claude Opus 5.5 Signed-off-by: cliffhall --- src/fetch/README.md | 1 + src/fetch/src/mcp_server_fetch/server.py | 10 ++++++++++ src/fetch/tests/test_protocol.py | 7 +++++++ 3 files changed, 18 insertions(+) diff --git a/src/fetch/README.md b/src/fetch/README.md index 5f64d7952b..65457d4501 100644 --- a/src/fetch/README.md +++ b/src/fetch/README.md @@ -22,6 +22,7 @@ The fetch tool will truncate the response, but by using the `start_index` argume - `max_length` (integer, optional): Maximum number of characters to return (default: 5000) - `start_index` (integer, optional): Start content from this character index (default: 0) - `raw` (boolean, optional): Get raw content without markdown conversion (default: false) + - Annotations: `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, `openWorldHint: true` (it only reads, and it reaches arbitrary hosts on the internet) ### Prompts diff --git a/src/fetch/src/mcp_server_fetch/server.py b/src/fetch/src/mcp_server_fetch/server.py index 6b0073f478..94914f6b65 100644 --- a/src/fetch/src/mcp_server_fetch/server.py +++ b/src/fetch/src/mcp_server_fetch/server.py @@ -20,6 +20,7 @@ PromptMessage, TextContent, Tool, + ToolAnnotations, INVALID_PARAMS, INTERNAL_ERROR, ) @@ -409,6 +410,15 @@ async def list_tools() -> list[Tool]: Although originally you did not have internet access, and were advised to refuse and tell the user this, this tool now grants you internet access. Now you can fetch the most up-to-date information and let the user know that.""", inputSchema=Fetch.model_json_schema(), + # Reads only, changes nothing, and repeating a request has no + # further side effect; it reaches arbitrary hosts on the + # internet (#3572). + annotations=ToolAnnotations( + readOnlyHint=True, + destructiveHint=False, + idempotentHint=True, + openWorldHint=True, + ), ) ] diff --git a/src/fetch/tests/test_protocol.py b/src/fetch/tests/test_protocol.py index deb5d9099f..01269c7002 100644 --- a/src/fetch/tests/test_protocol.py +++ b/src/fetch/tests/test_protocol.py @@ -117,6 +117,13 @@ async def test_list_tools_wire_shape(web: FakeWeb) -> None: ) assert "this tool now grants you internet access" in tool["description"] assert "outputSchema" not in tool + # #3572: the tool declares what it does, for clients' approval policies. + assert tool["annotations"] == { + "readOnlyHint": True, + "destructiveHint": False, + "idempotentHint": True, + "openWorldHint": True, + } assert tool["inputSchema"] == { "description": "Parameters for fetching a URL.", "properties": {