Skip to content

Commit 6e76890

Browse files
committed
Return tool argument validation failures as tool execution errors
## Motivation and Context MCP 2025-11-25 (SEP-1303) clarifies that input validation errors for `tools/call` should be returned as Tool Execution Errors (`{ content: [...], isError: true }`) rather than as JSON-RPC `-32602` protocol errors, so models can observe the validation message and self-correct on a follow-up call. This is a clarification rather than a brand-new requirement: the 2024-11-05, 2025-03-26, and 2025-06-18 specifications all defined two error categories with overlapping language ("Invalid arguments" listed under Protocol Errors *and* "Invalid input data" listed under Tool Execution Errors), leaving the routing of JSON-Schema validation failures ambiguous. The Ruby SDK had selected the "Protocol Errors / Invalid arguments" interpretation, while the TypeScript SDK (`packages/server/src/server/mcp.ts`, which wraps `validateToolInput` errors via `createToolError`) and Python SDK's FastMCP (which routes through the generic `is_error=True` path) had selected the other. SEP-1303 in 2025-11-25 disambiguates this by replacing the bullets with "Malformed requests (requests that fail to satisfy CallToolRequest schema)" under Protocol Errors and "Input validation errors (e.g., date in wrong format, value out of range)" under Tool Execution Errors, and explicitly states that the latter "contain actionable feedback that language models can use to self-correct". `tool_not_found` continues to be returned as a JSON-RPC `-32602` protocol error since the spec change only covers input validation. ## How Has This Been Tested? Updated existing tests that previously asserted `-32602` and "Invalid arguments" / "Missing required arguments" in the JSON-RPC error data to instead assert `result[:isError] == true` with the same text in the `content` block. Added new regression tests covering: - Instrumentation still records `:missing_required_arguments` and `:invalid_schema` on the new non-raising path - Nested schema validation failure (deep arrays of objects with required fields) returns a tool execution error rather than a protocol error - `tool_not_found` continues to return JSON-RPC `-32602` (regression guard against accidentally widening the change) `bundle exec rake test` and `bundle exec rake rubocop` both pass. ## Breaking Changes Yes. Clients that detect tool argument validation errors via `error.code == -32602` will need to switch to inspecting `result.isError == true` and reading `result.content[].text`. Note that the Ruby SDK's previous behavior was a defensible reading of the 2024-11-05 / 2025-03-26 / 2025-06-18 spec wording. The 2025-11-25 disambiguation is what makes the previous behavior non-conforming; the TypeScript and Python SDKs already shipped the new behavior.
1 parent 2ad3d21 commit 6e76890

2 files changed

Lines changed: 88 additions & 27 deletions

File tree

lib/mcp/server.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ def call_tool(request, session: nil, related_request_id: nil)
533533
add_instrumentation_data(error: :missing_required_arguments)
534534

535535
missing = tool.input_schema.missing_required_arguments(arguments).join(", ")
536-
raise RequestHandlerError.new("Missing required arguments: #{missing}", request, error_type: :invalid_params)
536+
return error_tool_response("Missing required arguments: #{missing}")
537537
end
538538

539539
if configuration.validate_tool_call_arguments && tool.input_schema
@@ -542,7 +542,7 @@ def call_tool(request, session: nil, related_request_id: nil)
542542
rescue Tool::InputSchema::ValidationError => e
543543
add_instrumentation_data(error: :invalid_schema)
544544

545-
raise RequestHandlerError.new(e.message, request, error_type: :invalid_params)
545+
return error_tool_response(e.message)
546546
end
547547
end
548548

test/mcp/server_test.rb

Lines changed: 86 additions & 25 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 protocol error in JSON-RPC format if required tool arguments are missing" do
315+
test "#handle tools/call returns tool execution error 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[: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"
339+
assert_nil response[:error]
340+
assert(response[:result][:isError])
341+
assert_equal "text", response[:result][:content][0][:type]
342+
assert_includes response[:result][:content][0][:text], "Missing required arguments: message"
343343
end
344344

345345
test "#handle_json tools/call executes tool and returns result" do
@@ -1562,11 +1562,10 @@ class Example < Tool
15621562
refute response[:result].key?(:instructions)
15631563
end
15641564

1565-
test "tools/call returns protocol error in JSON-RPC format for missing arguments" do
1566-
server = Server.new(
1567-
tools: [TestTool],
1568-
configuration: Configuration.new(validate_tool_call_arguments: true),
1569-
)
1565+
test "tools/call returns tool execution error for missing arguments" do
1566+
configuration = Configuration.new(validate_tool_call_arguments: true)
1567+
configuration.instrumentation_callback = instrumentation_helper.callback
1568+
server = Server.new(tools: [TestTool], configuration: configuration)
15701569

15711570
response = server.handle(
15721571
{
@@ -1581,17 +1580,22 @@ class Example < Tool
15811580

15821581
assert_equal "2.0", response[:jsonrpc]
15831582
assert_equal 1, response[:id]
1584-
assert_nil response[:result]
1585-
assert_equal(-32602, response[:error][:code])
1586-
assert_equal "Invalid params", response[:error][:message]
1587-
assert_includes response[:error][:data], "Missing required arguments"
1583+
assert_nil response[:error]
1584+
assert(response[:result][:isError])
1585+
assert_equal "text", response[:result][:content][0][:type]
1586+
assert_includes response[:result][:content][0][:text], "Missing required arguments"
1587+
assert_instrumentation_data({
1588+
method: "tools/call",
1589+
tool_name: "test_tool",
1590+
tool_arguments: {},
1591+
error: :missing_required_arguments,
1592+
})
15881593
end
15891594

1590-
test "tools/call returns protocol error in JSON-RPC format for invalid arguments when validate_tool_call_arguments is true" do
1591-
server = Server.new(
1592-
tools: [TestTool],
1593-
configuration: Configuration.new(validate_tool_call_arguments: true),
1594-
)
1595+
test "tools/call returns tool execution error for invalid arguments when validate_tool_call_arguments is true" do
1596+
configuration = Configuration.new(validate_tool_call_arguments: true)
1597+
configuration.instrumentation_callback = instrumentation_helper.callback
1598+
server = Server.new(tools: [TestTool], configuration: configuration)
15951599

15961600
response = server.handle(
15971601
{
@@ -1607,10 +1611,44 @@ class Example < Tool
16071611

16081612
assert_equal "2.0", response[:jsonrpc]
16091613
assert_equal 1, response[:id]
1610-
assert_nil response[:result]
1611-
assert_equal(-32602, response[:error][:code])
1612-
assert_equal "Invalid params", response[:error][:message]
1613-
assert_includes response[:error][:data], "Invalid arguments"
1614+
assert_nil response[:error]
1615+
assert(response[:result][:isError])
1616+
assert_equal "text", response[:result][:content][0][:type]
1617+
assert_includes response[:result][:content][0][:text], "Invalid arguments"
1618+
assert_instrumentation_data({
1619+
method: "tools/call",
1620+
tool_name: "test_tool",
1621+
tool_arguments: { message: 123 },
1622+
error: :invalid_schema,
1623+
})
1624+
end
1625+
1626+
test "tools/call returns tool execution error for nested schema validation failure" do
1627+
server = Server.new(
1628+
tools: [ComplexTypesTool],
1629+
configuration: Configuration.new(validate_tool_call_arguments: true),
1630+
)
1631+
1632+
response = server.handle(
1633+
{
1634+
jsonrpc: "2.0",
1635+
id: 1,
1636+
method: "tools/call",
1637+
params: {
1638+
name: "complex_types_tool",
1639+
arguments: {
1640+
numbers: [1, 2, 3],
1641+
strings: ["a", "b", "c"],
1642+
objects: [{ name: 123 }],
1643+
},
1644+
},
1645+
},
1646+
)
1647+
1648+
assert_nil response[:error]
1649+
assert(response[:result][:isError])
1650+
assert_equal "text", response[:result][:content][0][:type]
1651+
assert_includes response[:result][:content][0][:text], "Invalid arguments"
16141652
end
16151653

16161654
test "tools/call skips argument validation when validate_tool_call_arguments is false" do
@@ -1695,7 +1733,7 @@ class Example < Tool
16951733
assert_equal "OK", response[:result][:content][0][:content]
16961734
end
16971735

1698-
test "tools/call returns protocol error in JSON-RPC format when additionalProperties set to false" do
1736+
test "tools/call returns tool execution error when additionalProperties set to false" do
16991737
server = Server.new(
17001738
tools: [TestToolWithAdditionalPropertiesSetToFalse],
17011739
configuration: Configuration.new(validate_tool_call_arguments: true),
@@ -1718,10 +1756,33 @@ class Example < Tool
17181756

17191757
assert_equal "2.0", response[:jsonrpc]
17201758
assert_equal 1, response[:id]
1759+
assert_nil response[:error]
1760+
assert(response[:result][:isError])
1761+
assert_equal "text", response[:result][:content][0][:type]
1762+
assert_includes response[:result][:content][0][:text], "Invalid arguments"
1763+
end
1764+
1765+
test "tools/call returns JSON-RPC -32602 protocol error when tool is not found" do
1766+
server = Server.new(
1767+
tools: [TestTool],
1768+
)
1769+
1770+
response = server.handle(
1771+
{
1772+
jsonrpc: "2.0",
1773+
id: 1,
1774+
method: "tools/call",
1775+
params: {
1776+
name: "unknown_tool",
1777+
arguments: {},
1778+
},
1779+
},
1780+
)
1781+
17211782
assert_nil response[:result]
17221783
assert_equal(-32602, response[:error][:code])
17231784
assert_equal "Invalid params", response[:error][:message]
1724-
assert_includes response[:error][:data], "Invalid arguments"
1785+
assert_includes response[:error][:data], "Tool not found: unknown_tool"
17251786
end
17261787

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

0 commit comments

Comments
 (0)