Skip to content

Add structured content to the Rust weather server and client and update to v3 - #167

Open
olaservo wants to merge 4 commits into
modelcontextprotocol:mainfrom
olaservo:structured-output-2026-07-28/rust
Open

Add structured content to the Rust weather server and client and update to v3#167
olaservo wants to merge 4 commits into
modelcontextprotocol:mainfrom
olaservo:structured-output-2026-07-28/rust

Conversation

@olaservo

@olaservo olaservo commented Jul 26, 2026

Copy link
Copy Markdown
Member

Updates the Rust examples to rmcp 3.1 (lockfiles at 3.1.2) and protocol revision 2026-07-28, and gives both tools a declared output_schema with matching structured_content.

get_alerts declares Vec<Alert>, so it answers with a top-level JSON array. Before 2026-07-28 an output schema had to be object-rooted, so a tool returning a list had to invent a key to hang it off. get_forecast returns an object, for contrast.

This supersedes #143, which bumped rmcp 0.31.4 and failed CI because the 0.3 #[tool] macro API doesn't survive the jump. The rewrite was required either way, so the bump is folded in and taken to 3.1.

An rmcp bug this PR found, now fixed upstream

For most of this PR's life list_tools was hand-written here, purely because #[tool_handler] generated one hardcoding ttl_ms: None, cache_scope: None — both required on a paginated result at 2026-07-28 (SEP-2549), so a strict client rejected the response outright.

Filed as rust-sdk#1114, fixed by #1120, released in rmcp 3.1.1. The override is gone and the server is back on the plain #[tool_handler] path. That's why the lockfiles are at 3.1.2 — anyone pinned below 3.1.1 still needs the workaround.

get_info also now sets server_info: ServerInfo::default() reports rmcp's own crate name, so the server was identifying itself as rmcp/3.1.0 instead of weather/1.0.0 like the other four.

