Support structuredContent in tool response (#147)
This commit is contained in:
parent
99b57a3cc2
commit
f7cf080cc0
3 changed files with 75 additions and 3 deletions
32
README.md
32
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.
|
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
|
### 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.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,9 @@ module MCP
|
||||||
class Response
|
class Response
|
||||||
NOT_GIVEN = Object.new.freeze
|
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
|
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)
|
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
|
error = deprecated_error
|
||||||
|
|
@ -15,6 +15,7 @@ module MCP
|
||||||
|
|
||||||
@content = content
|
@content = content
|
||||||
@error = error
|
@error = error
|
||||||
|
@structured_content = structured_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def error?
|
def error?
|
||||||
|
|
@ -22,7 +23,7 @@ module MCP
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_h
|
def to_h
|
||||||
{ content:, isError: error? }.compact
|
{ content:, isError: error?, structuredContent: @structured_content }.compact
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,19 @@ module MCP
|
||||||
refute response.error?
|
refute response.error?
|
||||||
end
|
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
|
test "#error? for a standard response" do
|
||||||
response = Response.new(nil, error: false)
|
response = Response.new(nil, error: false)
|
||||||
refute response.error?
|
refute response.error?
|
||||||
|
|
@ -72,6 +85,32 @@ module MCP
|
||||||
assert_equal content, actual[:content]
|
assert_equal content, actual[:content]
|
||||||
assert actual[:isError]
|
assert actual[:isError]
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue