Skip to content

Conversation

dak2
Copy link
Contributor

@dak2 dak2 commented Aug 4, 2025

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?

  • Check existing and added test cases for this case
  • Check a sse server of examples can send logging message to the client

Breaking Changes

None

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context


module MCP
class LoggingMessageNotification
VALID_LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"].freeze
Copy link
Member

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?

Suggested change
VALID_LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"].freeze
LOG_LEVELS = ["debug", "info", "notice", "warning", "error", "critical", "alert", "emergency"].freeze

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

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

Copy link
Contributor Author

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
Copy link
Member

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

Copy link
Contributor Author

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.

https://github.com/modelcontextprotocol/python-sdk/blob/68e25d478b3b6a026b2d9a30b3e5f34f3b1290de/src/mcp/types.py#L905

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.

Copy link
Member

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.

@dak2 dak2 force-pushed the support-logging branch 2 times, most recently from 6f91a19 to 754f8cd Compare August 5, 2025 01:01
@@ -0,0 +1,48 @@
# typed: true
Copy link
Member

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dak2 dak2 force-pushed the support-logging branch from 754f8cd to aa15a5b Compare August 5, 2025 12:26
koic
koic previously approved these changes Aug 5, 2025
@koic
Copy link
Member

koic commented Aug 9, 2025

@dak2 Oops, can you rebase with the latest master to resolve the conflicts?

@dak2
Copy link
Contributor Author

dak2 commented Aug 10, 2025

@koic
Thank you for letting me know!
The conflict has been resolved. Could you please review it again?

koic
koic previously approved these changes Aug 10, 2025
@koic
Copy link
Member

koic commented Aug 13, 2025

Oops, sorry if my understanding of the specification and this implementation is different. As I understand it, with logging/setLevel, only messages at the specified level or higher should be sent from the server to the client as notifications/message. In other words, if the level error is specified, messages at info or warning level should not be notified.
I believe "Only sends error level and above" in the "Message Flow" section corresponds to this.
https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#message-flow

It seems to me that the current PR is not implemented in this way. Could you check?

@dak2
Copy link
Contributor Author

dak2 commented Aug 14, 2025

@koic
Thank you for checking it!

Oops, I missed it! That's true. This implementation doesn't meet a logging specification.
I'll implement it soon when I have free time.

@dak2
Copy link
Contributor Author

dak2 commented Aug 17, 2025

@koic

I implemented the logic to determine whether or not to send a notification based on level.
Could you please review it again?

https://github.com/modelcontextprotocol/ruby-sdk/compare/ef5049bab808dec719765a771c04e7c897c2a8a3..c9227469f523c930ab98b49567abf4dbd7213abe

If the notification level does not match, it returns nil.
According to the specification, there are no restrictions on notifications when the level does not match, and the java-sdk and go-sdk implementations appear to be the same.

@dak2 dak2 force-pushed the support-logging branch 2 times, most recently from cf8cfcf to cf2e38c Compare August 24, 2025 02:54
"notice" => 5,
"info" => 6,
"debug" => 7,
}.freeze
Copy link
Member

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.

Copy link
Contributor Author

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.

Copy link
Member

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

Copy link
Contributor Author

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.

@@ -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)
Copy link
Member

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

Copy link
Contributor Author

@dak2 dak2 Aug 26, 2025

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.

Copy link
Contributor Author

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")
Copy link
Member

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.

Copy link
Contributor Author

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.

return unless @transport
raise LoggingMessageNotification::NotSpecifiedLevelError unless logging_message_notification&.level

current_level = notification_level || logging_message_notification.level
Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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?

Copy link
Contributor Author

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.

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)
Copy link
Member

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.

Suggested change
return unless logging_message_notification.should_notify?(notification_level: current_level)
return unless logging_message_notification.should_notify?(level: log_level)

Copy link
Contributor Author

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?

Copy link
Member

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?

Copy link
Contributor Author

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.

@koic
Copy link
Member

koic commented Aug 24, 2025

User-facing documentation should be included in the README.

@dak2 dak2 force-pushed the support-logging branch 9 times, most recently from 8322249 to aba1877 Compare August 26, 2025 01:40
@dak2 dak2 force-pushed the support-logging branch from aba1877 to 378a469 Compare August 26, 2025 01:57
@dak2
Copy link
Contributor Author

dak2 commented Aug 26, 2025

@koic

User-facing documentation should be included in the README.

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
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
##### Usage Example
**Usage Example:**

README.md Outdated
Comment on lines 140 to 142
level: "error",
data: { error: "Connection Failed" },
logger: "DatabaseLogger"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Member

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?

Copy link
Contributor Author

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

Copy link
Contributor Author

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

Copy link
Contributor Author

@dak2 dak2 Aug 27, 2025

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" } }
Copy link
Member

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.
Copy link
Member

@koic koic Aug 26, 2025

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.

@dak2 dak2 force-pushed the support-logging branch 5 times, most recently from 4e53a64 to c590295 Compare September 1, 2025 22:43
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants