Add logging support
A server can send structured logging messages to the client. https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#logging Logging was specified in the 2024-11-05 specification, but since it was not supported in ruby-sdk, I implemented it. https://modelcontextprotocol.io/specification/2024-11-05/server/utilities/logging I also made it possible to output a simple notification message in the examples.
This commit is contained in:
parent
830913be5a
commit
afa2fb4077
6 changed files with 434 additions and 7 deletions
|
|
@ -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.
|
||||
|
|
|
|||
47
test/mcp/logging_message_notification_test.rb
Normal file
47
test/mcp/logging_message_notification_test.rb
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue