Skip to content

Instantly share code, notes, and snippets.

@urielhdz
Created August 27, 2025 16:59
Show Gist options
  • Save urielhdz/b567484be6442ec9ebadb1d220c33bd2 to your computer and use it in GitHub Desktop.
Save urielhdz/b567484be6442ec9ebadb1d220c33bd2 to your computer and use it in GitHub Desktop.
MCP Rails gem examples

└── examples ├── README.md ├── http_client.rb ├── http_server.rb ├── stdio_server.rb ├── streamable_http_client.rb └── streamable_http_server.rb

/examples/README.md:

1 | # MCP Ruby Examples 2 | 3 | This directory contains examples of how to use the Model Context Protocol (MCP) Ruby library. 4 | 5 | ## Available Examples 6 | 7 | ### 1. STDIO Server (stdio_server.rb) 8 | 9 | A simple server that communicates over standard input/output. This is useful for desktop applications and command-line tools. 10 | 11 | Usage: 12 | 13 | console 14 | $ ruby examples/stdio_server.rb 15 | {"jsonrpc":"2.0","id":0,"method":"tools/list"} 16 | 17 | 18 | ### 2. HTTP Server (http_server.rb) 19 | 20 | A standalone HTTP server built with Rack that implements the MCP Streamable HTTP transport protocol. This demonstrates how to create a web-based MCP server with session management and Server-Sent Events (SSE) support. 21 | 22 | Features: 23 | 24 | - HTTP transport with Server-Sent Events (SSE) for streaming 25 | - Session management with unique session IDs 26 | - Example tools, prompts, and resources 27 | - JSON-RPC 2.0 protocol implementation 28 | - Full MCP protocol compliance 29 | 30 | Usage: 31 | 32 | console 33 | $ ruby examples/http_server.rb 34 | 35 | 36 | The server will start on http://localhost:9292 and provide: 37 | 38 | - Tools: 39 | - ExampleTool - adds two numbers 40 | - echo - echoes back messages 41 | - Prompts: ExamplePrompt - echoes back arguments as a prompt 42 | - Resources: test_resource - returns example content 43 | 44 | ### 3. HTTP Client Example (http_client.rb) 45 | 46 | A client that demonstrates how to interact with the HTTP server using all MCP protocol methods. 47 | 48 | Usage: 49 | 50 | 1. Start the HTTP server in one terminal: 51 | 52 | console 53 | $ ruby examples/http_server.rb 54 | 55 | 56 | 2. Run the client example in another terminal: 57 | console 58 | $ ruby examples/http_client.rb 59 | 60 | 61 | The client will demonstrate: 62 | 63 | - Session initialization 64 | - Ping requests 65 | - Listing and calling tools 66 | - Listing and getting prompts 67 | - Listing and reading resources 68 | - Session cleanup 69 | 70 | ### 4. Streamable HTTP Server (streamable_http_server.rb) 71 | 72 | A specialized HTTP server designed to test and demonstrate Server-Sent Events (SSE) functionality in the MCP protocol. 73 | 74 | Features: 75 | 76 | - Tools specifically designed to trigger SSE notifications 77 | - Real-time progress updates and notifications 78 | - Detailed SSE-specific logging 79 | 80 | Available Tools: 81 | 82 | - NotificationTool - Send custom SSE notifications with optional delays 83 | - echo - Simple echo tool for basic testing 84 | 85 | Usage: 86 | 87 | console 88 | $ ruby examples/streamable_http_server.rb 89 | 90 | 91 | The server will start on http://localhost:9393 and provide detailed instructions for testing SSE functionality. 92 | 93 | ### 5. Streamable HTTP Client (streamable_http_client.rb) 94 | 95 | An interactive client that connects to the SSE stream and provides a menu-driven interface for testing SSE functionality. 96 | 97 | Features: 98 | 99 | - Automatic SSE stream connection 100 | - Interactive menu for triggering various SSE events 101 | - Real-time display of received SSE notifications 102 | - Session management 103 | 104 | Usage: 105 | 106 | 1. Start the SSE test server in one terminal: 107 | 108 | console 109 | $ ruby examples/streamable_http_server.rb 110 | 111 | 112 | 2. Run the SSE test client in another terminal: 113 | console 114 | $ ruby examples/streamable_http_client.rb 115 | 116 | 117 | The client will: 118 | 119 | - Initialize a session automatically 120 | - Connect to the SSE stream 121 | - Provide an interactive menu to trigger notifications 122 | - Display all received SSE events in real-time 123 | 124 | ### Testing SSE with cURL 125 | 126 | You can also test SSE functionality manually using cURL: 127 | 128 | 1. Initialize a session: 129 | 130 | console 131 | SESSION_ID=$(curl -D - -s -o /dev/null http://localhost:9393 \ 132 | --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') 133 | 134 | 135 | 2. Connect to SSE stream (in one terminal): 136 | 137 | console 138 | curl -i -N -H "Mcp-Session-Id: $SESSION_ID" http://localhost:9393 139 | 140 | 141 | 3. Trigger notifications (in another terminal): 142 | 143 | console 144 | # Send immediate notification 145 | curl -i http://localhost:9393 \ 146 | -H "Mcp-Session-Id: $SESSION_ID" \ 147 | --json '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"notification_tool","arguments":{"message":"Hello from cURL!"}}}' 148 | 149 | 150 | ## Streamable HTTP Transport Details 151 | 152 | ### Protocol Flow 153 | 154 | The HTTP server implements the MCP Streamable HTTP transport protocol: 155 | 156 | 1. Initialize Session: 157 | 158 | - Client sends POST request with initialize method 159 | - Server responds with session ID in Mcp-Session-Id header 160 | 161 | 2. Establish SSE Connection (optional): 162 | 163 | - Client sends GET request with Mcp-Session-Id header 164 | - Server establishes Server-Sent Events stream for notifications 165 | 166 | 3. Send Requests: 167 | 168 | - Client sends POST requests with JSON-RPC 2.0 format 169 | - Server processes and responds with results 170 | 171 | 4. Close Session: 172 | - Client sends DELETE request with Mcp-Session-Id header 173 | 174 | ### Example cURL Commands 175 | 176 | Initialize a session: 177 | 178 | console 179 | curl -i http://localhost:9292 \ 180 | --json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' 181 | 182 | 183 | List tools (using the session ID from initialization): 184 | 185 | console 186 | curl -i http://localhost:9292 \ 187 | -H "Mcp-Session-Id: YOUR_SESSION_ID" \ 188 | --json '{"jsonrpc":"2.0","method":"tools/list","id":2}' 189 | 190 | 191 | Call a tool: 192 | 193 | console 194 | curl -i http://localhost:9292 \ 195 | -H "Mcp-Session-Id: YOUR_SESSION_ID" \ 196 | --json '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"ExampleTool","arguments":{"a":5,"b":3}}}' 197 | 198 |


/examples/http_client.rb:

1 | # frozen_string_literal: true 2 | 3 | require "net/http" 4 | require "json" 5 | require "uri" 6 | 7 | # Simple HTTP client example for interacting with the MCP HTTP server 8 | class MCPHTTPClient 9 | def initialize(base_url = "http://localhost:9292") 10 | @base_url = base_url 11 | @session_id = nil 12 | end 13 | 14 | def send_request(method, params = nil, id = nil) 15 | uri = URI(@base_url) 16 | http = Net::HTTP.new(uri.host, uri.port) 17 | 18 | request = Net::HTTP::Post.new(uri.path.empty? ? "/" : uri.path) 19 | request["Content-Type"] = "application/json" 20 | request["Mcp-Session-Id"] = @session_id if @session_id 21 | 22 | body = { 23 | jsonrpc: "2.0", 24 | method: method, 25 | params: params, 26 | id: id || rand(10000), 27 | }.compact 28 | 29 | request.body = body.to_json 30 | 31 | response = http.request(request) 32 | 33 | # Store session ID if provided 34 | if response["Mcp-Session-Id"] 35 | @session_id = response["Mcp-Session-Id"] 36 | puts "Session ID: #{@session_id}" 37 | end 38 | 39 | JSON.parse(response.body) 40 | end 41 | 42 | def initialize_session 43 | puts "=== Initializing session ===" 44 | result = send_request("initialize", { 45 | protocolVersion: "2024-11-05", 46 | capabilities: {}, 47 | clientInfo: { 48 | name: "example_client", 49 | version: "1.0", 50 | }, 51 | }) 52 | puts "Response: #{JSON.pretty_generate(result)}" 53 | puts 54 | result 55 | end 56 | 57 | def ping 58 | puts "=== Sending ping ===" 59 | result = send_request("ping") 60 | puts "Response: #{JSON.pretty_generate(result)}" 61 | puts 62 | result 63 | end 64 | 65 | def list_tools 66 | puts "=== Listing tools ===" 67 | result = send_request("tools/list") 68 | puts "Response: #{JSON.pretty_generate(result)}" 69 | puts 70 | result 71 | end 72 | 73 | def call_tool(name, arguments) 74 | puts "=== Calling tool: #{name} ===" 75 | result = send_request("tools/call", { 76 | name: name, 77 | arguments: arguments, 78 | }) 79 | puts "Response: #{JSON.pretty_generate(result)}" 80 | puts 81 | result 82 | end 83 | 84 | def list_prompts 85 | puts "=== Listing prompts ===" 86 | result = send_request("prompts/list") 87 | puts "Response: #{JSON.pretty_generate(result)}" 88 | puts 89 | result 90 | end 91 | 92 | def get_prompt(name, arguments) 93 | puts "=== Getting prompt: #{name} ===" 94 | result = send_request("prompts/get", { 95 | name: name, 96 | arguments: arguments, 97 | }) 98 | puts "Response: #{JSON.pretty_generate(result)}" 99 | puts 100 | result 101 | end 102 | 103 | def list_resources 104 | puts "=== Listing resources ===" 105 | result = send_request("resources/list") 106 | puts "Response: #{JSON.pretty_generate(result)}" 107 | puts 108 | result 109 | end 110 | 111 | def read_resource(uri) 112 | puts "=== Reading resource: #{uri} ===" 113 | result = send_request("resources/read", { 114 | uri: uri, 115 | }) 116 | puts "Response: #{JSON.pretty_generate(result)}" 117 | puts 118 | result 119 | end 120 | 121 | def close_session 122 | return unless @session_id 123 | 124 | puts "=== Closing session ===" 125 | uri = URI(@base_url) 126 | http = Net::HTTP.new(uri.host, uri.port) 127 | 128 | request = Net::HTTP::Delete.new(uri.path.empty? ? "/" : uri.path) 129 | request["Mcp-Session-Id"] = @session_id 130 | 131 | response = http.request(request) 132 | result = JSON.parse(response.body) 133 | puts "Response: #{JSON.pretty_generate(result)}" 134 | puts 135 | 136 | @session_id = nil 137 | result 138 | end 139 | end 140 | 141 | # Main script 142 | if FILE == $PROGRAM_NAME 143 | puts "MCP HTTP Client Example" 144 | puts "Make sure the HTTP server is running (ruby examples/http_server.rb)" 145 | puts "=" * 50 146 | puts 147 | 148 | client = MCPHTTPClient.new 149 | 150 | begin 151 | # Initialize session 152 | client.initialize_session 153 | 154 | # Test ping 155 | client.ping 156 | 157 | # List available tools 158 | client.list_tools 159 | 160 | # Call the example_tool (note: snake_case name) 161 | client.call_tool("example_tool", { a: 5, b: 3 }) 162 | 163 | # Call the echo tool 164 | client.call_tool("echo", { message: "Hello from client!" }) 165 | 166 | # List prompts 167 | client.list_prompts 168 | 169 | # Get a prompt (note: snake_case name) 170 | client.get_prompt("example_prompt", { message: "This is a test message" }) 171 | 172 | # List resources 173 | client.list_resources 174 | 175 | # Read a resource 176 | client.read_resource("test_resource") 177 | rescue => e 178 | puts "Error: #{e.message}" 179 | puts e.backtrace 180 | ensure 181 | # Clean up session 182 | client.close_session 183 | end 184 | end 185 |


/examples/http_server.rb:

1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.expand_path("../lib", dir)) 4 | require "mcp" 5 | require "mcp/server/transports/streamable_http_transport" 6 | require "rack" 7 | require "rackup" 8 | require "json" 9 | require "logger" 10 | 11 | # Create a simple tool 12 | class ExampleTool < MCP::Tool 13 | description "A simple example tool that adds two numbers" 14 | input_schema( 15 | properties: { 16 | a: { type: "number" }, 17 | b: { type: "number" }, 18 | }, 19 | required: ["a", "b"], 20 | ) 21 | 22 | class << self 23 | def call(a:, b:) 24 | MCP::Tool::Response.new([{ 25 | type: "text", 26 | text: "The sum of #{a} and #{b} is #{a + b}", 27 | }]) 28 | end 29 | end 30 | end 31 | 32 | # Create a simple prompt 33 | class ExamplePrompt < MCP::Prompt 34 | description "A simple example prompt that echoes back its arguments" 35 | arguments [ 36 | MCP::Prompt::Argument.new( 37 | name: "message", 38 | description: "The message to echo back", 39 | required: true, 40 | ), 41 | ] 42 | 43 | class << self 44 | def template(args, server_context:) 45 | MCP::Prompt::Result.new( 46 | messages: [ 47 | MCP::Prompt::Message.new( 48 | role: "user", 49 | content: MCP::Content::Text.new(args[:message]), 50 | ), 51 | ], 52 | ) 53 | end 54 | end 55 | end 56 | 57 | # Set up the server 58 | server = MCP::Server.new( 59 | name: "example_http_server", 60 | tools: [ExampleTool], 61 | prompts: [ExamplePrompt], 62 | resources: [ 63 | MCP::Resource.new( 64 | uri: "https://test_resource.invalid", 65 | name: "test-resource", 66 | title: "Test Resource", 67 | description: "Test resource that echoes back the uri as its content", 68 | mime_type: "text/plain", 69 | ), 70 | ], 71 | ) 72 | 73 | server.define_tool( 74 | name: "echo", 75 | description: "A simple example tool that echoes back its arguments", 76 | input_schema: { properties: { message: { type: "string" } }, required: ["message"] }, 77 | ) do |message:| 78 | MCP::Tool::Response.new( 79 | [ 80 | { 81 | type: "text", 82 | text: "Hello from echo tool! Message: #{message}", 83 | }, 84 | ], 85 | ) 86 | end 87 | 88 | server.resources_read_handler do |params| 89 | [{ 90 | uri: params[:uri], 91 | mimeType: "text/plain", 92 | text: "Hello from HTTP server resource!", 93 | }] 94 | end 95 | 96 | # Create the Streamable HTTP transport 97 | transport = MCP::Server::Transports::StreamableHTTPTransport.new(server) 98 | server.transport = transport 99 | 100 | # Create a logger for MCP-specific logging 101 | mcp_logger = Logger.new($stdout) 102 | mcp_logger.formatter = proc do |_severity, _datetime, _progname, msg| 103 | "[MCP] #{msg}\n" 104 | end 105 | 106 | # Create a Rack application with logging 107 | app = proc do |env| 108 | request = Rack::Request.new(env) 109 | 110 | # Log MCP-specific details for POST requests 111 | if request.post? 112 | body = request.body.read 113 | request.body.rewind 114 | begin 115 | parsed_body = JSON.parse(body) 116 | mcp_logger.info("Request: #{parsed_body["method"]} (id: #{parsed_body["id"]})") 117 | mcp_logger.debug("Request body: #{JSON.pretty_generate(parsed_body)}") 118 | rescue JSON::ParserError 119 | mcp_logger.warn("Request body (raw): #{body}") 120 | end 121 | end 122 | 123 | # Handle the request 124 | response = transport.handle_request(request) 125 | 126 | # Log the MCP response details 127 | _, _, body = response 128 | if body.is_a?(Array) && !body.empty? && body.first 129 | begin 130 | parsed_response = JSON.parse(body.first) 131 | if parsed_response["error"] 132 | mcp_logger.error("Response error: #{parsed_response["error"]["message"]}") 133 | else 134 | mcp_logger.info("Response: #{parsed_response["result"] ? "success" : "empty"} (id: #{parsed_response["id"]})") 135 | end 136 | mcp_logger.debug("Response body: #{JSON.pretty_generate(parsed_response)}") 137 | rescue JSON::ParserError 138 | mcp_logger.warn("Response body (raw): #{body}") 139 | end 140 | end 141 | 142 | response 143 | end 144 | 145 | # Wrap the app with Rack middleware 146 | rack_app = Rack::Builder.new do 147 | # Use CommonLogger for standard HTTP request logging 148 | use(Rack::CommonLogger, Logger.new($stdout)) 149 | 150 | # Add other useful middleware 151 | use(Rack::ShowExceptions) 152 | 153 | run(app) 154 | end 155 | 156 | # Start the server 157 | puts "Starting MCP HTTP server on http://localhost:9292" 158 | puts "Use POST requests to initialize and send JSON-RPC commands" 159 | puts "Example initialization:" 160 | puts ' 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"}}}'' 161 | puts "" 162 | puts "The server will return a session ID in the Mcp-Session-Id header." 163 | puts "Use this session ID for subsequent requests." 164 | puts "" 165 | puts "Press Ctrl+C to stop the server" 166 | 167 | # Run the server 168 | # Use Rackup to run the server 169 | Rackup::Handler.get("puma").run(rack_app, Port: 9292, Host: "localhost") 170 |


/examples/stdio_server.rb:

1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.expand_path("../lib", dir)) 4 | require "mcp" 5 | require "mcp/server/transports/stdio_transport" 6 | 7 | # Create a simple tool 8 | class ExampleTool < MCP::Tool 9 | description "A simple example tool that adds two numbers" 10 | input_schema( 11 | properties: { 12 | a: { type: "number" }, 13 | b: { type: "number" }, 14 | }, 15 | required: ["a", "b"], 16 | ) 17 | 18 | class << self 19 | def call(a:, b:) 20 | MCP::Tool::Response.new([{ 21 | type: "text", 22 | text: "The sum of #{a} and #{b} is #{a + b}", 23 | }]) 24 | end 25 | end 26 | end 27 | 28 | # Create a simple prompt 29 | class ExamplePrompt < MCP::Prompt 30 | description "A simple example prompt that echoes back its arguments" 31 | arguments [ 32 | MCP::Prompt::Argument.new( 33 | name: "message", 34 | description: "The message to echo back", 35 | required: true, 36 | ), 37 | ] 38 | 39 | class << self 40 | def template(args, server_context:) 41 | MCP::Prompt::Result.new( 42 | messages: [ 43 | MCP::Prompt::Message.new( 44 | role: "user", 45 | content: MCP::Content::Text.new(args[:message]), 46 | ), 47 | ], 48 | ) 49 | end 50 | end 51 | end 52 | 53 | # Set up the server 54 | server = MCP::Server.new( 55 | name: "example_server", 56 | version: "1.0.0", 57 | tools: [ExampleTool], 58 | prompts: [ExamplePrompt], 59 | resources: [ 60 | MCP::Resource.new( 61 | uri: "https://test_resource.invalid", 62 | name: "test-resource", 63 | title: "Test Resource", 64 | description: "Test resource that echoes back the uri as its content", 65 | mime_type: "text/plain", 66 | ), 67 | ], 68 | ) 69 | 70 | server.define_tool( 71 | name: "echo", 72 | description: "A simple example tool that echoes back its arguments", 73 | input_schema: { properties: { message: { type: "string" } }, required: ["message"] }, 74 | ) do |message:| 75 | MCP::Tool::Response.new( 76 | [ 77 | { 78 | type: "text", 79 | text: "Hello from echo tool! Message: #{message}", 80 | }, 81 | ], 82 | ) 83 | end 84 | 85 | server.resources_read_handler do |params| 86 | [{ 87 | uri: params[:uri], 88 | mimeType: "text/plain", 89 | text: "Hello, world! URI: #{params[:uri]}", 90 | }] 91 | end 92 | 93 | # Create and start the transport 94 | transport = MCP::Server::Transports::StdioTransport.new(server) 95 | transport.open 96 |


/examples/streamable_http_client.rb:

1 | # frozen_string_literal: true 2 | 3 | require "net/http" 4 | require "uri" 5 | require "json" 6 | require "logger" 7 | 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 13 | 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 } 44 | end 45 | 46 | # Connect to SSE stream 47 | def connect_sse(session_id, logger) 48 | uri = URI(SERVER_URL) 49 | 50 | logger.info("Connecting to SSE stream...") 51 | 52 | Net::HTTP.start(uri.host, uri.port) do |http| 53 | request = Net::HTTP::Get.new(uri) 54 | request["Mcp-Session-Id"] = session_id 55 | request["Accept"] = "text/event-stream" 56 | request["Cache-Control"] = "no-cache" 57 | 58 | http.request(request) do |response| 59 | if response.code == "200" 60 | logger.info("SSE stream connected successfully") 61 | 62 | response.read_body do |chunk| 63 | chunk.split("\n").each do |line| 64 | 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 71 | elsif line.start_with?(": ") 72 | logger.debug("SSE keepalive received: #{line}") 73 | end 74 | end 75 | end 76 | else 77 | logger.error("Failed to connect to SSE: #{response.code} #{response.message}") 78 | end 79 | end 80 | end 81 | rescue Interrupt 82 | logger.info("SSE connection interrupted by user") 83 | rescue => e 84 | logger.error("SSE connection error: #{e.message}") 85 | end 86 | 87 | # Main client flow 88 | def 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 | puts "" 96 | 97 | # Step 1: Initialize session 98 | logger.info("Initializing session...") 99 | 100 | init_response = make_request( 101 | nil, 102 | "initialize", 103 | { 104 | protocolVersion: PROTOCOL_VERSION, 105 | capabilities: {}, 106 | clientInfo: { 107 | name: "sse-test-client", 108 | version: "1.0", 109 | }, 110 | }, 111 | "init-1", 112 | ) 113 | 114 | if init_response[:error] 115 | logger.error("Failed to initialize: #{init_response[:error]}") 116 | exit(1) 117 | end 118 | 119 | session_id = init_response[:headers]["mcp-session-id"]&.first 120 | 121 | if session_id.nil? 122 | logger.error("No session ID received") 123 | exit(1) 124 | end 125 | 126 | logger.info("Session initialized: #{session_id}") 127 | logger.info("Server info: #{init_response[:body]["result"]["serverInfo"]}") 128 | 129 | # Step 2: Start SSE connection in a separate thread 130 | sse_thread = Thread.new { connect_sse(session_id, logger) } 131 | 132 | # Give SSE time to connect 133 | sleep(1) 134 | 135 | # Step 3: Interactive menu 136 | loop do 137 | puts "\n=== Available Actions ===" 138 | puts "1. Send custom notification" 139 | puts "2. Test echo" 140 | puts "3. List tools" 141 | puts "0. Exit" 142 | puts "" 143 | print("Choose an action: ") 144 | 145 | choice = gets.chomp 146 | 147 | case choice 148 | when "1" 149 | print("Enter notification message: ") 150 | message = gets.chomp 151 | print("Enter delay in seconds (0 for immediate): ") 152 | delay = gets.chomp.to_f 153 | 154 | response = make_request( 155 | session_id, 156 | "tools/call", 157 | { 158 | name: "notification_tool", 159 | arguments: { 160 | message: message, 161 | delay: delay, 162 | }, 163 | }, 164 | ) 165 | if response[:body]["accepted"] 166 | logger.info("Notification sent successfully") 167 | else 168 | logger.error("Error: #{response[:body]["error"]}") 169 | end 170 | 171 | when "2" 172 | print("Enter message to echo: ") 173 | message = gets.chomp 174 | make_request(session_id, "tools/call", { name: "echo", arguments: { message: message } }) 175 | 176 | when "3" 177 | make_request(session_id, "tools/list") 178 | 179 | when "0" 180 | logger.info("Exiting...") 181 | break 182 | 183 | else 184 | puts "Invalid choice" 185 | end 186 | end 187 | 188 | # Clean up 189 | sse_thread.kill if sse_thread.alive? 190 | 191 | # Close session 192 | logger.info("Closing session...") 193 | make_request(session_id, "close") 194 | logger.info("Session closed") 195 | rescue Interrupt 196 | logger.info("Client interrupted by user") 197 | rescue => e 198 | logger.error("Client error: #{e.message}") 199 | logger.error(e.backtrace.join("\n")) 200 | end 201 | 202 | # Run the client 203 | if FILE == $PROGRAM_NAME 204 | main 205 | end 206 |


/examples/streamable_http_server.rb:

1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift(File.expand_path("../lib", dir)) 4 | require "mcp" 5 | require "mcp/server/transports/streamable_http_transport" 6 | require "rack" 7 | require "rackup" 8 | require "json" 9 | require "logger" 10 | 11 | # Create a logger for SSE-specific logging 12 | sse_logger = Logger.new($stdout) 13 | sse_logger.formatter = proc do |severity, datetime, _progname, msg| 14 | "[SSE] #{severity} #{datetime.strftime("%H:%M:%S.%L")} - #{msg}\n" 15 | end 16 | 17 | # Tool that returns a response that will be sent via SSE if a stream is active 18 | class NotificationTool < MCP::Tool 19 | tool_name "notification_tool" 20 | description "Returns a notification message that will be sent via SSE if stream is active" 21 | input_schema( 22 | properties: { 23 | message: { type: "string", description: "Message to send via SSE" }, 24 | delay: { type: "number", description: "Delay in seconds before returning (optional)" }, 25 | }, 26 | required: ["message"], 27 | ) 28 | 29 | class << self 30 | attr_accessor :logger 31 | 32 | def call(message:, delay: 0) 33 | sleep(delay) if delay > 0 34 | 35 | logger&.info("Returning notification message: #{message}") 36 | 37 | MCP::Tool::Response.new([{ 38 | type: "text", 39 | text: "Notification: #{message} (timestamp: #{Time.now.iso8601})", 40 | }]) 41 | end 42 | end 43 | end 44 | 45 | # Create the server 46 | server = MCP::Server.new( 47 | name: "sse_test_server", 48 | tools: [NotificationTool], 49 | prompts: [], 50 | resources: [], 51 | ) 52 | 53 | # Set logger for tools 54 | NotificationTool.logger = sse_logger 55 | 56 | # Add a simple echo tool for basic testing 57 | server.define_tool( 58 | name: "echo", 59 | description: "Simple echo tool", 60 | input_schema: { properties: { message: { type: "string" } }, required: ["message"] }, 61 | ) do |message:| 62 | MCP::Tool::Response.new([{ type: "text", text: "Echo: #{message}" }]) 63 | end 64 | 65 | # Create the Streamable HTTP transport 66 | transport = MCP::Server::Transports::StreamableHTTPTransport.new(server) 67 | server.transport = transport 68 | 69 | # Create a logger for MCP request/response logging 70 | mcp_logger = Logger.new($stdout) 71 | mcp_logger.formatter = proc do |_severity, _datetime, _progname, msg| 72 | "[MCP] #{msg}\n" 73 | end 74 | 75 | # Create the Rack application 76 | app = proc do |env| 77 | request = Rack::Request.new(env) 78 | 79 | # Log request details 80 | if request.post? 81 | body = request.body.read 82 | request.body.rewind 83 | begin 84 | parsed_body = JSON.parse(body) 85 | mcp_logger.info("Request: #{parsed_body["method"]} (id: #{parsed_body["id"]})") 86 | 87 | # Log SSE-specific setup 88 | if parsed_body["method"] == "initialize" 89 | sse_logger.info("New client initializing session") 90 | end 91 | rescue JSON::ParserError 92 | mcp_logger.warn("Invalid JSON in request") 93 | end 94 | elsif request.get? 95 | session_id = request.env["HTTP_MCP_SESSION_ID"] || 96 | Rack::Utils.parse_query(request.env["QUERY_STRING"])["sessionId"] 97 | sse_logger.info("SSE connection request for session: #{session_id}") 98 | end 99 | 100 | # Handle the request 101 | response = transport.handle_request(request) 102 | 103 | # Log response details 104 | status, headers, body = response 105 | if body.is_a?(Array) && !body.empty? && request.post? 106 | begin 107 | parsed_response = JSON.parse(body.first) 108 | if parsed_response["error"] 109 | mcp_logger.error("Response error: #{parsed_response["error"]["message"]}") 110 | elsif parsed_response["accepted"] 111 | # Response was sent via SSE 112 | sse_logger.info("Response sent via SSE stream") 113 | else 114 | mcp_logger.info("Response: success (id: #{parsed_response["id"]})") 115 | 116 | # Log session ID for initialization 117 | if headers["Mcp-Session-Id"] 118 | sse_logger.info("Session created: #{headers["Mcp-Session-Id"]}") 119 | end 120 | end 121 | rescue JSON::ParserError 122 | mcp_logger.warn("Invalid JSON in response") 123 | end 124 | elsif request.get? && status == 200 125 | sse_logger.info("SSE stream established") 126 | end 127 | 128 | response 129 | end 130 | 131 | # Build the Rack application with middleware 132 | rack_app = Rack::Builder.new do 133 | use(Rack::CommonLogger, Logger.new($stdout)) 134 | use(Rack::ShowExceptions) 135 | run(app) 136 | end 137 | 138 | # Print usage instructions 139 | puts "=== MCP Streaming HTTP Test Server ===" 140 | puts "" 141 | puts "Starting server on http://localhost:9393" 142 | puts "" 143 | puts "Available Tools:" 144 | puts "1. NotificationTool - Returns messages that are sent via SSE when stream is active" 145 | puts "2. echo - Simple echo tool" 146 | puts "" 147 | puts "Testing SSE:" 148 | puts "" 149 | puts "1. Initialize session:" 150 | puts " curl -i http://localhost:9393 \" 151 | puts ' --json '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"sse-test","version":"1.0"}}}'' 152 | puts "" 153 | puts "2. Connect SSE stream (use the session ID from step 1):" 154 | puts ' curl -i -N -H "Mcp-Session-Id: YOUR_SESSION_ID" http://localhost:9393' 155 | puts "" 156 | puts "3. In another terminal, test tools (responses will be sent via SSE if stream is active):" 157 | puts "" 158 | puts " Echo tool:" 159 | puts ' curl -i http://localhost:9393 -H "Mcp-Session-Id: YOUR_SESSION_ID" \' 160 | puts ' --json '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"echo","arguments":{"message":"Hello SSE!"}}}'' 161 | puts "" 162 | puts " Notification tool (with 2 second delay):" 163 | puts ' curl -i http://localhost:9393 -H "Mcp-Session-Id: YOUR_SESSION_ID" \' 164 | puts ' --json '{"jsonrpc":"2.0","method":"tools/call","id":3,"params":{"name":"notification_tool","arguments":{"message":"Hello SSE!", "delay": 2}}}'' 165 | puts "" 166 | puts "Note: When an SSE stream is active, tool responses will appear in the SSE stream and the POST request will return {"accepted": true}" 167 | puts "" 168 | puts "Press Ctrl+C to stop the server" 169 | puts "" 170 | 171 | # Start the server 172 | Rackup::Handler.get("puma").run(rack_app, Port: 9393, Host: "localhost") 173 |


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment