Skip to content

fix: keep logging levels per session - #667

Merged
koic merged 1 commit into
modelcontextprotocol:mainfrom
atesgoral:fix/set-level-per-session
Nov 8, 2026
Merged

koic merged 1 commit into
modelcontextprotocol:mainfrom
atesgoral:fix/set-level-per-session

Conversation

@atesgoral

@atesgoral atesgoral commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

Motivation and Context

Keep logging/setLevel on the calling session. Filter server-wide log messages per recipient through ServerSession#notify_log_message, using the transports' existing session state. A session's own level takes precedence over the application default. Preserve session-less logging and the default path for custom transports that do not expose sessions.

Protocol and TypeScript references checked before the change:

  • Legacy logging, Setting Log Level says clients MAY send logging/setLevel. The legacy schema says the server should send logs "at this level and higher" to that client, and "the server MAY decide which messages to send automatically" before that client sets a level. Keep the existing application default as that fallback.
  • TypeScript's logging handler stores _loggingLevels.set(transportSessionId, parseResult.data). Its sendLoggingMessage calls isMessageIgnored(params.level, sessionId). Its legacy request context uses _loggingLevels.get(ctx.sessionId) ?? this._loggingLevels.get(undefined).
  • Modern logging, Per-request log level makes io.modelcontextprotocol/logLevel optional but says the server MUST NOT emit request logs without it. With it, the server MAY send logs on that request's response stream, not another stream or subscriptions/listen. Keep modern logging request-scoped; do not include modern stdio sessions in broadcasts.
  • RFC 5424 section 6.2.1, Table 2 defines the severity ordering used by the logging specification. The existing level comparison and accepted fields are unchanged.

Restore the original stdio broadcast assertion and documentation. No second session registry or new validation rules.

Fixes #658.

How Has This Been Tested?

The restored broadcast assertion and four new HTTP/stdio tests fail on the previous published head: 34 runs, 178 assertions, 5 failures and no errors. The failures cover stdio delivery, per-recipient HTTP thresholds, isolated defaults and modern request scope.

  • Focused notification tests: 34 runs, 191 assertions, no failures, errors or skips.
  • Full Ruby suite: 2,507 runs, 8,345 assertions, no failures, errors or skips.
  • RuboCop with --force-exclusion: all six changed Ruby files clean.
  • git diff --check passes.

Tests use the installed Ruby 4.0.7 gems with MT_NO_PLUGINS=1, Minitest ~> 5.1 and ActiveSupport 7.2.2.2.

Breaking Changes

None. Client-configured logging continues to enable stdio broadcasts. Application defaults and session-less logging remain supported.

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

@atesgoral
atesgoral force-pushed the fix/set-level-per-session branch from 86f54e2 to 7f43690 Compare November 6, 2026 06:33
@atesgoral
atesgoral marked this pull request as ready for review November 6, 2026 06:53
@atesgoral
atesgoral marked this pull request as draft November 6, 2026 07:54
@atesgoral
atesgoral force-pushed the fix/set-level-per-session branch from 7f43690 to 20b8e86 Compare November 6, 2026 13:59
@atesgoral
atesgoral marked this pull request as ready for review November 6, 2026 14:14
Comment thread test/mcp/server_notification_test.rb Outdated
log_notifications = @mock_transport.notifications.select { |n| n[:method] == Methods::NOTIFICATIONS_MESSAGE }
assert_equal 1, log_notifications.size
assert_equal "broadcast log", log_notifications.first[:params]["data"]
assert_empty log_notifications

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This test pinned the behavior the specification asks for; with the per-session filter it keeps its original assertion of one delivered notification.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Restored the original assertion of one delivered notification and its data assertion. Broadcasts now use the recipient's session filter, so the stdio client still receives logs after setting its level.

Comment thread docs/_server/logging.md Outdated
transport.open
```

`Server#notify_log_message` broadcasts use only the server-wide level, not a session's level. Applications can set the server-wide level explicitly:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

With per-recipient filtering this paragraph, line 90 and the bullet on line 97 go away: broadcasts honor each session's level, and the application default remains the fallback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Restored docs/_server/index.md and docs/_server/logging.md to the base revision. The per-recipient filter uses each session's level and falls back to the application default, so that rewrite is no longer needed.

Comment thread lib/mcp/server.rb
if session
session.configure_logging(logging_message_notification)
else
@logging_message_notification = logging_message_notification

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Keeping the level on the calling session is right. But Server#notify_log_message (lines 448-450) filters only on the server-wide level, so after this change a client's logging/setLevel no longer enables broadcasts at all, including on a single-session stdio server, which the PR now declares as a breaking change. That trades one deviation from the specification for another.

Filtering the broadcast per recipient session avoids both: compare the notification's level with that session's level and fall back to the server-wide default when the session set none. That is how the TypeScript and Python servers resolve the threshold, it keeps the existing stdio flow working, and the breaking-change section becomes unnecessary.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Broadcasts now enumerate the transports' existing recipients and use each ServerSession to filter and deliver the log. A client's level stays local, the application default remains the fallback, and stdio delivery keeps working. HTTP tests cover different client levels and both existing and future unconfigured sessions.

@koic

koic commented Nov 7, 2026

Copy link
Copy Markdown
Member

Accurate problem definition, and the session-scoped half of the fix is correct. One change requested: filter broadcasts per recipient session instead of disabling them for clients that set a level (inline on lib/mcp/server.rb). With that, the docs rewrite and the breaking-change note are not needed.

@atesgoral
atesgoral marked this pull request as draft November 7, 2026 15:18
@atesgoral
atesgoral force-pushed the fix/set-level-per-session branch from 20b8e86 to 27e3536 Compare November 7, 2026 15:35
@atesgoral

Copy link
Copy Markdown
Contributor Author

Fixed per-recipient filtering, restored the original test assertions and docs, and removed the breaking-change note. I checked both protocol lifecycles and the TypeScript filters; the PR body now includes those references.

@atesgoral
atesgoral marked this pull request as ready for review November 7, 2026 15:57
end
end

def logging_sessions

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

logging_sessions (here and in StdioTransport) is a hook Server#notify_log_message looks up by duck typing, so it has to be a public method, but it is not part of the published API. A short YARD comment would make that clear: @api private, what it returns (the sessions a server-wide log broadcast is offered to, each applying its own level or the server default; nil keeps the server-wide filter), and that a custom transport may implement it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added short YARD comments to both hooks with @api private, their return types, the custom-transport contract and the nil fallback.

@atesgoral
atesgoral force-pushed the fix/set-level-per-session branch from 27e3536 to bdf060f Compare November 7, 2026 20:08
@koic
koic merged commit 9422c1b into modelcontextprotocol:main Nov 8, 2026
13 checks passed
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.

logging/setLevel from one session changes the log level of other sessions

2 participants