Skip to content

Support client-side elicitation with SEP-1034 schema defaults#443

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:elicitation_sep1034_defaults
Open

Support client-side elicitation with SEP-1034 schema defaults#443
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:elicitation_sep1034_defaults

Conversation

@koic

@koic koic commented Jul 7, 2026

Copy link
Copy Markdown
Member

Motivation and Context

Resolves the elicitation-sep1034-client-defaults client conformance scenario (SEP-1034, modelcontextprotocol/modelcontextprotocol#1034).

The Ruby HTTP client could not receive server-to-client requests at all: it never opened the spec's standalone GET SSE listening stream, and SSE events that were not responses were discarded. A server sending elicitation/create during a tools/call (such as the conformance harness, which is built on the TypeScript SDK server) either dropped the request silently or deadlocked the tool call waiting for an answer that could never arrive.

This change adds three pieces:

Server-to-client request dispatch

MCP::Client::HTTP#on_server_request(method) { |params| ... } registers a handler; when a JSON-RPC request (a message with both method and id) arrives on any SSE stream, the handler's return value is POSTed back to the server as the JSON-RPC result (the server ACKs with 202 Accepted, per the transport spec). Methods without a handler are answered with a -32601 method-not-found error. A handler may raise MCP::Client::ServerRequestError to answer with a specific JSON-RPC error code (for example -1 for a rejected sampling request, per the sampling spec), mirroring the TypeScript SDK's McpError and the Python SDK's ErrorData return; any other error is answered with a -32603 internal error. In every case the server is not left waiting, matching how the TypeScript and Python SDKs convert handler failures into JSON-RPC error responses.

A standalone GET SSE listening stream

Registering a handler (once connected) opens the spec's listening stream on a background thread, because servers deliver requests that are not tied to a client request on that stream - the TypeScript SDK's StreamableHTTPServerTransport silently drops them when no GET stream is open. Mirrors the TypeScript and Python SDK clients, which start listening right after the initialize handshake without awaiting stream establishment. Reconnection semantics also follow both SDKs: a gracefully closed stream is retried indefinitely after the server's retry: interval, while consecutive connection failures (HTTP errors and idle read timeouts) stop the listener once they reach the reconnection attempt cap, with a successful stream resetting the count; a 405 stops immediately without retrying, since it is the spec's answer from a server that offers no listening stream. The listening GET uses a 5-minute read timeout matching the Python SDK's sse_read_timeout default. close terminates the thread.

SEP-1034 defaults

MCP::Client::Elicitation.apply_defaults fills fields omitted from the elicitation content with the default values declared in requestedSchema properties, never overwriting provided values and skipping properties without a default. This follows the Python SDK, which applies defaults in the elicitation callback; the TypeScript SDK's automatic applyElicitationDefaults (gated on the elicitation.form.applyDefaults capability) is a possible follow-up. MCP::Client#on_elicitation registers a handler for elicitation/create as a convenience.

The conformance client gains an elicitation-sep1034-client-defaults branch that connects with capabilities: { elicitation: {} }, accepts the elicitation with defaults applied, and calls the harness's test_client_elicitation_defaults tool; the scenario is removed from the expected failures baseline.

How Has This Been Tested?

New unit tests covering: handler dispatch from a request's POST SSE stream with the response body and 202 ACK verified, the -32601 answer for unregistered methods, the -32603 answer for a raising handler, the custom-code answer when a handler raises ServerRequestError, listener startup from both registration-after-connect and connect-with-registered-handlers, listener shutdown after consecutive connection failures with the count resetting after a successful stream and an immediate stop on 405, and apply_defaults for all five SEP-1034 property types (string, integer, number, enum, boolean), non-overwriting of provided values, omission of properties without defaults, and symbol-keyed schemas

Breaking Changes

None. The listening stream and its background thread are created only when a server-request handler is registered; clients that never call on_server_request / on_elicitation keep the existing single-request behavior.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## Motivation and Context

Resolves the `elicitation-sep1034-client-defaults` client conformance scenario
(SEP-1034, modelcontextprotocol/modelcontextprotocol#1034).

The Ruby HTTP client could not receive server-to-client requests at all:
it never opened the spec's standalone GET SSE listening stream, and SSE events that
were not responses were discarded. A server sending `elicitation/create` during a tools/call
(such as the conformance harness, which is built on the TypeScript SDK server)
either dropped the request silently or deadlocked the tool call waiting for an answer that
could never arrive.

This change adds three pieces:

### Server-to-client request dispatch

`MCP::Client::HTTP#on_server_request(method) { |params| ... }` registers a handler;
when a JSON-RPC request (a message with both `method` and `id`) arrives on any SSE stream,
the handler's return value is POSTed back to the server as the JSON-RPC `result`
(the server ACKs with 202 Accepted, per the transport spec).
Methods without a handler are answered with a `-32601` method-not-found error.
A handler may raise `MCP::Client::ServerRequestError` to answer with a specific JSON-RPC error code
(for example `-1` for a rejected sampling request, per the sampling spec),
mirroring the TypeScript SDK's `McpError` and the Python SDK's `ErrorData` return;
any other error is answered with a `-32603` internal error. In every case the server is not left waiting,
matching how the TypeScript and Python SDKs convert handler failures into JSON-RPC error responses.

### A standalone GET SSE listening stream

Registering a handler (once connected) opens the spec's listening stream on a background thread,
because servers deliver requests that are not tied to a client request on that stream -
the TypeScript SDK's `StreamableHTTPServerTransport` silently drops them when no GET stream is open.
Mirrors the TypeScript and Python SDK clients, which start listening right after the `initialize` handshake
without awaiting stream establishment. Reconnection semantics also follow both SDKs:
a gracefully closed stream is retried indefinitely after the server's `retry:` interval,
while consecutive connection failures (HTTP errors and idle read timeouts) stop the listener once
they reach the reconnection attempt cap, with a successful stream resetting the count;
a 405 stops immediately without retrying, since it is the spec's answer from a server that
offers no listening stream. The listening GET uses a 5-minute read timeout matching
the Python SDK's `sse_read_timeout` default. `close` terminates the thread.

### SEP-1034 defaults

`MCP::Client::Elicitation.apply_defaults` fills fields omitted from the elicitation content with
the `default` values declared in `requestedSchema` properties, never overwriting provided values
and skipping properties without a default. This follows the Python SDK, which applies defaults in
the elicitation callback; the TypeScript SDK's automatic `applyElicitationDefaults`
(gated on the `elicitation.form.applyDefaults` capability) is a possible follow-up.
`MCP::Client#on_elicitation` registers a handler for `elicitation/create` as a convenience.

The conformance client gains an `elicitation-sep1034-client-defaults` branch that connects with
`capabilities: { elicitation: {} }`, accepts the elicitation with defaults applied, and calls
the harness's `test_client_elicitation_defaults` tool; the scenario is removed from the expected failures baseline.

## How Has This Been Tested?

New unit tests covering: handler dispatch from a request's POST SSE stream with
the response body and 202 ACK verified, the `-32601` answer for unregistered methods,
the `-32603` answer for a raising handler, the custom-code answer when a handler raises
`ServerRequestError`, listener startup from both registration-after-connect and
connect-with-registered-handlers, listener shutdown after consecutive connection failures with
the count resetting after a successful stream and an immediate stop on 405,
and `apply_defaults` for all five SEP-1034 property types (string, integer, number, enum, boolean),
non-overwriting of provided values, omission of properties without defaults, and symbol-keyed schemas

## Breaking Changes

None. The listening stream and its background thread are created only when a server-request handler is registered;
clients that never call `on_server_request` / `on_elicitation` keep the existing single-request behavior.
@koic koic force-pushed the elicitation_sep1034_defaults branch from 0cd11ca to 04bb739 Compare July 7, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant