Skip to content

Commit 8dd0379

Browse files
committed
fix: return protocol errors for invalid arguments and server errors
Per the MCP spec, invalid arguments and server errors are protocol errors (JSON-RPC error responses), not tool execution errors (isError: true). PR #165 moved all error cases to isError: true, and PR #231 only restored protocol errors for unknown tools. This fixes the remaining two cases: invalid arguments (-32602) and server errors (-32603). Ref: https://modelcontextprotocol.io/specification/2024-11-05/server/tools#error-handling
1 parent 5631990 commit 8dd0379

File tree

3 files changed

+59
-59
lines changed

3 files changed

+59
-59
lines changed

lib/mcp/server.rb

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -316,13 +316,12 @@ def handle_request(request, method, session: nil)
316316
add_instrumentation_data(client: client) if client
317317

318318
result
319+
rescue RequestHandlerError => e
320+
report_exception(e.original_error || e, { request: request })
321+
add_instrumentation_data(error: e.error_type)
322+
raise e
319323
rescue => e
320324
report_exception(e, { request: request })
321-
if e.is_a?(RequestHandlerError)
322-
add_instrumentation_data(error: e.error_type)
323-
raise e
324-
end
325-
326325
add_instrumentation_data(error: :internal_error)
327326
raise RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
328327
end
@@ -421,7 +420,7 @@ def call_tool(request, session: nil)
421420
add_instrumentation_data(error: :missing_required_arguments)
422421

423422
missing = tool.input_schema.missing_required_arguments(arguments).join(", ")
424-
return error_tool_response("Missing required arguments: #{missing}")
423+
raise RequestHandlerError.new("Missing required arguments: #{missing}", request, error_type: :invalid_params)
425424
end
426425

427426
if configuration.validate_tool_call_arguments && tool.input_schema
@@ -430,7 +429,7 @@ def call_tool(request, session: nil)
430429
rescue Tool::InputSchema::ValidationError => e
431430
add_instrumentation_data(error: :invalid_schema)
432431

433-
return error_tool_response(e.message)
432+
raise RequestHandlerError.new(e.message, request, error_type: :invalid_params)
434433
end
435434
end
436435

@@ -440,9 +439,12 @@ def call_tool(request, session: nil)
440439
rescue RequestHandlerError
441440
raise
442441
rescue => e
443-
report_exception(e, request: request)
444-
445-
error_tool_response("Internal error calling tool #{tool_name}: #{e.message}")
442+
raise RequestHandlerError.new(
443+
"Internal error calling tool #{tool_name}: #{e.message}",
444+
request,
445+
error_type: :internal_error,
446+
original_error: e,
447+
)
446448
end
447449

448450
def list_prompts(request)

test/mcp/server_context_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ def call(message:, server_context:)
199199
response[:result][:content][0][:content]
200200
end
201201

202-
test "tool with required server_context fails when server has no context" do
202+
test "tool with required server_context returns protocol error in JSON-RPC format when server has no context" do
203203
server_no_context = Server.new(
204204
name: "test_server_no_context",
205205
tools: [ToolWithRequiredContext],
@@ -217,10 +217,10 @@ def call(message:, server_context:)
217217

218218
response = server_no_context.handle(request)
219219

220-
assert_nil response[:error], "Expected no JSON-RPC error"
221-
assert response[:result][:isError]
222-
assert_equal "text", response[:result][:content][0][:type]
223-
assert_match(/Internal error calling tool tool_with_required_context: /, response[:result][:content][0][:text])
220+
assert_nil response[:result]
221+
assert_equal(-32603, response[:error][:code])
222+
assert_equal "Internal error", response[:error][:message]
223+
assert_match(/Internal error calling tool tool_with_required_context: /, response[:error][:data])
224224
end
225225

226226
test "call_tool_with_args correctly detects server_context parameter presence" do

test/mcp/server_test.rb

Lines changed: 42 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ class ServerTest < ActiveSupport::TestCase
312312
assert_instrumentation_data({ method: "tools/call", tool_name: tool_name, tool_arguments: tool_args })
313313
end
314314

315-
test "#handle tools/call returns error response with isError true if required tool arguments are missing" do
315+
test "#handle tools/call returns protocol error in JSON-RPC format if required tool arguments are missing" do
316316
tool_with_required_argument = Tool.define(
317317
name: "test_tool",
318318
title: "Test tool",
@@ -336,10 +336,10 @@ class ServerTest < ActiveSupport::TestCase
336336

337337
response = server.handle(request)
338338

339-
assert_nil response[:error], "Expected no JSON-RPC error"
340-
assert response[:result][:isError]
341-
assert_equal "text", response[:result][:content][0][:type]
342-
assert_equal "Missing required arguments: message", response[:result][:content][0][:text]
339+
assert_nil response[:result]
340+
assert_equal(-32602, response[:error][:code])
341+
assert_equal "Invalid params", response[:error][:message]
342+
assert_includes response[:error][:data], "Missing required arguments: message"
343343
end
344344

345345
test "#handle_json tools/call executes tool and returns result" do
@@ -407,17 +407,7 @@ def call(message:, server_context: nil)
407407
assert_equal({ content: [{ type: "text", content: "OK" }], isError: false }, response[:result])
408408
end
409409

410-
test "#handle tools/call returns error response with isError true if the tool raises an error" do
411-
@server.configuration.exception_reporter.expects(:call).with do |exception, server_context|
412-
assert_not_nil exception
413-
assert_equal(
414-
{
415-
request: { name: "tool_that_raises", arguments: { message: "test" } },
416-
},
417-
server_context,
418-
)
419-
end
420-
410+
test "#handle tools/call returns protocol error in JSON-RPC format if the tool raises an uncaught exception" do
421411
request = {
422412
jsonrpc: "2.0",
423413
method: "tools/call",
@@ -428,13 +418,18 @@ def call(message:, server_context: nil)
428418
id: 1,
429419
}
430420

421+
@server.configuration.exception_reporter.expects(:call).with do |exception, server_context|
422+
refute_kind_of MCP::Server::RequestHandlerError, exception
423+
assert_equal({ request: request }, server_context)
424+
end
425+
431426
response = @server.handle(request)
432427

433-
assert_nil response[:error], "Expected no JSON-RPC error"
434-
assert response[:result][:isError]
435-
assert_equal "text", response[:result][:content][0][:type]
436-
assert_match(/Internal error calling tool tool_that_raises: /, response[:result][:content][0][:text])
437-
assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" } })
428+
assert_nil response[:result]
429+
assert_equal(-32603, response[:error][:code])
430+
assert_equal "Internal error", response[:error][:message]
431+
assert_match(/Internal error calling tool tool_that_raises: /, response[:error][:data])
432+
assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" }, error: :internal_error })
438433
end
439434

440435
test "registers tools with the same class name in different namespaces" do
@@ -475,7 +470,7 @@ class Example < Tool
475470
MESSAGE
476471
end
477472

478-
test "#handle_json returns error response with isError true if the tool raises an error" do
473+
test "#handle_json returns protocol error in JSON-RPC format if the tool raises an uncaught exception" do
479474
request = JSON.generate({
480475
jsonrpc: "2.0",
481476
method: "tools/call",
@@ -487,14 +482,14 @@ class Example < Tool
487482
})
488483

489484
response = JSON.parse(@server.handle_json(request), symbolize_names: true)
490-
assert_nil response[:error], "Expected no JSON-RPC error"
491-
assert response[:result][:isError]
492-
assert_equal "text", response[:result][:content][0][:type]
493-
assert_match(/Internal error calling tool tool_that_raises: /, response[:result][:content][0][:text])
494-
assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" } })
485+
assert_nil response[:result]
486+
assert_equal(-32603, response[:error][:code])
487+
assert_equal "Internal error", response[:error][:message]
488+
assert_match(/Internal error calling tool tool_that_raises: /, response[:error][:data])
489+
assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" }, error: :internal_error })
495490
end
496491

497-
test "#handle tools/call returns error response with isError true if input_schema raises an error during validation" do
492+
test "#handle tools/call returns protocol error in JSON-RPC format if input_schema raises an error during validation" do
498493
tool = Tool.define(
499494
name: "tool_with_faulty_schema",
500495
title: "Tool with faulty schema",
@@ -518,10 +513,10 @@ class Example < Tool
518513

519514
response = server.handle(request)
520515

521-
assert_nil response[:error], "Expected no JSON-RPC error"
522-
assert response[:result][:isError]
523-
assert_equal "text", response[:result][:content][0][:type]
524-
assert_match(/Internal error calling tool tool_with_faulty_schema: Unexpected schema error/, response[:result][:content][0][:text])
516+
assert_nil response[:result]
517+
assert_equal(-32603, response[:error][:code])
518+
assert_equal "Internal error", response[:error][:message]
519+
assert_match(/Internal error calling tool tool_with_faulty_schema: Unexpected schema error/, response[:error][:data])
525520
end
526521

527522
test "#handle tools/call returns JSON-RPC error for unknown tool" do
@@ -1310,7 +1305,7 @@ class Example < Tool
13101305
refute response[:result].key?(:instructions)
13111306
end
13121307

1313-
test "tools/call handles missing arguments field" do
1308+
test "tools/call returns protocol error in JSON-RPC format for missing arguments" do
13141309
server = Server.new(
13151310
tools: [TestTool],
13161311
configuration: Configuration.new(validate_tool_call_arguments: true),
@@ -1329,12 +1324,13 @@ class Example < Tool
13291324

13301325
assert_equal "2.0", response[:jsonrpc]
13311326
assert_equal 1, response[:id]
1332-
assert_nil response[:error], "Expected no JSON-RPC error"
1333-
assert response[:result][:isError]
1334-
assert_includes response[:result][:content][0][:text], "Missing required arguments"
1327+
assert_nil response[:result]
1328+
assert_equal(-32602, response[:error][:code])
1329+
assert_equal "Invalid params", response[:error][:message]
1330+
assert_includes response[:error][:data], "Missing required arguments"
13351331
end
13361332

1337-
test "tools/call validates arguments against input schema when validate_tool_call_arguments is true" do
1333+
test "tools/call returns protocol error in JSON-RPC format for invalid arguments when validate_tool_call_arguments is true" do
13381334
server = Server.new(
13391335
tools: [TestTool],
13401336
configuration: Configuration.new(validate_tool_call_arguments: true),
@@ -1354,9 +1350,10 @@ class Example < Tool
13541350

13551351
assert_equal "2.0", response[:jsonrpc]
13561352
assert_equal 1, response[:id]
1357-
assert_nil response[:error], "Expected no JSON-RPC error"
1358-
assert response[:result][:isError]
1359-
assert_includes response[:result][:content][0][:text], "Invalid arguments"
1353+
assert_nil response[:result]
1354+
assert_equal(-32602, response[:error][:code])
1355+
assert_equal "Invalid params", response[:error][:message]
1356+
assert_includes response[:error][:data], "Invalid arguments"
13601357
end
13611358

13621359
test "tools/call skips argument validation when validate_tool_call_arguments is false" do
@@ -1441,7 +1438,7 @@ class Example < Tool
14411438
assert_equal "OK", response[:result][:content][0][:content]
14421439
end
14431440

1444-
test "tools/call disallows additional properties when additionalProperties set to false" do
1441+
test "tools/call returns protocol error in JSON-RPC format when additionalProperties set to false" do
14451442
server = Server.new(
14461443
tools: [TestToolWithAdditionalPropertiesSetToFalse],
14471444
configuration: Configuration.new(validate_tool_call_arguments: true),
@@ -1464,9 +1461,10 @@ class Example < Tool
14641461

14651462
assert_equal "2.0", response[:jsonrpc]
14661463
assert_equal 1, response[:id]
1467-
assert_nil response[:error], "Expected no JSON-RPC error"
1468-
assert response[:result][:isError]
1469-
assert_includes response[:result][:content][0][:text], "Invalid arguments"
1464+
assert_nil response[:result]
1465+
assert_equal(-32602, response[:error][:code])
1466+
assert_equal "Invalid params", response[:error][:message]
1467+
assert_includes response[:error][:data], "Invalid arguments"
14701468
end
14711469

14721470
test "#handle completion/complete returns default completion result" do

0 commit comments

Comments
 (0)