Merge pull request #335 from atesgoral/ag/use-sdk-streamable-http-client-example

Update Streamable HTTP client example to use MCP::Client
This commit is contained in:
Koichi ITO 2026-05-03 00:24:30 +09:00 • committed by GitHub
commit 541200ffcd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1,49 +1,25 @@
# frozen_string_literal: true # frozen_string_literal: true
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
require "mcp"
require "net/http" require "net/http"
require "uri" require "uri"
require "json" require "json"
require "logger" require "logger"
require "event_stream_parser"
# Logger for client operations SERVER_URL = "http://localhost:9393"
logger = Logger.new($stdout)
logger.formatter = proc do |severity, datetime, _progname, msg| def create_logger
"[CLIENT] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n" logger = Logger.new($stdout)
logger.formatter = proc do |severity, datetime, _progname, msg|
"[CLIENT] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n"
end
logger
end end
# Server configuration # The SDK does not yet implement the optional GET SSE stream, so this example
SERVER_URL = "http://localhost:9393/mcp" # uses MCP::Client for JSON-RPC requests and raw Net::HTTP for the event stream.
PROTOCOL_VERSION = "2024-11-05"
# Helper method to make JSON-RPC requests
def make_request(session_id, method, params = {}, id = nil)
uri = URI(SERVER_URL)
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri)
request["Content-Type"] = "application/json"
request["Mcp-Session-Id"] = session_id if session_id
body = {
jsonrpc: "2.0",
method: method,
params: params,
id: id || SecureRandom.uuid,
}
request.body = body.to_json
response = http.request(request)
{
status: response.code,
headers: response.to_hash,
body: JSON.parse(response.body),
}
rescue => e
{ error: e.message }
end
# Connect to SSE stream
def connect_sse(session_id, logger) def connect_sse(session_id, logger)
uri = URI(SERVER_URL) uri = URI(SERVER_URL)
@ -59,17 +35,13 @@ def connect_sse(session_id, logger)
if response.code == "200" if response.code == "200"
logger.info("SSE stream connected successfully") logger.info("SSE stream connected successfully")
parser = EventStreamParser::Parser.new
response.read_body do |chunk| response.read_body do |chunk|
chunk.split("\n").each do |line| parser.feed(chunk) do |type, data, _id|
if line.start_with?("data: ") if type.empty?
data = line[6..-1] logger.info("SSE event: #{data}")
begin else
logger.info("SSE data: #{data}") logger.info("SSE event (#{type}): #{data}")
rescue JSON::ParserError
logger.debug("Non-JSON SSE data: #{data}")
end
elsif line.start_with?(": ")
logger.debug("SSE keepalive received: #{line}")
end end
end end
end end
@ -79,129 +51,129 @@ def connect_sse(session_id, logger)
end end
end end
rescue Interrupt rescue Interrupt
logger.info("SSE connection interrupted by user") logger.info("SSE connection interrupted")
rescue => e rescue => e
logger.error("SSE connection error: #{e.message}") logger.error("SSE connection error: #{e.message}")
end end
# Main client flow def print_response(response)
if response.nil?
puts "Response accepted; watch the SSE stream for the server response."
else
puts "Response: #{JSON.pretty_generate(response)}"
end
end
def main def main
logger = Logger.new($stdout) logger = create_logger
logger.formatter = proc do |severity, datetime, _progname, msg|
"[CLIENT] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n"
end
puts "=== MCP SSE Test Client ===" puts <<~MESSAGE
MCP Streamable HTTP Client
Make sure the server is running (ruby examples/streamable_http_server.rb)
#{"=" * 60}
MESSAGE
# Step 1: Initialize session http_transport = MCP::Client::HTTP.new(url: SERVER_URL)
logger.info("Initializing session...") client = MCP::Client.new(transport: http_transport)
sse_thread = nil
init_response = make_request( begin
nil, puts "=== Initializing session ==="
"initialize", server_info = client.connect(
{ client_info: { name: "streamable-http-client", version: "1.0" },
protocolVersion: PROTOCOL_VERSION, )
capabilities: {},
clientInfo: {
name: "sse-test-client",
version: "1.0",
},
},
"init-1",
)
if init_response[:error] puts <<~MESSAGE
logger.error("Failed to initialize: #{init_response[:error]}") ID: #{http_transport.session_id}
exit(1) Version: #{http_transport.protocol_version}
end Server: #{server_info["serverInfo"]}
session_id = init_response[:headers]["mcp-session-id"]&.first
if session_id.nil?
logger.error("No session ID received")
exit(1)
end
if init_response[:body].dig("result", "capabilities", "logging")
make_request(session_id, "logging/setLevel", { level: "info" })
end
logger.info("Session initialized: #{session_id}")
logger.info("Server info: #{init_response[:body]["result"]["serverInfo"]}")
# Step 2: Start SSE connection in a separate thread
sse_thread = Thread.new { connect_sse(session_id, logger) }
# Give SSE time to connect
sleep(1)
# Step 3: Interactive menu
loop do
puts <<~MESSAGE.chomp
=== Available Actions ===
1. Send custom notification
2. Test echo
3. List tools
0. Exit
Choose an action:#{" "}
MESSAGE MESSAGE
choice = gets.chomp unless http_transport.session_id
logger.error("No session ID received; this example requires a stateful Streamable HTTP session.")
return
end
case choice puts "=== Listing tools ==="
when "1" tools = client.tools
print("Enter notification message: ") tools.each { |tool| puts " - #{tool.name}: #{tool.description}" }
message = gets.chomp
print("Enter delay in seconds (0 for immediate): ")
delay = gets.chomp.to_f
response = make_request( echo_tool = tools.find { |tool| tool.name == "echo" }
session_id, notification_tool = tools.find { |tool| tool.name == "notification_tool" }
"tools/call",
{ sse_thread = Thread.new { connect_sse(http_transport.session_id, logger) }
name: "notification_tool", sleep(1)
arguments: {
message: message, # Once the optional SSE stream is active, POST requests may receive only a
delay: delay, # 202 ACK while the actual JSON-RPC response is delivered over SSE.
}, loop do
}, puts <<~MENU.chomp
)
if response[:body]["accepted"] === Available Actions ===
logger.info("Notification sent successfully") 1. Send notification (triggers SSE event)
2. Echo message
3. Show cached tools
0. Exit
Choose an action:#{" "}
MENU
case gets.chomp
when "1"
if notification_tool
print("Enter notification message: ")
message = gets.chomp
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,
arguments: { message: message, delay: delay },
)
print_response(response)
else
puts "notification_tool not available"
end
when "2"
if echo_tool
print("Enter message to echo: ")
message = gets.chomp
puts "=== Calling tool: echo ==="
response = client.call_tool(tool: echo_tool, arguments: { message: message })
print_response(response)
else
puts "echo tool not available"
end
when "3"
puts "=== Cached tools ==="
tools.each { |tool| puts " - #{tool.name}: #{tool.description}" }
when "0"
logger.info("Exiting...")
break
else else
logger.error("Error: #{response[:body]["error"]}") puts "Invalid choice"
end end
when "2" end
print("Enter message to echo: ") rescue MCP::Client::SessionExpiredError => e
message = gets.chomp logger.error("Session expired: #{e.message}")
make_request(session_id, "tools/call", { name: "echo", arguments: { message: message } }) rescue MCP::Client::RequestHandlerError => e
when "3" logger.error("Request error: #{e.message}")
make_request(session_id, "tools/list") rescue Interrupt
when "0" logger.info("Client interrupted")
logger.info("Exiting...") rescue => e
break logger.error("Error: #{e.message}")
else logger.error(e.backtrace.first(5).join("\n"))
puts "Invalid choice" ensure
sse_thread.kill if sse_thread&.alive?
if http_transport.connected?
puts "=== Closing session ==="
http_transport.close
puts "Session closed"
end end
end end
# Clean up
sse_thread.kill if sse_thread.alive?
# Close session
logger.info("Closing session...")
make_request(session_id, "close")
logger.info("Session closed")
rescue Interrupt
logger.info("Client interrupted by user")
rescue => e
logger.error("Client error: #{e.message}")
logger.error(e.backtrace.join("\n"))
end end
# Run the client main if __FILE__ == $PROGRAM_NAME
if __FILE__ == $PROGRAM_NAME
main
end