Commit graph ruby-sdk/lib/mcp
Author SHA1 Message Date
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
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
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
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
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
8fbc2b4631 Release 0.9.2 2026-03-27 12:10:53 +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
Koichi ITO
805e020d40 Remove undocumented handler override methods
## Motivation and Context

The following public methods allow replacing the SDK's built-in request handlers:

- `tools_call_handler`
- `tools_list_handler`
- `resources_list_handler`
- `resources_templates_list_handler`
- `prompts_list_handler`
- `prompts_get_handler`

These were introduced in Internal Release 0.2.0 (6004f42) as part
of "allow setting handlers on Server", before declarative APIs (`define_tool`, `Prompt` class, `Resource` class) were
fully established.

Now that those declarative APIs exist, these handler overrides are redundant: none are documented in the README.md,
and no usage exists outside of the SDK's own tests.

These handler overrides also create architectural problems. Since custom handlers cannot receive session context,
they would not work correctly with a per-session architecture, such as session-scoped notifications.

### Changes

- Removed `tools_call_handler`, `tools_list_handler`, `resources_list_handler`,
`resources_templates_list_handler`, `prompts_list_handler`, and `prompts_get_handler`
from `Server`.
- Removed the associated tests.
- Retained `resources_read_handler` (documented in the README.md and actively used
in examples and conformance server, because resource reading is application-specific).

## Breaking Change

The following methods are removed without a deprecation period:
`tools_call_handler`, `tools_list_handler`, `resources_list_handler`,
`resources_templates_list_handler`, `prompts_list_handler`,
`prompts_get_handler`.

These were never documented in the README.md and have no known usage
outside of the SDK's own tests. As a result, little to no impact on
users is expected, and a deprecation warning would have no practical
audience. Users who relied on these should use `define_tool`,
prompt/resource registration, or `resources_read_handler` instead.
2026-03-26 12:14:11 +09:00
Koichi ITO
db40143402 Reject duplicate SSE connections with 409 to prevent stream hijacking
An attacker who obtains a valid session ID could issue a GET request to
establish a new SSE stream, silently replacing the victim's stream. All
subsequent server-to-client messages (tool responses, notifications)
would then be delivered to the attacker instead of the legitimate client,
with no indication to the victim.

The fix mirrors the Python SDK's approach: when a GET request arrives for
a session that already has an active SSE stream, respond with HTTP 409
Conflict ("Only one SSE stream is allowed per session") and leave the
existing connection intact. Additionally, `store_stream_for_session`
checks for an existing stream under the mutex before storing a new one,
closing the TOCTOU window between the HTTP-level check and the deferred
stream assignment.

Note: exploiting this requires prior knowledge of the session ID
(a `SecureRandom.uuid`), so the practical severity is low in typical local
MCP deployments. The change is still worthwhile as defense-in-depth,
especially for multi-user or network-exposed server deployments.
2026-03-24 19:24:45 +09:00
Koichi ITO
f29259c0b1 Release 0.9.1
This release includes a bug fix that did not make it into v0.9.0.
It also simplifies the example in https://github.com/modelcontextprotocol/quickstart-resources/pull/121.
2026-03-23 09:45:14 +09:00
Koichi ITO
73070f1fd7 Allow Client#call_tool to accept a tool name
## Motivation and Context

`Client#call_tool` previously required an `MCP::Client::Tool` object to call a tool.
This made the API harder to use compared to other MCP SDK implementations,
which accept a tool name string directly:

- Python SDK: `session.call_tool(name, arguments)` accepts a name string.
  https://github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/session.py#L370
- TypeScript SDK: `client.callTool({ name, arguments })` accepts a name string.
  ccb78f2/packages/client/src/client/client.ts (L834)

## How Has This Been Tested?

Updated existing tests and added new tests.

## Breaking Change

