Skip to content

Commit e7ea67c

Browse files
a-akimovclaude
andcommitted
Add tool annotations to both tools in every weather server
Both tools only read data from an outside service (the NWS API), so every server now declares readOnlyHint: true and openWorldHint: true on get_alerts and get_forecast. destructiveHint: false and idempotentHint: true are set explicitly as well. The Go SDK always sends idempotentHint, and the Ruby gem always sends all four hints with defaults meant for tools that write (destructive, not idempotent). Setting all four keeps the five servers' tools/list output identical. The test client now fails a server whose tools don't carry these four annotations. Each server README gets a short section on what the hints mean to a host. Closes #271 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NkL34oWhufSSqj9FbPx5dr
1 parent 71164fd commit e7ea67c

11 files changed

Lines changed: 113 additions & 2 deletions

File tree

‎tests/helpers/mcp-test-client.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Connects to a server, initializes, lists tools, and then checks the tools
66
* actually honour what they advertise:
77
*
8+
* - every tool declares the same annotations (read-only, open-world);
89
* - every tool that declares an `outputSchema` is called, and the result must
910
* carry `structuredContent` (the SDK validates it against the schema for us
1011
* and throws on a mismatch);
@@ -25,6 +26,18 @@ const TOOL_ARGUMENTS: { match: RegExp; args: Record<string, unknown> }[] = [
2526
{ match: /forecast/i, args: { latitude: 38.5816, longitude: -121.4944 } },
2627
];
2728

29+
/**
30+
* The annotations every weather tool must declare, identical across the five
31+
* servers. All four are set explicitly because the Go and Ruby SDKs always send
32+
* some hints, so leaving them to defaults would make the servers disagree.
33+
*/
34+
const EXPECTED_ANNOTATIONS = {
35+
readOnlyHint: true,
36+
destructiveHint: false,
37+
idempotentHint: true,
38+
openWorldHint: true,
39+
} as const;
40+
2841
function argumentsFor(name: string): Record<string, unknown> | undefined {
2942
return TOOL_ARGUMENTS.find(({ match }) => match.test(name))?.args;
3043
}
@@ -55,6 +68,18 @@ async function testServer(command: string, args: string[]) {
5568
const { tools } = await client.listTools();
5669
console.error(`✓ Listed ${tools.length} tools`);
5770

71+
for (const tool of tools) {
72+
for (const [hint, value] of Object.entries(EXPECTED_ANNOTATIONS)) {
73+
const actual = tool.annotations?.[hint as keyof typeof EXPECTED_ANNOTATIONS];
74+
if (actual !== value) {
75+
throw new Error(
76+
`${tool.name} should set annotations.${hint} to ${value}, got ${actual}`,
77+
);
78+
}
79+
}
80+
}
81+
console.error(`✓ All ${tools.length} tools carry the expected annotations`);
82+
5883
let checked = 0;
5984
for (const tool of tools) {
6085
if (!tool.outputSchema) continue;

‎weather-server-go/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ cd ../mcp-client-go
2525
go run main.go ../weather-server-go/weather
2626
```
2727

28+
## Tool annotations
29+
30+
Both tools declare the same [annotations](https://modelcontextprotocol.io/specification/2026-07-28/schema#toolannotations): `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true` and `openWorldHint: true`. They tell a host that the tools only read data, and that they read it from an outside service (the NWS API), so a host can, for example, run them without asking the user to confirm each call. Annotations are hints, not guarantees: a host should rely on them only for servers it trusts.
31+
2832
## Structured content
2933

3034
Both tools advertise an `outputSchema` and return `StructuredContent`. `get_forecast` returns an object; `get_alerts` returns a top-level JSON array, which protocol revision `2026-07-28` is the first to allow — see [Structured Content](https://modelcontextprotocol.io/specification/2026-07-28/server/tools#structured-content) in the spec.

‎weather-server-go/main.go‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,19 @@ func alertsOutputSchema() *jsonschema.Schema {
259259
return schema
260260
}
261261

262+
// weatherToolAnnotations returns hints for hosts: both tools only read data,
263+
// and they read it from an outside service (the NWS API). Hints are not
264+
// guarantees; a host decides how far to trust them.
265+
func weatherToolAnnotations() *mcp.ToolAnnotations {
266+
destructive, openWorld := false, true
267+
return &mcp.ToolAnnotations{
268+
ReadOnlyHint: true,
269+
DestructiveHint: &destructive,
270+
IdempotentHint: true,
271+
OpenWorldHint: &openWorld,
272+
}
273+
}
274+
262275
func main() {
263276
// Create MCP server.
264277
//
@@ -277,13 +290,15 @@ func main() {
277290
mcp.AddTool(server, &mcp.Tool{
278291
Name: "get_forecast",
279292
Description: "Get weather forecast for a location",
293+
Annotations: weatherToolAnnotations(),
280294
}, getForecast)
281295

282296
// Add get_alerts tool
283297
mcp.AddTool(server, &mcp.Tool{
284298
Name: "get_alerts",
285299
Description: "Get weather alerts for a US state",
286300
OutputSchema: alertsOutputSchema(),
301+
Annotations: weatherToolAnnotations(),
287302
}, getAlerts)
288303

289304
// Run server on stdio transport

‎weather-server-python/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ cd ../mcp-client-python
2727
uv run client.py ../weather-server-python/weather.py
2828
```
2929

30+
## Tool annotations
31+
32+
Both tools declare the same [annotations](https://modelcontextprotocol.io/specification/2026-07-28/schema#toolannotations): `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true` and `openWorldHint: true`. They tell a host that the tools only read data, and that they read it from an outside service (the NWS API), so a host can, for example, run them without asking the user to confirm each call. Annotations are hints, not guarantees: a host should rely on them only for servers it trusts.
33+
3034
## Structured content
3135

3236
Both tools declare an output schema and return `structured_content`. `get_forecast` returns an object; `get_alerts` returns a top-level JSON array, which protocol revision `2026-07-28` is the first to allow — see [Structured Content](https://modelcontextprotocol.io/specification/2026-07-28/server/tools#structured-content) in the spec.

‎weather-server-python/weather.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import httpx2
44
from mcp.server import MCPServer
55
from mcp.server.mcpserver.exceptions import ToolError
6+
from mcp.types import ToolAnnotations
67
from pydantic import BaseModel, Field, RootModel
78

89
# Initialize the MCP server
@@ -12,6 +13,16 @@
1213
NWS_API_BASE = "https://api.weather.gov"
1314
USER_AGENT = "weather-app/1.0"
1415

16+
# Hints for hosts: both tools only read data, and they read it from an outside
17+
# service (the NWS API). Hints are not guarantees; a host decides how far to
18+
# trust them.
19+
WEATHER_TOOL_ANNOTATIONS = ToolAnnotations(
20+
read_only_hint=True,
21+
destructive_hint=False,
22+
idempotent_hint=True,
23+
open_world_hint=True,
24+
)
25+
1526

1627
class Alert(BaseModel):
1728
"""One active weather alert."""
@@ -64,7 +75,7 @@ async def make_nws_request(url: str) -> dict[str, Any] | None:
6475
return None
6576

6677

67-
@mcp.tool()
78+
@mcp.tool(annotations=WEATHER_TOOL_ANNOTATIONS)
6879
async def get_alerts(state: str) -> Alerts:
6980
"""Get weather alerts for a US state.
7081
@@ -95,7 +106,7 @@ async def get_alerts(state: str) -> Alerts:
95106
)
96107

97108

98-
@mcp.tool()
109+
@mcp.tool(annotations=WEATHER_TOOL_ANNOTATIONS)
99110
async def get_forecast(latitude: float, longitude: float) -> Forecast:
100111
"""Get weather forecast for a location.
101112

‎weather-server-ruby/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ bundle install
2727
bundle exec ruby client.rb ../weather-server-ruby/weather.rb
2828
```
2929

30+
## Tool annotations
31+
32+
Both tools declare the same [annotations](https://modelcontextprotocol.io/specification/2026-07-28/schema#toolannotations): `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true` and `openWorldHint: true`. They tell a host that the tools only read data, and that they read it from an outside service (the NWS API), so a host can, for example, run them without asking the user to confirm each call. Annotations are hints, not guarantees: a host should rely on them only for servers it trusts.
33+
3034
## Structured content
3135

3236
Both tools declare an `output_schema` and return `structured_content`. `get_forecast` returns an object; `get_alerts` returns a top-level JSON array, which protocol revision `2026-07-28` is the first to allow — see [Structured Content](https://modelcontextprotocol.io/specification/2026-07-28/server/tools#structured-content) in the spec.

‎weather-server-ruby/weather.rb‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,17 @@
88
NWS_API_BASE = "https://api.weather.gov"
99
USER_AGENT = "weather-app/1.0"
1010

11+
# Hints for hosts: both tools only read data, and they read it from an outside
12+
# service (the NWS API). Hints are not guarantees; a host decides how far to
13+
# trust them. The gem sends all four hints, so set each one explicitly: its
14+
# defaults (destructive, not idempotent) suit a tool that writes.
15+
WEATHER_TOOL_ANNOTATIONS = {
16+
read_only_hint: true,
17+
destructive_hint: false,
18+
idempotent_hint: true,
19+
open_world_hint: true
20+
}.freeze
21+
1122
module HelperMethods
1223
def make_nws_request(url)
1324
uri = URI(url)
@@ -47,6 +58,7 @@ class GetAlerts < MCP::Tool
4758

4859
tool_name "get_alerts"
4960
description "Get weather alerts for a US state"
61+
annotations(**WEATHER_TOOL_ANNOTATIONS)
5062
input_schema(
5163
properties: {
5264
state: {
@@ -116,6 +128,7 @@ class GetForecast < MCP::Tool
116128

117129
tool_name "get_forecast"
118130
description "Get weather forecast for a location"
131+
annotations(**WEATHER_TOOL_ANNOTATIONS)
119132
input_schema(
120133
properties: {
121134
latitude: {

‎weather-server-rust/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ cd ../mcp-client-rust
3131
cargo run -- ../weather-server-rust/target/release/weather
3232
```
3333

34+
## Tool annotations
35+
36+
Both tools declare the same [annotations](https://modelcontextprotocol.io/specification/2026-07-28/schema#toolannotations): `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true` and `openWorldHint: true`. They tell a host that the tools only read data, and that they read it from an outside service (the NWS API), so a host can, for example, run them without asking the user to confirm each call. Annotations are hints, not guarantees: a host should rely on them only for servers it trusts.
37+
3438
## Structured content
3539

3640
Both tools declare an `output_schema` and return `structured_content`. `get_forecast` returns an object; `get_alerts` returns a top-level JSON array, which protocol revision `2026-07-28` is the first to allow — see [Structured Content](https://modelcontextprotocol.io/specification/2026-07-28/server/tools#structured-content) in the spec.

‎weather-server-rust/src/main.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ pub struct Weather {
169169
tool_router: ToolRouter<Weather>,
170170
}
171171

172+
// Both tools carry the same annotations, hints for hosts: they only read data,
173+
// and they read it from an outside service (the NWS API). Hints are not
174+
// guarantees; a host decides how far to trust them.
172175
#[tool_router(router = tool_router)]
173176
impl Weather {
174177
fn new() -> Self {
@@ -179,6 +182,12 @@ impl Weather {
179182

180183
#[tool(
181184
description = "Get weather alerts for a US state.",
185+
annotations(
186+
read_only_hint = true,
187+
destructive_hint = false,
188+
idempotent_hint = true,
189+
open_world_hint = true
190+
),
182191
output_schema = schema_for_output::<Vec<Alert>>()
183192
)]
184193
async fn get_alerts(
@@ -229,6 +238,12 @@ impl Weather {
229238

230239
#[tool(
231240
description = "Get weather forecast for a location.",
241+
annotations(
242+
read_only_hint = true,
243+
destructive_hint = false,
244+
idempotent_hint = true,
245+
open_world_hint = true
246+
),
232247
output_schema = schema_for_output::<Forecast>()
233248
)]
234249
async fn get_forecast(

‎weather-server-typescript/README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ npm install && npm run build
2828
node build/index.js ../weather-server-typescript/build/index.js
2929
```
3030

31+
## Tool annotations
32+
33+
Both tools declare the same [annotations](https://modelcontextprotocol.io/specification/2026-07-28/schema#toolannotations): `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true` and `openWorldHint: true`. They tell a host that the tools only read data, and that they read it from an outside service (the NWS API), so a host can, for example, run them without asking the user to confirm each call. Annotations are hints, not guarantees: a host should rely on them only for servers it trusts.
34+
3135
## Structured content
3236

3337
Both tools declare an `outputSchema` and return `structuredContent`. `get-forecast` returns an object; `get-alerts` returns a top-level JSON array, which protocol revision `2026-07-28` is the first to allow — see [Structured Content](https://modelcontextprotocol.io/specification/2026-07-28/server/tools#structured-content) in the spec.

0 commit comments

Comments
 (0)