Merge pull request #103 from dak2/support-logging

Add `logging` support
This commit is contained in:
Koichi ITO 2026-01-30 12:29:29 +09:00 • committed by GitHub
commit 0ab77c318f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 434 additions and 7 deletions

View file

@ -398,6 +398,96 @@ describe JsonRpcHandler do
}
end
it "returns an error with the code set to -32600 when error_type of RequestHandlerError is :invalid_request" do
register("test_method") do
raise MCP::Server::RequestHandlerError.new(
"Invalid request data",
{},
error_type: :invalid_request,
)
end
handle jsonrpc: "2.0", id: 1, method: "test_method"
assert_rpc_error expected_error: {
code: -32600,
message: "Invalid Request",
data: "Invalid request data",
}
end
it "returns an error with the code set to -32602 when error_type of RequestHandlerError is :invalid_params" do
register("test_method") do
raise MCP::Server::RequestHandlerError.new(
"Parameter validation failed",
{},
error_type: :invalid_params,
)
end
handle jsonrpc: "2.0", id: 1, method: "test_method"
assert_rpc_error expected_error: {
code: -32602,
message: "Invalid params",
data: "Parameter validation failed",
}
end
it "returns an error with the code set to -32700 when error_type of RequestHandlerError is :parse_error" do
register("test_method") do
raise MCP::Server::RequestHandlerError.new(
"Failed to parse input",
{},
error_type: :parse_error,
)
end
handle jsonrpc: "2.0", id: 1, method: "test_method"
assert_rpc_error expected_error: {
code: -32700,
message: "Parse error",
data: "Failed to parse input",
}
end
it "returns an error with the code set to -32603 when error_type of RequestHandlerError is :internal_error" do
register("test_method") do
raise MCP::Server::RequestHandlerError.new(
"Internal processing error",
{},
error_type: :internal_error,
)
end
handle jsonrpc: "2.0", id: 1, method: "test_method"
assert_rpc_error expected_error: {
code: -32603,
message: "Internal error",
data: "Internal processing error",
}
end
it "returns an error with the code set to -32603 when error_type of RequestHandlerError is unknown" do
register("test_method") do
raise MCP::Server::RequestHandlerError.new(
"Unknown error occurred",
{},
error_type: :unknown,
)
end
handle jsonrpc: "2.0", id: 1, method: "test_method"
assert_rpc_error expected_error: {
code: -32603,
message: "Internal error",
data: "Unknown error occurred",
}
end
# 6 Batch
#
# To send several Request objects at the same time, the Client MAY send an Array filled with Request objects.

View file

@ -0,0 +1,47 @@
# frozen_string_literal: true
require "test_helper"
module MCP
class LoggingMessageNotificationTest < ActiveSupport::TestCase
test "valid_level? returns true for valid levels" do
LoggingMessageNotification::LOG_LEVEL_SEVERITY.keys.each do |level|
logging_message_notification = LoggingMessageNotification.new(level: level)
assert logging_message_notification.valid_level?, "#{level} should be valid"
end
end
test "valid_level? returns false for invalid levels" do
invalid_levels = ["invalid", 1, "", nil, :fatal]
invalid_levels.each do |level|
logging_message_notification = LoggingMessageNotification.new(level: level)
refute logging_message_notification.valid_level?, "#{level} should be invalid"
end
end
test "should_notify? returns true when notification level is higher priority than threshold level or equals to it" do
logging_message_notification = LoggingMessageNotification.new(level: "warning")
assert logging_message_notification.should_notify?("warning")
assert logging_message_notification.should_notify?("error")
assert logging_message_notification.should_notify?("critical")
assert logging_message_notification.should_notify?("alert")
assert logging_message_notification.should_notify?("emergency")
end
test "should_notify? returns false when notification level is lower priority than threshold level" do
logging_message_notification = LoggingMessageNotification.new(level: "warning")
refute logging_message_notification.should_notify?("notice")
refute logging_message_notification.should_notify?("info")
refute logging_message_notification.should_notify?("debug")
end
test "should_notify? returns false for invalid log levels" do
logging_message_notification = LoggingMessageNotification.new(level: "warning")
refute logging_message_notification.should_notify?("invalid")
refute logging_message_notification.should_notify?(1)
refute logging_message_notification.should_notify?("")
refute logging_message_notification.should_notify?(nil)
refute logging_message_notification.should_notify?(:fatal)
end
end
end

View file

@ -79,8 +79,12 @@ module MCP
# Test resources notification
@server.notify_resources_list_changed
# Test log notification
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
# Check the notifications were sent
assert_equal 3, @mock_stdout.output.size
assert_equal 4, @mock_stdout.output.size
# Parse and verify each notification
notifications = @mock_stdout.output.map { |msg| JSON.parse(msg) }
@ -96,6 +100,10 @@ module MCP
assert_equal "2.0", notifications[2]["jsonrpc"]
assert_equal Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, notifications[2]["method"]
assert_nil notifications[2]["params"]
assert_equal "2.0", notifications[3]["jsonrpc"]
assert_equal Methods::NOTIFICATIONS_MESSAGE, notifications[3]["method"]
assert_equal({ "level" => "error", "data" => { "error" => "Connection Failed" } }, notifications[3]["params"])
end
test "notifications include params when provided" do
@ -120,6 +128,7 @@ module MCP
@server.notify_tools_list_changed
@server.notify_prompts_list_changed
@server.notify_resources_list_changed
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
end
end
@ -240,6 +249,16 @@ module MCP
assert_equal 2, @mock_stdout.output.size
second_notification = JSON.parse(@mock_stdout.output.last)
assert_equal Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, second_notification["method"]
# Set log level and notify
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
# Manually trigger notification
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
assert_equal 3, @mock_stdout.output.size
third_notification = JSON.parse(@mock_stdout.output.last)
assert_equal Methods::NOTIFICATIONS_MESSAGE, third_notification["method"]
assert_equal({ "data" => { "error" => "Connection Failed" }, "level" => "error" }, third_notification["params"])
end
end
end

View file

@ -51,6 +51,12 @@ module MCP
# Test resources notification
@server.notify_resources_list_changed
# Set log level to error for log notification
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
# Test log notification
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
# Check the notifications were received
io.rewind
output = io.read
@ -58,6 +64,7 @@ module MCP
assert_includes output, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED}\"}"
assert_includes output, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED}\"}"
assert_includes output, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED}\"}"
assert_includes output, "data: {\"jsonrpc\":\"2.0\",\"method\":\"#{Methods::NOTIFICATIONS_MESSAGE}\",\"params\":{\"data\":{\"error\":\"Connection Failed\"},\"level\":\"error\"}}\n\n"
end
test "notifications are broadcast to all connected sessions" do
@ -147,6 +154,7 @@ module MCP
@server.notify_tools_list_changed
@server.notify_prompts_list_changed
@server.notify_resources_list_changed
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
end
end

View file

@ -66,14 +66,58 @@ module MCP
assert_nil notification[:params]
end
test "#notify_log_message sends notification through transport" do
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
assert_equal 1, @mock_transport.notifications.size
assert_equal Methods::NOTIFICATIONS_MESSAGE, @mock_transport.notifications.first[:method]
assert_equal({ "data" => { error: "Connection Failed" }, "level" => "error" }, @mock_transport.notifications.first[:params])
end
test "#notify_log_message sends notification with logger through transport" do
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error", logger: "DatabaseLogger")
assert_equal 1, @mock_transport.notifications.size
notification = @mock_transport.notifications.first
assert_equal Methods::NOTIFICATIONS_MESSAGE, notification[:method]
assert_equal({ "data" => { error: "Connection Failed" }, "level" => "error", "logger" => "DatabaseLogger" }, notification[:params])
end
test "#notify_log_message does not send notification with invalid log level" do
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
@server.notify_log_message(data: { message: "test" }, level: "invalid")
assert_equal 0, @mock_transport.notifications.size
end
test "#notify_log_message does not send notification when level is below configured level" do
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
@server.notify_log_message(data: { message: "test" }, level: "info")
assert_equal 0, @mock_transport.notifications.size
end
test "#notify_log_message sends notification when level is above configured level through transport" do
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
@server.notify_log_message(data: { message: "test" }, level: "critical")
assert_equal 1, @mock_transport.notifications.size
assert_equal Methods::NOTIFICATIONS_MESSAGE, @mock_transport.notifications[0][:method]
assert_equal({ "data" => { message: "test" }, "level" => "critical" }, @mock_transport.notifications[0][:params])
end
test "notification methods work without transport" do
server_without_transport = Server.new(name: "test_server")
server_without_transport.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
# Should not raise any errors
assert_nothing_raised do
server_without_transport.notify_tools_list_changed
server_without_transport.notify_prompts_list_changed
server_without_transport.notify_resources_list_changed
server_without_transport.notify_log_message(data: { error: "Connection Failed" }, level: "error")
end
end
@ -86,16 +130,18 @@ module MCP
end.new(@server)
@server.transport = error_transport
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
# Mock the exception reporter
expected_contexts = [
{ notification: "tools_list_changed" },
{ notification: "prompts_list_changed" },
{ notification: "resources_list_changed" },
{ notification: "log_message" },
]
call_count = 0
@server.configuration.exception_reporter.expects(:call).times(3).with do |exception, context|
@server.configuration.exception_reporter.expects(:call).times(4).with do |exception, context|
assert_kind_of StandardError, exception
assert_equal "Transport error", exception.message
assert_includes expected_contexts, context
@ -108,22 +154,26 @@ module MCP
@server.notify_tools_list_changed
@server.notify_prompts_list_changed
@server.notify_resources_list_changed
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
end
assert_equal 3, call_count
assert_equal 4, call_count
end
test "multiple notification methods can be called in sequence" do
@server.notify_tools_list_changed
@server.notify_prompts_list_changed
@server.notify_resources_list_changed
@server.logging_message_notification = MCP::LoggingMessageNotification.new(level: "error")
@server.notify_log_message(data: { error: "Connection Failed" }, level: "error")
assert_equal 3, @mock_transport.notifications.size
assert_equal 4, @mock_transport.notifications.size
notifications = @mock_transport.notifications
assert_equal Methods::NOTIFICATIONS_TOOLS_LIST_CHANGED, notifications[0][:method]
assert_equal Methods::NOTIFICATIONS_PROMPTS_LIST_CHANGED, notifications[1][:method]
assert_equal Methods::NOTIFICATIONS_RESOURCES_LIST_CHANGED, notifications[2][:method]
assert_equal Methods::NOTIFICATIONS_MESSAGE, notifications[3][:method]
end
end
end

View file

@ -142,6 +142,7 @@ module MCP
prompts: { listChanged: true },
resources: { listChanged: true },
tools: { listChanged: true },
logging: {},
},
serverInfo: {
description: "Test server",
@ -762,6 +763,57 @@ module MCP
assert_instrumentation_data({ method: "resources/templates/list" })
end
test "#configure_logging_level returns an error object when invalid log level is provided" do
server = Server.new(
tools: [TestTool],
configuration: Configuration.new(validate_tool_call_arguments: true),
)
response = server.handle(
{
jsonrpc: "2.0",
id: 1,
method: "logging/setLevel",
params: {
level: "invalid_level",
},
},
)
assert_equal "2.0", response[:jsonrpc]
assert_equal 1, response[:id]
assert_equal(-32602, response[:error][:code])
assert_includes response[:error][:data], "Invalid log level invalid_level"
end
test "#configure_logging_level returns an error object when server has not logging capability" do
server = Server.new(
tools: [TestTool],
configuration: Configuration.new(validate_tool_call_arguments: true),
capabilities: {
tools: { listChanged: true },
prompts: { listChanged: true },
resources: { listChanged: true },
},
)
response = server.handle(
{
jsonrpc: "2.0",
id: 1,
method: "logging/setLevel",
params: {
level: "debug",
},
},
)
assert_equal "2.0", response[:jsonrpc]
assert_equal 1, response[:id]
assert_equal(-32603, response[:error][:code])
assert_includes response[:error][:data], "Server does not support logging"
end
test "#handle method with missing required top-level capability returns an error" do
@server.capabilities = {}