Skip to content

Add structured output to the Rust weather server and client#167

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

Add structured output to the Rust weather server and client#167
olaservo wants to merge 1 commit into
modelcontextprotocol:mainfrom
olaservo:structured-output-2026-07-28/rust

Conversation

@olaservo

@olaservo olaservo commented Jul 26, 2026

Copy link
Copy Markdown
Member

Draft until the 2026-07-28 revision and SDK is released

Brings the Rust examples onto 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.0.0-beta.2, the first version with the 2026-07-28 model.

Two rmcp details 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. The macro skips generation when the impl defines the method.

get_info does not pin ProtocolVersion::V_2026_07_28. rmcp's server never implements server/discover — the string appears only in its client — so pinning would overstate support.

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"]
tools/list      -> get_alerts outputSchema.type = "array"
  structuredContent: ARRAY
  content: 1 block(s)

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 four 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 deliberately not in the set: the mcp gem caps at protocol 2025-11-25.


🤖 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 <[email protected]>
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.

1 participant