Commit graph

610 commits

Author SHA1 Message Date
Koichi ITO
f62d813625 Reject non-Hash JSON-RPC bodies in StreamableHTTPTransport
## Motivation and Context

MCP 2025-11-25 does not support JSON-RPC batch (batch support was removed in 2025-06-18),
so request bodies are expected to be a single JSON-RPC message object. The previous code path:

```ruby
body = parse_request_body(body_string)
return body unless body.is_a?(Hash)
```

returned the parsed JSON value as if it were a Rack response when
the body was a JSON array (former batch shape), a string, a number,
or any other non-Hash JSON value. This produced a malformed Rack response
instead of a proper HTTP 400.

This was a pre-existing bug exposed during follow-up review of #347
(`MCP-Protocol-Version` header validation), which surfaced it but did not fix
the broken response shape for non-Hash bodies sent with a valid header.

### Behavior

- `parse_request_body` now raises a private `InvalidJsonError` on parse
  failure instead of returning a Rack response tuple. The overloaded
  return type and the `parse_error_tuple?` discriminator are gone.
- `handle_post` rescues `InvalidJsonError` and returns the same plain
  JSON 400 response as before (`{"error":"Invalid JSON"}`), so parse
  error handling is observably unchanged.
- Non-Hash parsed bodies (arrays, strings, numbers, booleans, null) now
  return HTTP 400 with a plain JSON body explaining that the body must
  be a single JSON-RPC message object, instead of producing a malformed
  Rack response.

The error response shape stays in the existing plain JSON style for
consistency with `validate_content_type`, `not_acceptable_response`,
and the other transport-level error helpers. Unifying all of these to
a JSON-RPC error envelope (matching the Python and TypeScript SDKs) is
deferred to a separate follow-up.

### SDK Comparison

- Python SDK (`src/mcp/server/streamable_http.py`): Already rejects
  non-Hash bodies (Array, primitive) with HTTP 400. Uses a JSON-RPC
  error envelope with `INVALID_PARAMS`.
- TypeScript SDK (`packages/server/src/server/streamableHttp.ts`):
  As of `main`, still processes JSON arrays as JSON-RPC batches and
  has not yet caught up with the 2025-06-18 batch removal. Primitives
  fail schema validation and return 400 with a JSON-RPC error envelope.

This change brings the Ruby SDK in line with the current MCP spec
(no batch). The Ruby SDK is now closer to the spec than the TypeScript
SDK's `main` branch on this specific point.

## How Has This Been Tested?

Existing regression test `test "handles POST request with invalid
JSON"` continues to pass unchanged (plain JSON response preserved).
Added new regression tests:

- POST with a JSON array body returns 400 with a clear error message
- POST with a non-object JSON body (e.g. `"foo"`) returns 400

`bundle exec rake test` and `bundle exec rake rubocop` both pass.

## Breaking Changes

None for compliant clients sending single JSON-RPC message objects.
Clients that were sending JSON arrays (batch requests, no longer
supported as of MCP 2025-11-25) or other non-object bodies will now
receive a proper HTTP 400 with a JSON error body instead of
the previous malformed Rack response; no client could have relied on
the broken pre-existing behavior.
2026-05-18 00:56:28 +09:00
Koichi ITO
d6b5392831
Merge pull request #347 from koic/validate_mcp-protocol-version_header_in_streamable_http_transport
Validate `MCP-Protocol-Version` header in StreamableHTTPTransport
2026-05-15 00:45:51 +09:00
Koichi ITO
e35841afe8
Merge pull request #348 from koic/release_0_16_0 v0.16.0
Release 0.16.0
2026-05-14 15:58:56 +09:00
Koichi ITO
c7b059cb91 Release 0.16.0
New features and changes have landed, making it a good time to cut a release.
Further feature proposals can be incorporated in future releases.
2026-05-14 12:02:17 +09:00
Koichi ITO
9c2e438d4a Validate MCP-Protocol-Version header in StreamableHTTPTransport
## Motivation and Context

