Add structured content to the TypeScript weather server and client and update to v2 - #165
Conversation
Both tools declare an outputSchema and return structuredContent alongside the
text.
get-alerts declares z.array(...), 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.
This costs older clients nothing. The server declares the array schema once
and never branches on protocol version: serveStdio serves both eras from one
factory, and the SDK projects the schema down to
{"type":"object","properties":{"result":...}} for a 2025-11-25 client, wrapping
the structured content to match. Verified against both eras.
Error paths throw: a tool declaring an outputSchema MUST return conforming
structured content, so a path with no data has to fail.
The client passes versionNegotiation {mode:"auto"}; the SDK default is
"legacy". The SDK validates every 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, structuredContent is used as data,
reporting how many items came back.
Moves to the 2.0 beta packages, where the 2026-07-28 support lives:
@modelcontextprotocol/sdk is replaced by @modelcontextprotocol/server and
@modelcontextprotocol/client. Model identifier moves to claude-sonnet-5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Updates the TypeScript weather server/client examples to MCP SDK v2 (split client/server packages) and adds declared tool outputSchema with matching structuredContent, including an array-rooted schema for get-alerts to align with protocol revision 2026-07-28.
Changes:
- Migrate server to
@modelcontextprotocol/serverand useserveStdio(buildServer)with per-tooloutputSchema+structuredContentforget-alerts(array) andget-forecast(object). - Migrate client to
@modelcontextprotocol/client, enable automatic protocol version negotiation, and surface basic structured-output handling. - Update READMEs and dependency manifests/lockfiles for the new package split and Zod v4 usage.
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-typescript/src/index.ts | Switches to v2 server APIs, adds output schemas + structured results, and refactors server startup via serveStdio. |
| weather-server-typescript/README.md | Documents structured output behavior and backward-compat projection. |
| weather-server-typescript/package.json | Replaces @modelcontextprotocol/sdk with @modelcontextprotocol/server and adds zod. |
| weather-server-typescript/package-lock.json | Locks updated dependency graph for server v2 + Zod v4. |
| mcp-client-typescript/index.ts | Switches to v2 client APIs, enables versionNegotiation: auto, and distinguishes content vs structuredContent. |
| mcp-client-typescript/README.md | Documents structured output handling and version negotiation. |
| mcp-client-typescript/package.json | Replaces @modelcontextprotocol/sdk with @modelcontextprotocol/client. |
| mcp-client-typescript/package-lock.json | Locks updated dependency graph for client v2. |
Files not reviewed (2)
- mcp-client-typescript/package-lock.json: Generated file
- weather-server-typescript/package-lock.json: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The 2.0 packages left beta on 2026-07-27, so the dependency ranges move off 2.0.0-beta.5. Also from review: engines.node goes to 20, which is what the 2.0 packages require, and 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>
get-alerts asked for /alerts?area=XX, which returns every alert NWS has on file for the state including expired ones. The Python, Go and Rust servers all ask for /alerts/active/area/XX. Measured against the live API for TX: 372 alerts before, 8 after, and the tools/call response drops from roughly 270KB to 5KB. The other four servers return the same 8. Pre-existing on main rather than introduced by the structured-output work, but it is the one thing that stopped the examples being comparable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
get-alerts published status and headline where Python, Go, Rust and Ruby all
publish description and instructions, and get-forecast periods used the NWS
camelCase spelling with shortForecast instead of detailed_forecast. The
divergence predates this round, but declaring an outputSchema turns it from a
difference in prose formatting into a difference in the published contract,
which defeats the point of a five-language example.
All five now advertise:
get_alerts -> [{event, area, severity, description, instructions}]
get_forecast -> {latitude, longitude, periods[{name, temperature,
temperature_unit, wind_speed, wind_direction,
detailed_forecast}]}
Also limits the forecast to the next 5 periods, which the other four already
did, and uses `??` on the alert fields because NWS sends explicit nulls
rather than omitting them.
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>
…6-07-28/typescript
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 8 changed files in this pull request and generated no new comments.
Files not reviewed (2)
- mcp-client-typescript/package-lock.json: Generated file
- weather-server-typescript/package-lock.json: Generated file
Suppressed comments (3)
weather-server-typescript/src/index.ts:226
- The new structured result turns a missing temperature into the valid-looking value
0°F.ForecastPeriod.temperatureis explicitly optional, so an incomplete upstream period will now publish incorrect machine-readable weather data rather than fail. Preserve the non-null output contract by rejecting a period that lacks its temperature (or unit) before constructing the result.
periods: rawPeriods.slice(0, 5).map((period) => ({
name: period.name ?? "Unknown",
temperature: period.temperature ?? 0,
temperature_unit: period.temperatureUnit ?? "F",
mcp-client-typescript/package.json:23
- The manifest now requires Node 20, but the root entry in
mcp-client-typescript/package-lock.jsonstill recordsengines.nodeas>=16.0.0. Regenerate/update the lockfile so installs and tooling see the same runtime contract.
"node": ">=20.0.0"
weather-server-typescript/package.json:25
- This dependency requires Node 20+, but unlike the updated client, the weather server package does not declare an
engines.nodefloor. Consumers on Node 18 can therefore select/install this example as if it were supported and only encounter the incompatibility through a transitive engine warning or at runtime. Add the Node 20 engine requirement here and refresh the lockfile root metadata.
"@modelcontextprotocol/server": "^2.0.0",
"zod": "^4.4.3"
|
|
||
| ## Structured content | ||
|
|
||
| Both tools declare an `outputSchema` and return `structuredContent`. `get-forecast` returns an object; `get-alerts` returns a top-level JSON array, which protocol revision `2026-07-28` is the first to allow — see [Structured Content](https://modelcontextprotocol.io/specification/draft/server/tools#structured-content) in the spec. |
There was a problem hiding this comment.
Let's change the url from draft to https://modelcontextprotocol.io/specification/2026-07-28/server/tools#structured-content ?
| .array( | ||
| z.object({ | ||
| name: z.string(), | ||
| temperature: z.number(), |
There was a problem hiding this comment.
Suggest making it nullable so we can set it to null later (not to 0)
temperature: z.number().nullable(),
| // Only show the next 5 periods. | ||
| periods: rawPeriods.slice(0, 5).map((period) => ({ | ||
| name: period.name ?? "Unknown", | ||
| temperature: period.temperature ?? 0, |
There was a problem hiding this comment.
Suggest chaning it to:
temperature: period.temperature ?? null,
| process.exit(1); | ||
| }); | ||
| // One factory serves both protocol eras. | ||
| serveStdio(buildServer); |
There was a problem hiding this comment.
Suggest error handling:
serveStdio(buildServer, {
onerror: (error) => {
console.error("Weather MCP Server error:", error);
},
});
| temperature: period.temperature ?? 0, | ||
| temperature_unit: period.temperatureUnit ?? "F", | ||
| wind_speed: period.windSpeed ?? "Unknown", | ||
| wind_direction: period.windDirection ?? "", |
There was a problem hiding this comment.
Should we also set it to Unknown?
wind_direction: period.windDirection ?? "Unknown",
Updates the TypeScript examples to MCP SDK 2.0.0 and protocol revision
2026-07-28, and gives both tools a declaredoutputSchemawith matchingstructuredContent.get-alertsnow returns 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.Backwards compatibility note: The server declares the array schema once and never branches on protocol version — the SDK projects it for whichever era connects, so a
2025-11-25client still sees{"type":"object","properties":{"result":{...}}}and gets{"result":[...]}. Verified against both.Two pre-existing bugs fixed here
Both stopped the five examples being comparable. Declaring an
outputSchematurns them from formatting differences into contract differences, which is why they're fixed rather than left.get-alertsqueried/alerts?area=XX(every alert ever, including expired) where the other four query/alerts/active/area/XX. For TX: 372 alerts before, 8 after, and the response drops from ~270KB to ~5KB.get-alertspublished{event, area, severity, status, headline}against{event, area, severity, description, instructions}everywhere else, and forecast periods used NWS camelCase and weren't capped at 5. All five examples now advertise identical shapes.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",structuredContentan 8-item array,contentone block of prose. Called against all five servers in the same minute — identical values from every one.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