Commit graph

518 commits

Author SHA1 Message Date
Koichi ITO
3f43b1feac [Doc] Restructure sampling section to lead with tool usage
Sampling is primarily used within tool execution via `server_context.create_sampling_message`,
but the documentation led with the less common direct `Server#create_sampling_message` usage.
This ordering could mislead readers into thinking direct usage is the recommended approach.
2026-04-06 03:17:18 +09:00
Koichi ITO
2a1c9b7ac9
Merge pull request #298 from koic/doc_restructure
[Doc] Reorganize README to surface key information earlier
2026-04-06 03:04:42 +09:00
Koichi ITO
f23d1b5088
Merge pull request #300 from koic/release_0_11_0 v0.11.0
Release 0.11.0
2026-04-06 03:04:25 +09:00
Koichi ITO
46f6c20cf3 Release 0.11.0
New features and fixes have been added, and it seems like a good time to cut a release.
Subsequent feature proposals can continue to be incorporated into future releases.
2026-04-06 02:35:49 +09:00
Koichi ITO
f9096347bc [Doc] Move stdio before Rails Controller in Usage
For Usage, stdio is easier to try first than a Rails controller.
Moved stdio to the top and placed Rails Controller after it.

Only sections were moved. No content was changed.
2026-04-05 13:29:47 +09:00
Koichi ITO
63f51bbc26 [Doc] Move Usage to the top of the README.md
Users are more likely to want to see Usage first, rather than a feature introduction like Sampling.
Sampling has been moved below Resource Templates so that Usage appears at the top.

Only sections were moved. No content was changed.
2026-04-05 13:29:47 +09:00
Koichi ITO
ee4a7bb4df [Doc] Move Custom methods to the bottom of the README
Custom methods are an advanced feature and may not be appropriate for the top of the README.md.
Move them to the bottom of the page.

Only sections were reorganized with headings. No content was changed.
2026-04-05 13:29:30 +09:00
Koichi ITO
40b048b8f4
Merge pull request #294 from koic/post_response_sse_stream
Support POST response SSE streams for server-to-client messages
2026-04-05 02:56:19 +09:00
Koichi ITO
8278ca6586 Support POST response SSE streams for server-to-client messages
## Motivation and Context

The MCP Streamable HTTP specification defines that servers can return POST responses
as SSE streams and send server-to-client JSON-RPC requests and notifications through them:

> If the input is a JSON-RPC request, the server MUST either return
> `Content-Type: text/event-stream`, to initiate an SSE stream, or
> `Content-Type: application/json`, to return one JSON object.

If the server initiates an SSE stream:

> The server MAY send JSON-RPC requests and notifications before
> sending the JSON-RPC response. These messages SHOULD relate to the
> originating client request.

See: https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server

Previously, when a GET SSE stream was connected, the Ruby SDK sent POST response bodies through
the GET stream and returned a 202 HTTP status for the POST itself. When no GET SSE stream was
connected, the SDK correctly returned `application/json` responses. The 202 behavior with GET SSE
was non-compliant with the specification, which requires POST requests to return `text/event-stream`
or `application/json`. Additionally, since GET SSE is optional per the specification, clients that
did not establish a GET SSE connection could not receive server-to-client messages (e.g.,
`sampling/createMessage`, log notifications) during request processing.

With this change, `handle_regular_request` always returns the POST response as an SSE stream for stateful sessions.
Each POST response stream is stored in `session[:post_request_streams]` keyed by `related_request_id` (the JSON-RPC request ID),
enabling correct routing when multiple POST requests are processed concurrently on the same session.
Server-to-client messages with a `related_request_id` are routed to the originating POST response stream only;
when `related_request_id` is nil, the GET SSE stream is used.

The TypeScript and Python SDKs already support this pattern.

### Internal Changes

`JsonRpcHandler.handle` and `JsonRpcHandler.handle_json` now pass both `method_name` and `request_id` to the method finder block.
This allows `Server#handle_request` to receive `related_request_id` directly from the protocol layer.
Without this, `related_request_id` would need to be relayed as a keyword argument through `Server#handle_json`,
`ServerSession#handle_json`, and `dispatch_handle_json`, unnecessarily exposing it on public method signatures.
This follows the same design as the TypeScript and Python SDKs, where the protocol layer extracts the request ID
and propagates it to the handler context.

