diff --git a/docs/_client/transports.md b/docs/_client/transports.md index 7f34e202..4d40cc64 100644 --- a/docs/_client/transports.md +++ b/docs/_client/transports.md @@ -184,6 +184,9 @@ since servers deliver requests that are not tied to a client request on that str a JSON-RPC `-32601` (method not found) error. To handle methods other than `elicitation/create`, register directly on the transport with `http_transport.on_server_request("method/name") { |params| ... }`. +If a handler raises an unexpected exception, the HTTP transport answers with JSON-RPC error `-32603`, `Internal error`. +The exception class and message are written to local stderr, not sent to the server. + On a [modern](/client/lifecycle/) connection servers cannot send requests at all; the same registered handlers instead drive the requests embedded in `input_required` results, as documented on [Multi Round-Trip Requests](/client/mrtr/). diff --git a/lib/mcp/client/http.rb b/lib/mcp/client/http.rb index 4e728748..635f9778 100644 --- a/lib/mcp/client/http.rb +++ b/lib/mcp/client/http.rb @@ -1196,12 +1196,13 @@ def dispatch_server_request(message) error: { code: e.code, message: e.message }, } rescue StandardError => e + warn("MCP::Client::HTTP: error handling #{message["method"]} request: #{e.class}: #{e.message.dump}") { jsonrpc: JsonRpcHandler::Version::V2_0, id: message["id"], error: { code: JsonRpcHandler::ErrorCode::INTERNAL_ERROR, - message: "Internal error handling #{message["method"]} request: #{e.message}", + message: "Internal error", }, } end diff --git a/test/mcp/client/http_test.rb b/test/mcp/client/http_test.rb index 3ed866d7..5c063efc 100644 --- a/test/mcp/client/http_test.rb +++ b/test/mcp/client/http_test.rb @@ -1660,6 +1660,9 @@ def test_send_request_answers_unregistered_server_request_with_method_not_found end def test_send_request_answers_raising_handler_with_internal_error + original_verbose = $VERBOSE + $VERBOSE = false + request = { jsonrpc: "2.0", id: "test_id", @@ -1671,7 +1674,7 @@ def test_send_request_answers_raising_handler_with_internal_error jsonrpc: "2.0", id: 9, method: "elicitation/create", - params: {}, + params: { message: "boom\r\n\e[31m" }, } tool_result = { jsonrpc: "2.0", id: "test_id", result: { content: [] } } sse_body = "event: message\ndata: #{server_request.to_json}\n\n" \ @@ -1688,7 +1691,7 @@ def test_send_request_answers_raising_handler_with_internal_error expected_error_response = { jsonrpc: "2.0", id: 9, - error: { code: -32603, message: "Internal error handling elicitation/create request: boom" }, + error: { code: -32603, message: "Internal error" }, } error_stub = stub_request(:post, url).with( body: expected_error_response.to_json, @@ -1696,12 +1699,16 @@ def test_send_request_answers_raising_handler_with_internal_error status: 202, body: "", ) - client.on_server_request("elicitation/create") { raise "boom" } + client.on_server_request("elicitation/create") { |params| raise params["message"] } - response = client.send_request(request: request) + assert_output("", "MCP::Client::HTTP: error handling elicitation/create request: RuntimeError: \"boom\\r\\n\\e[31m\"\n") do + response = client.send_request(request: request) - assert_equal({ "content" => [] }, response["result"]) - assert_requested(error_stub) + assert_equal({ "content" => [] }, response["result"]) + assert_requested(error_stub) + end + ensure + $VERBOSE = original_verbose end def test_send_request_answers_server_request_error_with_its_code