diff --git a/tests/helpers/mcp-test-client.ts b/tests/helpers/mcp-test-client.ts index aeede7fa..c7a670fd 100644 --- a/tests/helpers/mcp-test-client.ts +++ b/tests/helpers/mcp-test-client.ts @@ -5,6 +5,7 @@ * Connects to a server, initializes, lists tools, and then checks the tools * actually honour what they advertise: * + * - every tool declares the same annotations (read-only, open-world); * - every tool that declares an `outputSchema` is called, and the result must * carry `structuredContent` (the SDK validates it against the schema for us * and throws on a mismatch); @@ -25,6 +26,18 @@ const TOOL_ARGUMENTS: { match: RegExp; args: Record }[] = [ { match: /forecast/i, args: { latitude: 38.5816, longitude: -121.4944 } }, ]; +/** + * The annotations every weather tool must declare, identical across the five + * servers. All four are set explicitly because the Go and Ruby SDKs always send + * some hints, so leaving them to defaults would make the servers disagree. + */ +const EXPECTED_ANNOTATIONS = { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, +} as const; + function argumentsFor(name: string): Record | undefined { return TOOL_ARGUMENTS.find(({ match }) => match.test(name))?.args; } @@ -55,6 +68,18 @@ async function testServer(command: string, args: string[]) { const { tools } = await client.listTools(); console.error(`✓ Listed ${tools.length} tools`); + for (const tool of tools) { + for (const [hint, value] of Object.entries(EXPECTED_ANNOTATIONS)) { + const actual = tool.annotations?.[hint as keyof typeof EXPECTED_ANNOTATIONS]; + if (actual !== value) { + throw new Error( + `${tool.name} should set annotations.${hint} to ${value}, got ${actual}`, + ); + } + } + } + console.error(`✓ All ${tools.length} tools carry the expected annotations`); + let checked = 0; for (const tool of tools) { if (!tool.outputSchema) continue; diff --git a/weather-server-go/README.md b/weather-server-go/README.md index 801f4da0..bc2f8c3b 100644 --- a/weather-server-go/README.md +++ b/weather-server-go/README.md @@ -25,6 +25,10 @@ cd ../mcp-client-go go run main.go ../weather-server-go/weather ``` +## Tool annotations + +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. + ## Structured content 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. diff --git a/weather-server-go/main.go b/weather-server-go/main.go index 5b8e4185..39d4b745 100644 --- a/weather-server-go/main.go +++ b/weather-server-go/main.go @@ -259,6 +259,19 @@ func alertsOutputSchema() *jsonschema.Schema { return schema } +// weatherToolAnnotations returns hints for hosts: both tools only read data, +// and they read it from an outside service (the NWS API). Hints are not +// guarantees; a host decides how far to trust them. +func weatherToolAnnotations() *mcp.ToolAnnotations { + destructive, openWorld := false, true + return &mcp.ToolAnnotations{ + ReadOnlyHint: true, + DestructiveHint: &destructive, + IdempotentHint: true, + OpenWorldHint: &openWorld, + } +} + func main() { // Create MCP server. // @@ -277,6 +290,7 @@ func main() { mcp.AddTool(server, &mcp.Tool{ Name: "get_forecast", Description: "Get weather forecast for a location", + Annotations: weatherToolAnnotations(), }, getForecast) // Add get_alerts tool @@ -284,6 +298,7 @@ func main() { Name: "get_alerts", Description: "Get weather alerts for a US state", OutputSchema: alertsOutputSchema(), + Annotations: weatherToolAnnotations(), }, getAlerts) // Run server on stdio transport diff --git a/weather-server-python/README.md b/weather-server-python/README.md index 732e426a..b0685e69 100644 --- a/weather-server-python/README.md +++ b/weather-server-python/README.md @@ -27,6 +27,10 @@ cd ../mcp-client-python uv run client.py ../weather-server-python/weather.py ``` +## Tool annotations + +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. + ## Structured content 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. diff --git a/weather-server-python/weather.py b/weather-server-python/weather.py index b6b47705..65c0780f 100644 --- a/weather-server-python/weather.py +++ b/weather-server-python/weather.py @@ -3,6 +3,7 @@ import httpx2 from mcp.server import MCPServer from mcp.server.mcpserver.exceptions import ToolError +from mcp.types import ToolAnnotations from pydantic import BaseModel, Field, RootModel # Initialize the MCP server @@ -12,6 +13,16 @@ NWS_API_BASE = "https://api.weather.gov" USER_AGENT = "weather-app/1.0" +# Hints for hosts: both tools only read data, and they read it from an outside +# service (the NWS API). Hints are not guarantees; a host decides how far to +# trust them. +WEATHER_TOOL_ANNOTATIONS = ToolAnnotations( + read_only_hint=True, + destructive_hint=False, + idempotent_hint=True, + open_world_hint=True, +) + class Alert(BaseModel): """One active weather alert.""" @@ -64,7 +75,7 @@ async def make_nws_request(url: str) -> dict[str, Any] | None: return None -@mcp.tool() +@mcp.tool(annotations=WEATHER_TOOL_ANNOTATIONS) async def get_alerts(state: str) -> Alerts: """Get weather alerts for a US state. @@ -95,7 +106,7 @@ async def get_alerts(state: str) -> Alerts: ) -@mcp.tool() +@mcp.tool(annotations=WEATHER_TOOL_ANNOTATIONS) async def get_forecast(latitude: float, longitude: float) -> Forecast: """Get weather forecast for a location. diff --git a/weather-server-ruby/README.md b/weather-server-ruby/README.md index 72def02d..25d40e44 100644 --- a/weather-server-ruby/README.md +++ b/weather-server-ruby/README.md @@ -27,6 +27,10 @@ bundle install bundle exec ruby client.rb ../weather-server-ruby/weather.rb ``` +## Tool annotations + +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. + ## Structured content 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. diff --git a/weather-server-ruby/weather.rb b/weather-server-ruby/weather.rb index 22d5761d..6127d38a 100644 --- a/weather-server-ruby/weather.rb +++ b/weather-server-ruby/weather.rb @@ -8,6 +8,17 @@ NWS_API_BASE = "https://api.weather.gov" USER_AGENT = "weather-app/1.0" +# Hints for hosts: both tools only read data, and they read it from an outside +# service (the NWS API). Hints are not guarantees; a host decides how far to +# trust them. The gem sends all four hints, so set each one explicitly: its +# defaults (destructive, not idempotent) suit a tool that writes. +WEATHER_TOOL_ANNOTATIONS = { + read_only_hint: true, + destructive_hint: false, + idempotent_hint: true, + open_world_hint: true +}.freeze + module HelperMethods def make_nws_request(url) uri = URI(url) @@ -47,6 +58,7 @@ class GetAlerts < MCP::Tool tool_name "get_alerts" description "Get weather alerts for a US state" + annotations(**WEATHER_TOOL_ANNOTATIONS) input_schema( properties: { state: { @@ -116,6 +128,7 @@ class GetForecast < MCP::Tool tool_name "get_forecast" description "Get weather forecast for a location" + annotations(**WEATHER_TOOL_ANNOTATIONS) input_schema( properties: { latitude: { diff --git a/weather-server-rust/README.md b/weather-server-rust/README.md index 4646c193..afae6fc2 100644 --- a/weather-server-rust/README.md +++ b/weather-server-rust/README.md @@ -31,6 +31,10 @@ cd ../mcp-client-rust cargo run -- ../weather-server-rust/target/release/weather ``` +## Tool annotations + +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. + ## Structured content 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. diff --git a/weather-server-rust/src/main.rs b/weather-server-rust/src/main.rs index 4d4e7735..ad026ba1 100644 --- a/weather-server-rust/src/main.rs +++ b/weather-server-rust/src/main.rs @@ -169,6 +169,9 @@ pub struct Weather { tool_router: ToolRouter, } +// Both tools carry the same annotations, hints for hosts: they only read data, +// and they read it from an outside service (the NWS API). Hints are not +// guarantees; a host decides how far to trust them. #[tool_router(router = tool_router)] impl Weather { fn new() -> Self { @@ -179,6 +182,12 @@ impl Weather { #[tool( description = "Get weather alerts for a US state.", + annotations( + read_only_hint = true, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = true + ), output_schema = schema_for_output::>() )] async fn get_alerts( @@ -229,6 +238,12 @@ impl Weather { #[tool( description = "Get weather forecast for a location.", + annotations( + read_only_hint = true, + destructive_hint = false, + idempotent_hint = true, + open_world_hint = true + ), output_schema = schema_for_output::() )] async fn get_forecast( diff --git a/weather-server-typescript/README.md b/weather-server-typescript/README.md index 75891115..51b8f970 100644 --- a/weather-server-typescript/README.md +++ b/weather-server-typescript/README.md @@ -28,6 +28,10 @@ npm install && npm run build node build/index.js ../weather-server-typescript/build/index.js ``` +## Tool annotations + +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. + ## Structured content 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. diff --git a/weather-server-typescript/src/index.ts b/weather-server-typescript/src/index.ts index a003a53c..3a165788 100644 --- a/weather-server-typescript/src/index.ts +++ b/weather-server-typescript/src/index.ts @@ -119,6 +119,16 @@ function formatPeriod(period: Forecast["periods"][number]): string { ].join("\n"); } +// Hints for hosts: both tools only read data, and they read it from an outside +// service (the NWS API). Hints are not guarantees; a host decides how far to +// trust them. +const weatherToolAnnotations = { + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: true, +}; + function buildServer(): McpServer { const server = new McpServer({ name: "weather", @@ -129,6 +139,7 @@ function buildServer(): McpServer { "get-alerts", { title: "Get Weather Alerts", + annotations: weatherToolAnnotations, description: "Get weather alerts for a state", inputSchema: z.object({ state: z @@ -172,6 +183,7 @@ function buildServer(): McpServer { "get-forecast", { title: "Get Weather Forecast", + annotations: weatherToolAnnotations, description: "Get weather forecast for a location", inputSchema: z.object({ latitude: z