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
22 changes: 12 additions & 10 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -316,13 +316,12 @@ def handle_request(request, method, session: nil)
add_instrumentation_data(client: client) if client

result
rescue RequestHandlerError => e
report_exception(e.original_error || e, { request: request })
add_instrumentation_data(error: e.error_type)
raise e
rescue => e
report_exception(e, { request: request })
if e.is_a?(RequestHandlerError)
add_instrumentation_data(error: e.error_type)
raise e
end

add_instrumentation_data(error: :internal_error)
raise RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
end
Expand Down Expand Up @@ -421,7 +420,7 @@ def call_tool(request, session: nil)
add_instrumentation_data(error: :missing_required_arguments)

missing = tool.input_schema.missing_required_arguments(arguments).join(", ")
return error_tool_response("Missing required arguments: #{missing}")
raise RequestHandlerError.new("Missing required arguments: #{missing}", request, error_type: :invalid_params)
end

if configuration.validate_tool_call_arguments && tool.input_schema
Expand All @@ -430,7 +429,7 @@ def call_tool(request, session: nil)
rescue Tool::InputSchema::ValidationError => e
add_instrumentation_data(error: :invalid_schema)

return error_tool_response(e.message)
raise RequestHandlerError.new(e.message, request, error_type: :invalid_params)
end
end

Expand All @@ -440,9 +439,12 @@ def call_tool(request, session: nil)
rescue RequestHandlerError
raise
rescue => e
report_exception(e, request: request)

error_tool_response("Internal error calling tool #{tool_name}: #{e.message}")
raise RequestHandlerError.new(
"Internal error calling tool #{tool_name}: #{e.message}",
request,
error_type: :internal_error,
original_error: e,
)
end

def list_prompts(request)
Expand Down
10 changes: 5 additions & 5 deletions test/mcp/server_context_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ def call(message:, server_context:)
response[:result][:content][0][:content]
end

test "tool with required server_context fails when server has no context" do
test "tool with required server_context returns protocol error in JSON-RPC format when server has no context" do
server_no_context = Server.new(
name: "test_server_no_context",
tools: [ToolWithRequiredContext],
Expand All @@ -217,10 +217,10 @@ def call(message:, server_context:)

response = server_no_context.handle(request)

assert_nil response[:error], "Expected no JSON-RPC error"
assert response[:result][:isError]
assert_equal "text", response[:result][:content][0][:type]
assert_match(/Internal error calling tool tool_with_required_context: /, response[:result][:content][0][:text])
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])
end

test "call_tool_with_args correctly detects server_context parameter presence" do
Expand Down
86 changes: 42 additions & 44 deletions test/mcp/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ class ServerTest < ActiveSupport::TestCase
assert_instrumentation_data({ method: "tools/call", tool_name: tool_name, tool_arguments: tool_args })
end

test "#handle tools/call returns error response with isError true if required tool arguments are missing" do
test "#handle tools/call returns protocol error in JSON-RPC format if required tool arguments are missing" do
tool_with_required_argument = Tool.define(
name: "test_tool",
title: "Test tool",
Expand All @@ -336,10 +336,10 @@ class ServerTest < ActiveSupport::TestCase

response = server.handle(request)

assert_nil response[:error], "Expected no JSON-RPC error"
assert response[:result][:isError]
assert_equal "text", response[:result][:content][0][:type]
assert_equal "Missing required arguments: message", response[:result][:content][0][:text]
assert_nil response[:result]
assert_equal(-32602, response[:error][:code])
assert_equal "Invalid params", response[:error][:message]
assert_includes response[:error][:data], "Missing required arguments: message"
end

test "#handle_json tools/call executes tool and returns result" do
Expand Down Expand Up @@ -407,17 +407,7 @@ def call(message:, server_context: nil)
assert_equal({ content: [{ type: "text", content: "OK" }], isError: false }, response[:result])
end

test "#handle tools/call returns error response with isError true if the tool raises an error" do
@server.configuration.exception_reporter.expects(:call).with do |exception, server_context|
assert_not_nil exception
assert_equal(
{
request: { name: "tool_that_raises", arguments: { message: "test" } },
},
server_context,
)
end

test "#handle tools/call returns protocol error in JSON-RPC format if the tool raises an uncaught exception" do
request = {
jsonrpc: "2.0",
method: "tools/call",
Expand All @@ -428,13 +418,18 @@ def call(message:, server_context: nil)
id: 1,
}

@server.configuration.exception_reporter.expects(:call).with do |exception, server_context|
refute_kind_of MCP::Server::RequestHandlerError, exception
assert_equal({ request: request }, server_context)
end

response = @server.handle(request)

assert_nil response[:error], "Expected no JSON-RPC error"
assert response[:result][:isError]
assert_equal "text", response[:result][:content][0][:type]
assert_match(/Internal error calling tool tool_that_raises: /, response[:result][:content][0][:text])
assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" } })
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_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" }, error: :internal_error })
end

test "registers tools with the same class name in different namespaces" do
Expand Down Expand Up @@ -475,7 +470,7 @@ class Example < Tool
MESSAGE
end

test "#handle_json returns error response with isError true if the tool raises an error" do
test "#handle_json returns protocol error in JSON-RPC format if the tool raises an uncaught exception" do
request = JSON.generate({
jsonrpc: "2.0",
method: "tools/call",
Expand All @@ -487,14 +482,14 @@ class Example < Tool
})

response = JSON.parse(@server.handle_json(request), symbolize_names: true)
assert_nil response[:error], "Expected no JSON-RPC error"
assert response[:result][:isError]
assert_equal "text", response[:result][:content][0][:type]
assert_match(/Internal error calling tool tool_that_raises: /, response[:result][:content][0][:text])
assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" } })
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_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" }, error: :internal_error })
end

test "#handle tools/call returns error response with isError true if input_schema raises an error during validation" do
test "#handle tools/call returns protocol error in JSON-RPC format if input_schema raises an error during validation" do
tool = Tool.define(
name: "tool_with_faulty_schema",
title: "Tool with faulty schema",
Expand All @@ -518,10 +513,10 @@ class Example < Tool

response = server.handle(request)

assert_nil response[:error], "Expected no JSON-RPC error"
assert response[:result][:isError]
assert_equal "text", response[:result][:content][0][:type]
assert_match(/Internal error calling tool tool_with_faulty_schema: Unexpected schema error/, response[:result][:content][0][:text])
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])
end

