Skip to content
Open
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
16 changes: 12 additions & 4 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
49 changes: 49 additions & 0 deletions test/mcp/server/transports/streamable_http_transport_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
15 changes: 12 additions & 3 deletions test/mcp/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading