From 0217780ff303d58355a6bec0fc8a186842fe985f Mon Sep 17 00:00:00 2001 From: dak2 Date: Tue, 2 Sep 2025 10:08:07 +0900 Subject: [PATCH] Enable configuration of the `JSON-RPC` error object to be returned. Currently, the `modelcontextprotocol/ruby-sdk` delegates the handling of JSON-RPC error objects to the `json-rpc-handler`, which prevents flexible configuration of error objects returned by the SDK. For example, The logging utility should return the `JSON-RPC` error object `-32602`, `Invalid params`, when the log level is invalid. https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#error-handling Currently, however, the `modelcontextprotocol/ruby-sdk` side cannot freely configure the `Invalid params` error object. Therefore, we implemented a solution in which you can return the intended `JSON-RPC` error object by including code in the error class and raising it. The modelcontextprotocol/ruby-sdk could define and return `JSON-RPC` error objects directly. However, I believe that delegating this responsibility to the `json-rpc-handler` keeps things simpler. For this use case, setting `ErrorCode::InvalidParams` flexibly would have been sufficient. However, I implemented it this way because I thought that allowing the flexible invocation of other error classes would make it more universally reusable. --- lib/json_rpc_handler.rb | 24 +++++++-- test/json_rpc_handler_test.rb | 97 ++++++++++++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 6 deletions(-) diff --git a/lib/json_rpc_handler.rb b/lib/json_rpc_handler.rb index 7807429..cde1f2b 100644 --- a/lib/json_rpc_handler.rb +++ b/lib/json_rpc_handler.rb @@ -103,12 +103,26 @@ def process_request(request, &method_finder) success_response id:, result: rescue StandardError => e - error_response id:, error: { - code: ErrorCode::InternalError, - message: 'Internal error', - data: e.message, - } + handle_error e, id + end + end + + def handle_error(error, id) + raw_code = error.respond_to?(:code) ? error.code : nil + + code, message = case raw_code + when ErrorCode::InvalidRequest then [ErrorCode::InvalidRequest, 'Invalid Request'] + when ErrorCode::InvalidParams then [ErrorCode::InvalidParams, 'Invalid params'] + when ErrorCode::ParseError then [ErrorCode::ParseError, 'Parse error'] + when ErrorCode::InternalError then [ErrorCode::InternalError, 'Internal error'] + else [ErrorCode::InternalError, 'Internal error'] end + + error_response id:, error: { + code:, + message:, + data: error.message, + } end def valid_version?(version) diff --git a/test/json_rpc_handler_test.rb b/test/json_rpc_handler_test.rb index 83b70ca..5e8ca89 100644 --- a/test/json_rpc_handler_test.rb +++ b/test/json_rpc_handler_test.rb @@ -2,6 +2,15 @@ require 'test_helper' +class RequestHandlerError < StandardError + attr_reader :code + + def initialize(message, request = {}, code: nil, **_options) + super(message) + @code = code + end +end + describe JsonRpcHandler do before do @registry = {} @@ -454,7 +463,93 @@ it "returns an error with the id set to nil when the request is invalid" do handle_json({ jsonrpc: "0.0", id: 1, method: "add", params: { a: 1, b: 2 } }.to_json) - assert_nil @response[:id] + assert_nil @response[:id] + end + + describe 'handle_error' do + it "returns an error object as a Invalid request error when an exception with `-32602` code is raised" do + register "logging/setLevel" do |params| + raise RequestHandlerError.new("Invalid log level", {}, code: -32602) + end + + handle_json({ jsonrpc: "2.0", id: 1, method: "logging/setLevel", params: { level: "info" } }.to_json) + + assert_rpc_error expected_error: { + code: -32602, + message: 'Invalid params', + data: 'Invalid log level', + } + end + + it "returns an error object as a Invalid request error when an exception with `-32600` code is raised" do + register "logging/setLevel" do |params| + raise RequestHandlerError.new("Invalid log level", {}, code: -32600) + end + + handle_json({ jsonrpc: "2.0", id: 1, method: "logging/setLevel", params: { level: "info" } }.to_json) + + assert_rpc_error expected_error: { + code: -32600, + message: 'Invalid Request', + data: 'Invalid log level', + } + end + + it "returns an error object as a Parse error when an exception with `-32700` code is raised" do + register "logging/setLevel" do |params| + raise RequestHandlerError.new("Invalid log level", {}, code: -32700) + end + + handle_json({ jsonrpc: "2.0", id: 1, method: "logging/setLevel", params: { level: "info" } }.to_json) + + assert_rpc_error expected_error: { + code: -32700, + message: 'Parse error', + data: 'Invalid log level', + } + end + + it "returns an error object as a Internal error when an exception with undefined code is raised" do + register "logging/setLevel" do |params| + raise RequestHandlerError.new("Invalid log level", {}, code: -99999) + end + + handle_json({ jsonrpc: "2.0", id: 1, method: "logging/setLevel", params: { level: "info" } }.to_json) + + assert_rpc_error expected_error: { + code: -32603, + message: 'Internal error', + data: 'Invalid log level', + } + end + + it "returns an error object as a Internal error when an exception with nil code is raised" do + register "logging/setLevel" do |params| + raise RequestHandlerError.new("Invalid log level", {}, code: nil) + end + + handle_json({ jsonrpc: "2.0", id: 1, method: "logging/setLevel", params: { level: "info" } }.to_json) + + assert_rpc_error expected_error: { + code: -32603, + message: 'Internal error', + data: 'Invalid log level', + } + end + + it "returns an error object as a internal error when an exception without code is raised" do + register "logging/setLevel" do |params| + raise RequestHandlerError.new("Invalid log level", {}, error_type: :invalid_log_level) + end + + handle_json({ jsonrpc: "2.0", id: 1, method: "logging/setLevel", params: { level: "info" } }.to_json) + + assert_rpc_error expected_error: { + code: -32603, + message: 'Internal error', + data: 'Invalid log level', + } + end end end