test "#handle tools/call returns JSON-RPC error for unknown tool" do
Expand Down Expand Up @@ -1310,7 +1305,7 @@ class Example < Tool
refute response[:result].key?(:instructions)
end

test "tools/call handles missing arguments field" do
test "tools/call returns protocol error in JSON-RPC format for missing arguments" do
server = Server.new(
tools: [TestTool],
configuration: Configuration.new(validate_tool_call_arguments: true),
Expand All @@ -1329,12 +1324,13 @@ class Example < Tool

assert_equal "2.0", response[:jsonrpc]
assert_equal 1, response[:id]
assert_nil response[:error], "Expected no JSON-RPC error"
assert response[:result][:isError]
assert_includes response[:result][:content][0][:text], "Missing required arguments"
assert_nil response[:result]
assert_equal(-32602, response[:error][:code])
assert_equal "Invalid params", response[:error][:message]
assert_includes response[:error][:data], "Missing required arguments"
end

test "tools/call validates arguments against input schema when validate_tool_call_arguments is true" do
test "tools/call returns protocol error in JSON-RPC format for invalid arguments when validate_tool_call_arguments is true" do
server = Server.new(
tools: [TestTool],
configuration: Configuration.new(validate_tool_call_arguments: true),
Expand All @@ -1354,9 +1350,10 @@ class Example < Tool

assert_equal "2.0", response[:jsonrpc]
assert_equal 1, response[:id]
assert_nil response[:error], "Expected no JSON-RPC error"
assert response[:result][:isError]
assert_includes response[:result][:content][0][:text], "Invalid arguments"
assert_nil response[:result]
assert_equal(-32602, response[:error][:code])
assert_equal "Invalid params", response[:error][:message]
assert_includes response[:error][:data], "Invalid arguments"
end

test "tools/call skips argument validation when validate_tool_call_arguments is false" do
Expand Down Expand Up @@ -1441,7 +1438,7 @@ class Example < Tool
assert_equal "OK", response[:result][:content][0][:content]
end

test "tools/call disallows additional properties when additionalProperties set to false" do
test "tools/call returns protocol error in JSON-RPC format when additionalProperties set to false" do
server = Server.new(
tools: [TestToolWithAdditionalPropertiesSetToFalse],
configuration: Configuration.new(validate_tool_call_arguments: true),
Expand All @@ -1464,9 +1461,10 @@ class Example < Tool

assert_equal "2.0", response[:jsonrpc]
assert_equal 1, response[:id]
assert_nil response[:error], "Expected no JSON-RPC error"
assert response[:result][:isError]
assert_includes response[:result][:content][0][:text], "Invalid arguments"
assert_nil response[:result]
assert_equal(-32602, response[:error][:code])
assert_equal "Invalid params", response[:error][:message]
assert_includes response[:error][:data], "Invalid arguments"
end

test "#handle completion/complete returns default completion result" do
Expand Down