Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions tests/helpers/mcp-test-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -25,6 +26,18 @@ const TOOL_ARGUMENTS: { match: RegExp; args: Record<string, unknown> }[] = [
{ 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<string, unknown> | undefined {
return TOOL_ARGUMENTS.find(({ match }) => match.test(name))?.args;
}
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions weather-server-go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions weather-server-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand All @@ -277,13 +290,15 @@ func main() {
mcp.AddTool(server, &mcp.Tool{
Name: "get_forecast",
Description: "Get weather forecast for a location",
Annotations: weatherToolAnnotations(),
}, getForecast)

// Add get_alerts tool
mcp.AddTool(server, &mcp.Tool{
Name: "get_alerts",
Description: "Get weather alerts for a US state",
OutputSchema: alertsOutputSchema(),
Annotations: weatherToolAnnotations(),
}, getAlerts)

// Run server on stdio transport
Expand Down
4 changes: 4 additions & 0 deletions weather-server-python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 13 additions & 2 deletions weather-server-python/weather.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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."""
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
4 changes: 4 additions & 0 deletions weather-server-ruby/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions weather-server-ruby/weather.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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: {
Expand Down Expand Up @@ -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: {
Expand Down
4 changes: 4 additions & 0 deletions weather-server-rust/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions weather-server-rust/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ pub struct Weather {
tool_router: ToolRouter<Weather>,
}

// 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 {
Expand All @@ -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::<Vec<Alert>>()
)]
async fn get_alerts(
Expand Down Expand Up @@ -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::<Forecast>()
)]
async fn get_forecast(
Expand Down
4 changes: 4 additions & 0 deletions weather-server-typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 12 additions & 0 deletions weather-server-typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading