Update examples for the 2026-07-28 era

## Motivation and Context

The runnable examples drifted from the SDK they ship with:

- POST requests whose `Accept` header does not cover both `application/json` and `text/event-stream` are answered 406,
  so the cURL walkthroughs  no longer worked as printed.
- `MCP::Client#connect` now defaults to `mode: :auto`, which adopts the sessionless modern lifecycle (2026-07-28)
  against a dual-era server; the Streamable HTTP client example requires a session and broke against its own server.
- POST responses on an established session arrive as an SSE stream, which the hand-rolled HTTP client did not parse.
- `notification_tool` claimed to send SSE notifications but only returned a `Tool::Response`.
- `http_client.rb` read a resource URI the server never registered and skipped `notifications/initialized`.

Handshake examples now offer the latest handshake protocol version (`2025-11-25`) instead of `2024-11-05`,
the Streamable HTTP client pins `mode: :legacy` for its session-based demo, `notification_tool` reports
real `notifications/progress` events requested through `_meta.progressToken`, and the interactive client streams
those events from the POST response so they are visible on screen.

The modern lifecycle itself had no runnable example, so a new server and client pair (`modern_http_server.rb`,
`modern_http_client.rb`) shows the sessionless flow end to end: `server/discover` capability discovery,
the per-request `_meta` envelope with the `Mcp-Method` and `Mcp-Name` headers (stamped by the SDK client automatically),
`resultType` stamping, SEP-2549 cache hints, a SEP-2322 multi round-trip `deploy` tool resumed automatically by
the client's elicitation handler, and the removal of legacy-only methods such as `ping`. The same server keeps serving
the legacy `initialize` flow, routed per request by the `MCP-Protocol-Version` header. `examples/README.md` documents
the pair and adds a cURL walkthrough covering the modern headers, the envelope, and the multi round-trip exchange.

## How Has This Been Tested?

- `ruby examples/stdio_client.rb` against `stdio_server.rb`
- `ruby examples/http_client.rb` against `http_server.rb`
- `ruby examples/streamable_http_client.rb` against `streamable_http_server.rb`, including the progress stream
- `ruby examples/modern_http_client.rb` against `modern_http_server.rb` (discover, modern adoption, cache hints,
  `resultType`, the multi round-trip resume, `ping` rejection)
- The cURL walkthroughs in the banners and `examples/README.md`, including the `requestState` echo and a legacy `initialize`
  against the modern example server

## Breaking Changes

None.
This commit is contained in:
Koichi ITO 2026-08-17 03:07:22 +09:00
commit bfc6e551a3

View file

@ -18,8 +18,11 @@ def create_logger
logger
end
# The SDK does not yet implement the optional GET SSE stream, so this example
# uses MCP::Client for JSON-RPC requests and raw Net::HTTP for the event stream.
# MCP::Client::HTTP opens the optional GET SSE stream itself once a server-request handler
# (e.g. `client.on_elicitation`) is registered. This example instead opens the stream with
# raw `Net::HTTP` to show the SSE wire format. The GET stream carries server-initiated messages
# that are not tied to a client request; request-scoped notifications such as progress arrive on
# the POST's own SSE response stream (see `stream_tool_call` below).
def connect_sse(session_id, logger)
uri = URI(SERVER_URL)
@ -64,6 +67,42 @@ def print_response(response)
end
end
# Calls a tool with a raw streaming POST so each SSE event on the response stream is printed as
# it arrives: the `notifications/progress` events requested via `_meta.progressToken`,
# then the final JSON-RPC response. `MCP::Client#call_tool` reads the same stream but consumes
# the progress notifications internally, so the raw request makes them visible here.
def stream_tool_call(session_id, request_id, name:, arguments:, logger:)
uri = URI(SERVER_URL)
body = {
jsonrpc: "2.0",
id: request_id,
method: "tools/call",
params: {
name: name,
arguments: arguments,
_meta: { progressToken: "streamable-http-client" },
},
}
Net::HTTP.start(uri.host, uri.port) do |http|
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request["Accept"] = "application/json, text/event-stream"
request["Mcp-Session-Id"] = session_id
request.body = JSON.generate(body)
http.request(request) do |response|
parser = EventStreamParser::Parser.new
response.read_body do |chunk|
parser.feed(chunk) do |_type, data, _id|
logger.info("SSE event: #{data}")
end
end
end
end
end
def main
logger = create_logger
@ -76,11 +115,17 @@ def main
http_transport = MCP::Client::HTTP.new(url: SERVER_URL)
client = MCP::Client.new(transport: http_transport)
sse_thread = nil
raw_request_id = 0
begin
puts "=== Initializing session ==="
# The default `mode: :auto` adopts the sessionless modern lifecycle (2026-07-28)
# when the server supports it. This example demonstrates the legacy session's SSE stream,
# so pin the legacy `initialize` handshake.
server_info = client.connect(
client_info: { name: "streamable-http-client", version: "1.0" },
mode: :legacy,
)
puts <<~MESSAGE
@ -104,8 +149,6 @@ def main
sse_thread = Thread.new { connect_sse(http_transport.session_id, logger) }
sleep(1)
# Once the optional SSE stream is active, POST requests may receive only a
# 202 ACK while the actual JSON-RPC response is delivered over SSE.
loop do
puts <<~MENU.chomp
@ -126,12 +169,15 @@ def main
print("Enter delay in seconds (0 for immediate): ")
delay = gets.chomp.to_f
puts "=== Calling tool: notification_tool ==="
response = client.call_tool(
tool: notification_tool,
puts "=== Calling tool: notification_tool (raw streaming POST) ==="
raw_request_id += 1
stream_tool_call(
http_transport.session_id,
"raw-#{raw_request_id}",
name: notification_tool.name,
arguments: { message: message, delay: delay },
logger: logger,
)
print_response(response)
else
puts "notification_tool not available"
end