A server can send structured logging messages to the client. https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#logging Logging was specified in the 2024-11-05 specification, but since it was not supported in ruby-sdk, I implemented it. https://modelcontextprotocol.io/specification/2024-11-05/server/utilities/logging I also made it possible to output a simple notification message in the examples.
207 lines
5 KiB
Ruby
207 lines
5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "net/http"
|
|
require "uri"
|
|
require "json"
|
|
require "logger"
|
|
|
|
# Logger for client operations
|
|
logger = Logger.new($stdout)
|
|
logger.formatter = proc do |severity, datetime, _progname, msg|
|
|
"[CLIENT] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n"
|
|
end
|
|
|
|
# Server configuration
|
|
SERVER_URL = "http://localhost:9393/mcp"
|
|
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)
|
|
uri = URI(SERVER_URL)
|
|
|
|
logger.info("Connecting to SSE stream...")
|
|
|
|
Net::HTTP.start(uri.host, uri.port) do |http|
|
|
request = Net::HTTP::Get.new(uri)
|
|
request["Mcp-Session-Id"] = session_id
|
|
request["Accept"] = "text/event-stream"
|
|
request["Cache-Control"] = "no-cache"
|
|
|
|
http.request(request) do |response|
|
|
if response.code == "200"
|
|
logger.info("SSE stream connected successfully")
|
|
|
|
response.read_body do |chunk|
|
|
chunk.split("\n").each do |line|
|
|
if line.start_with?("data: ")
|
|
data = line[6..-1]
|
|
begin
|
|
logger.info("SSE data: #{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
|
|
else
|
|
logger.error("Failed to connect to SSE: #{response.code} #{response.message}")
|
|
end
|
|
end
|
|
end
|
|
rescue Interrupt
|
|
logger.info("SSE connection interrupted by user")
|
|
rescue => e
|
|
logger.error("SSE connection error: #{e.message}")
|
|
end
|
|
|
|
# Main client flow
|
|
def main
|
|
logger = Logger.new($stdout)
|
|
logger.formatter = proc do |severity, datetime, _progname, msg|
|
|
"[CLIENT] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n"
|
|
end
|
|
|
|
puts "=== MCP SSE Test Client ==="
|
|
|
|
# Step 1: Initialize session
|
|
logger.info("Initializing session...")
|
|
|
|
init_response = make_request(
|
|
nil,
|
|
"initialize",
|
|
{
|
|
protocolVersion: PROTOCOL_VERSION,
|
|
capabilities: {},
|
|
clientInfo: {
|
|
name: "sse-test-client",
|
|
version: "1.0",
|
|
},
|
|
},
|
|
"init-1",
|
|
)
|
|
|
|
if init_response[:error]
|
|
logger.error("Failed to initialize: #{init_response[:error]}")
|
|
exit(1)
|
|
end
|
|
|
|
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
|
|
|
|
choice = gets.chomp
|
|
|
|
case choice
|
|
when "1"
|
|
print("Enter notification message: ")
|
|
message = gets.chomp
|
|
print("Enter delay in seconds (0 for immediate): ")
|
|
delay = gets.chomp.to_f
|
|
|
|
response = make_request(
|
|
session_id,
|
|
"tools/call",
|
|
{
|
|
name: "notification_tool",
|
|
arguments: {
|
|
message: message,
|
|
delay: delay,
|
|
},
|
|
},
|
|
)
|
|
if response[:body]["accepted"]
|
|
logger.info("Notification sent successfully")
|
|
else
|
|
logger.error("Error: #{response[:body]["error"]}")
|
|
end
|
|
when "2"
|
|
print("Enter message to echo: ")
|
|
message = gets.chomp
|
|
make_request(session_id, "tools/call", { name: "echo", arguments: { message: message } })
|
|
when "3"
|
|
make_request(session_id, "tools/list")
|
|
when "0"
|
|
logger.info("Exiting...")
|
|
break
|
|
else
|
|
puts "Invalid choice"
|
|
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
|
|
|
|
# Run the client
|
|
if __FILE__ == $PROGRAM_NAME
|
|
main
|
|
end
|