Skip to content

Introduce ServerSession for per-connection state#275

Merged
koic merged 1 commit intomodelcontextprotocol:mainfrom
koic:introduce_server_sessoin_for_per_connection_state
Mar 29, 2026
Merged

Introduce ServerSession for per-connection state#275
koic merged 1 commit intomodelcontextprotocol:mainfrom
koic:introduce_server_sessoin_for_per_connection_state

Conversation

@koic
Copy link
Copy Markdown
Member

@koic koic commented Mar 28, 2026

Motivation and Context

The Ruby SDK uses a 1-Server-to-many-sessions model where session-specific state (@client, @logging_message_notification) is stored on the shared Server instance. This causes the last client to connect to overwrite state for all sessions, and requires session_id to be threaded through method signatures for session-scoped notifications.

The Python SDK avoids this by creating a ServerSession per connection that wraps a shared Server. Each session holds its own state and naturally scopes notifications through its own stream.

Changes

  • Added MCP::ServerSession class holding per-session state: client info, logging configuration, and a transport reference for session-scoped notification delivery.
  • StreamableHTTPTransport#handle_initialization creates a ServerSession per initialize request and stores it in the session hash.
  • handle_regular_request routes requests through the session's ServerSession instead of the shared Server directly.
  • Server#handle and Server#handle_json accept an optional session: keyword (ServerSession instance).
  • Server#init stores client info on the session when available.
  • Server#configure_logging_level stores logging config on the session.
  • ServerContext and Progress accept a notification_target: which can be either a ServerSession (session-scoped) or a Server (broadcast). No session_id: parameter needed.
  • ServerContext#notify_progress and #notify_log_message delegate to the notification_target without session_id threading.
  • StdioTransport creates a single ServerSession on open, making the session model transparent across both transports.

Design Decision

This follows the Python SDK's "shared Server + per-connection Session" pattern. The Server holds configuration (tools, prompts, resources, handlers). Each ServerSession holds per-connection state (client info, logging level, stream writer). Notifications from ServerSession go only to that session's stream.

How Has This Been Tested?

All existing tests pass. All conformance tests pass. Added tests verifying:

  • Session-scoped log notification is sent only to the originating session.
  • Session-scoped progress notification is sent only to the originating session.
  • Each session stores its own client info independently.
  • Each session stores its own logging level independently.
  • StdioTransport#open creates a ServerSession and stores client info on it.

Breaking Change

None for end users.

The public API (Server.new, define_tool,server_context.report_progress, server_context.notify_log_message, etc.) is unchanged. The following internal API changes affect only SDK internals:

  • ServerContext.new now requires notification_target: instead of justprogress:.
  • Progress.new now takes notification_target: instead of server:.
  • Server#handle and Server#handle_json accept an optional session: keyword.

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

@koic koic force-pushed the introduce_server_sessoin_for_per_connection_state branch 3 times, most recently from 61b6900 to 9caba05 Compare March 28, 2026 19:30
## Motivation and Context

The Ruby SDK uses a 1-Server-to-many-sessions model where session-specific state
(`@client`, `@logging_message_notification`) is stored on the shared `Server` instance.
This causes the last client to connect to overwrite state for all sessions,
and requires `session_id` to be threaded through method signatures for session-scoped notifications.

The Python SDK avoids this by creating a `ServerSession` per connection that wraps a shared `Server`.
Each session holds its own state and naturally scopes notifications through its own stream.

### Changes

- Added `MCP::ServerSession` class holding per-session state: client info,
  logging configuration, and a `transport` reference for session-scoped
  notification delivery.
- `StreamableHTTPTransport#handle_initialization` creates a `ServerSession`
  per `initialize` request and stores it in the session hash.
- `handle_regular_request` routes requests through the session's
  `ServerSession` instead of the shared `Server` directly.
- `Server#handle` and `Server#handle_json` accept an optional `session:`
  keyword (`ServerSession` instance).
- `Server#init` stores client info on the session when available.
- `Server#configure_logging_level` stores logging config on the session.
- `ServerContext` and `Progress` accept a `notification_target:` which
  can be either a `ServerSession` (session-scoped) or a `Server` (broadcast).
  No `session_id:` parameter needed.
- `ServerContext#notify_progress` and `#notify_log_message` delegate to
  the `notification_target` without `session_id` threading.
- `StdioTransport` creates a single `ServerSession` on `open`,
  making the session model transparent across both transports.

