From b629b58478bfbfc6caf430e155952785f24957c4 Mon Sep 17 00:00:00 2001 From: Anandaroop Roy Date: Thu, 25 Sep 2025 23:30:51 -0400 Subject: [PATCH] Support structuredContent in tool response --- README.md | 32 ++++++++++++++++++++++++++++ lib/mcp/tool/response.rb | 7 +++--- test/mcp/tool/response_test.rb | 39 ++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dafaf1a5..d7ad75a2 100644 --- a/README.md +++ b/README.md @@ -555,6 +555,38 @@ MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/2 The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients. +### Tool Responses with Structured Content + +Tools can return structured data alongside text content using the `structured_content` parameter. + +The structured content will be included in the JSON-RPC response as the `structuredContent` field. + +```ruby +class APITool < MCP::Tool + description "Get current weather and return structured data" + + def self.call(endpoint:, server_context:) + # Call weather API and structure the response + api_response = WeatherAPI.fetch(location, units) + weather_data = { + temperature: api_response.temp, + condition: api_response.description, + humidity: api_response.humidity_percent + } + + output_schema.validate_result(weather_data) + + MCP::Tool::Response.new( + [{ + type: "text", + text: weather_data.to_json + }], + structured_content: weather_data + ) + end +end +``` + ### Prompts MCP spec includes [Prompts](https://modelcontextprotocol.io/specification/2025-06-18/server/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs. diff --git a/lib/mcp/tool/response.rb b/lib/mcp/tool/response.rb index 1fd3bfb4..51d827df 100644 --- a/lib/mcp/tool/response.rb +++ b/lib/mcp/tool/response.rb @@ -5,9 +5,9 @@ class Tool class Response NOT_GIVEN = Object.new.freeze - attr_reader :content + attr_reader :content, :structured_content - def initialize(content, deprecated_error = NOT_GIVEN, error: false) + def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, structured_content: nil) if deprecated_error != NOT_GIVEN warn("Passing `error` with the 2nd argument of `Response.new` is deprecated. Use keyword argument like `Response.new(content, error: error)` instead.", uplevel: 1) error = deprecated_error @@ -15,6 +15,7 @@ def initialize(content, deprecated_error = NOT_GIVEN, error: false) @content = content @error = error + @structured_content = structured_content end def error? @@ -22,7 +23,7 @@ def error? end def to_h - { content:, isError: error? }.compact + { content:, isError: error?, structuredContent: @structured_content }.compact end end end diff --git a/test/mcp/tool/response_test.rb b/test/mcp/tool/response_test.rb index 8c2978e5..99bcea35 100644 --- a/test/mcp/tool/response_test.rb +++ b/test/mcp/tool/response_test.rb @@ -38,6 +38,19 @@ class ResponseTest < ActiveSupport::TestCase refute response.error? end + test "#initialize with content and structuredContent" do + content = [{ + type: "text", + text: "{\"code\":401,\"message\":\"Unauthorized\"}", + }] + structured_content = { code: 401, message: "Unauthorized" } + response = Response.new(content, structured_content: structured_content) + + assert_equal content, response.content + assert_equal structured_content, response.structured_content + refute response.error? + end + test "#error? for a standard response" do response = Response.new(nil, error: false) refute response.error? @@ -72,6 +85,32 @@ class ResponseTest < ActiveSupport::TestCase assert_equal content, actual[:content] assert actual[:isError] end + + test "#to_h for a standard response with content and structured content" do + content = [{ + type: "text", + text: "{\"code\":401,\"message\":\"Unauthorized\"}", + }] + structured_content = { code: 401, message: "Unauthorized" } + response = Response.new(content, structured_content: structured_content) + actual = response.to_h + + assert_equal [:content, :isError, :structuredContent].sort, actual.keys.sort + assert_equal content, actual[:content] + assert_equal structured_content, actual[:structuredContent] + refute actual[:isError] + end + + test "#to_h for a standard response with structured content only" do + structured_content = { code: 401, message: "Unauthorized" } + response = Response.new(structured_content: structured_content) + actual = response.to_h + + assert_equal [:isError, :structuredContent].sort, actual.keys.sort + assert_nil actual[:content] + assert_equal structured_content, actual[:structuredContent] + refute actual[:isError] + end end end end