Skip to content

Commit bfc6e55

Browse files
committed
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.
1 parent da4ce54 commit bfc6e55

8 files changed

Lines changed: 501 additions & 51 deletions

File tree

‎examples/README.md‎

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ A specialized HTTP server designed to test and demonstrate Server-Sent Events (S
9898

9999
**Available Tools:**
100100

101-
- `notification_tool` - Send custom SSE notifications with optional delays
101+
- `notification_tool` - Sends progress notifications over SSE, with optional delays
102102
- `echo` - Simple echo tool for basic testing
103103

104104
**Usage:**
@@ -124,14 +124,15 @@ An interactive client that connects to the SSE stream and provides a menu-driven
124124

125125
1. Start the SSE test server in one terminal:
126126

127-
```console
128-
$ ruby examples/streamable_http_server.rb
129-
```
127+
```console
128+
$ ruby examples/streamable_http_server.rb
129+
```
130130

131131
2. Run the SSE test client in another terminal:
132-
```console
133-
$ ruby examples/streamable_http_client.rb
134-
```
132+
133+
```console
134+
$ ruby examples/streamable_http_client.rb
135+
```
135136

136137
The client will:
137138

@@ -155,6 +156,34 @@ $ bundle exec puma --port 9292
155156

156157
The MCP endpoint is available at `http://localhost:9292/mcp`. See [`rails/README.md`](rails/README.md) for a full curl-based walkthrough.
157158

159+
### 8. Modern Lifecycle HTTP Server / Client (`modern_http_server.rb`, `modern_http_client.rb`)
160+
161+
A server and client pair demonstrating the 2026-07-28 modern lifecycle (SEP-2575), which replaces the `initialize` handshake and per-session state with sessionless, self-contained requests.
162+
163+
**Features:**
164+
165+
- `server/discover` capability discovery before (or instead of) a handshake
166+
- The per-request `_meta` envelope and `Mcp-Method` / `Mcp-Name` headers, stamped by the SDK automatically
167+
- `resultType` stamping and SEP-2549 cache hints (`ttlMs`, `cacheScope`) on results
168+
- A multi round-trip `deploy` tool (SEP-2322), resumed automatically by the client's elicitation handler
169+
- The removal of legacy-only methods such as `ping`
170+
171+
**Usage:**
172+
173+
1. Start the server in one terminal:
174+
175+
```console
176+
$ ruby examples/modern_http_server.rb
177+
```
178+
179+
2. Run the client in another terminal:
180+
181+
```console
182+
$ ruby examples/modern_http_client.rb
183+
```
184+
185+
The same server still accepts the legacy `initialize` flow: the transport routes each request to the legacy or modern lifecycle by its `MCP-Protocol-Version` header.
186+
158187
### Testing with MCP Inspector
159188

160189
[MCP Inspector](https://modelcontextprotocol.io/docs/tools/inspector) is a browser-based tool for testing and debugging MCP servers.
@@ -188,24 +217,78 @@ You can also test SSE functionality manually using cURL:
188217

189218
```console
190219
SESSION_ID=$(curl -D - -s -o /dev/null http://localhost:9393 \
191-
--json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}' | grep -i "Mcp-Session-Id:" | cut -d' ' -f2- | tr -d '\r')
220+
-H "Accept: application/json, text/event-stream" \
221+
--json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"curl-test","version":"1.0"}}}' | grep -i "Mcp-Session-Id:" | cut -d' ' -f2- | tr -d '\r')
192222
```
193223

194-
2. Connect to SSE stream (in one terminal):
224+
2. Optionally connect the standalone SSE stream, which carries server-initiated messages that are not tied to a request (in another terminal):
195225

196226
```console
197227
curl -i -N -H "Mcp-Session-Id: $SESSION_ID" http://localhost:9393
198228
```
199229

200-
3. Trigger notifications (in another terminal):
230+
3. Call the notification tool. The `notifications/progress` events (requested via the `progressToken` in `_meta`) and the final response arrive as SSE events on the POST response itself:
201231

202232
```console
203-
# Send immediate notification
204233
curl -i http://localhost:9393 \
234+
-H "Accept: application/json, text/event-stream" \
205235
-H "Mcp-Session-Id: $SESSION_ID" \
206-
--json '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"notification_tool","arguments":{"message":"Hello from cURL!"}}}'
236+
--json '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"notification_tool","arguments":{"message":"Hello from cURL!","delay":0.5},"_meta":{"progressToken":"curl-progress"}}}'
237+
```
238+
239+
### Testing the modern lifecycle with cURL
240+
241+
The modern lifecycle (2026-07-28, SEP-2575) is sessionless: there is no `initialize` handshake and no `Mcp-Session-Id`. Start `examples/modern_http_server.rb` and walk it manually:
242+
243+
1. Probe capabilities with `server/discover`:
244+
245+
```console
246+
curl -s http://localhost:9494 \
247+
-H "Accept: application/json, text/event-stream" \
248+
--json '{"jsonrpc":"2.0","id":0,"method":"server/discover"}'
249+
```
250+
251+
2. List tools (the `MCP-Protocol-Version` header selects the era, `Mcp-Method` mirrors the method, and `params._meta` carries the SEP-2575 envelope):
252+
253+
```console
254+
curl -s http://localhost:9494 \
255+
-H "Accept: application/json, text/event-stream" \
256+
-H "MCP-Protocol-Version: 2026-07-28" \
257+
-H "Mcp-Method: tools/list" \
258+
--json '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
259+
```
260+
261+
3. Call a tool (name-bearing methods additionally mirror the name in the `Mcp-Name` header):
262+
263+
```console
264+
curl -s http://localhost:9494 \
265+
-H "Accept: application/json, text/event-stream" \
266+
-H "MCP-Protocol-Version: 2026-07-28" \
267+
-H "Mcp-Method: tools/call" \
268+
-H "Mcp-Name: greet" \
269+
--json '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"greet","arguments":{"name":"curl"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{}}}}'
207270
```
208271

272+
4. Observe a multi round-trip result (SEP-2322), then resume it by echoing the `requestState` back together with the answer. Declaring the `elicitation` capability in the envelope is required before the server may embed elicitation requests:
273+
274+
```console
275+
curl -s http://localhost:9494 \
276+
-H "Accept: application/json, text/event-stream" \
277+
-H "MCP-Protocol-Version: 2026-07-28" \
278+
-H "Mcp-Method: tools/call" \
279+
-H "Mcp-Name: deploy" \
280+
--json '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"deploy","arguments":{"app":"storefront"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}'
281+
282+
curl -s http://localhost:9494 \
283+
-H "Accept: application/json, text/event-stream" \
284+
-H "MCP-Protocol-Version: 2026-07-28" \
285+
-H "Mcp-Method: tools/call" \
286+
-H "Mcp-Name: deploy" \
287+
--json '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"deploy","arguments":{"app":"storefront"},"inputResponses":{"environment":{"action":"accept","content":{"environment":"staging"}}},"requestState":"{\"app\":\"storefront\"}","_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientCapabilities":{"elicitation":{}}}}}'
288+
```
289+
290+
The same server still accepts the legacy flow from the previous sections, routed by the `MCP-Protocol-Version` header.
291+
209292
## Streamable HTTP Transport Details
210293

211294
### Protocol Flow
@@ -236,13 +319,15 @@ Initialize a session:
236319

237320
```console
238321
curl -i http://localhost:9292 \
239-
--json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
322+
-H "Accept: application/json, text/event-stream" \
323+
--json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
240324
```
241325

242326
List tools (using the session ID from initialization):
243327

244328
```console
245329
curl -i http://localhost:9292 \
330+
-H "Accept: application/json, text/event-stream" \
246331
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
247332
--json '{"jsonrpc":"2.0","method":"tools/list","id":2}'
248333
```
@@ -251,6 +336,7 @@ Call a tool:
251336

252337
```console
253338
curl -i http://localhost:9292 \
339+
-H "Accept: application/json, text/event-stream" \
254340
-H "Mcp-Session-Id: YOUR_SESSION_ID" \
255341
--json '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"example_tool","arguments":{"a":5,"b":3}}}'
256342
```

‎examples/http_client.rb‎

Lines changed: 58 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
require "json"
55
require "uri"
66

7-
# Simple HTTP client example for interacting with the MCP HTTP server
7+
# Simple HTTP client example for interacting with the MCP HTTP server.
88
class MCPHTTPClient
99
def initialize(base_url = "http://localhost:9292")
1010
@base_url = base_url
@@ -30,19 +30,51 @@ def send_request(method, params = nil, id = nil)
3030

3131
response = http.request(request)
3232

33-
# Store session ID if provided
33+
# Store session ID if provided.
3434
if response["Mcp-Session-Id"]
3535
@session_id = response["Mcp-Session-Id"]
3636
puts "Session ID: #{@session_id}"
3737
end
3838

39-
JSON.parse(response.body)
39+
parse_response_body(response)
40+
end
41+
42+
# In the transport's default SSE mode, POST responses on an established session arrive as
43+
# a Server-Sent Events stream whose `data:` line carries the JSON-RPC response; unwrap it
44+
# before parsing.
45+
def parse_response_body(response)
46+
body = response.body
47+
48+
if response["Content-Type"]&.start_with?("text/event-stream")
49+
data_lines = body.lines.select { |line| line.start_with?("data:") }
50+
body = data_lines.map { |line| line.sub(/\Adata:\s*/, "") }.join
51+
end
52+
53+
JSON.parse(body)
54+
end
55+
56+
def send_notification(method, params = nil)
57+
uri = URI(@base_url)
58+
http = Net::HTTP.new(uri.host, uri.port)
59+
60+
request = Net::HTTP::Post.new(uri.path.empty? ? "/" : uri.path)
61+
request["Content-Type"] = "application/json"
62+
request["Mcp-Session-Id"] = @session_id if @session_id
63+
64+
# Notifications carry no `id` and receive no JSON-RPC response body.
65+
request.body = {
66+
jsonrpc: "2.0",
67+
method: method,
68+
params: params,
69+
}.compact.to_json
70+
71+
http.request(request)
4072
end
4173

4274
def initialize_session
4375
puts "=== Initializing session ==="
4476
result = send_request("initialize", {
45-
protocolVersion: "2024-11-05",
77+
protocolVersion: "2025-11-25",
4678
capabilities: {},
4779
clientInfo: {
4880
name: "example_client",
@@ -54,6 +86,14 @@ def initialize_session
5486
result
5587
end
5688

89+
def notify_initialized
90+
puts "=== Sending notifications/initialized ==="
91+
response = send_notification("notifications/initialized")
92+
puts "Response status: #{response.code} #{response.message}"
93+
94+
response
95+
end
96+
5797
def ping
5898
puts "=== Sending ping ==="
5999
result = send_request("ping")
@@ -147,37 +187,40 @@ def main
147187
client = MCPHTTPClient.new
148188

149189
begin
150-
# Initialize session
190+
# Initialize session.
151191
client.initialize_session
152192

153-
# Test ping
193+
# Complete the handshake.
194+
client.notify_initialized
195+
196+
# Test ping.
154197
client.ping
155198

156-
# List available tools
199+
# List available tools.
157200
client.list_tools
158201

159-
# Call the example_tool (note: snake_case name)
202+
# Call the example_tool (note: snake_case name).
160203
client.call_tool("example_tool", { a: 5, b: 3 })
161204

162-
# Call the echo tool
205+
# Call the echo tool.
163206
client.call_tool("echo", { message: "Hello from client!" })
164207

165-
# List prompts
208+
# List prompts.
166209
client.list_prompts
167210

168-
# Get a prompt (note: snake_case name)
211+
# Get a prompt (note: snake_case name).
169212
client.get_prompt("example_prompt", { message: "This is a test message" })
170213

171-
# List resources
214+
# List resources.
172215
client.list_resources
173216

174-
# Read a resource
175-
client.read_resource("test_resource")
217+
# Read a resource (the URI registered by http_server.rb).
218+
client.read_resource("https://test_resource.invalid")
176219
rescue => e
177220
puts "Error: #{e.message}"
178221
puts e.backtrace
179222
ensure
180-
# Clean up session
223+
# Clean up session.
181224
client.close_session
182225
end
183226
end

‎examples/http_server.rb‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def template(args, server_context:)
5656
# Set up the server
5757
server = MCP::Server.new(
5858
name: "example_http_server",
59+
version: "1.0.0",
5960
tools: [ExampleTool],
6061
prompts: [ExamplePrompt],
6162
resources: [
@@ -169,7 +170,9 @@ def call(env)
169170
Starting MCP HTTP server on http://localhost:9292
170171
Use POST requests to initialize and send JSON-RPC commands
171172
Example initialization:
172-
curl -i http://localhost:9292 --json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
173+
curl -i http://localhost:9292 \\
174+
-H "Accept: application/json, text/event-stream" \\
175+
--json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}'
173176
174177
The server will return a session ID in the Mcp-Session-Id header.
175178
Use this session ID for subsequent requests.

0 commit comments

Comments
 (0)