## How Has This Been Tested?

Added tests for POST response stream:

- `send_request` via POST response stream (sampling with and without GET SSE)
- `send_notification` via POST response stream (logging without GET SSE)
- `progress` notification via POST response stream (without GET SSE)
- POST request returns SSE response even with GET SSE connected
- Session-scoped notifications (log, progress) are sent to POST
  response stream, not GET SSE stream
- `active_stream` does not fall back to GET SSE when
  `related_request_id` is given but request stream is missing

Updated existing tests to handle SSE response format where applicable.

## Breaking Changes (spec compliance fix)

POST responses for JSON-RPC requests in stateful sessions now return `Content-Type: text/event-stream`
instead of being sent through the GET SSE stream with a 202 HTTP status. Clients that relied on receiving responses
via the GET SSE stream will need to read the POST response body instead. This is a spec compliance fix:
the MCP specification requires POST requests to return `text/event-stream` or `application/json`,
not 202 with the response on a separate GET stream.
2026-04-04 19:20:31 +09:00
Koichi ITO
0d700d7d7d
Merge pull request #282 from koic/support_sampling
Support `sampling/createMessage` per MCP specification
2026-04-04 06:46:34 +09:00
Koichi ITO
da5f9225ac Support sampling/createMessage per MCP specification
## 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.
2026-04-04 05:55:27 +09:00
Koichi ITO
6f94d64ece
Merge pull request #290 from koic/thread_model_requirement
[Doc] Clarify that `StreamableHTTPTransport` requires a single-process server
2026-04-04 05:51:58 +09:00
Koichi ITO
5285277892
Merge pull request #291 from koic/improvement_close_streams_outside_mutex
Close streams outside mutex in session cleanup
2026-04-04 05:50:16 +09:00
Koichi ITO
66428ace04 Close streams outside mutex in session cleanup
## Motivation and Context

`cleanup_session_unsafe` was closing streams inside the mutex, which could block all session
operations if `stream.close` is slow or blocks (e.g., waiting for network I/O).
The original `reap_expired_sessions` avoided this by closing streams outside the mutex,
but the other 6 call sites of `cleanup_session_unsafe` did not, creating an inconsistency.

This change separates stream closing from session deletion. `cleanup_session_unsafe`
now only removes the session from `@sessions` and returns it.
All callers close streams outside the mutex via `close_stream_safely`,
ensuring consistent behavior and preventing mutex contention from blocking I/O.

## How Has This Been Tested?

Added tests that verify streams are closed outside the mutex by using `Mutex#try_lock`
inside mock stream close callbacks:

- `reap_expired_sessions closes stream outside mutex`
- `send_notification closes stream outside mutex on write error`

## Breaking Changes

None. This is an internal improvement with no change to public API behavior.
2026-04-04 03:46:00 +09:00
Koichi ITO
9ebb42a536 [Doc] Clarify that StreamableHTTPTransport requires a single-process server
## Motivation and Context

`StreamableHTTPTransport` stores session and SSE stream state in memory using
instance variables (`@sessions`, `@mutex`). This design requires all HTTP requests
to be handled within the same process to share state.

Process-based servers like Unicorn fork separate worker processes that do not share memory,
which breaks session management and SSE connections.
This requirement is not obvious from the current documentation.

## How Has This Been Tested?

Documentation-only change.

## Breaking Changes

None.
2026-04-04 02:08:01 +09:00
Koichi ITO
c563036f88
Merge pull request #289 from koic/feature_completion_complete
Support `completion/complete` per MCP specification
2026-04-04 01:39:03 +09:00
Koichi ITO
52371a31be
Merge pull request #286 from koic/use_rescue_standard_error_for_stream_close
Use `rescue StandardError` for stream close
2026-04-04 01:32:27 +09:00
Koichi ITO
6bada5fb05
Merge pull request #283 from koic/add_claude_md
Add CLAUDE.md referencing AGENTS.md
2026-04-04 01:32:00 +09:00
Koichi ITO
295dd781ae
Merge pull request #288 from koic/add_extra_rdoc_files_to_gemspec
Add `extra_rdoc_files` to gemspec
2026-04-04 01:31:38 +09:00
Koichi ITO
88e1f9a840 Support completion/complete per MCP specification
## Motivation and Context

