diff --git a/Gemfile b/Gemfile index 893c454..9454344 100644 --- a/Gemfile +++ b/Gemfile @@ -10,6 +10,5 @@ gem "minitest", "~> 5.1", require: false gem "rake", "~> 13.0" gem "rubocop-shopify", require: false -gem "activesupport" gem "minitest-reporters" gem "mocha" diff --git a/Gemfile.lock b/Gemfile.lock index 628866b..d52c94b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,8 +1,8 @@ PATH remote: . specs: - model_context_protocol (0.2.2) - activesupport + model_context_protocol (0.2.4) + json_rpc_handler (~> 0.1) GEM remote: https://rubygems.org/ @@ -32,6 +32,7 @@ GEM i18n (1.14.7) concurrent-ruby (~> 1.0) json (2.11.3) + json_rpc_handler (0.1.1) language_server-protocol (3.17.0.4) lint_roller (1.1.0) logger (1.7.0) diff --git a/README.md b/README.md index ca0f1c3..022b094 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ MCP spec includes [Tools](https://modelcontextprotocol.io/docs/concepts/tools) w This gem provides a `ModelContextProtocol::Tool` class that can be used to create tools in two ways: 1. As a class definition: + ```ruby class MyTool < ModelContextProtocol::Tool tool_name "my_tool" @@ -77,6 +78,7 @@ tool = MyTool.new ``` 2. By using the `ModelContextProtocol::Tool.define` method with a block: + ```ruby tool = ModelContextProtocol::Tool.define(name: "my_tool", description: "This tool performs specific functionality...") do |args| Tool::Response.new([{ type: "text", content: "OK" }]) @@ -90,6 +92,7 @@ MCP spec includes [Prompts](https://modelcontextprotocol.io/docs/concepts/prompt The `ModelContextProtocol::Prompt` class provides two ways to create prompts: 1. As a class definition with metadata: + ```ruby class MyPrompt < ModelContextProtocol::Prompt prompt_name "my_prompt" # Optional - defaults to underscored class name @@ -121,6 +124,7 @@ end ``` 2. Using the `ModelContextProtocol::Prompt.define` method: + ```ruby prompt = ModelContextProtocol::Prompt.define( name: "my_prompt", @@ -168,6 +172,7 @@ server = ModelContextProtocol::Server.new( ``` The server will handle prompt listing and execution through the MCP protocol methods: + - `prompts/list` - Lists all registered prompts and their schemas - `prompts/get` - Retrieves and executes a specific prompt with arguments diff --git a/lib/model_context_protocol.rb b/lib/model_context_protocol.rb index 56d0763..6444991 100644 --- a/lib/model_context_protocol.rb +++ b/lib/model_context_protocol.rb @@ -1,8 +1,8 @@ # typed: strict # frozen_string_literal: true -require "model_context_protocol/json_rpc" require "model_context_protocol/server" +require "model_context_protocol/string_utils" require "model_context_protocol/tool" require "model_context_protocol/content" require "model_context_protocol/resource" diff --git a/lib/model_context_protocol/json_rpc/error.rb b/lib/model_context_protocol/json_rpc/error.rb deleted file mode 100644 index bb9e125..0000000 --- a/lib/model_context_protocol/json_rpc/error.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true - -module ModelContextProtocol - module JsonRPC - class Error < StandardError - PARSE_ERROR_CODE = -32700 - INVALID_REQUEST_CODE = -32600 - METHOD_NOT_FOUND_CODE = -32601 - INVALID_PARAMS_CODE = -32602 - INTERNAL_ERROR_CODE = -32603 - - attr_reader :code, :message, :data - - def initialize(code:, message:, data: nil) - super(message) - @code = code - @message = message - @data = data - end - - def to_h - { code:, message:, data: }.compact - end - end - - class InvalidRequestError < Error - def initialize(message:) - super(code: INVALID_REQUEST_CODE, message: message) - end - end - - class ParseError < Error - def initialize(message:) - super(code: PARSE_ERROR_CODE, message: message) - end - end - - class MethodNotFoundError < Error - def initialize(message:) - super(code: METHOD_NOT_FOUND_CODE, message: message) - end - end - - class InvalidParamsError < Error - def initialize(message:) - super(code: INVALID_PARAMS_CODE, message: message) - end - end - - class InternalError < Error - def initialize(message:) - super(code: INTERNAL_ERROR_CODE, message: message) - end - end - - class InvalidResponse < StandardError; end - end -end diff --git a/lib/model_context_protocol/json_rpc/request.rb b/lib/model_context_protocol/json_rpc/request.rb deleted file mode 100644 index dcb6482..0000000 --- a/lib/model_context_protocol/json_rpc/request.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -require "json" - -module ModelContextProtocol - module JsonRPC - class Request - class << self - def parse(json) - parsed = JSON.parse(json) - from_h(parsed) - rescue JSON::ParserError => e - raise ParseError.new(message: e.message) - end - - private - - def from_h(hash) - new(version: hash["jsonrpc"], method: hash["method"], params: hash["params"], id: hash["id"]) - end - end - - attr_reader :version, :method, :params, :id - - def initialize(version:, method:, params: nil, id: nil) - @version = version - @method = method - @params = params - @id = id - end - - def notification? - id.nil? - end - - def valid? - @version == JsonRPC::VERSION && !method.start_with?("rpc.") - end - - def method_not_found! - raise MethodNotFoundError.new(message: "Method not found #{method}") - end - - def validate! - raise InvalidRequestError.new(message: "Method cannot start with 'rpc.'") if method.start_with?("rpc.") - raise InvalidRequestError.new(message: "Invalid JSON-RPC version") unless valid? - end - end - end -end diff --git a/lib/model_context_protocol/json_rpc/response.rb b/lib/model_context_protocol/json_rpc/response.rb deleted file mode 100644 index e2a81fa..0000000 --- a/lib/model_context_protocol/json_rpc/response.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module ModelContextProtocol - module JsonRPC - class Response - attr_reader :version, :result, :error, :id - - def initialize(result: nil, error: nil, id: nil) - @version = VERSION - @result = result - @error = error - @id = id - - validate! - end - - def to_h - { id:, result:, jsonrpc: @version, error: @error&.to_h }.compact - end - - private - - def validate! - raise InvalidResponse, "Result and error cannot both be present" if @result && @error - raise InvalidResponse, "Either result or error must be present" if @result.nil? && @error.nil? - end - end - end -end diff --git a/lib/model_context_protocol/prompt.rb b/lib/model_context_protocol/prompt.rb index a4b3cab..7bb329a 100644 --- a/lib/model_context_protocol/prompt.rb +++ b/lib/model_context_protocol/prompt.rb @@ -60,7 +60,7 @@ module ModelContextProtocol end def name_value - @name_value || name.demodulize.underscore + @name_value || StringUtils.handle_from_class_name(name) end def description(value) diff --git a/lib/model_context_protocol/server.rb b/lib/model_context_protocol/server.rb index bd66b3d..1145282 100644 --- a/lib/model_context_protocol/server.rb +++ b/lib/model_context_protocol/server.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "json_rpc_handler" + module ModelContextProtocol class Server PROTOCOL_VERSION = "2024-11-05" @@ -26,20 +28,20 @@ module ModelContextProtocol end def handle(request) - response = begin - parsed_request = JsonRPC::Request.parse(request) - parsed_request.validate! - - if parsed_request.notification? - handle_notification(parsed_request) + JsonRpcHandler.handle(request) do |method| + handler = case method + when "tools/list" + ->(params) { { tools: @handlers["tools/list"].call(params) } } + when "prompts/list" + ->(params) { { prompts: @handlers["prompts/list"].call(params) } } + when "resources/list" + ->(params) { { resources: @handlers["resources/list"].call(params) } } else - handle_method(parsed_request) + @handlers[method] end - rescue JsonRPC::Error => e - JsonRPC::Response.new(id: parsed_request&.id, error: e) - end - response + handler + end end def resources_list_handler(&block) @@ -86,29 +88,6 @@ module ModelContextProtocol } end - def handle_notification(request) - nil - end - - def handle_method(request) - request_handler = @handlers[request.method] - raise JsonRPC::MethodNotFoundError.new(message: "Method not found #{request.method}") unless request_handler - - result = request_handler.call(request) - wrapped_result = case request.method - when "tools/list" - { tools: result } - when "prompts/list" - { prompts: result } - when "resources/list" - { resources: result } - else - result - end - - JsonRPC::Response.new(id: request.id, result: wrapped_result) - end - def init(request) { protocolVersion: PROTOCOL_VERSION, @@ -121,43 +100,33 @@ module ModelContextProtocol "pong" end - def call_tool(request) - tool_name = request.params&.dig("name") - tool = tools[tool_name] - raise JsonRPC::MethodNotFoundError.new(message: "Tool not found #{tool_name}") unless tool - - tool_args = request.params&.dig("arguments") - - result = begin - tool.call(**tool_args) - rescue => e - raise JsonRPC::InternalError.new(message: e.message) - end - - result.to_h - end - def list_tools(request) @tools.map { |_, tool| tool.to_h } end + def call_tool(request) + tool_name = request[:name] + tool = tools[tool_name] + raise "Tool not found #{tool_name}" unless tool + + result = tool.call(**request[:arguments]) + result.to_h + end + def list_prompts(request) @prompts.map { |_, prompt| prompt.to_h } end def get_prompt(request) - prompt_name = request.params&.dig("name") + prompt_name = request[:name] prompt = @prompts[prompt_name] - raise JsonRPC::MethodNotFoundError.new(message: "Prompt not found #{prompt_name}") unless prompt - begin - prompt_args = request.params&.dig("arguments") - prompt.validate_arguments!(prompt_args) + raise "Prompt not found #{prompt_name}" unless prompt - result = prompt.template(prompt_args) - rescue ArgumentError => e - raise JsonRPC::InvalidParamsError.new(message: e.message) - end + prompt_args = request[:arguments] + prompt.validate_arguments!(prompt_args) + + result = prompt.template(prompt_args) result.to_h end @@ -167,10 +136,10 @@ module ModelContextProtocol end def read_resource(request) - resource_uri = request.params&.dig("uri") + resource_uri = request[:uri] resource = @resource_index[resource_uri] - raise JsonRPC::MethodNotFoundError.new(message: "Resource not found #{resource_uri}") unless resource + raise "Resource not found #{resource_uri}" unless resource resource.to_h end diff --git a/lib/model_context_protocol/string_utils.rb b/lib/model_context_protocol/string_utils.rb new file mode 100644 index 0000000..08bd5a8 --- /dev/null +++ b/lib/model_context_protocol/string_utils.rb @@ -0,0 +1,26 @@ +# typed: strict +# frozen_string_literal: true + +module ModelContextProtocol + module StringUtils + extend self + + def handle_from_class_name(class_name) + underscore(demodulize(class_name)) + end + + private + + def demodulize(path) + path.to_s.split("::").last || path.to_s + end + + def underscore(camel_cased_word) + camel_cased_word.dup + .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') + .gsub(/([a-z\d])([A-Z])/, '\1_\2') + .tr("-", "_") + .downcase + end + end +end diff --git a/lib/model_context_protocol/tool.rb b/lib/model_context_protocol/tool.rb index a22d452..2ccfbae 100644 --- a/lib/model_context_protocol/tool.rb +++ b/lib/model_context_protocol/tool.rb @@ -31,7 +31,7 @@ module ModelContextProtocol end def name_value - @name_value || name.demodulize.underscore + @name_value || StringUtils.handle_from_class_name(name) end def description(value) diff --git a/lib/model_context_protocol/version.rb b/lib/model_context_protocol/version.rb index 7a6743c..471886b 100644 --- a/lib/model_context_protocol/version.rb +++ b/lib/model_context_protocol/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ModelContextProtocol - VERSION = "0.2.2" + VERSION = "0.2.4" end diff --git a/model_context_protocol.gemspec b/model_context_protocol.gemspec index 2484af9..da6c422 100644 --- a/model_context_protocol.gemspec +++ b/model_context_protocol.gemspec @@ -27,5 +27,6 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ["lib"] - spec.add_dependency("activesupport") + spec.add_dependency("json_rpc_handler", "~> 0.1") + spec.add_development_dependency("activesupport") end diff --git a/test/model_context_protocol/json_rpc/error_test.rb b/test/model_context_protocol/json_rpc/error_test.rb deleted file mode 100644 index f02091b..0000000 --- a/test/model_context_protocol/json_rpc/error_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -module ModelContextProtocol - module JsonRPC - class ErrorTest < ActiveSupport::TestCase - test "Error#initialize sets attributes" do - code = -32700 - message = "Test error" - data = { foo: "bar" } - error = Error.new(code: code, message: message, data: data) - - assert_equal code, error.code - assert_equal message, error.message - assert_equal data, error.data - end - - test "Error#to_h returns hash representation" do - error = Error.new(code: -32700, message: "Test error", data: { foo: "bar" }) - expected = { - code: -32700, - message: "Test error", - data: { foo: "bar" }, - } - assert_equal expected, error.to_h - end - - test "Error#to_h excludes nil data" do - error = Error.new(code: -32700, message: "Test error", data: nil) - expected = { - code: -32700, - message: "Test error", - } - assert_equal expected, error.to_h - end - - test "InvalidRequestError sets correct code" do - error = InvalidRequestError.new(message: "Invalid request") - assert_equal Error::INVALID_REQUEST_CODE, error.code - assert_equal "Invalid request", error.message - end - - test "ParseError sets correct code" do - error = ParseError.new(message: "Parse error") - assert_equal Error::PARSE_ERROR_CODE, error.code - assert_equal "Parse error", error.message - end - - test "MethodNotFoundError sets correct code" do - error = MethodNotFoundError.new(message: "Method not found") - assert_equal Error::METHOD_NOT_FOUND_CODE, error.code - assert_equal "Method not found", error.message - end - - test "InvalidParamsError sets correct code" do - error = InvalidParamsError.new(message: "Invalid params") - assert_equal Error::INVALID_PARAMS_CODE, error.code - assert_equal "Invalid params", error.message - end - end - end -end diff --git a/test/model_context_protocol/json_rpc/request_test.rb b/test/model_context_protocol/json_rpc/request_test.rb deleted file mode 100644 index 4c6fd09..0000000 --- a/test/model_context_protocol/json_rpc/request_test.rb +++ /dev/null @@ -1,106 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -module ModelContextProtocol - module JsonRPC - class RequestTest < ActiveSupport::TestCase - test "Request.parse parses valid JSON-RPC request" do - json = { - jsonrpc: "2.0", - method: "test_method", - params: { foo: "bar" }, - id: 1, - }.to_json - - request = Request.parse(json) - assert_equal "2.0", request.version - assert_equal "test_method", request.method - assert_equal({ "foo" => "bar" }, request.params) - assert_equal 1, request.id - end - - test "Request.parse raises ParseError for invalid JSON" do - assert_raises(ParseError) do - Request.parse("invalid json") - end - end - - test "Request#initialize sets attributes" do - request = Request.new( - version: "2.0", - method: "test_method", - params: { "foo" => "bar" }, - id: 1, - ) - - assert_equal "2.0", request.version - assert_equal "test_method", request.method - assert_equal({ "foo" => "bar" }, request.params) - assert_equal 1, request.id - end - - test "Request#notification? returns true when id is nil" do - request = Request.new( - version: "2.0", - method: "test_method", - ) - assert request.notification? - end - - test "Request#notification? returns false when id is present" do - request = Request.new( - version: "2.0", - method: "test_method", - id: 1, - ) - refute request.notification? - end - - test "Request#valid? returns true for valid request" do - request = Request.new( - version: "2.0", - method: "test_method", - ) - assert request.valid? - end - - test "Request#valid? returns false for invalid version" do - request = Request.new( - version: "1.0", - method: "test_method", - ) - refute request.valid? - end - - test "Request#validate! raises InvalidRequestError for invalid version" do - assert_raises(InvalidRequestError) do - Request.new( - version: "1.0", - method: "test_method", - ).validate! - end - end - - test "Request#validate! raises InvalidRequestError for rpc prefixed method" do - assert_raises(InvalidRequestError) do - Request.new( - version: "2.0", - method: "rpc.test", - ).validate! - end - end - - test "Request#method_not_found! raises MethodNotFoundError" do - request = Request.new( - version: "2.0", - method: "test_method", - ) - - assert_raises(MethodNotFoundError) do - request.method_not_found! - end - end - end - end -end diff --git a/test/model_context_protocol/json_rpc/response_test.rb b/test/model_context_protocol/json_rpc/response_test.rb deleted file mode 100644 index 21cf6da..0000000 --- a/test/model_context_protocol/json_rpc/response_test.rb +++ /dev/null @@ -1,70 +0,0 @@ -# frozen_string_literal: true - -require "test_helper" - -module ModelContextProtocol - module JsonRPC - class ResponseTest < ActiveSupport::TestCase - test "Response#initialize sets attributes with result" do - result = { foo: "bar" } - id = 1 - response = Response.new(result: result, id: id) - - assert_equal "2.0", response.version - assert_equal result, response.result - assert_nil response.error - assert_equal id, response.id - end - - test "Response#initialize sets attributes with error" do - error = Error.new(code: -32700, message: "Test error") - id = 1 - response = Response.new(error: error, id: id) - - assert_equal "2.0", response.version - assert_nil response.result - assert_equal error, response.error - assert_equal id, response.id - end - - test "Response#to_h returns hash representation with result" do - result = { foo: "bar" } - id = 1 - response = Response.new(result: result, id: id) - expected = { - jsonrpc: "2.0", - result: result, - id: id, - } - assert_equal expected, response.to_h - end - - test "Response#to_h returns hash representation with error" do - error = Error.new(code: -32700, message: "Test error") - id = 1 - response = Response.new(error: error, id: id) - expected = { - jsonrpc: "2.0", - error: error.to_h, - id: id, - } - assert_equal expected, response.to_h - end - - test "Response#initialize raises InvalidResponse when both result and error present" do - assert_raises(InvalidResponse) do - Response.new( - result: { foo: "bar" }, - error: Error.new(code: -32700, message: "Test error"), - ) - end - end - - test "Response#initialize raises InvalidResponse when neither result nor error present" do - assert_raises(InvalidResponse) do - Response.new - end - end - end - end -end diff --git a/test/model_context_protocol/server_test.rb b/test/model_context_protocol/server_test.rb index 459d140..ba14653 100644 --- a/test/model_context_protocol/server_test.rb +++ b/test/model_context_protocol/server_test.rb @@ -39,13 +39,13 @@ module ModelContextProtocol jsonrpc: "2.0", method: "ping", id: 1, - }.to_json + } response = @server.handle(request) refute_nil response - assert_equal "pong", response.result - assert_equal 1, response.id + assert_equal "pong", response[:result] + assert_equal 1, response[:id] end test "#handle initialize request returns protocol info, server info, and capabilities" do @@ -53,13 +53,12 @@ module ModelContextProtocol jsonrpc: "2.0", method: "initialize", id: 1, - }.to_json + } response = @server.handle(request) refute_nil response - result = response.result - + result = response[:result] assert_equal Server::PROTOCOL_VERSION, result[:protocolVersion] assert_kind_of Hash, result[:capabilities] assert_equal @server_name, result[:serverInfo][:name] @@ -70,29 +69,24 @@ module ModelContextProtocol request = { jsonrpc: "2.0", method: "some_notification", - }.to_json + } assert_nil @server.handle(request) end - test "#handle returns error response for invalid requests" do - response = @server.handle("invalid json") - assert_instance_of JsonRPC::Response, response - assert_instance_of(JsonRPC::ParseError, response.error) - end - test "#handle tools/list returns available tools" do request = { jsonrpc: "2.0", method: "tools/list", id: 1, - }.to_json + } response = @server.handle(request) - assert_kind_of Array, response.result[:tools] - assert_equal "test_tool", response.result[:tools][0][:name] - assert_equal "Test tool", response.result[:tools][0][:description] - assert_equal({}, response.result[:tools][0][:inputSchema]) + result = response[:result] + assert_kind_of Array, result[:tools] + assert_equal "test_tool", result[:tools][0][:name] + assert_equal "Test tool", result[:tools][0][:description] + assert_equal({}, result[:tools][0][:inputSchema]) end test "#tools_list_handler sets the tools/list handler" do @@ -104,10 +98,11 @@ module ModelContextProtocol jsonrpc: "2.0", method: "tools/list", id: 1, - }.to_json + } response = @server.handle(request) - assert_equal({ tools: [{ name: "hammer", description: "Hammer time!" }] }, response.result) + result = response[:result] + assert_equal({ tools: [{ name: "hammer", description: "Hammer time!" }] }, result) end test "#handle tools/call executes tool and returns result" do @@ -125,10 +120,10 @@ module ModelContextProtocol arguments: tool_args, }, id: 1, - }.to_json + } response = @server.handle(request) - assert_equal tool_response.to_h, response.result + assert_equal tool_response.to_h, response[:result] end test "#handle tools/call returns error if the tool raises an error" do @@ -142,12 +137,12 @@ module ModelContextProtocol arguments: {}, }, id: 1, - }.to_json + } response = @server.handle(request) - assert_instance_of(JsonRPC::InternalError, response.error) - assert_equal "Tool error", response.error&.message + assert_equal "Internal error", response[:error][:message] + assert_equal "Tool error", response[:error][:data] end test "#handle tools/call returns error for unknown tool" do @@ -159,16 +154,16 @@ module ModelContextProtocol arguments: {}, }, id: 1, - }.to_json + } response = @server.handle(request) - assert_instance_of(JsonRPC::Response, response) - assert_instance_of(JsonRPC::MethodNotFoundError, response.error) + assert_equal "Internal error", response[:error][:message] + assert_equal "Tool not found unknown_tool", response[:error][:data] end test "#tools_call_handler sets the tools/call handler" do @server.tools_call_handler do |request| - tool_name = request.params&.dig("name") + tool_name = request[:name] return Tool::Response.new("#{tool_name} called successfully").to_h end @@ -177,10 +172,10 @@ module ModelContextProtocol method: "tools/call", params: { name: "my_tool", arguments: {} }, id: 1, - }.to_json + } response = @server.handle(request) - assert_equal({ content: "my_tool called successfully", is_error: false }, response.result) + assert_equal({ content: "my_tool called successfully", is_error: false }, response[:result]) end test "#handle prompts/list returns list of prompts" do @@ -188,10 +183,10 @@ module ModelContextProtocol jsonrpc: "2.0", method: "prompts/list", id: 1, - }.to_json + } response = @server.handle(request) - assert_equal({ prompts: [@prompt.to_h] }, response.result) + assert_equal({ prompts: [@prompt.to_h] }, response[:result]) end test "#prompts_list_handler sets the prompts/list handler" do @@ -203,10 +198,10 @@ module ModelContextProtocol jsonrpc: "2.0", method: "prompts/list", id: 1, - }.to_json + } response = @server.handle(request) - assert_equal({ prompts: [{ name: "foo_prompt", description: "Foo prompt" }] }, response.result) + assert_equal({ prompts: [{ name: "foo_prompt", description: "Foo prompt" }] }, response[:result]) end test "#handle prompts/get returns templated prompt" do @@ -218,7 +213,7 @@ module ModelContextProtocol name: "test_prompt", arguments: { "test_argument" => "Hello, friend!" }, }, - }.to_json + } expected_result = { description: "Hello, world!", @@ -228,8 +223,7 @@ module ModelContextProtocol } response = @server.handle(request) - - assert_equal(expected_result, response.result) + assert_equal(expected_result, response[:result]) end test "#handle prompts/get returns error if prompt is not found" do @@ -241,11 +235,10 @@ module ModelContextProtocol name: "unknown_prompt", arguments: {}, }, - }.to_json + } response = @server.handle(request) - assert_instance_of(JsonRPC::Response, response) - assert_instance_of(JsonRPC::MethodNotFoundError, response.error) + assert_equal("Prompt not found unknown_prompt", response[:error][:data]) end test "#handle prompts/get returns error if prompt arguments are invalid" do @@ -257,21 +250,19 @@ module ModelContextProtocol name: "test_prompt", arguments: { "unknown_argument" => "Hello, friend!" }, }, - }.to_json + } response = @server.handle(request) - assert_instance_of(JsonRPC::Response, response) - assert_instance_of(JsonRPC::InvalidParamsError, response.error) - assert_equal "Missing required arguments: test_argument", response.error.message + assert_equal "Missing required arguments: test_argument", response[:error][:data] end test "#prompts_get_handler sets the prompts/get handler" do @server.prompts_get_handler do |request| - prompt_name = request.params&.dig("name") + prompt_name = request[:name] return Prompt::Result.new( description: prompt_name, messages: [ - Prompt::Message.new(role: "user", content: Content::Text.new(request.params&.dig("arguments", "foo"))), + Prompt::Message.new(role: "user", content: Content::Text.new(request[:arguments][:foo])), ], ).to_h end @@ -281,12 +272,12 @@ module ModelContextProtocol method: "prompts/get", id: 1, params: { name: "foo_bar_prompt", arguments: { "foo" => "bar" } }, - }.to_json + } response = @server.handle(request) assert_equal( { description: "foo_bar_prompt", messages: [{ role: "user", content: { text: "bar" } }] }, - response.result, + response[:result], ) end @@ -295,10 +286,10 @@ module ModelContextProtocol jsonrpc: "2.0", method: "resources/list", id: 1, - }.to_json + } response = @server.handle(request) - assert_equal({ resources: [@resource.to_h] }, response.result) + assert_equal({ resources: [@resource.to_h] }, response[:result]) end test "#resources_list_handler sets the resources/list handler" do @@ -310,12 +301,12 @@ module ModelContextProtocol jsonrpc: "2.0", method: "resources/list", id: 1, - }.to_json + } response = @server.handle(request) assert_equal( { resources: [{ uri: "test_resource", name: "Test resource", description: "Test resource" }] }, - response.result, + response[:result], ) end @@ -327,10 +318,10 @@ module ModelContextProtocol params: { uri: @resource.uri, }, - }.to_json + } response = @server.handle(request) - assert_equal(@resource.to_h, response.result) + assert_equal(@resource.to_h, response[:result]) end test "#handle resources/read returns error if resource is not found" do @@ -341,16 +332,15 @@ module ModelContextProtocol params: { uri: "unknown_resource", }, - }.to_json + } response = @server.handle(request) - assert_instance_of(JsonRPC::Response, response) - assert_equal "Resource not found unknown_resource", response.error.message + assert_equal "Resource not found unknown_resource", response[:error][:data] end test "#resources_read_handler sets the resources/read handler" do @server.resources_read_handler do |request| - uri = request.params&.dig("uri") + uri = request[:uri] Resource.new( uri: uri, @@ -368,7 +358,7 @@ module ModelContextProtocol params: { uri: "test_resource", }, - }.to_json + } response = @server.handle(request) assert_equal( @@ -379,7 +369,7 @@ module ModelContextProtocol mimeType: "text/plain", contents: [{ text: "Lorem ipsum dolor sit amet" }], }, - response.result, + response[:result], ) end @@ -388,11 +378,12 @@ module ModelContextProtocol jsonrpc: "2.0", id: 1, method: "unknown_method", - }.to_json + } response = @server.handle(request) - assert_instance_of(JsonRPC::Response, response) - assert_instance_of(JsonRPC::MethodNotFoundError, response.error) + + assert_equal "Method not found", response[:error][:message] + assert_equal "unknown_method", response[:error][:data] end end end diff --git a/test/model_context_protocol/string_utils_test.rb b/test/model_context_protocol/string_utils_test.rb new file mode 100644 index 0000000..b43d1ee --- /dev/null +++ b/test/model_context_protocol/string_utils_test.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require "test_helper" + +module ModelContextProtocol + class StringUtilsTest < Minitest::Test + def test_handle_from_class_name_returns_the_class_name_without_the_module_for_a_class_without_a_module + assert_equal("test", StringUtils.handle_from_class_name("Test")) + assert_equal("test_class", StringUtils.handle_from_class_name("TestClass")) + end + + def test_handle_from_class_name_returns_the_class_name_without_the_module_for_a_class_with_a_single_parent_module + assert_equal("test", StringUtils.handle_from_class_name("Module::Test")) + assert_equal("test_class", StringUtils.handle_from_class_name("Module::TestClass")) + end + + def test_handle_from_class_name_returns_the_class_name_without_the_module_for_a_class_with_multiple_parent_modules + assert_equal("test", StringUtils.handle_from_class_name("Module::Submodule::Test")) + assert_equal("test_class", StringUtils.handle_from_class_name("Module::Submodule::TestClass")) + end + end +end