-
Notifications
You must be signed in to change notification settings - Fork 62
Add logging
support
#103
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add logging
support
#103
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -111,6 +111,7 @@ The server provides three notification methods: | |||||
- `notify_tools_list_changed` - Send a notification when the tools list changes | ||||||
- `notify_prompts_list_changed` - Send a notification when the prompts list changes | ||||||
- `notify_resources_list_changed` - Send a notification when the resources list changes | ||||||
- `notify_log_message` - Send a structured logging notification message | ||||||
|
||||||
#### Notification Format | ||||||
|
||||||
|
@@ -119,6 +120,28 @@ Notifications follow the JSON-RPC 2.0 specification and use these method names: | |||||
- `notifications/tools/list_changed` | ||||||
- `notifications/prompts/list_changed` | ||||||
- `notifications/resources/list_changed` | ||||||
- `notifications/message` | ||||||
|
||||||
#### Notification Logging Message Flow | ||||||
|
||||||
The `notifications/message` notification is used for structured logging between client and server. | ||||||
|
||||||
1. **Client sends logging configuration**: The client first sends a `logging/setLevel` request to configure the desired log level. | ||||||
2. **Server processes and notifies**: Upon receiving the log level configuration, the server uses `notify_log_message` to send log messages at the configured level and higher priority levels.For example, if "error" is configured, the server can send "error", "critical", "alert", and "emergency" messages. Please refer to `lib/mcp/logging_message_notification.rb` for log priorities in details. | ||||||
|
||||||
##### Usage Example | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
```ruby | ||||||
# Client sets logging level | ||||||
# Request: { "jsonrpc": "2.0", "method": "logging/setLevel", "params": { "level": "error" } } | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be better to show how to set the logging level in the Ruby SDK with Ruby code. |
||||||
|
||||||
# Server sends notifications for log events | ||||||
server.notify_log_message( | ||||||
data: { error: "Connection Failed" }, | ||||||
level: "error", | ||||||
logger: "DatabaseLogger" | ||||||
) | ||||||
``` | ||||||
|
||||||
#### Transport Support | ||||||
|
||||||
|
@@ -139,7 +162,6 @@ server.notify_tools_list_changed | |||||
|
||||||
### Unsupported Features ( to be implemented in future versions ) | ||||||
|
||||||
- Log Level | ||||||
- Resource subscriptions | ||||||
- Completions | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# frozen_string_literal: true | ||
|
||
require "json_rpc_handler" | ||
|
||
module MCP | ||
class LoggingMessageNotification | ||
LOG_LEVELS = { | ||
"debug" => 0, | ||
"info" => 1, | ||
"notice" => 2, | ||
"warning" => 3, | ||
"error" => 4, | ||
"critical" => 5, | ||
"alert" => 6, | ||
"emergency" => 7, | ||
}.freeze | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Intuitively I think There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had the same thought, but I prioritized meeting the specifications of RFC 5424's Numerical Code explicitly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems my explanation was not clear. What is actually expected is the following. LOG_LEVELS = {
"debug" => 0,
"info" => 1,
"notice" => 2,
"warning" => 3,
"error" => 4,
"critical" => 5,
"alert" => 6,
"emergency" => 7,
}.freeze There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's more intuitive. Fixed. |
||
|
||
private attr_reader :level | ||
|
||
def initialize(level:) | ||
@level = level | ||
end | ||
|
||
def valid_level? | ||
LOG_LEVELS.keys.include?(level) | ||
end | ||
|
||
def should_notify?(log_level) | ||
LOG_LEVELS[log_level] >= LOG_LEVELS[level] | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
require "json_rpc_handler" | ||
require_relative "instrumentation" | ||
require_relative "methods" | ||
require_relative "logging_message_notification" | ||
|
||
module MCP | ||
class Server | ||
|
@@ -31,7 +32,7 @@ def initialize(method_name) | |
|
||
include Instrumentation | ||
|
||
attr_accessor :name, :title, :version, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport | ||
attr_accessor :name, :title, :version, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport, :logging_message_notification | ||
|
||
def initialize( | ||
name: "model_context_protocol", | ||
|
@@ -65,6 +66,7 @@ def initialize( | |
end | ||
|
||
@capabilities = capabilities || default_capabilities | ||
@logging_message_notification = nil | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I understand correctly, the Python SDK uses "info" level as the default. What do you think about doing the same? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it's necessary to set a default value, but what do you think? The log_level literal specified in the MCP spec appears to be defined in mcp/types.py, and it seems that no default value has been set. The log_level in fastmcp/server.py#L132 appears to set the default value for uvicorn's log_level. However, if this literal is the same as the one specified in the MCP spec, I don't think it meets the logging specifications, as levels such as There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's true. There's no need to set something that's not explicitly specified in the specification. |
||
|
||
@handlers = { | ||
Methods::RESOURCES_LIST => method(:list_resources), | ||
|
@@ -77,12 +79,12 @@ def initialize( | |
Methods::INITIALIZE => method(:init), | ||
Methods::PING => ->(_) { {} }, | ||
Methods::NOTIFICATIONS_INITIALIZED => ->(_) {}, | ||
Methods::LOGGING_SET_LEVEL => method(:logging_level=), | ||
|
||
# No op handlers for currently unsupported methods | ||
Methods::RESOURCES_SUBSCRIBE => ->(_) {}, | ||
Methods::RESOURCES_UNSUBSCRIBE => ->(_) {}, | ||
Methods::COMPLETION_COMPLETE => ->(_) {}, | ||
Methods::LOGGING_SET_LEVEL => ->(_) {}, | ||
} | ||
@transport = transport | ||
end | ||
|
@@ -141,6 +143,21 @@ def notify_resources_list_changed | |
report_exception(e, { notification: "resources_list_changed" }) | ||
end | ||
|
||
def notify_log_message(data:, level:, logger: nil) | ||
return unless @transport | ||
unless logging_message_notification | ||
raise RequestHandlerError.new("logging_message_notification must not be null", {}, error_type: :logging_message_notification_not_specified) | ||
end | ||
return unless logging_message_notification.should_notify?(level) | ||
|
||
params = { data:, level: } | ||
params[:logger] = logger if logger | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems there are no test cases for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you said, I couldn't test it. Added. |
||
|
||
@transport.send_notification(Methods::NOTIFICATIONS_MESSAGE, params) | ||
rescue => e | ||
report_exception(e, { notification: "log_message" }) | ||
end | ||
|
||
def resources_list_handler(&block) | ||
@handlers[Methods::RESOURCES_LIST] = block | ||
end | ||
|
@@ -214,6 +231,7 @@ def default_capabilities | |
tools: { listChanged: true }, | ||
prompts: { listChanged: true }, | ||
resources: { listChanged: true }, | ||
logging: {}, | ||
} | ||
end | ||
|
||
|
@@ -234,6 +252,15 @@ def init(request) | |
}.compact | ||
end | ||
|
||
def logging_level=(request) | ||
logging_message_notification = LoggingMessageNotification.new(level: request[:level]) | ||
unless logging_message_notification.valid_level? | ||
raise RequestHandlerError.new("Invalid log level #{request[:level]}", request, error_type: :invalid_log_level) | ||
end | ||
|
||
@logging_message_notification = logging_message_notification | ||
end | ||
|
||
def list_tools(request) | ||
@tools.map { |_, tool| tool.to_h } | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# frozen_string_literal: true | ||
|
||
require "test_helper" | ||
|
||
module MCP | ||
class LoggingMessageNotificationTest < ActiveSupport::TestCase | ||
test "valid_level? returns true for valid levels" do | ||
LoggingMessageNotification::LOG_LEVELS.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 | ||
end | ||
end |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to provide an explanation of the Ruby API as part of the Ruby SDK. As for the MCP specification, I think it would be helpful to include a link to https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging.