-
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
Conversation
|
||
module MCP | ||
class LoggingMessageNotification | ||
VALID_LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"].freeze |
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.
Can you rename to the folliwng?
VALID_LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"].freeze | |
LOG_LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"].freeze |
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.
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.
I'd suggest to also use the string array directly for easier parsing:
LOG_LEVELS = %w[debug info notice warning error critical alert emergency].freeze
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.
You are right according to the Ruby style guide.
However, I don't think it needs to be fixed.
This repository depends on rubocop-shopify. It disables the Style/WordArray
cop.
See: https://github.com/Shopify/ruby-style-guide/blob/0de5b278c576ad2f537f9f51f5897587a9b33841/rubocop.yml#L1405-L1407
Additionally, there don't seem to be any compelling reasons to enable it for this case in this repository.
@@ -55,6 +56,7 @@ def initialize( | |||
@server_context = server_context | |||
@configuration = MCP.configuration.merge(configuration) | |||
@capabilities = capabilities || default_capabilities | |||
@logging_message_notification = nil |
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.
If I understand correctly, the Python SDK uses "info" level as the default. What do you think about doing the same?
https://github.com/modelcontextprotocol/python-sdk/blob/v1.12.3/src/mcp/server/fastmcp/server.py#L132
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.
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 emergency
and notice
are not defined.
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.
That's true. There's no need to set something that's not explicitly specified in the specification.
Your current suggestion makes sense to me.
6f91a19
to
754f8cd
Compare
@@ -0,0 +1,48 @@ | |||
# typed: true |
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.
Wouldn't the test pass even without this # typed: true
?
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.
Oh, it's probably a copy-paste mistake. Deleted.
@dak2 Oops, can you rebase with the latest master to resolve the conflicts? |
@koic |
Oops, sorry if my understanding of the specification and this implementation is different. As I understand it, with It seems to me that the current PR is not implemented in this way. Could you check? |
@koic Oops, I missed it! That's true. This implementation doesn't meet a logging specification. |
I implemented the logic to determine whether or not to send a notification based on level. If the notification level does not match, it returns nil. |
cf8cfcf
to
cf2e38c
Compare
"notice" => 5, | ||
"info" => 6, | ||
"debug" => 7, | ||
}.freeze |
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.
Intuitively I think debug
with the lowest level of information should be ordered as 0
and emergency
with the highest level of information as 7
. In other words, the keys are in reverse order.
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.
I had the same thought, but I prioritized meeting the specifications of RFC 5424's Numerical Code explicitly.
However, looking at the java-sdk, it seems to be defined in reverse order.
Since that order is intuitive and easy to understand, I made the same correction.
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 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 comment
The reason will be displayed to describe this comment to others. Learn more.
That's more intuitive. Fixed.
lib/mcp/server.rb
Outdated
@@ -138,6 +140,22 @@ def notify_resources_list_changed | |||
report_exception(e, { notification: "resources_list_changed" }) | |||
end | |||
|
|||
def notify_logging_message(notification_level: nil, logger: nil, data: nil) |
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.
notify_log_message
looks a bit simple. Was there any SDK or other reference you based the naming on? Also, level
keyword might be simpler for users than the notification_level
keyword argument.
And, it looks like everything except the logger
keyword is required. As for the ordering, the schema's data
, level
, logger
sequence seems easier to understand.
https://modelcontextprotocol.io/specification/2025-06-18/schema#notifications%2Fmessage
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.
notify_log_message looks a bit simple. Was there any SDK or other reference you based the naming on?
That may be true. I also felt it was a little redundant when I implemented it. I referred to typescript-sdk implementation. (Although the prefix is send
)
However, as you said, notify_log_message is simpler, so I changed it.
It looks like python-sdk uses the name send_log_message
.
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.
Also, level keyword might be simpler for users than the notification_level keyword argument.
And, it looks like everything except the logger keyword is required. As for the ordering, the schema's data, level, logger sequence seems easier to understand.
Fixed them.
|
||
test "should_notify? returns false when notification level is lower priority than threshold level" do | ||
logging_message_notification = LoggingMessageNotification.new(level: "warning") | ||
assert_not logging_message_notification.should_notify?(notification_level: "notice") |
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.
That's just my two cents. refute
appears to be preferred to assert_not
in this repository.
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.
That seems to be the case. I don't have any particular preferences, so I'll just go with the style of the project.
Fixed it.
lib/mcp/server.rb
Outdated
return unless @transport | ||
raise LoggingMessageNotification::NotSpecifiedLevelError unless logging_message_notification&.level | ||
|
||
current_level = notification_level || logging_message_notification.level |
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.
current_level
would be more precise as log_level
. And log_level
is a required parameter, I didn't understand the intention of falling back to the global logging_message_notification.level
.
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.
The KW argument for level was implemented with a default value of nil, so it seems that it was implemented to falling back. I removed it because it was unnecessary.
return unless logging_message_notification.should_notify?(notification_level: current_level) | ||
|
||
params = { level: current_level } | ||
params[:logger] = logger if logger |
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 seems there are no test cases for the logger
keyword. Can you add it?
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.
As you said, I couldn't test it. Added.
lib/mcp/server.rb
Outdated
raise LoggingMessageNotification::NotSpecifiedLevelError unless logging_message_notification&.level | ||
|
||
current_level = notification_level || logging_message_notification.level | ||
return unless logging_message_notification.should_notify?(notification_level: current_level) |
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.
notification
is already clear from the receiver name, so level
alone might be sufficient.
return unless logging_message_notification.should_notify?(notification_level: current_level) | |
return unless logging_message_notification.should_notify?(level: log_level) |
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.
If it is defined as level
name, I think it will be difficult to distinguish the KW argument name of should_notify?
from the level instance variable name in lib/mcp/logging_message_notification.rb
.
As an alternative, how about setting the KW argument name of should_notify?
to log_level
?
I think log_level
is easier to understand because it indicates data received when notifying the log. What do you think?
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.
Ah, it doesn't really seem necessary for it to be a keyword argument in the first place. How about making it a positional argument instead?
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.
Thank you. You're absolutely right. Fixed.
User-facing documentation should be included in the README. |
8322249
to
aba1877
Compare
I wrote how to use notifications/message in the README.md, but I would like feedback on whether the text is easy to understand. |
"notice" => 5, | ||
"info" => 6, | ||
"debug" => 7, | ||
}.freeze |
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 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
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 comment
The reason will be displayed to describe this comment to others. Learn more.
##### Usage Example | |
**Usage Example:** |
README.md
Outdated
level: "error", | ||
data: { error: "Connection Failed" }, | ||
logger: "DatabaseLogger" |
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.
level: "error", | |
data: { error: "Connection Failed" }, | |
logger: "DatabaseLogger" | |
data: { error: "Connection Failed" }, | |
level: "error", | |
logger: "DatabaseLogger" |
class InvalidLevelError < StandardError | ||
def initialize | ||
super("Invalid log level provided. Valid levels are: #{LOG_LEVELS.keys.join(", ")}") | ||
@code = JsonRpcHandler::ErrorCode::InvalidParams |
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.
What use cases is this @code
set for the user?
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.
Its purpose is to communicate the log level is invalid. However, I realized that it must be returned as a JSON-RPC error object. This needs to be fixed.
https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#error-handling
https://www.jsonrpc.org/specification#error_object
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.
Upon further review, it appears that the JSON-RPC error objects are returned and handled in https://github.com/Shopify/json-rpc-handler.
I'm considering writing a patch for json-rpc-handler
that explicitly calls the error objects -32602
for invalid log levels and -32603
for configuration errors.
The As it stands now: https://github.com/modelcontextprotocol/ruby-sdk/pull/103/files#diff-140fb0b13518d178300d879c19c9751cdd1a69f9c4ce07d3fb317b2c801b7d80R1109 expects -32602
, but actually returns -32603
.
It appears that this is likely being rescued by https://github.com/Shopify/json-rpc-handler/blob/a8a632681e24e53223a669612987686662975039/lib/json_rpc_handler.rb#L105-L111, resulting in -32603
being returned.
When an invalid log level is encountered, the MCP specification requires returning an error object with code -32602
. However, it currently appears there is no way to specify and return -32602
within json-rpc-handler. (I can write it to return the error object directly in the ruby-sdk
, but delegating it to json-rpc-handler
makes the responsibilities appear simpler.)
class NotSpecifiedLevelError < StandardError | ||
def initialize | ||
super("Log level not specified. Please set a valid log level.") | ||
@code = JsonRpcHandler::ErrorCode::InternalError |
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.
Ditto.
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.
Its purpose is to communicate the configuration error. This is also the same issue.
#103 (comment)
|
||
```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 comment
The 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.
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. |
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.
4e53a64
to
c590295
Compare
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.
c590295
to
9be18f8
Compare
Motivation and Context
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.
How Has This Been Tested?
Breaking Changes
None
Types of changes
Checklist
Additional context