From a65512b3b4e623e234de7570a32a5c648fcc7fa8 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Thu, 15 Oct 2026 03:00:38 +0900 Subject: [PATCH] Answer a method of an undeclared capability with Method not found ## Motivation and Context A request for a method whose capability the server does not declare, such as `prompts/list` on a server declaring only `tools`, is answered with `-32603` Internal error (issue #602). To a client that reads as a crash, while the server merely has no such method: the TypeScript and Python servers answer `-32601` Method not found for a handler they never registered, and the Streamable HTTP transport of 2026-07-28 has the server answer a method it does not implement with `404 Not Found` and `-32601`, which this transport already does for an unknown method but not for this one. `Server#handle_request` now re-raises `Methods::MissingRequiredCapabilityError` as `-32601` Method not found, keeping the capability it names in the error `data`, so the modern transport answers 404 as for any other method it lacks. Fixes #602. ## How Has This Been Tested? `test/mcp/server_test.rb` (the three capability tests now check the code and message) and new tests in `test/mcp/server/transports/streamable_http_transport_test.rb` for both lifecycles. Against the previous library they fail with `-32603`, the modern one also with a 200. ## Breaking Changes A client that matched the `-32603` of an undeclared capability's method sees `-32601` now, and a 404 instead of a 200 on the modern lifecycle. The error `data` text is unchanged. --- lib/mcp/server.rb | 16 ++++-- .../streamable_http_transport_test.rb | 49 +++++++++++++++++++ test/mcp/server_test.rb | 15 ++++-- 3 files changed, 73 insertions(+), 7 deletions(-) diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index afb3e845..340f27e2 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -645,10 +645,18 @@ def handle_request(request, method, session: nil, related_request_id: nil, auth_ 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) + # A method of a capability the server does not declare is one the server does not implement, so it is answered as Method not found, + # which the TypeScript and Python servers return for a handler they never registered, and which the Streamable HTTP transport maps to + # 404 on the modern lifecycle. Re-raised 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( + "Method not found", + request, + error_type: :method_not_found, + error_code: JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND, + error_data: e.message, + original_error: e, + ) end # `initialize` MUST NOT be cancelled (MCP spec 2025-11-25, cancellation item 2), diff --git a/test/mcp/server/transports/streamable_http_transport_test.rb b/test/mcp/server/transports/streamable_http_transport_test.rb index 1e1ebc7a..67df6584 100644 --- a/test/mcp/server/transports/streamable_http_transport_test.rb +++ b/test/mcp/server/transports/streamable_http_transport_test.rb @@ -6047,6 +6047,55 @@ def string assert_equal(-32601, JSON.parse(response[2][0]).dig("error", "code")) end + test "modern POST maps a method of an undeclared capability to 404 with -32601" do + server = Server.new(name: "tools_only", capabilities: { tools: {} }) + transport = StreamableHTTPTransport.new(server) + + response = transport.handle_request(modern_rack_request(modern_body("prompts/list", {}))) + + assert_equal 404, response[0] + + body = JSON.parse(response[2][0]) + + assert_equal(-32601, body.dig("error", "code")) + assert_equal "Server does not support prompts (required for prompts/list)", body.dig("error", "data") + assert_equal 1, body["id"] + ensure + transport.close + end + + test "legacy POST answers a method of an undeclared capability with 200 and -32601" do + server = Server.new(name: "tools_only", capabilities: { tools: {} }) + transport = StreamableHTTPTransport.new(server) + init_response = transport.handle_request(create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json" }, + { jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json, + )) + session_id = init_response[1]["mcp-session-id"] + + response = transport.handle_request(create_rack_request( + "POST", + "/", + { "CONTENT_TYPE" => "application/json", "HTTP_MCP_SESSION_ID" => session_id }, + { jsonrpc: "2.0", method: "prompts/list", id: 1 }.to_json, + )) + + assert_equal 200, response[0] + + io = StringIO.new + response[2].call(io) + body = JSON.parse(io.string.match(/^data: (.+)$/)[1]) + + assert_equal(-32601, body.dig("error", "code")) + assert_equal "Method not found", body.dig("error", "message") + assert_equal "Server does not support prompts (required for prompts/list)", body.dig("error", "data") + assert_equal 1, body["id"] + ensure + transport.close + end + test "modern POST requires the _meta envelope" do response = @transport.handle_request(modern_rack_request( { jsonrpc: "2.0", method: "tools/list", id: 1 }.to_json, diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index c1ac383f..1819a5e6 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -1880,23 +1880,32 @@ def read_resource_request(uri) assert_equal "2.0", response[:jsonrpc] assert_equal 1, response[:id] - assert_equal(-32603, response[:error][:code]) + assert_equal(-32601, response[:error][:code]) + assert_equal "Method not found", response[:error][:message] assert_includes response[:error][:data], "Server does not support logging" end - test "#handle method with missing required top-level capability returns an error" do + test "#handle method with missing required top-level capability returns Method not found" do + # A method of an undeclared capability is one the server does not implement, as the TypeScript and Python + # servers answer for a handler they never registered; -32603 would read as a crash. @server.capabilities = {} response = @server.handle({ jsonrpc: "2.0", method: "prompts/list", id: 1 }) + assert_equal(-32601, response[:error][:code]) + assert_equal "Method not found", response[:error][:message] assert_equal "Server does not support prompts (required for prompts/list)", response[:error][:data] response = @server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 }) + assert_equal(-32601, response[:error][:code]) + assert_equal "Method not found", response[:error][:message] assert_equal "Server does not support resources (required for resources/list)", response[:error][:data] end - test "#handle method with missing required nested capability returns an error" do + test "#handle method with missing required nested capability returns Method not found" do @server.capabilities = { resources: {} } response = @server.handle({ jsonrpc: "2.0", method: "resources/subscribe", id: 1 }) + assert_equal(-32601, response[:error][:code]) + assert_equal "Method not found", response[:error][:message] assert_equal "Server does not support resources.subscribe (required for resources/subscribe)", response[:error][:data] end