No. This change adds a `name:` parameter so callers can pass a tool name string
directly without looking up a `Tool` object first. The existing `tool:` parameter
continues to work for backward compatibility.
2026-03-23 00:43:59 +09:00
Koichi ITO
1b8e45fef3
Merge pull request #261 from koic/return_404_for_invalid_session_id_in_handle_delete
Return 404 for invalid session ID in `handle_delete`
2026-03-21 00:42:55 +09:00
Koichi ITO
12922a6448 Release 0.9.0
Some 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-03-20 11:02:56 +09:00
Koichi ITO
09b4e30da6
Merge pull request #262 from koic/stdio_client_transport
Add `MCP::Client::Stdio` transport
2026-03-20 07:56:02 +09:00
Koichi ITO
abc7d6c7ea Return 404 for invalid session ID in handle_delete
## Motivation and Context

The MCP specification requires that when a server receives a request containing
a session ID that is no longer valid, it MUST respond with HTTP 404 Not Found.
`handle_delete` was unconditionally calling `cleanup_session` and returning 200
even for nonexistent session IDs.

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

> The server MAY terminate the session at any time, after which it MUST respond
> to requests containing that session ID with HTTP 404 Not Found.

## How Has This Been Tested?

Added tests for DELETE with invalid session ID, and lifecycle tests for
delete-then-POST and delete-then-DELETE scenarios.

## Breaking Change

DELETE requests with an invalid session ID now return HTTP 404 "Session not found"
instead of HTTP 200.

However, this change is considered a bug fix because it brings the behavior into
compliance with the MCP specification.
2026-03-19 14:52:13 +09:00
Koichi ITO
21eef8df9a Add MCP::Client::Stdio transport
## Motivation and Context