The MCP specification defines `completion/complete` for providing autocompletion
suggestions for prompt arguments and resource template URIs.
The Ruby SDK previously had only a no-op handler that returned empty results,
with no way for users to define custom completion logic.

This aligns the Ruby SDK with the Python and TypeScript SDKs,
both of which support user-defined completion handlers.

## How Has This Been Tested?

Server tests cover: default handler, custom handler for `ref/prompt` and `ref/resource`,
context argument passing, 100-item truncation, and error responses for nonexistent prompts,
nonexistent resource templates, and invalid ref types.

Client tests cover: request structure, context parameter inclusion,
and fallback when result is missing.

## Breaking Changes

None. The existing default no-op handler behavior is preserved.
The `completion_handler` method is purely additive. The only behavioral change
is that `completion/complete` requests now validate that the referenced prompt
or resource template exists before calling the handler,
returning an `invalid_params` error for unknown references.
2026-04-03 01:35:07 +09:00
Koichi ITO
17e9603474 Add extra_rdoc_files to gemspec
This makes `LICENSE` and `README.md` available to RDoc documentation.
The files remain included in the gem package because `Gem::Specification#files`
automatically merges `extra_rdoc_files`.
2026-04-03 00:38:24 +09:00
Koichi ITO
d3f400f525 Use rescue StandardError for stream close
## Motivation and Context

Replace bare `rescue` with explicit `rescue StandardError` in stream cleanup to clarify intent,
with a comment explaining why.
In Ruby, `rescue` without an exception class is equivalent to `rescue StandardError`,
so this is a readability improvement with no behavior change.

## How Has This Been Tested?

Existing tests pass.

## Breaking Changes

None.
2026-04-02 03:48:56 +09:00
Koichi ITO
6669601b5d
Merge pull request #285 from sqrlington/fix/revert-error-handling-to-protocol-errors
Fix: return protocol errors for invalid arguments and server errors
2026-04-02 03:37:46 +09:00
Sarah Pickett
8dd0379f16
fix: return protocol errors for invalid arguments and server errors
Per the MCP spec, invalid arguments and server errors are protocol
errors (JSON-RPC error responses), not tool execution errors
(isError: true). PR #165 moved all error cases to isError: true,
and PR #231 only restored protocol errors for unknown tools.

This fixes the remaining two cases: invalid arguments (-32602) and
server errors (-32603).

Ref: https://modelcontextprotocol.io/specification/2024-11-05/server/tools#error-handling
2026-04-01 14:22:56 -04:00
Koichi ITO
0141d487fc Add CLAUDE.md referencing AGENTS.md
CLAUDE.md delegates to AGENTS.md via the `@AGENTS.md` directive,
avoiding duplication of project guidelines.
2026-04-01 01:03:43 +09:00
Koichi ITO
56319902dd
Merge pull request #280 from koic/fix_stdio_test_thread_race
Join server threads before closing IO pipes in stdio client tests
2026-03-31 15:51:32 +09:00
Koichi ITO
11f2e05945
Merge pull request #281 from koic/client_raise_on_error_response
Fix client methods silently swallowing JSON-RPC error responses
2026-03-31 15:14:47 +09:00
Koichi ITO
eacf51ec94
Merge pull request #279 from koic/refactor__use_symbolize_names
Use `symbolize_names: true` in `JSON.parse` calls
2026-03-31 15:14:01 +09:00
Koichi ITO
590feb13a5 Fix client methods silently swallowing JSON-RPC error responses
## Motivation and Context

Client methods (`tools`, `resources`, `resource_templates`, `prompts`, `read_resource`, `get_prompt`, `call_tool`)
silently swallowed JSON-RPC error responses by falling back to empty defaults. This did not conform to JSON-RPC 2.0,
which clearly distinguishes error responses from success responses.
Both the Python SDK (`MCPError`) and TypeScript SDK (`ProtocolError`) raise exceptions on error responses.
The Ruby SDK was the only one silently discarding them.

Introduces a common `request` method that handles JSON-RPC request construction and error checking in one place,
replacing the duplicated `transport.send_request` calls across all client methods.

## How Has This Been Tested?

Added tests for each affected method verifying that `ServerError` is raised with the correct `code`, `message`,
and `data` attributes.

## Breaking Changes

The previous behavior of silently returning empty defaults on server errors did not conform to JSON-RPC 2.0.
Error responses should be surfaced to callers, not swallowed.
Callers that relied on the silent fallback behavior will need to rescue `MCP::Client::ServerError`.
2026-03-31 12:51:11 +09:00
Koichi ITO
760346818e Join server threads before closing IO pipes in stdio client tests
Background threads simulating server responses could race with the `ensure` block
that closes IO pipes, intermittently producing this warning depending on thread scheduling:

```console
#<Thread:0x00007fb69290cbd0 test/mcp/client/stdio_test.rb:28 run>
terminated with exception (report_on_exception is true):
test/mcp/client/stdio_test.rb:55:in `write': stream closed in another thread (IOError)
```

Store each background thread in a `server_thread` variable and call `server_thread&.join(1)`
at the top of each `ensure` block to wait for the thread to finish before closing pipes.
2026-03-31 04:34:21 +09:00
Koichi ITO
3a68937bb7 Use symbolize_names: true in JSON.parse calls
Replace custom `deep_transform_keys` in `Tool::Schema` and
string key access in `StreamableHTTPTransport` with
the `symbolize_names: true` option of `JSON.parse`.

This aligns `StreamableHTTPTransport` with `StdioTransport`,
which already uses `symbolize_names: true`.
2026-03-31 04:15:42 +09:00
Koichi ITO
6b415e2f54
Merge pull request #278 from koic/cname_ruby_sdk_modelcontextprotocol_io
[Doc] Add CNAME file to preserve custom domain across deployments
2026-03-31 00:16:53 +09:00
Koichi ITO
5c51f7dde8 [Doc] Add CNAME file to preserve custom domain across deployments
Accessing https://ruby.sdk.modelcontextprotocol.io/ is expected to show
the content of https://modelcontextprotocol.github.io/ruby-sdk/,
but it currently does not. This change fixes that issue.

The `generate-gh-pages.sh` script cleans all files from the gh-pages branch root
before copying from `docs/`. This removes the CNAME file that GitHub Pages uses for
the custom domain, causing `ruby.sdk.modelcontextprotocol.io` to break on every release.
https://github.com/modelcontextprotocol/ruby-sdk/blob/v0.10.0/bin/generate-gh-pages.sh#L91-L93

By placing the CNAME file in `docs/`, it will be automatically copied to `gh-pages` during each deployment.
https://github.com/modelcontextprotocol/ruby-sdk/blob/v0.10.0/bin/generate-gh-pages.sh#L95-L97

The following is a PR related to the configuration of ruby.sdk.modelcontextprotocol.io.

- https://github.com/modelcontextprotocol/ruby-sdk/pull/181
- https://github.com/modelcontextprotocol/dns/pull/14
- https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2486
2026-03-30 13:27:58 +09:00
Koichi ITO
03a78f9344
Merge pull request #277 from koic/release_0_10_0 v0.10.0
Release 0.10.0
2026-03-30 11:26:59 +09:00
Koichi ITO
73b018c1e1 Release 0.10.0
This release focuses primarily on changes related to the internal session architecture.
It also removes some publicly exposed methods that are effectively obsolete.
2026-03-30 06:13:16 +09:00
Koichi ITO
f0186d08b0
Merge pull request #268 from koic/security__missing_session_expiry_controls
Support session expiry controls for `StreamableHTTPTransport`
2026-03-30 05:46:07 +09:00
Koichi ITO
9f3435aeb6
Merge pull request #276 from koic/remove_notify_progress_broadcast
Remove `Server#notify_progress` broadcast API
2026-03-29 15:35:17 +09:00
Koichi ITO
b89f6e315d Remove Server#notify_progress broadcast API
## 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.
2026-03-29 15:11:28 +09:00
Koichi ITO
49fc501abc Support session expiry controls for StreamableHTTPTransport
## Motivation and Context

The MCP specification recommends expiring session IDs to reduce session hijacking risks:
https://modelcontextprotocol.io/specification/latest/basic/security_best_practices#session-hijacking

