Add structured output to the Ruby weather server and client

The mcp gem 1.1.0 (2026-08-01) is the first to negotiate 2026-07-28, so the
Ruby examples can finally show the same thing the other four do: get_alerts
declares an array-rooted output_schema and returns a bare top-level JSON
array, get_forecast returns an object. Declaring `type:` at the root is what
suppresses the SDK object-root default.

Failure paths return an error result rather than a bare text one, because a
tool declaring an output_schema MUST return conforming structured content.
No alerts is an empty array, not an error.

The client compiles each declared schema at connect time and validates every
non-error result. The SDK client does not do this itself, so the spec
client-side SHOULD costs real code here, as it does in Go and Rust.

NOT READY TO SHIP. The gem never emits the `resultType` field that the
2026-07-28 schema marks mandatory, and no user-level knob sets it, so a
spec-strict client rejects every response including tools/list. Verified
end-to-end against the Ruby client, which does not check the field; blocked
against the TypeScript SDK client. See the server README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
olaservo 2026-08-01 12:07:16 -07:00
commit d913f3a150
3 changed files with 171 additions and 43 deletions

View file

@ -4,4 +4,4 @@ source "https://rubygems.org"
gem "anthropic", '>= 1.60.0'
gem "dotenv"
gem "mcp", '>= 0.15.0'
gem "mcp", ">= 1.1.0"

View file

@ -1,3 +1,11 @@
# An LLM-Powered Chatbot MCP Client written in Ruby
See the [Build an MCP client](https://modelcontextprotocol.io/docs/develop/build-client) tutorial for more information.
## Structured output
The spec says clients SHOULD validate structured results against the schema the tool declares, and this SDK's client does not do it for you — `MCP::Client::Tool#output_schema` hands back the raw schema from the wire and nothing checks results against it. So the client compiles each declared schema once at connect time with `MCP::Tool::OutputSchema.new` and validates every non-error result.
The two channels go to different readers: `content` is forwarded to the model, while `structured_content` is used as data — when a tool returns an array, the client counts its items rather than re-reading the prose. See [Structured Content](https://modelcontextprotocol.io/specification/draft/server/tools#structured-content).
`MCP::Client::Stdio` offers `2026-07-28` by default, so no negotiation setup is needed here.

View file

@ -30,6 +30,13 @@ class MCPClient
@mcp_client = MCP::Client.new(transport: @transport)
@mcp_client.connect
# The spec says clients SHOULD validate structured results against the
# schema the tool declares. This SDK's client does not do it, so compile
# each declared schema once here and check results as they arrive.
@output_schemas = @mcp_client.tools.each_with_object({}) do |tool, schemas|
schemas[tool.name] = MCP::Tool::OutputSchema.new(tool.output_schema) if tool.output_schema
end
tool_names = @mcp_client.tools.map(&:name)
puts "\nConnected to server with tools: #{tool_names}"
end
@ -96,6 +103,15 @@ class MCPClient
result = @mcp_client.call_tool(name: content.name, arguments: content.input)
response_parts << "[Calling tool #{content.name} with args #{content.input.to_json}]"
# structured_content is data the application can use directly; when a
# tool returns an array, count its items rather than re-reading prose.
structured = result.dig("result", "structuredContent")
unless result.dig("result", "isError")
@output_schemas[content.name]&.validate_result(structured)
response_parts << "[#{content.name} returned #{structured.length} items]" if structured.is_a?(Array)
end
# content is what the model reads.
tool_result_content = result.dig("result", "content")
result_text = if tool_result_content.is_a?(Array)
tool_result_content.filter_map { |content_item| content_item["text"] }.join("\n")