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:
parent
12ef7bc7d6
commit
d913f3a150
3 changed files with 171 additions and 43 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
Loading…
Reference in a new issue