Skip to content

Add structured output to the Go weather server and client#166

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

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

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 Go examples onto protocol revision 2026-07-28 and gives both tools a declared OutputSchema with matching StructuredContent.

get_alerts returns []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 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 declared schema narrows the inferred ["null","array"] root to "array": jsonschema-go infers the nullable form because a nil slice is valid Go, but the handler always builds a slice.

Other changes

Error paths return an error. A tool declaring an OutputSchema 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. jsonschema-go is already an SDK dependency, so this adds none.

Each channel goes to its stated reader. Content is forwarded to the model; StructuredContent is used as data, reporting how many items came back.

A missing .env is no longer fatal, and ANTHROPIC_API_KEY is checked after connecting rather than before, matching the Python and TypeScript clients. That is what lets the connection be exercised without credentials.

Model identifier moves to claude-sonnet-5. The second call in the tool loop used a hardcoded ModelClaude3_7SonnetLatest while the first used the package variable; both now use the same one.

Requires go-sdk v1.7.0-pre.3 — non-object output schema support (SEP-2106) landed after v1.6.1 and is in no stable release.

One caveat

The SDK 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:

Invalid result for tools/list: path ["tools", 0, "outputSchema", "type"], expected "object"

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: 2 block(s)

Two blocks because the SDK appends the serialised JSON as a backwards-compatibility fallback alongside the tool's own text.

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. CI here is green, but only because main's smoke test does not cover the Go examples at all — nothing in this diff is exercised by it. That coverage lands once #163 is in, since it depends on helpers introduced there. Until then this PR is verified by the raw-wire capture above, not by CI.

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 OutputSchema and return StructuredContent alongside the
text.

get_alerts returns []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. The declared schema narrows the inferred ["null","array"] root to
"array": jsonschema-go infers the nullable form because a nil slice is valid
Go, but the handler always builds a slice.

Error paths return an error: a tool declaring an OutputSchema 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, using jsonschema-go - already an SDK dependency, so no new
one. Each channel goes to its stated reader: Content is forwarded to the
model, StructuredContent is used as data, reporting how many items came back.

The client no longer treats a missing .env as fatal and checks for
ANTHROPIC_API_KEY after connecting rather than before, matching the Python and
TypeScript clients, so the connection can be exercised without credentials.

Requires go-sdk v1.7.0-pre.3: non-object output schema support (SEP-2106)
landed after v1.6.1 and is in no stable release. Model identifier moves to
claude-sonnet-5; the second call in the tool loop used a hardcoded
ModelClaude3_7SonnetLatest while the first used the package variable, so both
now use the same one.

Note that the SDK 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