The `MCP::Client` currently only supports HTTP transport (`MCP::Client::HTTP`).
To achieve parity with other MCP SDKs (Python, TypeScript, C#) which provide
stdio client transports for spawning and communicating with server subprocesses,
the Ruby SDK needs a stdio client transport.
https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#stdio

This enables building MCP clients that launch server processes and communicate
via stdin/stdout using newline-delimited JSON-RPC 2.0 messages.

## How Has This Been Tested?

Added tests for process spawning, protocol initialization handshake,
notification handling, error handling, and transport close lifecycle.

## Additional context

Enabled the `Lint/IncompatibleIoSelectWithFiberScheduler` cop in `.rubocop.yml`
to detect accidental use of `IO.select` instead of `IO#wait_readable`.
2026-03-19 12:41:27 +09:00
Koichi ITO
75d03f334e Use missing_session_id_response method in handle_delete
## Motivation and Context

`handle_delete` had an inline error response for "Missing session ID" that
was identical to the existing `missing_session_id_response` helper method.
`handle_get` already uses this helper for the same purpose.

## How Has This Been Tested?

All existing tests pass. The behavior is unchanged.

## Breaking Change

None.
2026-03-18 12:17:23 +09:00
Koichi ITO
be1bbc25c1 Use mutex-protected session_exists? in handle_regular_request
## Motivation and Context

`handle_regular_request` was checking `@sessions.key?(session_id)` directly
without holding `@mutex`, while concurrent threads could modify `@sessions`
via `cleanup_session` or `handle_delete`. This created a TOCTOU race where
the check could pass but the session could be deleted before subsequent use.

The class already provides a mutex-protected `session_exists?` helper, and
`handle_get` already uses it. This change makes `handle_regular_request`
consistent with `handle_get`.

## How Has This Been Tested?

Added a test that verifies `handle_regular_request` delegates to the
mutex-protected `session_exists?` helper instead of accessing
`@sessions` directly.

All existing tests pass.

## Breaking Change

None.
2026-03-18 04:31:08 +09:00
Koichi ITO
cf1d578433 Return 404 for invalid session ID in handle_regular_request
## Motivation and Context

The MCP specification requires that when a server receives a request containing
a session ID that is no longer valid, it MUST respond with HTTP 404 Not Found.
The `handle_get` path already returned 404 correctly, but `handle_regular_request`
(the POST path) was returning 400 "Invalid session ID" instead.

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

> The server MAY terminate the session at any time, after which it MUST respond
> to requests containing that session ID with HTTP 404 Not Found.

## How Has This Been Tested?

Added a test for POST requests with an invalid session ID, verifying that the
response is 404 with "Session not found" error message. All existing tests pass.

## Breaking Change

POST requests with an invalid session ID now return HTTP 404 "Session not found"
instead of HTTP 400 "Invalid session ID". Clients that match on the 400 status
code or the old error message will need to be updated.

However, this change is considered a bug fix because it brings the behavior into compliance with
the MCP specification.
2026-03-17 02:29:30 +09:00
Koichi ITO
6e35d13479
Merge pull request #256 from koic/remove_dead_code_in_handle_regular_request
Remove dead code in `handle_regular_request`
2026-03-17 01:04:32 +09:00
Koichi ITO
628e5203d5
Merge pull request #255 from koic/use_autoload_to_defer_loading_of_unused_subsystems
Use `autoload` to defer loading of unused subsystems
2026-03-16 00:06:27 +09:00
Koichi ITO
95200b5467
Merge pull request #253 from koic/support_cors_and_accept_wildcard_for_browser_based_mcp_clients
Support CORS and Accept wildcard for browser-based MCP clients
2026-03-15 15:31:42 +09:00
Koichi ITO
9d9a1347d9 Remove dead code in handle_regular_request
Notifications never reach `handle_regular_request` because `handle_post`
checks `notification?(body)` first and returns 202 via `handle_accepted`.
Since only notifications cause `handle_json` to return `nil`, `response`
is always non-`nil` here.
2026-03-13 18:26:33 +09:00
Koichi ITO
0c729d0a2d Use autoload to defer loading of unused subsystems
## 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.
2026-03-12 14:08:27 +09:00
Koichi ITO
bbe637612b Support progress notifications per MCP specification
## Motivation and Context

The MCP specification defines `notifications/progress` for reporting incremental progress
during long-running tool operations. The Ruby SDK did not yet support this, so tools had no way to
send progress updates to clients. This caused the `tools-call-with-progress` conformance test to fail.

This adds an `MCP::ServerContext` wrapper that provides a `report_progress` method on
`server_context`. Tools call `server_context.report_progress(value, total:, message:)` to send
notifications back to the client. The method is a no-op when no `progressToken` was provided,
so tools can call it unconditionally without checking for a token.

This design is consistent with the TypeScript and Python SDKs, which provide progress
reporting through a context object rather than a separate parameter.

On the client side, `Client#call_tool` now accepts a `progress_token:` parameter that sets
`_meta.progressToken` in the request.

Ref: https://modelcontextprotocol.io/specification/latest/server/utilities/progress

## How Has This Been Tested?

Added comprehensive tests in `test/mcp/server_progress_test.rb` covering `notify_progress`,
tool integration with progress, block-defined tools, `**kwargs` tools, and no-op behavior
without a token.

Added client tests for `progress_token:` parameter inclusion and omission.

Added `test/mcp/server_context_test.rb` test for non-Hash `server_context` preservation
when `_meta` is present.

Conformance: `tools-call-with-progress` removed from expected failures.

## Breaking Change

`server_context` is now always an `MCP::ServerContext` instance when passed to tools,
even if the server has no context configured. Code that checks `server_context ?` to
detect a nil context will need to be updated, as `ServerContext` is always truthy.
`ServerContext` delegates method calls to the underlying context object via
`method_missing`, so `server_context[:key]` and other accessor patterns continue to work.

In practice, this is unlikely to affect users. A tool that declares `server_context:`
is opting in to use the context; if the server has no context configured, that is a
server configuration issue rather than something the tool should handle with a nil check.
2026-03-10 01:49:09 +09:00
Koichi ITO
e5d3b2c135 Support CORS and Accept wildcard for browser-based MCP clients
## Summary

When using browser-based MCP clients such as MCP Inspector, connections to
the example HTTP server failed due to two issues:

1. CORS preflight (OPTIONS) requests returned 405 Method Not Allowed,
causing browsers to block all cross-origin requests.
2. `Accept: */*` (commonly sent by browsers and fetch API) returned 406 Not Acceptable
because the transport required the Accept header to explicitly list `application/json`
and `text/event-stream`.

Add `rack-cors` middleware to the example HTTP servers so that preflight requests are handled correctly
and `Mcp-Session-Id` is exposed to browser clients. Also fix `validate_accept_header` in `StreamableHTTPTransport`
to treat `*/*` as satisfying all required Accept types.

Fixes #141.

## Repro Steps

### 1. Start the example server (Terminal 1)

```console
$ bundle exec ruby examples/streamable_http_server.rb
```

### 2. Start MCP Inspector (Terminal 2)

```console
$ npx @modelcontextprotocol/inspector
```

### 3. Open the Inspector Web UI (http://localhost:6274) in a browser

- Set Transport Type to "Streamable HTTP"
- Set URL to http://localhost:9393
- Disable the Authorization header toggle (the example server does not require authentication)
- Click "Connect"

Before this change, the connection fails due to CORS or 406 errors.
After this change, the connection succeeds and tools are listed.

These steps have been added to examples/README.md.
2026-03-09 10:45:53 +09:00
Eric Kreutzer
50cd86c01c
Add automatic _meta parameter extraction support 2026-03-04 10:11:07 -07:00
Koichi ITO
2da465c985 Release 0.8.0
Some fixes have been identified during the implementation of the conformance infrastructure.
It should be possible to proceed with a release at this stage.
2026-03-03 01:19:53 +09:00
Koichi ITO
ba7a08aa04 Add Content::EmbeddedResource class for embedded resource content type
The MCP spec defines an embedded resource content type, but the SDK had
no corresponding class. Add `Content::EmbeddedResource` that wraps a
resource object and serializes it via `to_h`.

https://modelcontextprotocol.io/specification/2025-11-25/server/tools#embedded-resources
2026-03-02 01:33:05 +09:00
Koichi ITO
60f69351ee
Merge pull request #249 from koic/fix_econnreset_in_sse_notification
Handle `Errno::ECONNRESET` in SSE stream operations
2026-03-02 01:30:40 +09:00
Koichi ITO
2fc4f5e1d2
Merge pull request #243 from koic/add_content_audio
Add `Content::Audio` class for audio content type
2026-03-02 01:30:20 +09:00
Koichi ITO
29a32900fd
Merge pull request #246 from koic/fix_prompt_nil_arguments
Fix `Prompt#validate_arguments!` crash when arguments are `nil`
2026-03-01 18:38:14 +09:00
Koichi ITO
cbb81a836c Add Content::Audio class for audio content type
The MCP spec defines an audio content type, but the SDK only had
`Content::Text` and `Content::Image`. Add `Content::Audio` with the
same structure as `Content::Image`.

https://modelcontextprotocol.io/specification/2025-11-25/server/tools#audio-content
2026-03-01 17:30:17 +09:00
Koichi ITO
9c1c2491b1 Handle Errno::ECONNRESET in SSE stream operations
`Errno::ECONNRESET` is a `SystemCallError`, not an `IOError`, so it was
not caught by the existing `rescue IOError, Errno::EPIPE` clauses. When
a client disconnected with a TCP RST during SSE notification broadcast,
the unhandled exception aborted the entire broadcast loop, causing
notifications to be lost for all remaining sessions.

This caused the `tools-call-with-logging` conformance scenario to fail
in CI (#248), where stale sessions from prior scenarios triggered
`ECONNRESET` on the first notification write, aborting the broadcast
and losing log notifications.

Add `Errno::ECONNRESET` to all four rescue clauses in
`StreamableHTTPTransport`: `send_notification` (targeted and broadcast),
`send_response_to_stream`, and `send_keepalive_ping`.
2026-03-01 17:25:38 +09:00
Koichi ITO
200826c7fd
Merge pull request #247 from koic/fix_default_handler_return_values
Fix default handler return values to comply with MCP spec
2026-03-01 17:23:42 +09:00
Koichi ITO
ef5c76373b Fix Prompt#validate_arguments! crash when arguments are nil
`Prompt#validate_arguments!` called `required_args` which does `arguments_value.filter_map`,
crashing with `NoMethodError` when `arguments_value` is `nil` (the default for prompts without
declared arguments). Additionally, passing `nil` as args caused a crash on `args.keys`.

Fix by defaulting `arguments_value` to `[]` and guarding against `nil` args.
2026-03-01 17:21:41 +09:00
Koichi ITO
76ab822ad7
Merge pull request #245 from koic/fix_sse_response_status
Return 202 Accepted for SSE responses per MCP spec
2026-03-01 17:17:26 +09:00
Koichi ITO
145dc1f2c5
Merge pull request #242 from koic/support_ref_in_tool_schema
Allow `$ref` in `Tool::Schema` for protocol version 2025-11-25
2026-03-01 17:16:47 +09:00
Koichi ITO
7b910bca23 Fix default handler return values to comply with MCP spec
The no-op handlers for `completion/complete`, `resources/subscribe`,
and `resources/unsubscribe` returned `nil`.
When a client accesses the expected response structure (e.g., `result[:completion][:values]`),
a `nil` result raises `NoMethodError`.

The MCP spec requires specific response structures even for no-op handlers.
Returning spec-compliant defaults ensures that clients receive valid responses without
requiring servers to explicitly override each handler.

`completion/complete` now returns `{ completion: { values: [], hasMore: false } }`.
ref: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion

`resources/subscribe` and `resources/unsubscribe` now return `{}` (empty result).
ref: https://modelcontextprotocol.io/specification/2025-11-25/server/resources#subscriptions
2026-03-01 00:55:31 +09:00
Koichi ITO
a02a4572c4 Return 202 Accepted for SSE responses per MCP spec
`StreamableHTTPTransport#send_response_to_stream` returned 200 with
`{ accepted: true }` body, but the MCP spec requires 202 with empty body
when a response is sent via SSE stream. This has been a requirement since
Streamable HTTP was introduced in protocol version 2025-03-26.

- https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#sending-messages-to-the-server
- https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server
2026-02-28 17:59:54 +09:00
Koichi ITO
99cbefc610 Allow $ref in Tool::Schema for protocol version 2025-11-25
`Tool::Schema` rejected `$ref` with an `ArgumentError`.
Earlier MCP spec versions (2024-11-05 through 2025-06-18) did not define
a JSON Schema dialect or reference the `$ref` keyword. `inputSchema` was described
only as "JSON Schema defining expected parameters" with no further constraints:

- https://modelcontextprotocol.io/specification/2024-11-05/server/tools
- https://modelcontextprotocol.io/specification/2025-06-18/server/tools

The MCP spec 2025-11-25 introduced a "JSON Schema Usage" section that adopts
JSON Schema 2020-12 as the default dialect. Since `$ref` is a core keyword in 2020-12,
it is now allowed for protocol version 2025-11-25 and later.

- https://modelcontextprotocol.io/specification/2025-11-25/basic#json-schema-usage
- https://modelcontextprotocol.io/specification/2025-11-25/server/tools
- https://json-schema.org/draft/2020-12/release-notes

For backward compatibility with older protocol versions (2025-06-18 and earlier),
`Server#validate!` continues to raise `ArgumentError` when a tool input schema contains `$ref`.
This preserves the existing behavior for servers targeting those spec versions.
2026-02-27 14:33:36 +09:00
Koichi ITO
37563e59bd Fix Content::Image#to_h to return mimeType (camelCase) per MCP spec
This is a fix for a similar bug that remained, as in #235.

`Content::Image#to_h` returned `mime_type` (snake_case), but the MCP spec
requires `mimeType` (camelCase) for the MIME type field in image content.
2026-02-27 12:51:38 +09:00
Koichi ITO
4af216fa32 Release 0.7.1
This is a release that includes several bug fixes.
2026-02-21 03:33:28 +09:00
Koichi ITO
a05da399ba
Merge pull request #235 from koic/fix_property_field_names_in_resource_contents
Fix `Resource::Contents#to_h` to use correct property names per MCP spec
2026-02-20 11:51:34 +09:00
Koichi ITO
08832c6b43 Fix Resource::Contents#to_h to use correct property names per MCP spec
This PR fixes thr following fields with the MCP specification which requires:

- Text resources use `text` and `mimeType` fields
- Binary resources use `blob` (base64-encoded) and `mimeType` fields

https://modelcontextprotocol.io/specification/2025-11-25/server/resources#resource-contents

Fixes #234.
2026-02-19 13:50:10 +09:00