## Motivation and Context
Transport subclasses (StreamableHTTPTransport, StdioTransport) already
receive the server in their constructor, but users must manually call
`server.transport = transport` after creating a transport instance.
This is boilerplate that can be eliminated by setting the connection
automatically in `Transport#initialize`.
## How Has This Been Tested?
All existing tests pass. Redundant `server.transport = transport` lines
were removed from tests, examples, README.md, and conformance server.
## Breaking Changes
None. Existing code that manually sets `server.transport = transport`
continues to work (idempotent assignment).
## Motivation and Context
The MCP specification defines `sampling/createMessage` to request LLM completions from the client.
The Ruby SDK did not yet support server-to-client requests,
so the server had no way to ask the client to sample an LLM.
This caused the `tools-call-sampling` conformance test to fail.
This adds `Server#create_sampling_message` to send a `sampling/createMessage` request
to the client via the transport layer. The method validates that the client declared
the `sampling` capability during initialization, and optionally checks the
`sampling.tools` capability when tools or tool_choice parameters are provided.
On the transport side, `Transport#send_request` is introduced as an abstract method,
with implementations in both `StdioTransport` and `StreamableHTTPTransport`.
Each transport assigns a unique request ID, sends the JSON-RPC request to the client,
and blocks until the client responds.
Client capabilities are stored per session via `ServerSession`, so validation is scoped
to the originating client. `ServerSession#create_sampling_message` uses
`build_sampling_params` for capability validation and sends the request directly through
the transport with the session ID, following the same pattern as `notify_progress`.
`Server#create_sampling_message` is the public API for single-client transports
(e.g., StdioTransport).
For multi-client transports (e.g., StreamableHTTPTransport),
`ServerSession#create_sampling_message` must be used to route requests to the correct client.
Tools using `server_context.create_sampling_message` automatically route to the correct session
regardless of transport.
`StreamableHTTPTransport#send_request` requires `session_id` to prevent broadcasting sampling requests
to all connected clients. Missing or invalid session IDs raise explicit errors instead of silently failing.
The `include_context` parameter is soft-deprecated in the MCP specification but is included
for compatibility with existing clients that declare the `sampling.context` capability.
Python and TypeScript SDKs also retain this parameter.
Ref: https://modelcontextprotocol.io/specification/latest/server/utilities/sampling
## How Has This Been Tested?
Added comprehensive tests in `test/mcp/server_sampling_test.rb` covering required and optional parameters,
capability validation, error handling, nil-param omission, per-session capability isolation,
and HTTP init capability scoping.
Added transport tests in `test/mcp/server/transports/stdio_transport_test.rb` and
`test/mcp/server/transports/streamable_http_transport_test.rb` for `send_request` round-trip behavior,
error responses, stateless mode rejection, missing session_id, invalid session_id, and no-active-stream error.
Conformance: `tools-call-sampling` is removed from expected failures. The conformance server's `TestSampling`
tool now calls `server_context.create_sampling_message` instead of returning a stub error.
This aligns the Ruby SDK with the MCP specification and other SDK implementations.
## Breaking Changes
None for end users.
`Transport#send_request` is a new abstract method. `ServerSession#create_sampling_message` is a new method.
## 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.