Also

  • Error paths return a tool-level error — a tool declaring an output_schema MUST return conforming structured content.
  • The client compiles every declared schema at connect and validates results (the spec's client-side SHOULD). rmcp doesn't do this and its docs point at the jsonschema crate, which is the one new dependency here.
  • content goes to the model, structured_content is used as data.
  • A missing .env is no longer fatal, and the key is checked after connecting — matching Python and TypeScript.
  • Model identifier moves to claude-sonnet-5, replacing an identifier past end of life.

One caveat

rmcp sends an array-rooted schema as written rather than projecting it down the way TypeScript does, so a 2025-11-25 client rejects the tool list. Use an object root if you need to serve both eras. The README says so.

Verification

A real get_alerts("TX") call against live NWS, captured off the raw wire with no SDK on the client side: outputSchema.type = "array", resultType: "complete", ttlMs: 0 and cacheScope: "public" (emitted by the macro — nothing here sets them), structuredContent an 8-item array, content one block of prose. server/discover returns versions through 2026-07-28 and serverInfo weather/1.0.0.

If you probe server/discover by hand, it must carry the _meta keys the revision requires (io.modelcontextprotocol/protocolVersion and io.modelcontextprotocol/clientCapabilities) — without them rmcp's stdio server answers nothing at all, which is rust-sdk#1157, not anything about this example.

One of a set, one PR per language: #164 (Python, merged), #165 (TypeScript), #166 (Go), #167 (Rust); #163 was the shared test prerequisite and has merged. Ruby is not in the set — the mcp gem never emits the mandatory resultType, so a spec-strict client rejects every response. That needs an upstream fix.


🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the Rust MCP weather examples (server + client) to rmcp 3.0.0-beta.2 and the 2026-07-28 protocol semantics by declaring tool output_schema and returning matching structured_content (including an array-rooted schema for get_alerts), with the Rust client additionally compiling and validating each declared output schema at connect time.

Changes:

  • Weather server: add explicit structured output types (Alert, Forecast) and return dual-channel tool results (content + structured_content) with declared output_schema.
  • Weather server: implement a custom list_tools to include required pagination metadata fields (ttl_ms, cache_scope) for the newer revision expectations.
  • Rust client: compile each tool’s declared outputSchema using jsonschema and validate non-error tool results; make .env optional and check ANTHROPIC_API_KEY after connecting.

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
weather-server-rust/src/main.rs Adds structured output types, dual-channel tool results, output schemas, and a custom list_tools; updates transport usage.
weather-server-rust/README.md Documents structured output behavior and the array-rooted schema caveat for older clients.
weather-server-rust/Cargo.toml Bumps rmcp to 3.0.0-beta.2 (and retains required deps for new code paths).
weather-server-rust/Cargo.lock Lockfile updates for the rmcp upgrade.
mcp-client-rust/src/main.rs Adds output-schema compilation + validation, routes content vs structured_content, and makes .env optional with deferred API-key checking.
mcp-client-rust/README.md Documents the client’s structured output validation behavior.
mcp-client-rust/Cargo.toml Adds jsonschema and bumps rmcp to 3.0.0-beta.2.
mcp-client-rust/Cargo.lock Lockfile updates for the rmcp upgrade and new dependency.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

handler::server::{router::tool::ToolRouter, tool::schema_for_output, wrapper::Parameters},
model::*,
schemars, tool, tool_handler, tool_router,
schemars::{self, JsonSchema},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The self is load-bearing, so this one stays as written.

schemars' derive macro expands to schemars::-qualified paths, so the module name has to be in scope wherever #[derive(JsonSchema)] is used — which is five types in this file. Importing JsonSchema alone brings the trait but not the path root.

Tested rather than argued: rewriting the import as schemars::JsonSchema and rebuilding gives 20 errors, all error[E0433]: cannot find module or crate \schemars` in this scope, one per derive site. cargo buildon the file as it stands is clean with no unused-import warning, which is the compiler agreeing thatself` is used.

@olaservo olaservo changed the title Add structured output to the Rust weather server and client and update to v3 Add structured content to the Rust weather server and client and update to v3 Aug 2, 2026
olaservo and others added 4 commits August 9, 2026 15:36
Both tools declare an output_schema and return structured_content alongside
the text.

get_alerts declares Vec<Alert>, so it answers with a top-level JSON array
rather than an array nested in an object, which protocol revision 2026-07-28
is the first to allow. "No alerts" is simply []. get_forecast returns an
object, for contrast.

Error paths return a tool-level error: a tool declaring an output_schema MUST
return conforming structured content, so a path with no data has to fail.

The client compiles every declared outputSchema at connect time and validates
results against it. rmcp does not do this; its docs point at the jsonschema
crate, which is the one new dependency here. Each channel goes to its stated
reader: content is forwarded to the model, structured_content is used as data,
reporting how many items came back. A missing .env is no longer fatal, and the
API key is checked after connecting rather than before.

This supersedes modelcontextprotocol#143, which bumped rmcp 0.3 -> 1.4 and failed CI because the
0.3 #[tool] macro API does not survive the jump. The rewrite was required
either way, so the bump is folded in and taken to 3.0.0-beta.2, the first
version with the 2026-07-28 model.

list_tools is hand-written. #[tool_handler] generates one hardcoding
ttl_ms: None, cache_scope: None, but both are required on a paginated result
at 2026-07-28 (SEP-2549) and a strict client rejects the response outright.
get_info does not pin ProtocolVersion::V_2026_07_28 either: rmcp's server
never implements server/discover, so pinning would overstate support.

Model identifier moves to claude-sonnet-5, replacing claude-sonnet-4-20250514,
which is past end of life.

Note that rmcp sends an array-rooted schema as written on every connection
rather than projecting it down, so an older client rejects the tool list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
rmcp left beta on 2026-07-28 and reached 3.1.0 on 2026-07-31, so the pin
moves off 3.0.0-beta.2.

get_info now sets server_info. ServerInfo::default() reports rmcp own crate
name and version, so the server was identifying itself as rmcp 3.1.0
rather than weather 1.0.0 as the other three quickstarts do.

The hand-written list_tools stays: rmcp-macros 3.1.0 still generates
ttl_ms: None, cache_scope: None, which a strict client rejects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The spec never uses the phrase "structured output". It defines two separate
things under Tool Result: "Structured Content" (the `structuredContent` field)
and "Output Schema" (the `outputSchema` field). Collapsing them into
"structured output" conflates the two.

It is also actively confusing here. In LLM tooling "structured output" means
constrained decoding — making the *model* emit conforming JSON. These clients
call a model API, so a reader could reasonably take the phrase to mean the
tool constrains the model's response, which is the opposite of what is going
on: the tool describes the shape of its own result.

Headings and prose now say "structured content". References to real
identifiers are left alone: the Python SDK's own docs page is called
Structured Output and lives at docs/servers/structured-output.md, and its
decorator parameter is `structured_output`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`#[tool_handler]` generated `ttl_ms: None, cache_scope: None`, but both are
required on a paginated result at 2026-07-28, so a strict client rejected
tools/list before it could reach a tool call. The server worked around that by
hand-writing list_tools purely to set the two fields.

rust-sdk#1120 fixed the macro, released in rmcp 3.1.1: it now emits the fields
when the negotiated revision expects them and omits them otherwise. Both
lockfiles move to 3.1.2 and the override goes away, so the server is back to the
plain `#[tool_handler]` path the SDK documents.

Verified on the raw wire: after a 2026-07-28 handshake, tools/list carries
`ttlMs: 0` and `cacheScope: "public"` with no code setting them, and the smoke
test's strict client still validates both tools' structured content.
@olaservo
olaservo force-pushed the structured-output-2026-07-28/rust branch from f87bbff to ce055ae Compare August 10, 2026 00:57
@olaservo
olaservo marked this pull request as ready for review August 10, 2026 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants