Skip to content

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

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

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

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 Python examples onto MCP SDK 2.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",  ... }
]

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.

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.

Matched to modelcontextprotocol/modelcontextprotocol#3124 so the tutorials and this code agree: the short from mcp.server import MCPServer spelling; httpx2 instead of httpx, with httpx dropped from the dependencies because httpx2>=2.5.0 is a hard dependency of mcp and listing it installed a second HTTP stack; result.content narrowed to TextContent before being forwarded 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 (10953 bytes)
  content: 1 block(s)
    [0] type=text bytes=11278 serialized-JSON=true

structuredContent is a JSON array at the top level, not an object wrapping one, and the tool's 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, the model calls the tool and answers from the structured result:

Connected over protocol 2026-07-28 with tools: ['get_alerts', 'get_forecast']
[Calling tool get_alerts with args {'state': 'TX'}]
Here are the active weather alerts in Texas: ...

Draft status

Two separate gates:

1. #163 has to merge first. The smoke test on main uses an MCP SDK v1 helper, which negotiates 2025-11-25.

2. The revision has to ship. 2026-07-28 support is only in the mcp 2.0 prereleases, so this pins mcp[cli]>=2.0.0b2 and sets [tool.uv] prerelease = "allow" in pyproject.toml so plain uv run and uv sync work without extra flags. Ideally this PR waits for mcp 2.0 stable and the pin is the only line that changes.

I will rebase and mark ready once both are cleared.

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


🤖 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

`Client(transport, mode="auto")` probes `server/discover` and falls back to the `2025-11-25` handshake; `client.protocol_version` reports which era you got. See [Protocol versions](https://py.sdk.modelcontextprotocol.io/v2/protocol-versions/).

Requires the `mcp` 2.0 prereleases, so `pyproject.toml` sets `[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.

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


Note that an array-rooted schema requires a `2026-07-28` client. This SDK does not project it down for older ones — it raises instead.

Requires the `mcp` 2.0 prereleases, so `pyproject.toml` sets `[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.

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 <[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