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.
https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1303
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.
This commit is contained in:
parent
2ad3d21492
commit
48d4fb9272
2 changed files with 88 additions and 27 deletions
|
|
@ -533,7 +533,7 @@ module MCP
|
|||
add_instrumentation_data(error: :missing_required_arguments)
|
||||
|
||||
missing = tool.input_schema.missing_required_arguments(arguments).join(", ")
|
||||
raise RequestHandlerError.new("Missing required arguments: #{missing}", request, error_type: :invalid_params)
|
||||
return error_tool_response("Missing required arguments: #{missing}")
|
||||
end
|
||||
|
||||
if configuration.validate_tool_call_arguments && tool.input_schema
|
||||
|
|
@ -542,7 +542,7 @@ module MCP
|
|||
rescue Tool::InputSchema::ValidationError => e
|
||||
add_instrumentation_data(error: :invalid_schema)
|
||||
|
||||
raise RequestHandlerError.new(e.message, request, error_type: :invalid_params)
|
||||
return error_tool_response(e.message)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -312,7 +312,7 @@ module MCP
|
|||
assert_instrumentation_data({ method: "tools/call", tool_name: tool_name, tool_arguments: tool_args })
|
||||
end
|
||||
|
||||
test "#handle tools/call returns protocol error in JSON-RPC format if required tool arguments are missing" do
|
||||
test "#handle tools/call returns tool execution error if required tool arguments are missing" do
|
||||
tool_with_required_argument = Tool.define(
|
||||
name: "test_tool",
|
||||
title: "Test tool",
|
||||
|
|
@ -336,10 +336,10 @@ module MCP
|
|||
|
||||
response = server.handle(request)
|
||||
|
||||
assert_nil response[:result]
|
||||
assert_equal(-32602, response[:error][:code])
|
||||
assert_equal "Invalid params", response[:error][:message]
|
||||
assert_includes response[:error][:data], "Missing required arguments: message"
|
||||
assert_nil response[:error]
|
||||
assert(response[:result][:isError])
|
||||
assert_equal "text", response[:result][:content][0][:type]
|
||||
assert_includes response[:result][:content][0][:text], "Missing required arguments: message"
|
||||
end
|
||||
|
||||
test "#handle_json tools/call executes tool and returns result" do
|
||||
|
|
@ -1562,11 +1562,10 @@ module MCP
|
|||
refute response[:result].key?(:instructions)
|
||||
end
|
||||
|
||||
test "tools/call returns protocol error in JSON-RPC format for missing arguments" do
|
||||
server = Server.new(
|
||||
tools: [TestTool],
|
||||
configuration: Configuration.new(validate_tool_call_arguments: true),
|
||||
)
|
||||
test "tools/call returns tool execution error for missing arguments" do
|
||||
configuration = Configuration.new(validate_tool_call_arguments: true)
|
||||
configuration.instrumentation_callback = instrumentation_helper.callback
|
||||
server = Server.new(tools: [TestTool], configuration: configuration)
|
||||
|
||||
response = server.handle(
|
||||
{
|
||||
|
|
@ -1581,17 +1580,22 @@ module MCP
|
|||
|
||||
assert_equal "2.0", response[:jsonrpc]
|
||||
assert_equal 1, response[:id]
|
||||
assert_nil response[:result]
|
||||
assert_equal(-32602, response[:error][:code])
|
||||
assert_equal "Invalid params", response[:error][:message]
|
||||
assert_includes response[:error][:data], "Missing required arguments"
|
||||
assert_nil response[:error]
|
||||
assert(response[:result][:isError])
|
||||
assert_equal "text", response[:result][:content][0][:type]
|
||||
assert_includes response[:result][:content][0][:text], "Missing required arguments"
|
||||
assert_instrumentation_data({
|
||||
method: "tools/call",
|
||||
tool_name: "test_tool",
|
||||
tool_arguments: {},
|
||||
error: :missing_required_arguments,
|
||||
})
|
||||
end
|
||||
|
||||
test "tools/call returns protocol error in JSON-RPC format for invalid arguments when validate_tool_call_arguments is true" do
|
||||
server = Server.new(
|
||||
tools: [TestTool],
|
||||
configuration: Configuration.new(validate_tool_call_arguments: true),
|
||||
)
|
||||
test "tools/call returns tool execution error for invalid arguments when validate_tool_call_arguments is true" do
|
||||
configuration = Configuration.new(validate_tool_call_arguments: true)
|
||||
configuration.instrumentation_callback = instrumentation_helper.callback
|
||||
server = Server.new(tools: [TestTool], configuration: configuration)
|
||||
|
||||
response = server.handle(
|
||||
{
|
||||
|
|
@ -1607,10 +1611,44 @@ module MCP
|
|||
|
||||
assert_equal "2.0", response[:jsonrpc]
|
||||
assert_equal 1, response[:id]
|
||||
assert_nil response[:result]
|
||||
assert_equal(-32602, response[:error][:code])
|
||||
assert_equal "Invalid params", response[:error][:message]
|
||||
assert_includes response[:error][:data], "Invalid arguments"
|
||||
assert_nil response[:error]
|
||||
assert(response[:result][:isError])
|
||||
assert_equal "text", response[:result][:content][0][:type]
|
||||
assert_includes response[:result][:content][0][:text], "Invalid arguments"
|
||||
assert_instrumentation_data({
|
||||
method: "tools/call",
|
||||
tool_name: "test_tool",
|
||||
tool_arguments: { message: 123 },
|
||||
error: :invalid_schema,
|
||||
})
|
||||
end
|
||||
|
||||
test "tools/call returns tool execution error for nested schema validation failure" do
|
||||
server = Server.new(
|
||||
tools: [ComplexTypesTool],
|
||||
configuration: Configuration.new(validate_tool_call_arguments: true),
|
||||
)
|
||||
|
||||
response = server.handle(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "complex_types_tool",
|
||||
arguments: {
|
||||
numbers: [1, 2, 3],
|
||||
strings: ["a", "b", "c"],
|
||||
objects: [{ name: 123 }],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert_nil response[:error]
|
||||
assert(response[:result][:isError])
|
||||
assert_equal "text", response[:result][:content][0][:type]
|
||||
assert_includes response[:result][:content][0][:text], "Invalid arguments"
|
||||
end
|
||||
|
||||
test "tools/call skips argument validation when validate_tool_call_arguments is false" do
|
||||
|
|
@ -1695,7 +1733,7 @@ module MCP
|
|||
assert_equal "OK", response[:result][:content][0][:content]
|
||||
end
|
||||
|
||||
test "tools/call returns protocol error in JSON-RPC format when additionalProperties set to false" do
|
||||
test "tools/call returns tool execution error when additionalProperties set to false" do
|
||||
server = Server.new(
|
||||
tools: [TestToolWithAdditionalPropertiesSetToFalse],
|
||||
configuration: Configuration.new(validate_tool_call_arguments: true),
|
||||
|
|
@ -1718,10 +1756,33 @@ module MCP
|
|||
|
||||
assert_equal "2.0", response[:jsonrpc]
|
||||
assert_equal 1, response[:id]
|
||||
assert_nil response[:error]
|
||||
assert(response[:result][:isError])
|
||||
assert_equal "text", response[:result][:content][0][:type]
|
||||
assert_includes response[:result][:content][0][:text], "Invalid arguments"
|
||||
end
|
||||
|
||||
test "tools/call returns JSON-RPC -32602 protocol error when tool is not found" do
|
||||
server = Server.new(
|
||||
tools: [TestTool],
|
||||
)
|
||||
|
||||
response = server.handle(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "tools/call",
|
||||
params: {
|
||||
name: "unknown_tool",
|
||||
arguments: {},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
assert_nil response[:result]
|
||||
assert_equal(-32602, response[:error][:code])
|
||||
assert_equal "Invalid params", response[:error][:message]
|
||||
assert_includes response[:error][:data], "Invalid arguments"
|
||||
assert_includes response[:error][:data], "Tool not found: unknown_tool"
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns default completion result" do
|
||||
|
|
|
|||
Loading…
Reference in a new issue