Skip to content

Add structured content to the Python weather server and client and update to v2 - #164

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

Add structured content to the Python weather server and client and update to v2#164
olaservo wants to merge 5 commits into
modelcontextprotocol:mainfrom
olaservo:structured-output-2026-07-28/python

Conversation

@olaservo

@olaservo olaservo commented Jul 26, 2026

Copy link
Copy Markdown
Member

Brings the Python examples onto MCP SDK 2.0.0 and protocol revision 2026-07-28, and gives both tools a declared outputSchema with matching structuredContent.

This also clears the staleness that modelcontextprotocol/modelcontextprotocol#3124 flags against this repository. That PR modernises the Python in the tutorials and notes that the "complete code" they link to here had fallen behind: weather.py imported mcp.server.fastmcp, which does not exist in v2, and client.py still held the ClientSession version.

get_alerts 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 outputSchema 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.

The bare array without dropping to the low-level API

Alerts is a RootModel[list[Alert]] rather than a plain list[Alert]. A list[...] annotation is silently wrapped as {"result": ...}, but _try_create_model_and_schema takes any BaseModel subclass as the schema verbatim and RootModel is one — so the ordinary @mcp.tool() decorator yields an array-rooted schema and a bare array.

This is undocumented: docs/servers/structured-output.md still presents the wrapper as unavoidable for a list return. It is the difference between Python being able to demonstrate this spec feature idiomatically and not, and it is worth a docs PR upstream.

Other changes

Error paths raise. Once a tool declares an output schema it MUST return conforming structured content, so a path with no data to return has to fail rather than answer with a bare text result. "No alerts" is an empty array, not an error.

A null-field crash is fixed. props.get("instruction", "…") supplies its default only when the key is absent, and NWS sends these fields as explicit nulls. None reached a str field and Pydantic rejected the whole result, so get_alerts failed outright for any state with such an alert — one Texas alert in eight was enough. All five fields now use or.

The client negotiates. It moves to the 2.0 Client API and passes mode="auto" — one server/discover probe, falling back to the 2025-11-25 handshake — and prints the negotiated version on connect. call_tool revalidates non-error results against the declared schema, so the spec's client-side SHOULD needs no code here.

Dependencies are declared rather than inherited. weather.py imports httpx2 and client.py imports mcp_types, so both are listed explicitly instead of relying on mcp pulling them in. httpx stays out — mcp depends on httpx2, and listing httpx installs a second HTTP stack.

Matched to modelcontextprotocol/modelcontextprotocol#3124 so the tutorials and this code agree: the short from mcp.server import MCPServer spelling; TextContent narrowing before forwarding content to the model API; and input() moved onto a worker thread so it does not block the event loop.

Verification

Captured off the raw wire — a real get_alerts("TX") call against live NWS data, with no SDK on the client side, so this is literally what the server sends:

server/discover -> OK, versions=["2026-07-28"]
tools/list      -> get_alerts outputSchema.type = "array"

tools/call keys: ["content","isError","resultType","structuredContent"]
  resultType: "complete"
  structuredContent: ARRAY (8 items)
  content: 1 block(s), serialized JSON

structuredContent is a JSON array at the top level, not an object wrapping one. The single content block is the serialised JSON — the backwards-compatibility fallback the tools specification recommends for structured results.

Driven through the client with a real API key on mcp 2.0.0, the model calls the tool and answers from the structured result:

Connected over protocol 2026-07-28 with tools: ['get_alerts', 'get_forecast']
Query: What weather alerts are active in Texas right now?
[Calling tool get_alerts with args {'state': 'TX'}]
[get_alerts returned 8 items]
Here's a summary of active weather alerts in Texas right now: ...

The 8 items line comes from structured_content, not from re-reading the prose — the two channels going to their stated readers. That run also covers the null-field fix above: one of the eight is a Special Weather Statement with a null instruction, which is exactly the alert that failed before it.

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), with #163 as the shared prerequisite.

#163 has to merge first. The smoke test on main uses an MCP SDK v1 helper which negotiates 2025-11-25, and it rejects this server's array-rooted schema before reaching a tool call, so CI here stays red until that lands. The failure is Handler returned an invalid result from the Python server raising server-side; nothing in this diff causes it.

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

@olaservo olaservo changed the title Add structured output to the Python weather server and client Add structured output to the Python weather server and client and update to v2 Jul 26, 2026
@olaservo
olaservo force-pushed the structured-output-2026-07-28/python branch 4 times, most recently from 0eac5ce to 704cea2 Compare July 26, 2026 03:34
Comment thread mcp-client-python/README.md Outdated
Comment thread weather-server-python/pyproject.toml Outdated
# The 2026-07-28 protocol revision only ships in the mcp 2.0 prereleases, so
# allow them without every command needing --prerelease=allow.
[tool.uv]
prerelease = "allow"

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.

Will remove this before un-drafting.

Comment thread weather-server-python/README.md Outdated
Both tools declare an output schema - the return type annotation - and return
structured_content alongside the text.

get_alerts 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.

The detail worth reading the source for: Alerts is a RootModel[list[Alert]],
not a plain list[Alert]. A list is not a JSON object, so the SDK wraps it as
{"result": [...]} and advertises an object-rooted schema to match, with no
opt-out. A RootModel is a BaseModel, so it is taken as the schema exactly as
written, and a RootModel over a list serializes as the bare list.

Error paths raise: a tool declaring an output schema MUST return conforming
structured content, so a path with no data has to fail.

The client moves to the 2.0 Client API with mode="auto" and prints the
negotiated version on connect. call_tool already revalidates every non-error
result against the declared schema, so the client-side SHOULD needs no code.
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.

This clears the staleness modelcontextprotocol#3124 flags against this
repository - weather.py imported mcp.server.fastmcp, client.py held the
ClientSession version - and matches that PR's idioms: the short
"from mcp.server import MCPServer" spelling, httpx2 with httpx dropped from
the dependencies since httpx2>=2.5.0 is a hard dependency of mcp,
result.content narrowed to TextContent, and input() on a worker thread.

Requires mcp 2.0.0b2; pyproject.toml sets [tool.uv] prerelease = "allow" so
plain uv run and uv sync work without flags. Model identifier moves to
claude-sonnet-5.

Note that an array-rooted schema only works on a 2026-07-28 connection. This
SDK does not project it down for older clients; it raises server-side instead.

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 Python weather MCP server and client to MCP SDK v2 (protocol revision 2026-07-28) and adds explicit structured output via declared outputSchema / structuredContent, including an array-rooted schema for get_alerts.

Changes:

  • Migrates the Python weather server to MCPServer, switches HTTP stack usage to httpx2, and returns Pydantic-typed structured results (array root for alerts, object root for forecast).
  • Migrates the Python client to the v2 Client API with mode="auto" version negotiation and uses structured_content as app data while forwarding only text blocks from content to the model.
  • Updates Python project config/locks and docs to allow prereleases (uv prerelease mode) and document structured output behavior.

Reviewed changes

Copilot reviewed 6 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
weather-server-python/weather.py Moves to MCP SDK v2 server API and returns structured Pydantic models (including array-root alerts).
weather-server-python/uv.lock Updates locked deps for MCP 2.0 prerelease and enables prerelease mode.
weather-server-python/README.md Documents structured output behavior and protocol requirements.
weather-server-python/pyproject.toml Pins mcp[cli]>=2.0.0b2 and configures uv prerelease allowance.
mcp-client-python/client.py Migrates to v2 Client with auto negotiation; forwards only text content and uses structured content as data.
mcp-client-python/uv.lock Updates locked deps for MCP 2.0 prerelease and enables prerelease mode.
mcp-client-python/README.md Documents structured output usage and version negotiation behavior.
mcp-client-python/pyproject.toml Pins mcp>=2.0.0b2 and configures uv prerelease allowance.

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

Comment thread weather-server-python/pyproject.toml Outdated
Comment on lines 7 to 11
# httpx2 is a hard dependency of mcp, so it is not listed here — installing
# `mcp` already brings it in, and adding httpx would install a second HTTP stack.
dependencies = [
"httpx>=0.28.1",
"mcp[cli]>=1.26.0",
"mcp[cli]>=2.0.0b2",
]
Comment on lines 7 to 11
dependencies = [
"anthropic>=0.87.0",
"mcp>=1.28.1",
"mcp>=2.0.0b2",
"python-dotenv>=1.2.2",
]
olaservo and others added 3 commits August 1, 2026 11:08
The SDK left beta on 2026-07-28, so the pins move off 2.0.0b2 and both
projects drop `[tool.uv] prerelease = allow` — plain `uv run` works now.

Also from review: declare the imports rather than lean on transitive
resolution. weather.py imports httpx2 and client.py imports mcp_types, so
both are listed explicitly. The client README no longer implies every
structured result is counted — only array-rooted ones are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`dict.get(key, default)` returns the default only when the key is absent. The
NWS API sends these fields as explicit nulls instead of omitting them, so
`props.get(instruction, ...)` passed None straight into a `str` field and
Pydantic rejected the whole result.

One alert in eight for TX is a Special Weather Statement with a null
instruction, which was enough to fail the entire call:

  Error executing tool get_alerts: 1 validation error for Alert
  instructions
    Input should be a valid string [type=string_type, input_value=None]

The smoke test missed it twice over: it calls get_alerts with CA, whose
alerts happen to have no nulls today, and it reports an isError result as a
skip rather than a failure.

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 Python weather server and client and update to v2 Add structured content to the Python weather server and client and update to v2 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.

3 participants