MCP 2025-11-25 (basic/transports#protocol-version-header) requires servers
to respond with `400 Bad Request` when receiving a request with an invalid
or unsupported `MCP-Protocol-Version` header:

> If the server receives a request with an invalid or unsupported
> `MCP-Protocol-Version`, it MUST respond with `400 Bad Request`.

The Ruby SDK was not reading the header at all, accepting any value
(including malformed strings like `not-a-version` and unsupported versions
like `1900-01-01`) and dispatching requests normally with HTTP 200.

## Behavior

`StreamableHTTPTransport` now validates the `MCP-Protocol-Version` header
on POST (non-initialize), GET, and DELETE requests. Missing headers are
accepted as before; the spec SHOULD requirement to default to `2025-03-26`
is intentionally left for a follow-up. The `initialize` POST is exempt
because the client does not know the negotiated version until the response
arrives, matching the Python (`src/mcp/server/streamable_http.py`) and
TypeScript (`packages/server/src/server/streamableHttp.ts`) SDKs.

The error response uses a JSON-RPC envelope with `code: -32600`
(`INVALID_REQUEST`) to match the Python SDK shape:

```json
{
  "jsonrpc": "2.0",
  "id": null,
  "error": {
    "code": -32600,
    "message": "Bad Request: Unsupported protocol version: <value>. Supported versions: ..."
  }
}
```

Validation order is session then protocol version, matching the Python and
TypeScript SDKs.

`handle_post` was reorganized so that non-Hash request bodies (e.g., JSON
arrays, which MCP 2025-11-25 no longer supports as batches) also pass
through the header check. The pre-existing broken response for Array
bodies sent with a valid header is unchanged and is tracked as a separate
follow-up.

## How Has This Been Tested?

Added regression tests covering:

- POST `initialize` ignores the header (bypass)
- POST non-initialize with unsupported / malformed / valid / missing values
- POST array body with unsupported value returns 400
- GET with unsupported / missing values
- DELETE with unsupported value in both stateful and stateless modes
- DELETE validates session before protocol version

`bundle exec rake test` and `bundle exec rake rubocop` both pass.

## Breaking Changes

Strictly additive for compliant clients. Third-party clients sending stale
or malformed `MCP-Protocol-Version` values will now receive `400` instead
of `200`, which is the intended spec behavior.

Fixes #346.
2026-05-14 01:03:21 +09:00
Koichi ITO
9d2fdd02ed
Merge pull request #342 from koic/json_schema_2020_12
Advertise JSON Schema 2020-12 dialect on emitted tool schemas
2026-05-13 10:38:26 +09:00
Koichi ITO
075ae28655
Merge pull request #344 from atesgoral/feature/output-schema-validation
feat: add opt-in tool output validation
2026-05-13 10:37:16 +09:00
Ates Goral
407b064140 feat: add opt-in tool output validation 2026-05-12 16:20:43 -04:00
Koichi ITO
d1e0afa158
Merge pull request #343 from atesgoral/fix/client-output-schema
fix: preserve client tool output schemas
2026-05-13 01:21:37 +09:00
Ates Goral
f67cc745c3 fix: preserve client tool output schemas 2026-05-11 21:44:26 -04:00
Koichi ITO
5153c399a9
Merge pull request #341 from atesgoral/dark-mode
Add dark mode support to docs
2026-05-12 01:17:34 +09:00
Ates Goral
5f94073503 docs: add dark mode support 2026-05-11 08:46:40 -04:00
Koichi ITO
1fc698bb8e Advertise JSON Schema 2020-12 dialect on emitted tool schemas
## Motivation and Context

MCP 2025-11-25 (SEP-1613) establishes JSON Schema 2020-12 as the default dialect
for MCP schema definitions.
 https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1613

The Ruby SDK was emitting tool input and output schemas with no `$schema` field,
leaving the dialect implicit and inconsistent with the TypeScript SDK
(`packages/core/src/util/standardSchema.ts`, which emits with `target: 'draft-2020-12'`)
and the Python SDK (which preserves `$schema` in its types and conformance fixtures).

`Tool::Schema#to_h` now includes `"$schema": "https://json-schema.org/draft/2020-12/schema"`
unless the user supplied a different `$schema` value, so clients see the canonical dialect URI
on the wire.

Runtime metaschema and argument validation remain on draft-04 because the `json-schema` gem
does not yet support 2020-12. Switching the runtime validator is left as a separate change
because it requires either replacing the gem or waiting for upstream 2020-12 support.

## How Has This Been Tested?

Updated `Tool::Schema#to_h` expectations across `input_schema_test`, `output_schema_test`,
`tool_test`, and the `tools/list` server tests to reflect the new `$schema` field.
Added focused regression tests:

- `to_h` preserves a user-supplied `$schema` URI rather than overwriting it
  (covered for both symbol-keyed input and the string-keyed Hash form that
  goes through the initializer's `symbolize_names` normalization)
- `validate_arguments` / `validate_result` continue to work when the user supplies
  a 2020-12 `$schema` URI on the schema (the validator strips the unknown top-level
  dialect URI before invoking the draft-04 validator, instead of raising `JSON::Schema::SchemaError`)
- `tools/list` JSON-RPC response includes the 2020-12 `$schema` on both `inputSchema`
  and `outputSchema` (transport-boundary assertion)

`bundle exec rake test` and `bundle exec rake rubocop` both pass.

## Breaking Changes

Additive change. Tolerant consumers of the `tools/list` payload are unaffected.
Consumers that strictly assert hash equality on the emitted `inputSchema` / `outputSchema`
need to account for the new `$schema` key, but no schema semantics change.

Schemas using 2020-12-only keywords (e.g. `prefixItems`, nested `$schema` on subschemas)
are still rejected by the draft-04 metaschema validator, as before.
Lifting that restriction depends on adopting a 2020-12-capable validator and is out of scope
for this change.
2026-05-11 13:44:30 +09:00
Koichi ITO
9090cb6ee3
Merge pull request #340 from koic/use_server_tools_instead_of_instance_variable_get
Use `Server#tools` instead of `instance_variable_get`
2026-05-10 23:34:35 +09:00
Koichi ITO
e47607defa
Merge pull request #293 from koic/conformance_client
Support running MCP client conformance tests against the Ruby SDK
2026-05-10 23:34:17 +09:00
Koichi ITO
25f3f9a33a Use Server#tools instead of instance_variable_get
Follow-up to https://github.com/modelcontextprotocol/ruby-sdk/pull/301#discussion_r3046699277.

`Server#tools` method can be used instead of a hack like `instance_variable_get(:@tools)`.
2026-05-10 14:54:34 +09:00
Koichi ITO
811c56ae80
Merge pull request #339 from koic/remove_useless_condition_in_examples
Remove useless condition in example
2026-05-09 12:25:20 +09:00
Koichi ITO
9603b2fe93 Support running MCP client conformance tests against the Ruby SDK
## Motivation and Context

PR #248 introduced server conformance testing using `@modelcontextprotocol/conformance`.
This extends coverage to the client side so that `MCP::Client` behavior can also be validated against
the MCP specification.

### Scope

Client conformance is driven by the conformance runner, which spawns test servers for each scenario
and invokes `conformance/client.rb` against them. The client script delegates the MCP initialize
handshake to `MCP::Client#connect` and executes scenario-specific operations (for example listing
tools or calling a tool).

### Usage

Run all conformance tests (server + client):

```console
bundle exec rake conformance
```

Run a single client scenario:

```console
bundle exec rake conformance SCENARIO=initialize
```

List available scenarios:

```console
bundle exec rake conformance_list
```

## How Has This Been Tested?

Ran `bundle exec rake conformance` locally.

- Server conformance: 40 passed, 0 failed.
- Client conformance: 3 passed, 54 failed (all 54 listed in `expected_failures.yml`).

## Breaking Change

No. This change only adds test infrastructure under `conformance/` and does not modify SDK behavior.
2026-05-08 15:11:28 +09:00
Koichi ITO
d18698890a
Merge pull request #301 from amorde/define-tool-output-schema
Fix missing output_schema argument in 'define_tool' API
2026-05-07 11:56:29 +09:00
Koichi ITO
ebc656966a
Merge pull request #337 from koic/release_0_15_0 v0.15.0
Release 0.15.0
2026-05-05 08:22:40 +09:00
Koichi ITO
32b6c9b646 Release 0.15.0
New features and changes have landed, making it a good time to cut a release.
Further feature proposals can be incorporated in future releases.

It would be best to make the usage from #336 available sooner rather than later.
2026-05-05 08:14:35 +09:00
Koichi ITO
be8a5df160
Merge pull request #336 from koic/add_connect_to_stdio_client
Add client-level connect handshake to stdio transport
2026-05-04 22:27:57 +09:00
Koichi ITO
46b738e945 Add client-level connect handshake to stdio transport
## Motivation and Context

This commit implements Phase 1 of 3 for #334.

The MCP specification requires an `initialize` request followed by
a `notifications/initialized` notification before any other interaction.
PR #327 added `MCP::Client#connect` and `MCP::Client::HTTP#connect`
to expose this handshake explicitly for the HTTP transport,
but stdio remained on its private `initialize_session` lazy-init path,
inconsistent with the Python SDK (`ClientSession.initialize()`) and
TypeScript SDK (`Client.connect(transport)`),
both of which require an explicit handshake call regardless of transport.

The implicit init path on the first `send_request` is
preserved as a non-breaking compatibility shim. Public API added on `Stdio`:

- `connect(client_info:, protocol_version:, capabilities:)` performs the
  handshake, validates the negotiated `protocolVersion` against
  `MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS`,
  caches the `InitializeResult`, and is idempotent.
- `connected?` reports handshake completion.
- `server_info` exposes the cached `InitializeResult`.

Documentation and examples are updated so new users learn the explicit pattern
from the start.

## How Has This Been Tested?

Fifteen regression tests cover: explicit handshake, `server_info` caching,
idempotence, custom keyword arguments, JSON-RPC error response, missing
or non-Hash `result`, unsupported protocol version with state rollback,
state rollback when the `notifications/initialized` write fails,
retry after a failed handshake, and the `connected?` / `server_info` lifecycle.

The pre-existing stdio tests pass without modification, verifying that
the implicit-init compatibility path still works.

## Breaking Changes

None for the typical "implicit init via `client.tools` / `client.call_tool`" usage.
Existing stdio clients keep working unchanged.

Three observable behavior changes for code that inspects transport state directly:

- `Client#connected?` for a never-initialized stdio client returns `false`
  (previously fell through to a `true` default via the `respond_to?` fallback).
- `Client#server_info` for stdio returns the cached `InitializeResult`
  after the handshake (previously always `nil` via the fallback).
- The implicit handshake path now validates the server's negotiated
  protocol version, raising `RequestHandlerError` for unsupported versions.
  Production servers that return one of the supported versions are unaffected;
  this closes a pre-existing spec-conformance gap on stdio.
2026-05-03 15:57:22 +09:00
Koichi ITO
3d9437736b
Merge pull request #332 from koic/feature_cancellation
Support `notifications/cancelled` per MCP specification
2026-05-03 15:56:55 +09:00
Koichi ITO
704d362a12 Remove useless condition in example
## Motivation and Context

The example files `examples/http_client.rb` and `examples/streamable_http_client.rb` are
intended to be run as standalone scripts and are not designed to be reused as modules (e.g., via `require`).
The `if __FILE__ == $PROGRAM_NAME` guard therefore serves no purpose and is removed.

## How Has This Been Tested?

Manually verified that each example script can still be executed directly
(`ruby examples/http_client.rb`, `ruby examples/streamable_http_client.rb`).
No automated tests cover files under `examples/`.

## Breaking Changes

None. The files under `examples/` are standalone scripts and are not part of the gem's public API.
2026-05-03 15:55:13 +09:00
Koichi ITO
a97e391353 Support notifications/cancelled per MCP specification
## Motivation and Context

The MCP specification defines `notifications/cancelled` so either party can stop
a previously-issued in-flight request. The Ruby SDK declared the method constant
but had no receive logic, no in-flight tracking, and no way for handlers to participate
in cancellation; cancellation notifications fell through to the unsupported-method path.

This PR implements the **server-side** half of the spec. The server stops processing
the targeted request cooperatively and suppresses its JSON-RPC response, matching
the Python SDK's anyio `CancelScope` and the TypeScript SDK's `RequestHandlerExtra.signal`.
Cancellation is observable by every user-overridable request handler:

- `Tool.call` (tools/call)
- prompt templates (prompts/get)
- blocks registered via `resources_read_handler` / `completion_handler` /
  `resources_subscribe_handler` / `resources_unsubscribe_handler` / `define_custom_method`

Each handler opts in by declaring a `server_context:` keyword and polls `server_context.cancelled?`
or calls `server_context.raise_if_cancelled!` (raising `MCP::CancelledError`) inside long-running work.
Handlers that keep their existing `|params|` (or `|args|`) signature continue to work unchanged.

On `StreamableHTTPTransport`, cancelling a parent `tools/call` automatically cancels n
ested server-to-client requests (`sampling/createMessage`, `elicitation/create`);
the nested `send_request` raises `MCP::CancelledError` and a cancel notification is routed to
the peer on the parent's POST response stream. `StdioTransport` is single-threaded and blocks on `$stdin.gets`,
so it deliberately does not propagate nested cancellation.
Tools running on stdio still observe cancellation between calls via `server_context.cancelled?`.

The design is cooperative-only (no `Thread#raise`) because preemptive cancellation is unsafe
across Rack adapters and arbitrary handler code. The `initialize` request is never cancellable,
satisfying the spec rule that it MUST NOT be cancelled. Unknown / completed / duplicate cancel notifications
are silently ignored per the spec.

`MCP::Client#cancel` (an equivalent that aborts the calling thread's synchronous wait) is deferred to a follow-up PR.

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

## How Has This Been Tested?

`test/mcp/cancellation_test.rb` covers the `MCP::Cancellation` token contract.

`test/mcp/server_cancellation_test.rb` covers end-to-end cancellation:

- A handler spins on `cancelled?` in a background thread; the test sends `notifications/cancelled`
  and asserts the handler observed cancellation and the JSON-RPC response was suppressed.
  Each user-overridable handler type (tool, prompt template, resources/read, completion, custom method)
  has its own regression test.
- `initialize` is not cancellable; unknown / duplicate / late-after-completion cancels are silently ignored.
- The `reason` propagates into the `cancellation_reason` instrumentation field.
- Custom transports implementing only the abstract `(method, params = nil)` contract keep working;
  the new kwargs (`session_id:`, `related_request_id:`, `parent_cancellation:`, `server_session:`)
  are silently dropped when the transport's signature does not declare them.

`StreamableHTTPTransport` tests cover nested cancellation, hook deregistration after normal completion
(so a late parent cancel does not emit a stray `notifications/cancelled`), and the first-writer-wins race
when a real response and a cancel arrive concurrently.

## Breaking Change

None. `MCP::ServerContext#initialize` gains an optional `cancellation:` keyword (defaults to `nil`),
and `StreamableHTTPTransport#send_request` / `#send_notification` gain optional cancellation kwargs.
The abstract `Transport` base class and `StdioTransport` keep their existing `(method, params = nil)` signatures,
and custom transports following those signatures continue to work. `StreamableHTTPTransport` now dispatches
client-originated notifications through `ServerSession#handle_json` before returning 202;
previously these were accepted without dispatch, but the existing handlers for `notifications/initialized`
and `notifications/progress` were already no-ops, so no user-visible effect is expected.
Adding `server_context:` to a handler block is strictly opt-in.
2026-05-03 04:02:22 +09:00
Koichi ITO
541200ffcd
Merge pull request #335 from atesgoral/ag/use-sdk-streamable-http-client-example
Update Streamable HTTP client example to use MCP::Client
2026-05-03 00:24:30 +09:00
Ates Goral
933cf94a09 Update Streamable HTTP client example to use SDK client 2026-05-02 10:09:17 -04:00
Koichi ITO
0c46dcd0ab
Merge pull request #327 from atesgoral/ag/client-connect-handshake
Add client-level connect for initialize handshake
2026-05-02 15:40:55 +09:00
Ates Goral
ddfbfad81e Add client-level connect for initialize handshake 2026-05-01 20:17:45 -04:00
Koichi ITO
38d952adce
Merge pull request #333 from koic/tool_validation_error
Return tool argument validation failures as tool execution errors
2026-05-02 01:09:26 +09:00
Koichi ITO
48d4fb9272 Return tool argument validation failures as tool execution errors
## Motivation and Context

MCP 2025-11-25 (SEP-1303) clarifies that input validation errors for `tools/call`
should be returned as Tool Execution Errors (`{ content: [...], isError: true }`)
rather than as JSON-RPC `-32602` protocol errors, so models can observe
the validation message and self-correct on a follow-up call.
https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1303

This is a clarification rather than a brand-new requirement: the 2024-11-05, 2025-03-26,
and 2025-06-18 specifications all defined two error categories with overlapping language
("Invalid arguments" listed under Protocol Errors *and* "Invalid input data" listed under
Tool Execution Errors), leaving the routing of JSON-Schema validation failures ambiguous.
The Ruby SDK had selected the "Protocol Errors / Invalid arguments" interpretation,
while the TypeScript SDK (`packages/server/src/server/mcp.ts`, which wraps
`validateToolInput` errors via `createToolError`) and Python SDK's FastMCP
(which routes through the generic `is_error=True` path) had selected the other.
SEP-1303 in 2025-11-25 disambiguates this by replacing the bullets with
"Malformed requests (requests that fail to satisfy CallToolRequest schema)"
under Protocol Errors and "Input validation errors (e.g., date in wrong format,
value out of range)" under Tool Execution Errors, and explicitly states that the latter
"contain actionable feedback that language models can use to self-correct".

`tool_not_found` continues to be returned as a JSON-RPC `-32602` protocol error since
the spec change only covers input validation.

## How Has This Been Tested?

Updated existing tests that previously asserted `-32602` and "Invalid arguments" /
"Missing required arguments" in the JSON-RPC error data to instead
assert `result[:isError] == true` with the same text in the `content` block.
Added new regression tests covering:

- Instrumentation still records `:missing_required_arguments` and
  `:invalid_schema` on the new non-raising path
- Nested schema validation failure (deep arrays of objects with
  required fields) returns a tool execution error rather than a
  protocol error
- `tool_not_found` continues to return JSON-RPC `-32602` (regression
  guard against accidentally widening the change)

`bundle exec rake test` and `bundle exec rake rubocop` both pass.

## Breaking Changes

Yes. Clients that detect tool argument validation errors via `error.code == -32602`
will need to switch to inspecting `result.isError == true` and reading `result.content[].text`.

Note that the Ruby SDK's previous behavior was a defensible reading of the 2024-11-05 /
2025-03-26 / 2025-06-18 spec wording. The 2025-11-25 disambiguation is what makes
the previous behavior non-conforming; the TypeScript and Python SDKs already shipped
the new behavior.
2026-05-01 03:18:29 +09:00
Koichi ITO
2ad3d21492
Merge pull request #331 from koic/remove_obsolete_transport_module
Remove obsolete `MCP::Transports` module
2026-04-29 00:20:50 +09:00
Koichi ITO
82f85611f3
Merge pull request #330 from koic/fix_typo_for_read_resources
[Doc] Fix a typo in README.md
2026-04-28 12:22:43 +09:00
Koichi ITO
ceea440ca5 Remove obsolete MCP::Transports module
## Motivation and Context

`lib/mcp/transports/stdio.rb` was a deprecated shim that redirected users
from `MCP::Transports::StdioTransport` to `MCP::Server::Transports::StdioTransport`.
It has been 10 months since the migration path was introduced, providing sufficient time
for users to update their code.

## How Has This Been Tested?

All tests pass (`rake test`), and no code in the repository references `MCP::Transports`
or `mcp/transports/stdio`.

## Breaking Changes

`require "mcp/transports/stdio"` and `MCP::Transports::StdioTransport` are removed.
Use `require "mcp/server/transports/stdio_transport"` and `MCP::Server::Transports::StdioTransport` instead.
2026-04-26 15:54:07 +09:00
Koichi ITO
07fb0a8e56 [Doc] Fix a typo in README.md
The method name in the client feature list was `MCP::Client#read_resources` (plural),
but the actual method is `MCP::Client#read_resource` (singular).
2026-04-25 11:23:30 +09:00
Koichi ITO
27ff7e4bd1
Merge pull request #329 from koic/release_0_14_0 v0.14.0
Release 0.14.0
2026-04-24 14:44:19 +09:00
Koichi ITO
b03ec40d23 Release 0.14.0
New features and changes have landed, making it a good time to cut a release.
Further feature proposals can be incorporated in future releases.
2026-04-24 11:27:41 +09:00
Koichi ITO
a89047af60
Merge pull request #328 from koic/support_json_response_mode
Support JSON response mode for `StreamableHTTPTransport`
2026-04-23 14:05:50 +09:00
Koichi ITO
017b0bb4c9 Support JSON response mode for StreamableHTTPTransport
## Motivation and Context

The MCP Streamable HTTP specification allows servers to return POST responses as either
`text/event-stream` (SSE) or `application/json`:

> 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.

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

The TypeScript and Python SDKs support a configurable JSON response mode
via `enableJsonResponse` / `is_json_response_enabled`.

JSON response mode is suitable for simple tool servers that do not need server-initiated requests.
It avoids SSE framing overhead and returns a single JSON object for the POST response.

## Behavior

- POST responses use `Content-Type: application/json` and return a single JSON object.
- The POST `Accept` header requirement is relaxed to `application/json` only
  (matching the Python SDK's lenient behavior).
- Request-scoped notifications (`progress`, `log`) cannot ride along with the single-object response
  and are silently dropped.
- Session-scoped standalone notifications (`resources/updated`, `elicitation/complete`)
  and broadcast notifications (`tools/list_changed`, etc.) continue to flow to clients connected
  to the GET SSE stream.
- All server-to-client requests (`sampling/createMessage`, `roots/list`, `elicitation/create`)
  raise an error in JSON response mode.
- Combines with `stateless: true` for simple single-response servers without session tracking.

## How Has This Been Tested?

Added tests for JSON response mode:

- POST request returns `application/json` response
- Accept header validation requires only `application/json`
- Returns 406 when Accept header is missing
- Accepts wildcard `*/*` in Accept header
- Request-scoped notifications (progress, log) during tool execution are silently dropped
- Request-scoped notifications do not leak to GET SSE even when connected
- Session-scoped standalone notifications are delivered via GET SSE
- Broadcast notifications are delivered via GET SSE
- `send_request` raises for `sampling/createMessage`, `roots/list`, `elicitation/create`
- Combined with `stateless: true`: POST returns JSON without a session ID, GET returns 405

## Breaking Changes

None. JSON response mode is opt-in via `enable_json_response: true`.
The default behavior (SSE responses) is unchanged.
2026-04-22 08:30:28 +09:00
Koichi ITO
a2706cc7ef
Merge pull request #326 from atesgoral/ag/client-session-termination
Add HTTP client close for explicit session termination
2026-04-21 10:05:45 +09:00
Ates Goral
ff3a42f3fb Add HTTP client close for explicit session termination
Per MCP spec, clients that no longer need a session SHOULD send an HTTP
DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly
terminate it. Servers MAY respond with 405 Method Not Allowed when they
don't support client-initiated termination.

`MCP::Client::HTTP#close` sends the DELETE with session headers, clears
local session state regardless of outcome, and is a no-op when no
session has been established.

https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
2026-04-20 13:58:41 -04:00
Koichi ITO
0770e27c67
Merge pull request #325 from atesgoral/ag/client-session-tracking
Track Mcp-Session-Id and protocol version in HTTP client
2026-04-21 00:11:27 +09:00
Ates Goral
07226b7cf1 Track Mcp-Session-Id and protocol version in HTTP client
Per the Streamable HTTP spec, if the server returns an Mcp-Session-Id
header during `initialize`, the client MUST include it on subsequent
requests. Capture the session ID and protocol version from the
`initialize` response and attach them automatically on subsequent
POSTs.

Per the Protocol Version Header section of the spec, the client MUST
also include `MCP-Protocol-Version` on all subsequent requests so the
server can respond based on the negotiated protocol version.

Map 404 responses to a new `SessionExpiredError` (a subclass of
`RequestHandlerError` for backward compatibility) and clear local
session state so callers can start a fresh session with a new
`initialize` request, as required by the spec.

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 00:23:07 -04:00
Koichi ITO
baa093dfd5
Merge pull request #324 from koic/ping_client_to_server
Support `ping` client API per MCP specification
2026-04-20 12:15:21 +09:00
Koichi ITO
cbe81453fd
Merge pull request #323 from atesgoral/ag/client-202-accepted
Handle 202 Accepted response in HTTP client
2026-04-20 11:16:09 +09:00
Ates Goral
cfc42dbfe7 Handle 202 Accepted response in HTTP client
Per the Streamable HTTP spec, a server returns 202 Accepted with no
body as the ACK for a JSON-RPC notification or response. The client
previously errored on 202 because it fell through Content-Type
dispatch. Short-circuit to return nil before Content-Type handling so
fire-and-forget messages (e.g. notifications/initialized) no longer
raise.

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

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 14:06:07 -04:00
Koichi ITO
5c0fee309f
Merge pull request #322 from atesgoral/ag/client-sse-parser
Parse SSE responses in HTTP client via event_stream_parser
2026-04-20 00:58:59 +09:00
Ates Goral
b832933f9f Parse SSE responses in HTTP client via event_stream_parser
The Streamable HTTP spec allows servers to respond with either
`application/json` or `text/event-stream`. The client previously
rejected the latter. Add SSE parsing via the optional
`event_stream_parser` gem (users add it to their Gemfile when their
server uses SSE), dispatch on the response Content-Type, and scan the
event stream for the first JSON-RPC response message.

Document the new optional dependency in both the README's HTTP
Transport Layer section and docs/building-clients.md.

No other Streamable HTTP features (202 handling, session IDs,
`connect`, DELETE termination) are included here; those will land in
follow-up PRs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-19 11:54:03 -04:00
Koichi ITO
493321ca46 Support ping client API per MCP specification
## Motivation and Context

The MCP specification defines the `ping` utility for verifying that the peer of
a connection is still responsive. The Ruby SDK server side already replies to
incoming `ping` requests with an empty result, but `MCP::Client` had no public API
to send a `ping` request. Users had to build the JSON-RPC payload themselves and
call the transport directly.

This adds a client-side counterpart to the existing server handler and aligns
the Ruby SDK in spirit with the Python and TypeScript SDKs, which expose
`ClientSession.send_ping` and `Client.ping` respectively. Those SDKs validate
the response envelope via type schemas (`EmptyResult` / `EmptyResultSchema`) and raise
their validation library's generic exception (Pydantic's `ValidationError` / Zod's `ZodError`).
The Ruby client applies a Hash type check on `result`
(matching Python's behavior of requiring a dict while allowing arbitrary contents)
and raises a new `MCP::Client::ValidationError` for malformed responses,
keeping `MCP::Client::ServerError` reserved for JSON-RPC error responses.

## How Has This Been Tested?

Client tests cover:

- request structure (method is `ping`, no `params`)
- empty result on success
- `ServerError` on JSON-RPC error response
- `ValidationError` on a response missing `result`
- `ValidationError` on a `result` of the wrong type (e.g., String)
- propagation of transport-level errors raised by `transport.send_request`

The existing server-side `ping` handler tests continue to pass unchanged.

## Breaking Changes

None. `MCP::Client#ping` and `MCP::Client::ValidationError` are purely additive.
The server-side `ping` handler behavior is unchanged.
2026-04-19 23:14:50 +09:00