diff --git a/lib/json_rpc_handler.rb b/lib/json_rpc_handler.rb index 0b008fa1..6cbce1cb 100644 --- a/lib/json_rpc_handler.rb +++ b/lib/json_rpc_handler.rb @@ -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 diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index ef8cfde8..5fbb4a63 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -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. @@ -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, diff --git a/test/json_rpc_handler_test.rb b/test/json_rpc_handler_test.rb index e823e42f..07db0ecf 100644 --- a/test/json_rpc_handler_test.rb +++ b/test/json_rpc_handler_test.rb @@ -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] @@ -452,7 +451,6 @@ assert_rpc_error expected_error: { code: -32603, message: "Internal error", - data: "Something bad happened", } end diff --git a/test/mcp/server_context_test.rb b/test/mcp/server_context_test.rb index 07d60727..0b26bd08 100644 --- a/test/mcp/server_context_test.rb +++ b/test/mcp/server_context_test.rb @@ -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 diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index 108e6521..d3ebc6ae 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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