Several other SDKs (Go, C#, PHP, Python) already implement session expiry controls, but
the Ruby SDK did not. Sessions persisted indefinitely unless explicitly deleted or
a stream error occurred, leaving abandoned sessions to accumulate in memory.

This adds a `session_idle_timeout:` option to `StreamableHTTPTransport#initialize`. When
set, sessions that receive no HTTP requests for the specified duration (in seconds) are
automatically expired. Expired sessions return 404 on subsequent requests (GET and POST),
matching the MCP specification's behavior for terminated sessions. Each request resets
the idle timer, so actively used sessions are not interrupted.

A background reaper thread periodically cleans up expired sessions to handle orphaned
sessions that receive no further requests. The reaper only starts when
`session_idle_timeout` is configured.

The default is `nil` (no expiry) for backward compatibility, consistent with the Python
SDK's approach. The Python SDK recommends 1800 seconds (30 minutes) for most deployments:
https://github.com/modelcontextprotocol/python-sdk/pull/2022

Resolves #265.

## How Has This Been Tested?

Added tests for session expiry, idle timeout reset, reaper thread lifecycle, input
validation, and default behavior in `streamable_http_transport_test.rb`. All existing
tests continue to pass.

## Breaking Change

None. The default value of `session_idle_timeout` is `nil`, which preserves the existing
behavior of sessions never expiring. The new `last_active_at` field in the internal
session hash is not part of the public API. Existing code that instantiates
`StreamableHTTPTransport.new(server)` or
`StreamableHTTPTransport.new(server, stateless: true)` continues to work without changes.
2026-03-29 12:34:58 +09:00
Koichi ITO
8969edf4b9
Merge pull request #274 from koic/reject_missing_session_id
Reject POST requests without session ID in stateful mode
2026-03-29 12:34:00 +09:00
Koichi ITO
0cc8a300b8
Merge pull request #275 from koic/introduce_server_sessoin_for_per_connection_state
Introduce `ServerSession` for per-connection state
2026-03-29 11:46:24 +09:00
Koichi ITO
719e5e7fee Introduce ServerSession for per-connection state
## 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.
2026-03-29 08:04:56 +09:00
Koichi ITO
234d134029
Merge pull request #269 from koic/align_stdio_casing_with_spec
[Doc] Align stdio with specification
2026-03-28 07:18:09 +09:00
Koichi ITO
88d9bed69d Reject POST requests without session ID in stateful mode
Per the MCP specification (Streamable HTTP > Session Management):

> Servers that require a session ID SHOULD respond to requests without an `Mcp-Session-Id` header
> (other than initialization) with HTTP 400 Bad Request.

https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management

Previously, non-initialize POST requests without `Mcp-Session-Id` in stateful mode were processed
with HTTP 200 (for regular requests) or HTTP 202 (for notifications/responses).

This change adds an explicit check in `handle_post` to return HTTP 400 Bad Request when the session
ID is missing in stateful mode for all non-initialize requests, aligning with the specification.
2026-03-28 03:07:42 +09:00
Koichi ITO
a9e45148f6
Merge pull request #270 from koic/remove_undocumented_handler_override_methods
Remove undocumented handler override methods
2026-03-28 00:24:04 +09:00
Koichi ITO
3fc7bcd268
Merge pull request #272 from koic/release_0_9_2 v0.9.2
Release 0.9.2
2026-03-27 12:29:19 +09:00
Koichi ITO
8fbc2b4631 Release 0.9.2 2026-03-27 12:10:53 +09:00
Koichi ITO
6b092797c9
Merge pull request #273 from koic/use_accessor_in_server_context_with_meta
Use accessor method in `server_context_with_meta` instead of ivar
2026-03-27 12:08:43 +09:00
Koichi ITO
a2575b24e5 Use accessor method in server_context_with_meta instead of ivar
## Motivation and Context

`server_context_with_meta` reads `@server_context` (the instance variable)
directly instead of calling the `server_context` accessor method.
This prevents subclasses from overriding `server_context`. Ffor example,
to provide thread-local context in multi-threaded servers like Puma.

Fixes #271.

## How Has This Been Tested?

Added a regression test and passed.

## Breaking Changes

None. This restores the expected behavior of `attr_accessor :server_context`.
2026-03-27 12:01:33 +09:00
Paul Carleton
e189d7852a
Merge commit from fork
Reject duplicate SSE connections with 409 to prevent stream hijacking
2026-03-26 13:17:27 +00:00