diff --git a/lib/mcp.rb b/lib/mcp.rb index 84ccce4..afefdfc 100644 --- a/lib/mcp.rb +++ b/lib/mcp.rb @@ -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" diff --git a/lib/mcp/tool/structured_response.rb b/lib/mcp/tool/structured_response.rb new file mode 100644 index 0000000..5b6b58c --- /dev/null +++ b/lib/mcp/tool/structured_response.rb @@ -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. + # @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 diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index 976707b..7c132c4 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -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"