## 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.
## Motivation and Context
`lib/mcp.rb` currently declares all `require_relative` statements in one place.
As a result, Server and Client are both loaded, the stdio and Streamable HTTP
transports are both loaded, and Tool, Prompt, and Resource are all loaded,
regardless of which components are actually used.
These components are selected by the user depending on their use case,
so loading everything upfront is unnecessary.
Using `autoload` for selected constants defers loading until the constant
is actually referenced, so only the subsystems required by the user are loaded.
## How Has This Been Tested?
All existing tests pass unchanged.
## Breaking Change
None. This change only defers loading of unused components.
This PR suppresses the following backtrace of `Interrupt` (Ctrl+c) exception for stdio server:
## Before
```console
$ ./examples/stdio_server.rb
^C/Users/koic/src/github.com/modelcontextprotocol/ruby-sdk/lib/mcp/server/transports/stdio_transport.rb:20:
in 'IO#gets': Interrupt
from /Users/koic/src/github.com/modelcontextprotocol/ruby-sdk/lib/mcp/server/transports/stdio_transport.rb:20:
in 'MCP::Server::Transports::StdioTransport#open'
from ./examples/stdio_server.rb:95:in '<main>'
$ echo $?
130
```
## After
```console
$ ./examples/stdio_server.rb
^C
Exiting...
$ echo $?
130
```
This change should prevent unexpected backtrace from appearing when running the example:
https://github.com/modelcontextprotocol/ruby-sdk?tab=readme-ov-file#stdio-transport
Follow-up to #27.
This PR fixes the following build error:
```console
$ bundle exec rake
/Users/koic/.rbenv/versions/3.5-dev/lib/ruby/3.5.0+1/bundled_gems.rb:59:in 'Kernel.require': cannot load such file -- mcp/transports/stdio (LoadError)
Did you mean? mcp/transports/stdio_transport_test
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/3.5.0+1/bundled_gems.rb:59:in 'block (2 levels) in Kernel#replace_require'
from /Users/koic/src/github.com/modelcontextprotocol/ruby-sdk/test/mcp/transports/stdio_transport_test.rb:4:in '<top (required)>'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/3.5.0+1/bundled_gems.rb:59:in 'Kernel.require'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/3.5.0+1/bundled_gems.rb:59:in 'block (2 levels) in Kernel#replace_require'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/gems/3.5.0+1/gems/rake-13.3.0/lib/rake/rake_test_loader.rb:21:in 'block in <main>'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/gems/3.5.0+1/gems/rake-13.3.0/lib/rake/rake_test_loader.rb:6:in 'Array#select'
from /Users/koic/.rbenv/versions/3.5-dev/lib/ruby/gems/3.5.0+1/gems/rake-13.3.0/lib/rake/rake_test_loader.rb:6:in '<main>'
rake aborted!
Command failed with status (1)
/Users/koic/.rbenv/versions/3.5-dev/bin/bundle:25:in 'Kernel#load'
/Users/koic/.rbenv/versions/3.5-dev/bin/bundle:25:in '<main>'
Tasks: TOP => default => test
(See full trace by running task with --trace)
```
https://github.com/modelcontextprotocol/ruby-sdk/actions/runs/15428236624/job/43420346011
The following is stated in #27.
> Breaking Changes
> `MCP::Transports::StdioTransport` is now `MCP::Server::Transports::StdioTransport`
What is shown here is the new name `MCP::Server::Transports::StdioTransport`, not `MCP::Server::Transports::Stdio`.
Therefore, the file path has been updated from `lib/mcp/server/transports/stdio.rb` to `lib/mcp/server/transports/stdio_transport.rb` to
match the module name. If `MCP::Server::Transports::Stdio` is preferred instead, please let me know and I will open a separate PR.
2025-06-04 11:48:25 +09:00
Renamed from lib/mcp/server/transports/stdio.rb (Previous history)