11# frozen_string_literal: true
22
3+ require "mcp"
4+ require "mcp/client"
5+ require "mcp/client/http"
6+ require "mcp/client/tool"
37require "net/http"
48require "uri"
59require "json"
610require "logger"
711
8- # Logger for client operations
9- logger = Logger . new ( $stdout)
10- logger . formatter = proc do |severity , datetime , _progname , msg |
11- "[CLIENT] #{ severity } #{ datetime . strftime ( "%H:%M:%S.%L" ) } - #{ msg } \n "
12- end
12+ SERVER_URL = "http://localhost:9393"
1313
14- # Server configuration
15- SERVER_URL = "http://localhost:9393/mcp"
16- PROTOCOL_VERSION = "2024-11-05"
17-
18- # Helper method to make JSON-RPC requests
19- def make_request ( session_id , method , params = { } , id = nil )
20- uri = URI ( SERVER_URL )
21- http = Net ::HTTP . new ( uri . host , uri . port )
22-
23- request = Net ::HTTP ::Post . new ( uri )
24- request [ "Content-Type" ] = "application/json"
25- request [ "Mcp-Session-Id" ] = session_id if session_id
26-
27- body = {
28- jsonrpc : "2.0" ,
29- method : method ,
30- params : params ,
31- id : id || SecureRandom . uuid ,
32- }
33-
34- request . body = body . to_json
35- response = http . request ( request )
36-
37- {
38- status : response . code ,
39- headers : response . to_hash ,
40- body : JSON . parse ( response . body ) ,
41- }
42- rescue => e
43- { error : e . message }
14+ # Logger for client operations
15+ def create_logger
16+ logger = Logger . new ( $stdout)
17+ logger . formatter = proc do |severity , datetime , _progname , msg |
18+ "[CLIENT] #{ severity } #{ datetime . strftime ( "%H:%M:%S.%L" ) } - #{ msg } \n "
19+ end
20+ logger
4421end
4522
46- # Connect to SSE stream
23+ # Connect to SSE stream for real-time notifications
24+ # Note: The SDK doesn't support SSE streaming yet, so we use raw Net::HTTP
4725def connect_sse ( session_id , logger )
4826 uri = URI ( SERVER_URL )
4927
5028 logger . info ( "Connecting to SSE stream..." )
5129
5230 Net ::HTTP . start ( uri . host , uri . port ) do |http |
5331 request = Net ::HTTP ::Get . new ( uri )
54- request [ "Mcp -Session-Id" ] = session_id
32+ request [ "MCP -Session-Id" ] = session_id
5533 request [ "Accept" ] = "text/event-stream"
5634 request [ "Cache-Control" ] = "no-cache"
5735
@@ -62,14 +40,10 @@ def connect_sse(session_id, logger)
6240 response . read_body do |chunk |
6341 chunk . split ( "\n " ) . each do |line |
6442 if line . start_with? ( "data: " )
65- data = line [ 6 ..-1 ]
66- begin
67- logger . info ( "SSE data: #{ data } " )
68- rescue JSON ::ParserError
69- logger . debug ( "Non-JSON SSE data: #{ data } " )
70- end
43+ data = line [ 6 ..]
44+ logger . info ( "SSE event: #{ data } " )
7145 elsif line . start_with? ( ": " )
72- logger . debug ( "SSE keepalive received : #{ line } " )
46+ logger . debug ( "SSE keepalive: #{ line } " )
7347 end
7448 end
7549 end
@@ -79,125 +53,126 @@ def connect_sse(session_id, logger)
7953 end
8054 end
8155rescue Interrupt
82- logger . info ( "SSE connection interrupted by user " )
56+ logger . info ( "SSE connection interrupted" )
8357rescue => e
8458 logger . error ( "SSE connection error: #{ e . message } " )
8559end
8660
87- # Main client flow
8861def main
89- logger = Logger . new ( $stdout)
90- logger . formatter = proc do |severity , datetime , _progname , msg |
91- "[CLIENT] #{ severity } #{ datetime . strftime ( "%H:%M:%S.%L" ) } - #{ msg } \n "
92- end
93-
94- puts "=== MCP SSE Test Client ==="
95-
96- # Step 1: Initialize session
97- logger . info ( "Initializing session..." )
98-
99- init_response = make_request (
100- nil ,
101- "initialize" ,
102- {
103- protocolVersion : PROTOCOL_VERSION ,
104- capabilities : { } ,
105- clientInfo : {
106- name : "sse-test-client" ,
107- version : "1.0" ,
108- } ,
109- } ,
110- "init-1" ,
111- )
112-
113- if init_response [ :error ]
114- logger . error ( "Failed to initialize: #{ init_response [ :error ] } " )
115- exit ( 1 )
116- end
117-
118- session_id = init_response [ :headers ] [ "mcp-session-id" ] &.first
119-
120- if session_id . nil?
121- logger . error ( "No session ID received" )
122- exit ( 1 )
123- end
124-
125- logger . info ( "Session initialized: #{ session_id } " )
126- logger . info ( "Server info: #{ init_response [ :body ] [ "result" ] [ "serverInfo" ] } " )
127-
128- # Step 2: Start SSE connection in a separate thread
129- sse_thread = Thread . new { connect_sse ( session_id , logger ) }
130-
131- # Give SSE time to connect
132- sleep ( 1 )
133-
134- # Step 3: Interactive menu
135- loop do
136- puts <<~MESSAGE . chomp
137-
138- === Available Actions ===
139- 1. Send custom notification
140- 2. Test echo
141- 3. List tools
142- 0. Exit
143-
144- Choose an action:#{ " " }
145- MESSAGE
146-
147- choice = gets . chomp
148-
149- case choice
150- when "1"
151- print ( "Enter notification message: " )
152- message = gets . chomp
153- print ( "Enter delay in seconds (0 for immediate): " )
154- delay = gets . chomp . to_f
155-
156- response = make_request (
157- session_id ,
158- "tools/call" ,
159- {
160- name : "notification_tool" ,
161- arguments : {
162- message : message ,
163- delay : delay ,
164- } ,
165- } ,
166- )
167- if response [ :body ] [ "accepted" ]
168- logger . info ( "Notification sent successfully" )
62+ logger = create_logger
63+
64+ puts <<~MESSAGE
65+ MCP Streamable HTTP Client (SDK + SSE)
66+ Make sure the server is running (ruby examples/streamable_http_server.rb)
67+ #{ "=" * 60 }
68+ MESSAGE
69+
70+ # Initialize SDK client
71+ transport = MCP ::Client ::HTTP . new ( url : SERVER_URL )
72+ client = MCP ::Client . new ( transport : transport )
73+
74+ begin
75+ # Initialize session using SDK
76+ puts "=== Initializing session ==="
77+ init_response = client . connect (
78+ client_info : { name : "streamable-http-client" , version : "1.0" } ,
79+ )
80+ puts "Session ID: #{ client . session_id } "
81+ puts "Protocol Version: #{ client . protocol_version } "
82+ puts "Server Info: #{ init_response . dig ( "result" , "serverInfo" ) } "
83+
84+ # Get available tools BEFORE establishing SSE connection
85+ # (Once SSE is active, server sends responses via SSE stream, not POST response)
86+ puts "=== Listing tools ==="
87+ tools = client . tools
88+ tools . each { |t | puts " - #{ t . name } : #{ t . description } " }
89+
90+ echo_tool = tools . find { |t | t . name == "echo" }
91+ notification_tool = tools . find { |t | t . name == "notification_tool" }
92+
93+ # Start SSE connection in a separate thread (uses raw HTTP)
94+ # Note: After this, server responses will be sent via SSE, not POST
95+ sse_thread = Thread . new { connect_sse ( client . session_id , logger ) }
96+
97+ # Give SSE time to connect
98+ sleep ( 1 )
99+
100+ # Interactive menu
101+ loop do
102+ puts <<~MENU . chomp
103+
104+ === Available Actions ===
105+ 1. Send notification (triggers SSE event)
106+ 2. Echo message
107+ 3. List tools
108+ 0. Exit
109+
110+ Choose an action:#{ " " }
111+ MENU
112+
113+ choice = gets &.chomp
114+
115+ case choice
116+ when "1"
117+ if notification_tool
118+ print ( "Enter notification message: " )
119+ message = gets &.chomp || "Test"
120+ print ( "Enter delay in seconds (0 for immediate): " )
121+ delay = ( gets &.chomp || "0" ) . to_f
122+
123+ puts "=== Calling tool: notification_tool ==="
124+ response = client . call_tool (
125+ tool : notification_tool ,
126+ arguments : { message : message , delay : delay } ,
127+ )
128+ puts "Response: #{ JSON . pretty_generate ( response ) } "
129+ else
130+ puts "notification_tool not available"
131+ end
132+ when "2"
133+ if echo_tool
134+ print ( "Enter message to echo: " )
135+ message = gets &.chomp || "Hello"
136+
137+ puts "=== Calling tool: echo ==="
138+ response = client . call_tool ( tool : echo_tool , arguments : { message : message } )
139+ puts "Response: #{ JSON . pretty_generate ( response ) } "
140+ else
141+ puts "echo tool not available"
142+ end
143+ when "3"
144+ puts "=== Listing tools ==="
145+ puts "(Note: Response will appear in SSE stream when active)"
146+ client . tools . each do |tool |
147+ puts " - #{ tool . name } : #{ tool . description } "
148+ end
149+ when "0" , nil
150+ logger . info ( "Exiting..." )
151+ break
169152 else
170- logger . error ( "Error: #{ response [ :body ] [ "error" ] } " )
153+ puts "Invalid choice"
171154 end
172- when "2"
173- print ( "Enter message to echo: " )
174- message = gets . chomp
175- make_request ( session_id , "tools/call" , { name : "echo" , arguments : { message : message } } )
176- when "3"
177- make_request ( session_id , "tools/list" )
178- when "0"
179- logger . info ( "Exiting..." )
180- break
181- else
182- puts "Invalid choice"
183155 end
156+ rescue MCP ::Client ::SessionExpiredError => e
157+ logger . error ( "Session expired: #{ e . message } " )
158+ rescue MCP ::Client ::RequestHandlerError => e
159+ logger . error ( "Request error: #{ e . message } " )
160+ rescue Interrupt
161+ logger . info ( "Client interrupted" )
162+ rescue => e
163+ logger . error ( "Error: #{ e . message } " )
164+ logger . error ( e . backtrace . first ( 5 ) . join ( "\n " ) )
165+ ensure
166+ # Clean up SSE thread
167+ sse_thread &.kill if sse_thread &.alive?
168+
169+ # Close session using SDK
170+ puts "=== Closing session ==="
171+ client . close
172+ puts "Session closed"
184173 end
185-
186- # Clean up
187- sse_thread . kill if sse_thread . alive?
188-
189- # Close session
190- logger . info ( "Closing session..." )
191- make_request ( session_id , "close" )
192- logger . info ( "Session closed" )
193- rescue Interrupt
194- logger . info ( "Client interrupted by user" )
195- rescue => e
196- logger . error ( "Client error: #{ e . message } " )
197- logger . error ( e . backtrace . join ( "\n " ) )
198174end
199175
200- # Run the client
201176if __FILE__ == $PROGRAM_NAME
202177 main
203178end
0 commit comments