Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions lib/json_rpc_handler.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,12 @@ def process_request(request, id_validation_pattern:, &method_finder)
success_response(id: id, result: result)
rescue MCP::Server::RequestHandlerError => e
handle_request_error(e, id, id_validation_pattern)
rescue StandardError => e
rescue StandardError
# The exception message is deliberately not echoed into `data`: it can carry
# internals that must not reach untrusted clients (CWE-209).
error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
code: ErrorCode::INTERNAL_ERROR,
message: "Internal error",
data: e.message,
})
end
end
Expand Down
14 changes: 12 additions & 2 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,14 @@ def handle_request(request, method, session: nil, related_request_id: nil)
return
end

Methods.ensure_capability!(method, capabilities)
begin
Methods.ensure_capability!(method, capabilities)
rescue Methods::MissingRequiredCapabilityError => e
# Re-raise through RequestHandlerError, the one channel whose message is
# deliberately surfaced to clients: `JsonRpcHandler`'s blind `rescue StandardError`
# no longer echoes exception messages into the error `data` member (CWE-209).
raise RequestHandlerError.new(e.message, request, error_type: :internal_error, original_error: e)
end

# `initialize` MUST NOT be cancelled (MCP spec 2025-11-25, cancellation item 2),
# so do not track it in the in-flight registry.
Expand Down Expand Up @@ -864,8 +871,11 @@ def call_tool(request, session: nil, related_request_id: nil, cancellation: nil,
# `JsonRpcHandler::NO_RESPONSE` per the MCP cancellation spec.
raise
rescue => e
# `e.message` is deliberately not included: it can carry internals (class, method
# and host names) that must not reach untrusted clients (CWE-209). The original
# exception still reaches `configuration.exception_reporter` via `original_error`.
raise RequestHandlerError.new(
"Internal error calling tool #{tool_name}: #{e.message}",
"Internal error calling tool #{tool_name}",
request,
error_type: :internal_error,
original_error: e,
Expand Down
2 changes: 0 additions & 2 deletions test/json_rpc_handler_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,6 @@
assert_rpc_error expected_error: {
code: -32603,
message: "Internal error",
data: "Something bad happened",
}
assert_equal 1, @response[:id]
assert_nil @response[:result]
Expand Down Expand Up @@ -452,7 +451,6 @@
assert_rpc_error expected_error: {
code: -32603,
message: "Internal error",
data: "Something bad happened",
}
end

Expand Down
2 changes: 1 addition & 1 deletion test/mcp/server_context_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ def call(message:, server_context:)
assert_nil response[:result]
assert_equal(-32603, response[:error][:code])
assert_equal "Internal error", response[:error][:message]
assert_match(/Internal error calling tool tool_with_required_context: /, response[:error][:data])
assert_equal "Internal error calling tool tool_with_required_context", response[:error][:data]
end

test "call_tool_with_args correctly detects server_context parameter presence" do
Expand Down
14 changes: 7 additions & 7 deletions test/mcp/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ def call(message:, server_context: nil)
assert_nil response[:result]
assert_equal(-32603, response[:error][:code])
assert_equal "Internal error", response[:error][:message]
assert_match(/Internal error calling tool tool_that_raises: /, response[:error][:data])
assert_equal "Internal error calling tool tool_that_raises", response[:error][:data]
assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" }, error: :internal_error })
end

Expand Down Expand Up @@ -941,7 +941,7 @@ class Example < Tool
assert_nil response[:result]
assert_equal(-32603, response[:error][:code])
assert_equal "Internal error", response[:error][:message]
assert_match(/Internal error calling tool tool_that_raises: /, response[:error][:data])
assert_equal "Internal error calling tool tool_that_raises", response[:error][:data]
assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" }, error: :internal_error })
end

Expand Down Expand Up @@ -972,7 +972,7 @@ class Example < Tool
assert_nil response[:result]
assert_equal(-32603, response[:error][:code])
assert_equal "Internal error", response[:error][:message]
assert_match(/Internal error calling tool tool_with_faulty_schema: Unexpected schema error/, response[:error][:data])
assert_equal "Internal error calling tool tool_with_faulty_schema", response[:error][:data]
end

test "#handle tools/call returns JSON-RPC error for unknown tool" do
Expand Down Expand Up @@ -1727,7 +1727,7 @@ def read_resource_request(uri)

assert_equal(["tool failure", "around ensure boom"], reported)
assert_equal(JsonRpcHandler::ErrorCode::INTERNAL_ERROR, response[:error][:code])
assert_equal("around ensure boom", response[:error][:data])
assert_nil(response[:error][:data])
end

test "#handle reports the same exception object reused across requests on every call" do
Expand Down Expand Up @@ -1779,7 +1779,7 @@ def read_resource_request(uri)
response = server.handle(request)

assert_equal([frozen_error], reported)
assert_includes(response[:error][:data], "frozen failure")
assert_equal("Internal error calling tool frozen_tool", response[:error][:data])
end

test "#handle still reports via exception_reporter when around_request swallows the tool failure" do
Expand Down Expand Up @@ -2635,7 +2635,7 @@ def read_resource_request(uri)
assert_nil response[:result]
assert_equal(-32603, response[:error][:code])
assert_equal "Internal error", response[:error][:message]
assert_match(/Internal error calling tool invalid_structured_content_tool: Invalid result:/, response[:error][:data])
assert_equal "Internal error calling tool invalid_structured_content_tool", response[:error][:data]
end

test "tools/call returns JSON-RPC error when output schema validation is enabled and structuredContent is missing" do
Expand Down Expand Up @@ -2664,7 +2664,7 @@ def read_resource_request(uri)
assert_nil response[:result]
assert_equal(-32603, response[:error][:code])
assert_equal "Internal error", response[:error][:message]
assert_match(/Internal error calling tool missing_structured_content_tool: Invalid result:/, response[:error][:data])
assert_equal "Internal error calling tool missing_structured_content_tool", response[:error][:data]
end

test "tools/call skips output schema validation for error responses" do
Expand Down