Add structured content to the Rust weather server and client and update to v3 - #167
Add structured content to the Rust weather server and client and update to v3#167olaservo wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
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 declaredoutput_schema. - Weather server: implement a custom
list_toolsto include required pagination metadata fields (ttl_ms,cache_scope) for the newer revision expectations. - Rust client: compile each tool’s declared
outputSchemausingjsonschemaand validate non-error tool results; make.envoptional and checkANTHROPIC_API_KEYafter 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}, |
There was a problem hiding this comment.
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.
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.
f87bbff to
ce055ae
Compare
Updates the Rust examples to
rmcp 3.1(lockfiles at3.1.2) and protocol revision2026-07-28, and gives both tools a declaredoutput_schemawith matchingstructured_content.get_alertsdeclaresVec<Alert>, so it answers with a top-level JSON array. Before2026-07-28an output schema had to be object-rooted, so a tool returning a list had to invent a key to hang it off.get_forecastreturns an object, for contrast.This supersedes #143, which bumped
rmcp0.3→1.4and failed CI because the0.3#[tool]macro API doesn't survive the jump. The rewrite was required either way, so the bump is folded in and taken to3.1.An rmcp bug this PR found, now fixed upstream
For most of this PR's life
list_toolswas hand-written here, purely because#[tool_handler]generated one hardcodingttl_ms: None, cache_scope: None— both required on a paginated result at2026-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 at3.1.2— anyone pinned below3.1.1still needs the workaround.get_infoalso now setsserver_info:ServerInfo::default()reports rmcp's own crate name, so the server was identifying itself asrmcp/3.1.0instead ofweather/1.0.0like the other four.Also
output_schemaMUST return conforming structured content.jsonschemacrate, which is the one new dependency here.contentgoes to the model,structured_contentis used as data..envis no longer fatal, and the key is checked after connecting — matching Python and TypeScript.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-25client 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: 0andcacheScope: "public"(emitted by the macro — nothing here sets them),structuredContentan 8-item array,contentone block of prose.server/discoverreturns versions through2026-07-28andserverInfoweather/1.0.0.If you probe
server/discoverby hand, it must carry the_metakeys the revision requires (io.modelcontextprotocol/protocolVersionandio.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
mcpgem never emits the mandatoryresultType, so a spec-strict client rejects every response. That needs an upstream fix.🤖 Generated with Claude Code