Skip to content

Commit 450f73a

Browse files
authored
Merge pull request #611 from koic/answer_an_undeclared_capability_with_method_not_found
Answer a method of an undeclared capability with Method not found
2 parents 0d4b3f3 + a65512b commit 450f73a

3 files changed

Lines changed: 73 additions & 7 deletions

File tree

‎lib/mcp/server.rb‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -645,10 +645,18 @@ def handle_request(request, method, session: nil, related_request_id: nil, auth_
645645
begin
646646
Methods.ensure_capability!(method, capabilities)
647647
rescue Methods::MissingRequiredCapabilityError => e
648-
# Re-raise through RequestHandlerError, the one channel whose message is
649-
# deliberately surfaced to clients: `JsonRpcHandler`'s blind `rescue StandardError`
650-
# no longer echoes exception messages into the error `data` member (CWE-209).
651-
raise RequestHandlerError.new(e.message, request, error_type: :internal_error, original_error: e)
648+
# 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,
649+
# which the TypeScript and Python servers return for a handler they never registered, and which the Streamable HTTP transport maps to
650+
# 404 on the modern lifecycle. Re-raised through RequestHandlerError, the one channel whose message is deliberately surfaced to clients:
651+
# `JsonRpcHandler`'s blind `rescue StandardError` no longer echoes exception messages into the error `data` member (CWE-209).
652+
raise RequestHandlerError.new(
653+
"Method not found",
654+
request,
655+
error_type: :method_not_found,
656+
error_code: JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND,
657+
error_data: e.message,
658+
original_error: e,
659+
)
652660
end
653661

654662
# `initialize` MUST NOT be cancelled (MCP spec 2025-11-25, cancellation item 2),

‎test/mcp/server/transports/streamable_http_transport_test.rb‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6047,6 +6047,55 @@ def string
60476047
assert_equal(-32601, JSON.parse(response[2][0]).dig("error", "code"))
60486048
end
60496049

6050+
test "modern POST maps a method of an undeclared capability to 404 with -32601" do
6051+
server = Server.new(name: "tools_only", capabilities: { tools: {} })
6052+
transport = StreamableHTTPTransport.new(server)
6053+
6054+
response = transport.handle_request(modern_rack_request(modern_body("prompts/list", {})))
6055+
6056+
assert_equal 404, response[0]
6057+
6058+
body = JSON.parse(response[2][0])
6059+
6060+
assert_equal(-32601, body.dig("error", "code"))
6061+
assert_equal "Server does not support prompts (required for prompts/list)", body.dig("error", "data")
6062+
assert_equal 1, body["id"]
6063+
ensure
6064+
transport.close
6065+
end
6066+
6067+
test "legacy POST answers a method of an undeclared capability with 200 and -32601" do
6068+
server = Server.new(name: "tools_only", capabilities: { tools: {} })
6069+
transport = StreamableHTTPTransport.new(server)
6070+
init_response = transport.handle_request(create_rack_request(
6071+
"POST",
6072+
"/",
6073+
{ "CONTENT_TYPE" => "application/json" },
6074+
{ jsonrpc: "2.0", method: "initialize", id: "init", params: initialize_params }.to_json,
6075+
))
6076+
session_id = init_response[1]["mcp-session-id"]
6077+
6078+
response = transport.handle_request(create_rack_request(
6079+
"POST",
6080+
"/",
6081+
{ "CONTENT_TYPE" => "application/json", "HTTP_MCP_SESSION_ID" => session_id },
6082+
{ jsonrpc: "2.0", method: "prompts/list", id: 1 }.to_json,
6083+
))
6084+
6085+
assert_equal 200, response[0]
6086+
6087+
io = StringIO.new
6088+
response[2].call(io)
6089+
body = JSON.parse(io.string.match(/^data: (.+)$/)[1])
6090+
6091+
assert_equal(-32601, body.dig("error", "code"))
6092+
assert_equal "Method not found", body.dig("error", "message")
6093+
assert_equal "Server does not support prompts (required for prompts/list)", body.dig("error", "data")
6094+
assert_equal 1, body["id"]
6095+
ensure
6096+
transport.close
6097+
end
6098+
60506099
test "modern POST requires the _meta envelope" do
60516100
response = @transport.handle_request(modern_rack_request(
60526101
{ jsonrpc: "2.0", method: "tools/list", id: 1 }.to_json,

‎test/mcp/server_test.rb‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1880,23 +1880,32 @@ def read_resource_request(uri)
18801880

18811881
assert_equal "2.0", response[:jsonrpc]
18821882
assert_equal 1, response[:id]
1883-
assert_equal(-32603, response[:error][:code])
1883+
assert_equal(-32601, response[:error][:code])
1884+
assert_equal "Method not found", response[:error][:message]
18841885
assert_includes response[:error][:data], "Server does not support logging"
18851886
end
18861887

1887-
test "#handle method with missing required top-level capability returns an error" do
1888+
test "#handle method with missing required top-level capability returns Method not found" do
1889+
# A method of an undeclared capability is one the server does not implement, as the TypeScript and Python
1890+
# servers answer for a handler they never registered; -32603 would read as a crash.
18881891
@server.capabilities = {}
18891892

18901893
response = @server.handle({ jsonrpc: "2.0", method: "prompts/list", id: 1 })
1894+
assert_equal(-32601, response[:error][:code])
1895+
assert_equal "Method not found", response[:error][:message]
18911896
assert_equal "Server does not support prompts (required for prompts/list)", response[:error][:data]
18921897

18931898
response = @server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })
1899+
assert_equal(-32601, response[:error][:code])
1900+
assert_equal "Method not found", response[:error][:message]
18941901
assert_equal "Server does not support resources (required for resources/list)", response[:error][:data]
18951902
end
18961903

1897-
test "#handle method with missing required nested capability returns an error" do
1904+
test "#handle method with missing required nested capability returns Method not found" do
18981905
@server.capabilities = { resources: {} }
18991906
response = @server.handle({ jsonrpc: "2.0", method: "resources/subscribe", id: 1 })
1907+
assert_equal(-32601, response[:error][:code])
1908+
assert_equal "Method not found", response[:error][:message]
19001909
assert_equal "Server does not support resources.subscribe (required for resources/subscribe)",
19011910
response[:error][:data]
19021911
end

0 commit comments

Comments
 (0)