Skip to content

Commit 6b237e0

Browse files
authored
Merge pull request #278 from modelcontextprotocol/feat/tool-annotations
Add tool annotations to both tools in every weather server
2 parents 7ccc971 + e7ea67c commit 6b237e0

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)