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:
parent
da4ce54075
commit
bfc6e551a3
1 changed files with 501 additions and 51 deletions
|
|
@ -4,7 +4,7 @@ require "net/http"
|
|||
require "json"
|
||||
require "uri"
|
||||
|
||||
# Simple HTTP client example for interacting with the MCP HTTP server
|
||||
# Simple HTTP client example for interacting with the MCP HTTP server.
|
||||
class MCPHTTPClient
|
||||
def initialize(base_url = "http://localhost:9292")
|
||||
@base_url = base_url
|
||||
|
|
@ -30,19 +30,51 @@ class MCPHTTPClient
|
|||
|
||||
response = http.request(request)
|
||||
|
||||
# Store session ID if provided
|
||||
# Store session ID if provided.
|
||||
if response["Mcp-Session-Id"]
|
||||
@session_id = response["Mcp-Session-Id"]
|
||||
puts "Session ID: #{@session_id}"
|
||||
end
|
||||
|
||||
JSON.parse(response.body)
|
||||
parse_response_body(response)
|
||||
end
|
||||
|
||||
# In the transport's default SSE mode, POST responses on an established session arrive as
|
||||
# a Server-Sent Events stream whose `data:` line carries the JSON-RPC response; unwrap it
|
||||
# before parsing.
|
||||
def parse_response_body(response)
|
||||
body = response.body
|
||||
|
||||
if response["Content-Type"]&.start_with?("text/event-stream")
|
||||
data_lines = body.lines.select { |line| line.start_with?("data:") }
|
||||
body = data_lines.map { |line| line.sub(/\Adata:\s*/, "") }.join
|
||||
end
|
||||
|
||||
JSON.parse(body)
|
||||
end
|
||||
|
||||
def send_notification(method, params = nil)
|
||||
uri = URI(@base_url)
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
|
||||
request = Net::HTTP::Post.new(uri.path.empty? ? "/" : uri.path)
|
||||
request["Content-Type"] = "application/json"
|
||||
request["Mcp-Session-Id"] = @session_id if @session_id
|
||||
|
||||
# Notifications carry no `id` and receive no JSON-RPC response body.
|
||||
request.body = {
|
||||
jsonrpc: "2.0",
|
||||
method: method,
|
||||
params: params,
|
||||
}.compact.to_json
|
||||
|
||||
http.request(request)
|
||||
end
|
||||
|
||||
def initialize_session
|
||||
puts "=== Initializing session ==="
|
||||
result = send_request("initialize", {
|
||||
protocolVersion: "2024-11-05",
|
||||
protocolVersion: "2025-11-25",
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: "example_client",
|
||||
|
|
@ -54,6 +86,14 @@ class MCPHTTPClient
|
|||
result
|
||||
end
|
||||
|
||||
def notify_initialized
|
||||
puts "=== Sending notifications/initialized ==="
|
||||
response = send_notification("notifications/initialized")
|
||||
puts "Response status: #{response.code} #{response.message}"
|
||||
|
||||
response
|
||||
end
|
||||
|
||||
def ping
|
||||
puts "=== Sending ping ==="
|
||||
result = send_request("ping")
|
||||
|
|
@ -147,37 +187,40 @@ def main
|
|||
client = MCPHTTPClient.new
|
||||
|
||||
begin
|
||||
# Initialize session
|
||||
# Initialize session.
|
||||
client.initialize_session
|
||||
|
||||
# Test ping
|
||||
# Complete the handshake.
|
||||
client.notify_initialized
|
||||
|
||||
# Test ping.
|
||||
client.ping
|
||||
|
||||
# List available tools
|
||||
# List available tools.
|
||||
client.list_tools
|
||||
|
||||
# Call the example_tool (note: snake_case name)
|
||||
# Call the example_tool (note: snake_case name).
|
||||
client.call_tool("example_tool", { a: 5, b: 3 })
|
||||
|
||||
# Call the echo tool
|
||||
# Call the echo tool.
|
||||
client.call_tool("echo", { message: "Hello from client!" })
|
||||
|
||||
# List prompts
|
||||
# List prompts.
|
||||
client.list_prompts
|
||||
|
||||
# Get a prompt (note: snake_case name)
|
||||
# Get a prompt (note: snake_case name).
|
||||
client.get_prompt("example_prompt", { message: "This is a test message" })
|
||||
|
||||
# List resources
|
||||
# List resources.
|
||||
client.list_resources
|
||||
|
||||
# Read a resource
|
||||
client.read_resource("test_resource")
|
||||
# Read a resource (the URI registered by http_server.rb).
|
||||
client.read_resource("https://test_resource.invalid")
|
||||
rescue => e
|
||||
puts "Error: #{e.message}"
|
||||
puts e.backtrace
|
||||
ensure
|
||||
# Clean up session
|
||||
# Clean up session.
|
||||
client.close_session
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue