Skip to content

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

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

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

Conversation

@olaservo

@olaservo olaservo commented Jul 26, 2026

Copy link
Copy Markdown
Member

Brings the Rust examples onto rmcp 3.1 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:

[
  { "event": "Flood Warning", "area": "La Salle County", ... },
  { "event": "Heat Advisory",  "area": "Wheeler County",  ... }
]

Through 2025-11-25 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

That PR bumped rmcp 0.31.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.1.

One rmcp detail worth knowing

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 with ttlMs expected number, received undefined. The macro skips generation when the impl defines the method, which is the workaround used here.

Re-checked against rmcp-macros 3.1.0: still generated verbatim at src/tool_handler.rs:76-77, so this is a stable-release bug rather than a beta one and the workaround stays. Worth an upstream issue.

What changed since this PR was opened

rmcp now implements server/discover. An earlier revision of this description said the server never did and that the string appeared only in rmcp's client code. That was true of 3.0.0-beta.2 and is no longer true: 3.1.0 dispatches it at src/handler/server.rs:108. Confirmed on the wire — a raw server/discover opener now returns supportedVersions through 2026-07-28 instead of dying with expect initialized request, but received: DiscoverRequest.

get_info still does not pin ProtocolVersion::V_2026_07_28, and does not need to: discovery already advertises it. Note ProtocolVersion::LATEST remains V_2025_11_25 as of 3.1.0 (src/model.rs:175), so rmcp clients still pin explicitly — a default rather than a bug, and one with an expiry date: rust-sdk#1105 flips it to V_2026_07_28 and is open.

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

Other changes

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 rather than answer with a bare text result.

The client validates. It compiles every declared outputSchema at connect time and checks results against it — the spec's client-side SHOULD. 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, matching the Python and TypeScript clients.

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

One caveat

rmcp sends an array-rooted schema as written on every connection rather than projecting it down the way the TypeScript SDK 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

Captured off the raw wire — a real get_alerts("TX") call against live NWS data, no SDK on the client side:

server/discover -> OK, versions=[..., "2026-07-28"], serverInfo={"name":"weather","version":"1.0.0"}
tools/list      -> get_alerts outputSchema.type = "array"
                   ttlMs: 60000, cacheScope: "public"

tools/call:
  resultType: "complete"
  structuredContent: ARRAY (8 items)
  content: 1 block(s), human-readable prose

Related

One of a set bringing the examples onto 2026-07-28, one PR per language so the four can be compared: #164 (Python), #165 (TypeScript), #166 (Go), #167 (Rust). #163 is the shared prerequisite — the smoke test on main uses an MCP SDK v1 helper that negotiates 2025-11-25 and rejects this server's array-rooted schema (Invalid input: expected "object"), so CI here stays red until that merges. The other tests pass. Smoke-test coverage for the Rust client follows once #163 is in, since it depends on helpers introduced there.

The Ruby examples are not in the set. The mcp gem 1.1.0 does now negotiate 2026-07-28, but its server never emits the resultType field that the revision makes mandatory, so a spec-strict client rejects every response including tools/list. That is an upstream fix, not something an example can work around.


🤖 Generated with Claude Code

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>

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 and others added 2 commits August 1, 2026 11:08
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>
@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
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