### Design Decision

This follows the Python SDK's "shared `Server` + per-connection `Session`" pattern.
The `Server` holds configuration (tools, prompts, resources, handlers).
Each `ServerSession` holds per-connection state (client info, logging level, stream writer).
Notifications from `ServerSession` go only to that session's stream.

## How Has This Been Tested?

All existing tests pass. All conformance tests pass. Added tests verifying:

- Session-scoped log notification is sent only to the originating session.
- Session-scoped progress notification is sent only to the originating session.
- Each session stores its own client info independently.
- Each session stores its own logging level independently.
- `StdioTransport#open` creates a `ServerSession` and stores client info on it.

## Breaking Change

None for end users.

The public API (`Server.new`, `define_tool`,`server_context.report_progress`,
`server_context.notify_log_message`, etc.) is unchanged. The following internal API
changes affect only SDK internals:

- `ServerContext.new` now requires `notification_target:` instead of just`progress:`.
- `Progress.new` now takes `notification_target:` instead of `server:`.
- `Server#handle` and `Server#handle_json` accept an optional `session:` keyword.
@koic koic force-pushed the introduce_server_sessoin_for_per_connection_state branch from 9caba05 to 719e5e7 Compare March 28, 2026 23:06

- `Server` holds shared configuration (tools, prompts, resources, handlers)
- `ServerSession` holds per-connection state (client info, logging level)
- Both `StdioTransport` and `StreamableHTTPTransport` create a `ServerSession` per connection, making the session model transparent across transports
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

❤️

Copy link
Copy Markdown
Contributor

@atesgoral atesgoral left a comment

Choose a reason for hiding this comment

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

This is great!

@koic koic merged commit 0cc8a30 into modelcontextprotocol:main Mar 29, 2026
11 checks passed
@koic koic deleted the introduce_server_sessoin_for_per_connection_state branch March 29, 2026 02:46
koic added a commit that referenced this pull request Mar 29, 2026
## Motivation and Context

`Server#notify_progress` broadcasts progress notifications to all connected clients.
Progress notifications are tied to a specific request's `progressToken` and have no
meaningful broadcast use case. Neither the Python SDK nor the TypeScript SDK provides
a server-level progress broadcast capability.

The MCP specification requires that progress notifications only reference tokens
provided in an active request:

> Progress notifications MUST only reference tokens that:
> - Were provided in an active request
> - Are associated with an in-progress operation

Ref: https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/progress

Broadcasting progress to all clients does not comply with this requirement,
since clients that did not originate the request have no matching `progressToken`.

The introduction of `ServerSession` for per-connection state (#275) made
this removal possible: `Progress` and `ServerContext` now use `notification_target:`
(a `ServerSession`) to route notifications to the originating session only.

### Changes

- Removed `Server#notify_progress` entirely. The method is no longer needed as
  `notification_target` in `call_tool_with_args` now passes `session` directly
  (nil when no session is available, in which case `Progress#report` and
  `ServerContext#notify_log_message` are no-ops).
- Removed `ServerContext#notify_progress` as well. Progress notifications should
  only be sent via `server_context.report_progress`, which enforces the correct
  `progressToken` from the originating request.
- Added nil guards to `Progress#report` and `ServerContext#notify_log_message`
  for when no session is available.
- Rewrote progress tests to use `ServerSession#handle` instead of
  `Server#handle`, reflecting that progress notifications are always session-scoped.
- Removed 4 tests that called `server.notify_progress` directly as a public broadcast API.
- Removed `notify_progress` from the README.md notification methods list.
- Removed the "Server-Side: Direct `notify_progress` Usage" section from the README.md.

Progress notifications should be sent via `server_context.report_progress` inside tool handlers,
which automatically scopes them to the originating client session.

## Breaking Changes

Progress notifications are scoped to a specific request via `progressToken` per the MCP specification,
so the broadcast behavior of `Server#notify_progress` was a spec violation.
This is treated as a bug fix and is made without a deprecation period.
This aligns the Ruby SDK with the Python and TypeScript SDKs, neither of which provides
a server-level progress broadcast API.

`Server#notify_progress` and `ServerContext#notify_progress` are no longer available.
Users should use `server_context.report_progress` inside tool handlers instead, which
provides session-scoped delivery with the correct `progressToken`.

This feature was only recently introduced in mcp 0.9.0:
https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.9.0

So an early release should help limit its impact on adoption.
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.

2 participants