Skip to content

[tools] add support for structured responses #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/mcp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
require_relative "mcp/tool"
require_relative "mcp/tool/input_schema"
require_relative "mcp/tool/response"
require_relative "mcp/tool/structured_response"
require_relative "mcp/tool/annotations"
require_relative "mcp/transport"
require_relative "mcp/version"
Expand Down
24 changes: 24 additions & 0 deletions lib/mcp/tool/structured_response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

module MCP
class Tool
class StructuredResponse
attr_reader :content, :is_error

# @param structured_content [Hash] The structured content of the response, must be provided.
# @param content [String, nil] The content array of the response, can be nil. If nil will generate a single element with structured content converted to JSON string.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For backwards compatibility, a tool that returns structured content SHOULD also return the serialized JSON in a TextContent block.

Not a MUST, but a SHOULD, but I agree with doing it for backwards compatibility in this gem.

# @param is_error [Boolean] Indicates if the response is an error.
def initialize(structured_content, content: nil, is_error: false)
raise ArgumentError, "structured_content must be provided" if structured_content.nil?

@structured_content = structured_content
@content = content || [{ type: :text, text: @structured_content.to_json }]
@is_error = is_error
end

def to_h
{ content:, structuredContent: @structured_content, isError: is_error }.compact
end
end
end
end
25 changes: 25 additions & 0 deletions test/mcp/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,31 @@ class ServerTest < ActiveSupport::TestCase
assert_instrumentation_data({ method: "tools/call", tool_name: })
end

test "#handle_json tools/call executes tool and returns structured response result" do
tool_name = "test_tool"
tool_args = { arg: "value" }
tool_response = Tool::StructuredResponse.new({ result: "success" })

@tool.expects(:call).with(arg: "value", server_context: nil).returns(tool_response)

request = JSON.generate({
jsonrpc: "2.0",
method: "tools/call",
params: { name: tool_name, arguments: tool_args },
id: 1,
})
expected_response = {
content: [{ type: "text", text: "{\"result\":\"success\"}" }],
structuredContent: { result: "success" },
isError: false,
}

raw_response = @server.handle_json(request)
response = JSON.parse(raw_response, symbolize_names: true) if raw_response
assert_equal expected_response, response[:result] if response
assert_instrumentation_data({ method: "tools/call", tool_name: })
end

test "#handle_json tools/call executes tool and returns result, when the tool is typed with Sorbet" do
class TypedTestTool < Tool
tool_name "test_tool"
Expand Down
2 changes: 1 addition & 1 deletion test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

require_relative "instrumentation_test_helper"

Minitest::Reporters.use!(Minitest::Reporters::ProgressReporter.new)
Minitest::Reporters.use!(Minitest::Reporters::ProgressReporter.new) unless ENV["RM_INFO"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain where/when RM_INFO comes from? (Explicitly calling out this change in the PR description or punting it to its specific PR would be favourable.)


module ActiveSupport
class TestCase
Expand Down