Skip to content

Commit f7cf080

Browse files
authored
Support structuredContent in tool response (#147)
1 parent 99b57a3 commit f7cf080

File tree

3 files changed

+75
-3
lines changed

3 files changed

+75
-3
lines changed

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -555,6 +555,38 @@ MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/2
555555

556556
The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.
557557

558+
### Tool Responses with Structured Content
559+
560+
Tools can return structured data alongside text content using the `structured_content` parameter.
561+
562+
The structured content will be included in the JSON-RPC response as the `structuredContent` field.
563+
564+
```ruby
565+
class APITool < MCP::Tool
566+
description "Get current weather and return structured data"
567+
568+
def self.call(endpoint:, server_context:)
569+
# Call weather API and structure the response
570+
api_response = WeatherAPI.fetch(location, units)
571+
weather_data = {
572+
temperature: api_response.temp,
573+
condition: api_response.description,
574+
humidity: api_response.humidity_percent
575+
}
576+
577+
output_schema.validate_result(weather_data)
578+
579+
MCP::Tool::Response.new(
580+
[{
581+
type: "text",
582+
text: weather_data.to_json
583+
}],
584+
structured_content: weather_data
585+
)
586+
end
587+
end
588+
```
589+
558590
### Prompts
559591

560592
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.

lib/mcp/tool/response.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,25 @@ class Tool
55
class Response
66
NOT_GIVEN = Object.new.freeze
77

8-
attr_reader :content
8+
attr_reader :content, :structured_content
99

10-
def initialize(content, deprecated_error = NOT_GIVEN, error: false)
10+
def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, structured_content: nil)
1111
if deprecated_error != NOT_GIVEN
1212
warn("Passing `error` with the 2nd argument of `Response.new` is deprecated. Use keyword argument like `Response.new(content, error: error)` instead.", uplevel: 1)
1313
error = deprecated_error
1414
end
1515

1616
@content = content
1717
@error = error
18+
@structured_content = structured_content
1819
end
1920

2021
def error?
2122
!!@error
2223
end
2324

2425
def to_h
25-
{ content:, isError: error? }.compact
26+
{ content:, isError: error?, structuredContent: @structured_content }.compact
2627
end
2728
end
2829
end

test/mcp/tool/response_test.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ class ResponseTest < ActiveSupport::TestCase
3838
refute response.error?
3939
end
4040

41+
test "#initialize with content and structuredContent" do
42+
content = [{
43+
type: "text",
44+
text: "{\"code\":401,\"message\":\"Unauthorized\"}",
45+
}]
46+
structured_content = { code: 401, message: "Unauthorized" }
47+
response = Response.new(content, structured_content: structured_content)
48+
49+
assert_equal content, response.content
50+
assert_equal structured_content, response.structured_content
51+
refute response.error?
52+
end
53+
4154
test "#error? for a standard response" do
4255
response = Response.new(nil, error: false)
4356
refute response.error?
@@ -72,6 +85,32 @@ class ResponseTest < ActiveSupport::TestCase
7285
assert_equal content, actual[:content]
7386
assert actual[:isError]
7487
end
88+
89+
test "#to_h for a standard response with content and structured content" do
90+
content = [{
91+
type: "text",
92+
text: "{\"code\":401,\"message\":\"Unauthorized\"}",
93+
}]
94+
structured_content = { code: 401, message: "Unauthorized" }
95+
response = Response.new(content, structured_content: structured_content)
96+
actual = response.to_h
97+
98+
assert_equal [:content, :isError, :structuredContent].sort, actual.keys.sort
99+
assert_equal content, actual[:content]
100+
assert_equal structured_content, actual[:structuredContent]
101+
refute actual[:isError]
102+
end
103+
104+
test "#to_h for a standard response with structured content only" do
105+
structured_content = { code: 401, message: "Unauthorized" }
106+
response = Response.new(structured_content: structured_content)
107+
actual = response.to_h
108+
109+
assert_equal [:isError, :structuredContent].sort, actual.keys.sort
110+
assert_nil actual[:content]
111+
assert_equal structured_content, actual[:structuredContent]
112+
refute actual[:isError]
113+
end
75114
end
76115
end
77116
end

0 commit comments

Comments
 (0)