ruby-mcp-client 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
 - data/README.md +79 -3
 - data/lib/mcp_client/client.rb +90 -3
 - data/lib/mcp_client/server_base.rb +6 -0
 - data/lib/mcp_client/server_sse.rb +103 -155
 - data/lib/mcp_client/version.rb +1 -1
 - metadata +31 -3
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 79a86302428257274c4e620fae0b7ad45c6229cf381a956ef3b0a7e736f17f62
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 80727b0aa48a992054dafd7c484eb611404b47d878ef2869e59e937a815718dc
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 977acc1ae8ca48a17b7827f006ed82578eb7bc24bf9ba08b03e77493e3447febc82287242a410861fb675b1fb74b0b67525dc4f92c9f8b448cd1cad2e3afc875
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: f940ed03a52065a5d8687d1722a588693d69a9c69f820771b328fed3b1bb69068fc7110dbedc745ea4fc42d047afa8afe975b2ed821aa5d33ac8c4e25fac9da7
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -99,12 +99,71 @@ client.send_rpc('custom_method', params: { key: 'value' }, server: :sse) # Uses 
     | 
|
| 
       99 
99 
     | 
    
         
             
            result = client.send_rpc('another_method', params: { data: 123 }) # Uses first available server
         
     | 
| 
       100 
100 
     | 
    
         
             
            client.send_notification('status_update', params: { status: 'ready' })
         
     | 
| 
       101 
101 
     | 
    
         | 
| 
      
 102 
     | 
    
         
            +
            # Check server connectivity 
         
     | 
| 
      
 103 
     | 
    
         
            +
            client.ping # Basic connectivity check (zero-parameter heartbeat call)
         
     | 
| 
      
 104 
     | 
    
         
            +
            client.ping(server_index: 1) # Ping a specific server by index
         
     | 
| 
      
 105 
     | 
    
         
            +
             
     | 
| 
       102 
106 
     | 
    
         
             
            # Clear cached tools to force fresh fetch on next list
         
     | 
| 
       103 
107 
     | 
    
         
             
            client.clear_cache
         
     | 
| 
       104 
108 
     | 
    
         
             
            # Clean up connections
         
     | 
| 
       105 
109 
     | 
    
         
             
            client.cleanup
         
     | 
| 
       106 
110 
     | 
    
         
             
            ```
         
     | 
| 
       107 
111 
     | 
    
         | 
| 
      
 112 
     | 
    
         
            +
            ### Server-Sent Events (SSE) Example
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
            The SSE transport provides robust connection handling for remote MCP servers:
         
     | 
| 
      
 115 
     | 
    
         
            +
             
     | 
| 
      
 116 
     | 
    
         
            +
            ```ruby
         
     | 
| 
      
 117 
     | 
    
         
            +
            require 'mcp_client'
         
     | 
| 
      
 118 
     | 
    
         
            +
            require 'logger'
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
            # Optional logger for debugging
         
     | 
| 
      
 121 
     | 
    
         
            +
            logger = Logger.new($stdout)
         
     | 
| 
      
 122 
     | 
    
         
            +
            logger.level = Logger::INFO
         
     | 
| 
      
 123 
     | 
    
         
            +
             
     | 
| 
      
 124 
     | 
    
         
            +
            # Create an MCP client that connects to a Playwright MCP server via SSE
         
     | 
| 
      
 125 
     | 
    
         
            +
            # First run: npx @playwright/mcp@latest --port 8931
         
     | 
| 
      
 126 
     | 
    
         
            +
            sse_client = MCPClient.create_client(
         
     | 
| 
      
 127 
     | 
    
         
            +
              mcp_server_configs: [
         
     | 
| 
      
 128 
     | 
    
         
            +
                MCPClient.sse_config(
         
     | 
| 
      
 129 
     | 
    
         
            +
                  base_url: 'http://localhost:8931/sse',
         
     | 
| 
      
 130 
     | 
    
         
            +
                  read_timeout: 30,  # Timeout in seconds
         
     | 
| 
      
 131 
     | 
    
         
            +
                )
         
     | 
| 
      
 132 
     | 
    
         
            +
              ]
         
     | 
| 
      
 133 
     | 
    
         
            +
            )
         
     | 
| 
      
 134 
     | 
    
         
            +
             
     | 
| 
      
 135 
     | 
    
         
            +
            # List available tools
         
     | 
| 
      
 136 
     | 
    
         
            +
            tools = sse_client.list_tools
         
     | 
| 
      
 137 
     | 
    
         
            +
             
     | 
| 
      
 138 
     | 
    
         
            +
            # Launch a browser
         
     | 
| 
      
 139 
     | 
    
         
            +
            result = sse_client.call_tool('browser_install', {})
         
     | 
| 
      
 140 
     | 
    
         
            +
            result = sse_client.call_tool('browser_navigate', { url: 'about:blank' })
         
     | 
| 
      
 141 
     | 
    
         
            +
            # No browser ID needed with these tool names
         
     | 
| 
      
 142 
     | 
    
         
            +
             
     | 
| 
      
 143 
     | 
    
         
            +
            # Create a new page
         
     | 
| 
      
 144 
     | 
    
         
            +
            page_result = sse_client.call_tool('browser_tab_new', {})
         
     | 
| 
      
 145 
     | 
    
         
            +
            # No page ID needed with these tool names
         
     | 
| 
      
 146 
     | 
    
         
            +
             
     | 
| 
      
 147 
     | 
    
         
            +
            # Navigate to a website
         
     | 
| 
      
 148 
     | 
    
         
            +
            sse_client.call_tool('browser_navigate', { url: 'https://example.com' })
         
     | 
| 
      
 149 
     | 
    
         
            +
             
     | 
| 
      
 150 
     | 
    
         
            +
            # Get page title
         
     | 
| 
      
 151 
     | 
    
         
            +
            title_result = sse_client.call_tool('browser_snapshot', {})
         
     | 
| 
      
 152 
     | 
    
         
            +
            puts "Page snapshot: #{title_result}"
         
     | 
| 
      
 153 
     | 
    
         
            +
             
     | 
| 
      
 154 
     | 
    
         
            +
            # Take a screenshot
         
     | 
| 
      
 155 
     | 
    
         
            +
            screenshot_result = sse_client.call_tool('browser_take_screenshot', {})
         
     | 
| 
      
 156 
     | 
    
         
            +
             
     | 
| 
      
 157 
     | 
    
         
            +
            # Ping the server to verify connectivity
         
     | 
| 
      
 158 
     | 
    
         
            +
            ping_result = sse_client.ping
         
     | 
| 
      
 159 
     | 
    
         
            +
            puts "Ping successful: #{ping_result.inspect}"
         
     | 
| 
      
 160 
     | 
    
         
            +
             
     | 
| 
      
 161 
     | 
    
         
            +
            # Clean up
         
     | 
| 
      
 162 
     | 
    
         
            +
            sse_client.cleanup
         
     | 
| 
      
 163 
     | 
    
         
            +
            ```
         
     | 
| 
      
 164 
     | 
    
         
            +
             
     | 
| 
      
 165 
     | 
    
         
            +
            See `examples/mcp_sse_server_example.rb` for the full Playwright SSE example.
         
     | 
| 
      
 166 
     | 
    
         
            +
             
     | 
| 
       108 
167 
     | 
    
         
             
            ### Integration Examples
         
     | 
| 
       109 
168 
     | 
    
         | 
| 
       110 
169 
     | 
    
         
             
            The repository includes examples for integrating with popular AI APIs:
         
     | 
| 
         @@ -191,6 +250,7 @@ Complete examples can be found in the `examples/` directory: 
     | 
|
| 
       191 
250 
     | 
    
         
             
            - `ruby_openai_mcp.rb` - Integration with alexrudall/ruby-openai gem
         
     | 
| 
       192 
251 
     | 
    
         
             
            - `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
         
     | 
| 
       193 
252 
     | 
    
         
             
            - `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
         
     | 
| 
      
 253 
     | 
    
         
            +
            - `mcp_sse_server_example.rb` - SSE transport with Playwright MCP
         
     | 
| 
       194 
254 
     | 
    
         | 
| 
       195 
255 
     | 
    
         
             
            ## MCP Server Compatibility
         
     | 
| 
       196 
256 
     | 
    
         | 
| 
         @@ -200,7 +260,20 @@ This client works with any MCP-compatible server, including: 
     | 
|
| 
       200 
260 
     | 
    
         
             
            - [@playwright/mcp](https://www.npmjs.com/package/@playwright/mcp) - Browser automation
         
     | 
| 
       201 
261 
     | 
    
         
             
            - Custom servers implementing the MCP protocol
         
     | 
| 
       202 
262 
     | 
    
         | 
| 
       203 
     | 
    
         
            -
             
     | 
| 
      
 263 
     | 
    
         
            +
            ## Key Features
         
     | 
| 
      
 264 
     | 
    
         
            +
             
     | 
| 
      
 265 
     | 
    
         
            +
            ### Client Features
         
     | 
| 
      
 266 
     | 
    
         
            +
             
     | 
| 
      
 267 
     | 
    
         
            +
            - **Multiple transports** - Support for both stdio and SSE transports
         
     | 
| 
      
 268 
     | 
    
         
            +
            - **Multiple servers** - Connect to multiple MCP servers simultaneously
         
     | 
| 
      
 269 
     | 
    
         
            +
            - **Tool discovery** - Find tools by name or pattern
         
     | 
| 
      
 270 
     | 
    
         
            +
            - **Atomic tool calls** - Simple API for invoking tools with parameters
         
     | 
| 
      
 271 
     | 
    
         
            +
            - **Batch support** - Call multiple tools in a single operation
         
     | 
| 
      
 272 
     | 
    
         
            +
            - **API conversions** - Built-in format conversion for OpenAI and Anthropic APIs
         
     | 
| 
      
 273 
     | 
    
         
            +
            - **Thread safety** - Synchronized access for thread-safe operation
         
     | 
| 
      
 274 
     | 
    
         
            +
            - **Server notifications** - Support for JSON-RPC notifications
         
     | 
| 
      
 275 
     | 
    
         
            +
            - **Custom RPC methods** - Send any custom JSON-RPC method
         
     | 
| 
      
 276 
     | 
    
         
            +
            - **Consistent error handling** - Rich error types for better exception handling
         
     | 
| 
       204 
277 
     | 
    
         | 
| 
       205 
278 
     | 
    
         
             
            ### Server-Sent Events (SSE) Implementation
         
     | 
| 
       206 
279 
     | 
    
         | 
| 
         @@ -215,10 +288,13 @@ The SSE client implementation provides these key features: 
     | 
|
| 
       215 
288 
     | 
    
         
             
            - **Custom RPC methods**: Send any custom JSON-RPC method or notification through `send_rpc` and `send_notification`
         
     | 
| 
       216 
289 
     | 
    
         
             
            - **Configurable retries**: All RPC requests support configurable retries with exponential backoff
         
     | 
| 
       217 
290 
     | 
    
         
             
            - **Consistent logging**: Tagged, leveled logging across all components for better debugging
         
     | 
| 
      
 291 
     | 
    
         
            +
            - **Graceful fallbacks**: Automatic fallback to synchronous HTTP when SSE connection fails
         
     | 
| 
      
 292 
     | 
    
         
            +
            - **URL normalization**: Consistent URL handling that respects user-provided formats
         
     | 
| 
      
 293 
     | 
    
         
            +
            - **Server connectivity check**: Built-in `ping` method to test server connectivity and health
         
     | 
| 
       218 
294 
     | 
    
         | 
| 
       219 
295 
     | 
    
         
             
            ## Requirements
         
     | 
| 
       220 
296 
     | 
    
         | 
| 
       221 
     | 
    
         
            -
            - Ruby >= 2. 
     | 
| 
      
 297 
     | 
    
         
            +
            - Ruby >= 3.2.0
         
     | 
| 
       222 
298 
     | 
    
         
             
            - No runtime dependencies
         
     | 
| 
       223 
299 
     | 
    
         | 
| 
       224 
300 
     | 
    
         
             
            ## Implementing an MCP Server
         
     | 
| 
         @@ -266,4 +342,4 @@ This gem is available as open source under the [MIT License](LICENSE). 
     | 
|
| 
       266 
342 
     | 
    
         
             
            ## Contributing
         
     | 
| 
       267 
343 
     | 
    
         | 
| 
       268 
344 
     | 
    
         
             
            Bug reports and pull requests are welcome on GitHub at
         
     | 
| 
       269 
     | 
    
         
            -
            https://github.com/simonx1/ruby-mcp-client.
         
     | 
| 
      
 345 
     | 
    
         
            +
            https://github.com/simonx1/ruby-mcp-client.
         
     | 
    
        data/lib/mcp_client/client.rb
    CHANGED
    
    | 
         @@ -31,9 +31,9 @@ module MCPClient 
     | 
|
| 
       31 
31 
     | 
    
         
             
                  # Register default and user-defined notification handlers on each server
         
     | 
| 
       32 
32 
     | 
    
         
             
                  @servers.each do |server|
         
     | 
| 
       33 
33 
     | 
    
         
             
                    server.on_notification do |method, params|
         
     | 
| 
       34 
     | 
    
         
            -
                      # Default  
     | 
| 
       35 
     | 
    
         
            -
                       
     | 
| 
       36 
     | 
    
         
            -
                      # Invoke user listeners
         
     | 
| 
      
 34 
     | 
    
         
            +
                      # Default notification processing (e.g., cache invalidation, logging)
         
     | 
| 
      
 35 
     | 
    
         
            +
                      process_notification(server, method, params)
         
     | 
| 
      
 36 
     | 
    
         
            +
                      # Invoke user-defined listeners
         
     | 
| 
       37 
37 
     | 
    
         
             
                      @notification_listeners.each { |cb| cb.call(server, method, params) }
         
     | 
| 
       38 
38 
     | 
    
         
             
                    end
         
     | 
| 
       39 
39 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -163,8 +163,95 @@ module MCPClient 
     | 
|
| 
       163 
163 
     | 
    
         
             
                  end
         
     | 
| 
       164 
164 
     | 
    
         
             
                end
         
     | 
| 
       165 
165 
     | 
    
         | 
| 
      
 166 
     | 
    
         
            +
                # Ping the MCP server to check connectivity (zero-parameter heartbeat call)
         
     | 
| 
      
 167 
     | 
    
         
            +
                # @param server_index [Integer, nil] optional index of a specific server to ping, nil for first available
         
     | 
| 
      
 168 
     | 
    
         
            +
                # @return [Object] result from the ping request
         
     | 
| 
      
 169 
     | 
    
         
            +
                # @raise [MCPClient::Errors::ServerNotFound] if no server is available
         
     | 
| 
      
 170 
     | 
    
         
            +
                def ping(server_index: nil)
         
     | 
| 
      
 171 
     | 
    
         
            +
                  if server_index.nil?
         
     | 
| 
      
 172 
     | 
    
         
            +
                    # Ping first available server
         
     | 
| 
      
 173 
     | 
    
         
            +
                    raise MCPClient::Errors::ServerNotFound, 'No server available for ping' if @servers.empty?
         
     | 
| 
      
 174 
     | 
    
         
            +
             
     | 
| 
      
 175 
     | 
    
         
            +
                    @servers.first.ping
         
     | 
| 
      
 176 
     | 
    
         
            +
                  else
         
     | 
| 
      
 177 
     | 
    
         
            +
                    # Ping specified server
         
     | 
| 
      
 178 
     | 
    
         
            +
                    if server_index >= @servers.length
         
     | 
| 
      
 179 
     | 
    
         
            +
                      raise MCPClient::Errors::ServerNotFound,
         
     | 
| 
      
 180 
     | 
    
         
            +
                            "Server at index #{server_index} not found"
         
     | 
| 
      
 181 
     | 
    
         
            +
                    end
         
     | 
| 
      
 182 
     | 
    
         
            +
             
     | 
| 
      
 183 
     | 
    
         
            +
                    @servers[server_index].ping
         
     | 
| 
      
 184 
     | 
    
         
            +
                  end
         
     | 
| 
      
 185 
     | 
    
         
            +
                end
         
     | 
| 
      
 186 
     | 
    
         
            +
             
     | 
| 
      
 187 
     | 
    
         
            +
                # Send a raw JSON-RPC request to a server
         
     | 
| 
      
 188 
     | 
    
         
            +
                # @param method [String] JSON-RPC method name
         
     | 
| 
      
 189 
     | 
    
         
            +
                # @param params [Hash] parameters for the request
         
     | 
| 
      
 190 
     | 
    
         
            +
                # @param server [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
         
     | 
| 
      
 191 
     | 
    
         
            +
                # @return [Object] result from the JSON-RPC response
         
     | 
| 
      
 192 
     | 
    
         
            +
                def send_rpc(method, params: {}, server: nil)
         
     | 
| 
      
 193 
     | 
    
         
            +
                  srv = select_server(server)
         
     | 
| 
      
 194 
     | 
    
         
            +
                  srv.rpc_request(method, params)
         
     | 
| 
      
 195 
     | 
    
         
            +
                end
         
     | 
| 
      
 196 
     | 
    
         
            +
             
     | 
| 
      
 197 
     | 
    
         
            +
                # Send a raw JSON-RPC notification to a server (no response expected)
         
     | 
| 
      
 198 
     | 
    
         
            +
                # @param method [String] JSON-RPC method name
         
     | 
| 
      
 199 
     | 
    
         
            +
                # @param params [Hash] parameters for the notification
         
     | 
| 
      
 200 
     | 
    
         
            +
                # @param server [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
         
     | 
| 
      
 201 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
      
 202 
     | 
    
         
            +
                def send_notification(method, params: {}, server: nil)
         
     | 
| 
      
 203 
     | 
    
         
            +
                  srv = select_server(server)
         
     | 
| 
      
 204 
     | 
    
         
            +
                  srv.rpc_notify(method, params)
         
     | 
| 
      
 205 
     | 
    
         
            +
                end
         
     | 
| 
      
 206 
     | 
    
         
            +
             
     | 
| 
       166 
207 
     | 
    
         
             
                private
         
     | 
| 
       167 
208 
     | 
    
         | 
| 
      
 209 
     | 
    
         
            +
                # Process incoming JSON-RPC notifications with default handlers
         
     | 
| 
      
 210 
     | 
    
         
            +
                # @param server [MCPClient::ServerBase] the server that emitted the notification
         
     | 
| 
      
 211 
     | 
    
         
            +
                # @param method [String] JSON-RPC notification method
         
     | 
| 
      
 212 
     | 
    
         
            +
                # @param params [Hash] parameters for the notification
         
     | 
| 
      
 213 
     | 
    
         
            +
                # @return [void]
         
     | 
| 
      
 214 
     | 
    
         
            +
                def process_notification(server, method, params)
         
     | 
| 
      
 215 
     | 
    
         
            +
                  case method
         
     | 
| 
      
 216 
     | 
    
         
            +
                  when 'notifications/tools/list_changed'
         
     | 
| 
      
 217 
     | 
    
         
            +
                    logger.warn("[#{server.class}] Tool list has changed, clearing tool cache")
         
     | 
| 
      
 218 
     | 
    
         
            +
                    clear_cache
         
     | 
| 
      
 219 
     | 
    
         
            +
                  when 'notifications/resources/updated'
         
     | 
| 
      
 220 
     | 
    
         
            +
                    logger.warn("[#{server.class}] Resource #{params['uri']} updated")
         
     | 
| 
      
 221 
     | 
    
         
            +
                  when 'notifications/prompts/list_changed'
         
     | 
| 
      
 222 
     | 
    
         
            +
                    logger.warn("[#{server.class}] Prompt list has changed")
         
     | 
| 
      
 223 
     | 
    
         
            +
                  when 'notifications/resources/list_changed'
         
     | 
| 
      
 224 
     | 
    
         
            +
                    logger.warn("[#{server.class}] Resource list has changed")
         
     | 
| 
      
 225 
     | 
    
         
            +
                  end
         
     | 
| 
      
 226 
     | 
    
         
            +
                end
         
     | 
| 
      
 227 
     | 
    
         
            +
             
     | 
| 
      
 228 
     | 
    
         
            +
                # Select a server based on index, type, or instance
         
     | 
| 
      
 229 
     | 
    
         
            +
                # @param server_arg [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
         
     | 
| 
      
 230 
     | 
    
         
            +
                # @return [MCPClient::ServerBase]
         
     | 
| 
      
 231 
     | 
    
         
            +
                def select_server(server_arg)
         
     | 
| 
      
 232 
     | 
    
         
            +
                  case server_arg
         
     | 
| 
      
 233 
     | 
    
         
            +
                  when nil
         
     | 
| 
      
 234 
     | 
    
         
            +
                    raise MCPClient::Errors::ServerNotFound, 'No server available' if @servers.empty?
         
     | 
| 
      
 235 
     | 
    
         
            +
             
     | 
| 
      
 236 
     | 
    
         
            +
                    @servers.first
         
     | 
| 
      
 237 
     | 
    
         
            +
                  when Integer
         
     | 
| 
      
 238 
     | 
    
         
            +
                    @servers.fetch(server_arg) do
         
     | 
| 
      
 239 
     | 
    
         
            +
                      raise MCPClient::Errors::ServerNotFound, "Server at index #{server_arg} not found"
         
     | 
| 
      
 240 
     | 
    
         
            +
                    end
         
     | 
| 
      
 241 
     | 
    
         
            +
                  when String, Symbol
         
     | 
| 
      
 242 
     | 
    
         
            +
                    key = server_arg.to_s.downcase
         
     | 
| 
      
 243 
     | 
    
         
            +
                    srv = @servers.find { |s| s.class.name.split('::').last.downcase.end_with?(key) }
         
     | 
| 
      
 244 
     | 
    
         
            +
                    raise MCPClient::Errors::ServerNotFound, "Server of type #{server_arg} not found" unless srv
         
     | 
| 
      
 245 
     | 
    
         
            +
             
     | 
| 
      
 246 
     | 
    
         
            +
                    srv
         
     | 
| 
      
 247 
     | 
    
         
            +
                  else
         
     | 
| 
      
 248 
     | 
    
         
            +
                    raise ArgumentError, "Invalid server argument: #{server_arg.inspect}" unless @servers.include?(server_arg)
         
     | 
| 
      
 249 
     | 
    
         
            +
             
     | 
| 
      
 250 
     | 
    
         
            +
                    server_arg
         
     | 
| 
      
 251 
     | 
    
         
            +
             
     | 
| 
      
 252 
     | 
    
         
            +
                  end
         
     | 
| 
      
 253 
     | 
    
         
            +
                end
         
     | 
| 
      
 254 
     | 
    
         
            +
             
     | 
| 
       168 
255 
     | 
    
         
             
                # Validate parameters against tool JSON schema (checks required properties)
         
     | 
| 
       169 
256 
     | 
    
         
             
                # @param tool [MCPClient::Tool] tool definition with schema
         
     | 
| 
       170 
257 
     | 
    
         
             
                # @param parameters [Hash] parameters to validate
         
     | 
| 
         @@ -45,6 +45,12 @@ module MCPClient 
     | 
|
| 
       45 
45 
     | 
    
         
             
                  raise NotImplementedError, 'Subclasses must implement rpc_notify'
         
     | 
| 
       46 
46 
     | 
    
         
             
                end
         
     | 
| 
       47 
47 
     | 
    
         | 
| 
      
 48 
     | 
    
         
            +
                # Ping the MCP server to check connectivity (zero-parameter heartbeat call)
         
     | 
| 
      
 49 
     | 
    
         
            +
                # @return [Object] result from the ping request
         
     | 
| 
      
 50 
     | 
    
         
            +
                def ping
         
     | 
| 
      
 51 
     | 
    
         
            +
                  rpc_request('ping')
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
       48 
54 
     | 
    
         
             
                # Register a callback to receive JSON-RPC notifications
         
     | 
| 
       49 
55 
     | 
    
         
             
                # @yield [method, params] invoked when a notification is received
         
     | 
| 
       50 
56 
     | 
    
         
             
                # @return [void]
         
     | 
| 
         @@ -1,17 +1,17 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # frozen_string_literal: true
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
3 
     | 
    
         
             
            require 'uri'
         
     | 
| 
       4 
     | 
    
         
            -
            require 'net/http'
         
     | 
| 
       5 
4 
     | 
    
         
             
            require 'json'
         
     | 
| 
       6 
     | 
    
         
            -
            require 'openssl'
         
     | 
| 
       7 
5 
     | 
    
         
             
            require 'monitor'
         
     | 
| 
       8 
6 
     | 
    
         
             
            require 'logger'
         
     | 
| 
      
 7 
     | 
    
         
            +
            require 'faraday'
         
     | 
| 
      
 8 
     | 
    
         
            +
            require 'faraday/retry'
         
     | 
| 
       9 
9 
     | 
    
         | 
| 
       10 
10 
     | 
    
         
             
            module MCPClient
         
     | 
| 
       11 
11 
     | 
    
         
             
              # Implementation of MCP server that communicates via Server-Sent Events (SSE)
         
     | 
| 
       12 
12 
     | 
    
         
             
              # Useful for communicating with remote MCP servers over HTTP
         
     | 
| 
       13 
13 
     | 
    
         
             
              class ServerSSE < ServerBase
         
     | 
| 
       14 
     | 
    
         
            -
                attr_reader :base_url, :tools, : 
     | 
| 
      
 14 
     | 
    
         
            +
                attr_reader :base_url, :tools, :server_info, :capabilities
         
     | 
| 
       15 
15 
     | 
    
         | 
| 
       16 
16 
     | 
    
         
             
                # @param base_url [String] The base URL of the MCP server
         
     | 
| 
       17 
17 
     | 
    
         
             
                # @param headers [Hash] Additional headers to include in requests
         
     | 
| 
         @@ -26,16 +26,19 @@ module MCPClient 
     | 
|
| 
       26 
26 
     | 
    
         
             
                  @logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
         
     | 
| 
       27 
27 
     | 
    
         
             
                  @max_retries = retries
         
     | 
| 
       28 
28 
     | 
    
         
             
                  @retry_backoff = retry_backoff
         
     | 
| 
       29 
     | 
    
         
            -
                   
     | 
| 
      
 29 
     | 
    
         
            +
                  # Normalize base_url: strip any trailing slash, use exactly as provided
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @base_url = base_url.chomp('/')
         
     | 
| 
       30 
31 
     | 
    
         
             
                  @headers = headers.merge({
         
     | 
| 
       31 
32 
     | 
    
         
             
                                             'Accept' => 'text/event-stream',
         
     | 
| 
       32 
33 
     | 
    
         
             
                                             'Cache-Control' => 'no-cache',
         
     | 
| 
       33 
34 
     | 
    
         
             
                                             'Connection' => 'keep-alive'
         
     | 
| 
       34 
35 
     | 
    
         
             
                                           })
         
     | 
| 
       35 
     | 
    
         
            -
                   
     | 
| 
      
 36 
     | 
    
         
            +
                  # HTTP client is managed via Faraday
         
     | 
| 
       36 
37 
     | 
    
         
             
                  @tools = nil
         
     | 
| 
       37 
38 
     | 
    
         
             
                  @read_timeout = read_timeout
         
     | 
| 
       38 
     | 
    
         
            -
             
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
                  # SSE-provided JSON-RPC endpoint path for POST requests
         
     | 
| 
      
 41 
     | 
    
         
            +
                  @rpc_endpoint = nil
         
     | 
| 
       39 
42 
     | 
    
         
             
                  @tools_data = nil
         
     | 
| 
       40 
43 
     | 
    
         
             
                  @request_id = 0
         
     | 
| 
       41 
44 
     | 
    
         
             
                  @sse_results = {}
         
     | 
| 
         @@ -45,6 +48,8 @@ module MCPClient 
     | 
|
| 
       45 
48 
     | 
    
         
             
                  @connection_established = false
         
     | 
| 
       46 
49 
     | 
    
         
             
                  @connection_cv = @mutex.new_cond
         
     | 
| 
       47 
50 
     | 
    
         
             
                  @initialized = false
         
     | 
| 
      
 51 
     | 
    
         
            +
                  # Whether to use SSE transport; may disable if handshake fails
         
     | 
| 
      
 52 
     | 
    
         
            +
                  @use_sse = true
         
     | 
| 
       48 
53 
     | 
    
         
             
                end
         
     | 
| 
       49 
54 
     | 
    
         | 
| 
       50 
55 
     | 
    
         
             
                # Stream tool call fallback for SSE transport (yields single result)
         
     | 
| 
         @@ -129,19 +134,7 @@ module MCPClient 
     | 
|
| 
       129 
134 
     | 
    
         
             
                  @mutex.synchronize do
         
     | 
| 
       130 
135 
     | 
    
         
             
                    return true if @connection_established
         
     | 
| 
       131 
136 
     | 
    
         | 
| 
       132 
     | 
    
         
            -
                     
     | 
| 
       133 
     | 
    
         
            -
                    @http_client = Net::HTTP.new(uri.host, uri.port)
         
     | 
| 
       134 
     | 
    
         
            -
             
     | 
| 
       135 
     | 
    
         
            -
                    if uri.scheme == 'https'
         
     | 
| 
       136 
     | 
    
         
            -
                      @http_client.use_ssl = true
         
     | 
| 
       137 
     | 
    
         
            -
                      @http_client.verify_mode = OpenSSL::SSL::VERIFY_PEER
         
     | 
| 
       138 
     | 
    
         
            -
                    end
         
     | 
| 
       139 
     | 
    
         
            -
             
     | 
| 
       140 
     | 
    
         
            -
                    @http_client.open_timeout = 10
         
     | 
| 
       141 
     | 
    
         
            -
                    @http_client.read_timeout = @read_timeout
         
     | 
| 
       142 
     | 
    
         
            -
                    @http_client.keep_alive_timeout = 60
         
     | 
| 
       143 
     | 
    
         
            -
             
     | 
| 
       144 
     | 
    
         
            -
                    @http_client.start
         
     | 
| 
      
 137 
     | 
    
         
            +
                    # Start SSE listener using Faraday HTTP client
         
     | 
| 
       145 
138 
     | 
    
         
             
                    start_sse_thread
         
     | 
| 
       146 
139 
     | 
    
         | 
| 
       147 
140 
     | 
    
         
             
                    timeout = 10
         
     | 
| 
         @@ -176,7 +169,6 @@ module MCPClient 
     | 
|
| 
       176 
169 
     | 
    
         
             
                    end
         
     | 
| 
       177 
170 
     | 
    
         | 
| 
       178 
171 
     | 
    
         
             
                    @tools = nil
         
     | 
| 
       179 
     | 
    
         
            -
                    @session_id = nil
         
     | 
| 
       180 
172 
     | 
    
         
             
                    @connection_established = false
         
     | 
| 
       181 
173 
     | 
    
         
             
                    @sse_connected = false
         
     | 
| 
       182 
174 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -201,41 +193,42 @@ module MCPClient 
     | 
|
| 
       201 
193 
     | 
    
         
             
                # @return [void]
         
     | 
| 
       202 
194 
     | 
    
         
             
                def rpc_notify(method, params = {})
         
     | 
| 
       203 
195 
     | 
    
         
             
                  ensure_initialized
         
     | 
| 
       204 
     | 
    
         
            -
                   
     | 
| 
       205 
     | 
    
         
            -
                   
     | 
| 
       206 
     | 
    
         
            -
                   
     | 
| 
       207 
     | 
    
         
            -
                   
     | 
| 
       208 
     | 
    
         
            -
                     
     | 
| 
       209 
     | 
    
         
            -
                     
     | 
| 
      
 196 
     | 
    
         
            +
                  uri = URI.parse(@base_url)
         
     | 
| 
      
 197 
     | 
    
         
            +
                  base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
         
     | 
| 
      
 198 
     | 
    
         
            +
                  rpc_ep = @mutex.synchronize { @rpc_endpoint }
         
     | 
| 
      
 199 
     | 
    
         
            +
                  @rpc_conn ||= Faraday.new(url: base) do |f|
         
     | 
| 
      
 200 
     | 
    
         
            +
                    f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
         
     | 
| 
      
 201 
     | 
    
         
            +
                    f.options.open_timeout = @read_timeout
         
     | 
| 
      
 202 
     | 
    
         
            +
                    f.options.timeout = @read_timeout
         
     | 
| 
      
 203 
     | 
    
         
            +
                    f.adapter Faraday.default_adapter
         
     | 
| 
       210 
204 
     | 
    
         
             
                  end
         
     | 
| 
       211 
     | 
    
         
            -
                   
     | 
| 
       212 
     | 
    
         
            -
             
     | 
| 
       213 
     | 
    
         
            -
             
     | 
| 
       214 
     | 
    
         
            -
             
     | 
| 
       215 
     | 
    
         
            -
             
     | 
| 
       216 
     | 
    
         
            -
             
     | 
| 
       217 
     | 
    
         
            -
                     
     | 
| 
       218 
     | 
    
         
            -
             
     | 
| 
       219 
     | 
    
         
            -
                    headers.except('Accept', 'Cache-Control').each { |k, v| http_req[k] = v }
         
     | 
| 
       220 
     | 
    
         
            -
                    response = http.request(http_req)
         
     | 
| 
       221 
     | 
    
         
            -
                    unless response.is_a?(Net::HTTPSuccess)
         
     | 
| 
       222 
     | 
    
         
            -
                      raise MCPClient::Errors::ServerError, "Notification failed: #{response.code} #{response.message}"
         
     | 
| 
      
 205 
     | 
    
         
            +
                  response = @rpc_conn.post(rpc_ep) do |req|
         
     | 
| 
      
 206 
     | 
    
         
            +
                    req.headers['Content-Type'] = 'application/json'
         
     | 
| 
      
 207 
     | 
    
         
            +
                    req.headers['Accept'] = 'application/json'
         
     | 
| 
      
 208 
     | 
    
         
            +
                    (@headers.dup.tap do |h|
         
     | 
| 
      
 209 
     | 
    
         
            +
                      h.delete('Accept')
         
     | 
| 
      
 210 
     | 
    
         
            +
                      h.delete('Cache-Control')
         
     | 
| 
      
 211 
     | 
    
         
            +
                    end).each do |k, v|
         
     | 
| 
      
 212 
     | 
    
         
            +
                      req.headers[k] = v
         
     | 
| 
       223 
213 
     | 
    
         
             
                    end
         
     | 
| 
      
 214 
     | 
    
         
            +
                    req.body = { jsonrpc: '2.0', method: method, params: params }.to_json
         
     | 
| 
      
 215 
     | 
    
         
            +
                  end
         
     | 
| 
      
 216 
     | 
    
         
            +
                  unless response.success?
         
     | 
| 
      
 217 
     | 
    
         
            +
                    raise MCPClient::Errors::ServerError, "Notification failed: #{response.status} #{response.reason_phrase}"
         
     | 
| 
       224 
218 
     | 
    
         
             
                  end
         
     | 
| 
       225 
219 
     | 
    
         
             
                rescue StandardError => e
         
     | 
| 
       226 
220 
     | 
    
         
             
                  raise MCPClient::Errors::TransportError, "Failed to send notification: #{e.message}"
         
     | 
| 
       227 
     | 
    
         
            -
                ensure
         
     | 
| 
       228 
     | 
    
         
            -
                  rpc_http.finish if rpc_http&.started?
         
     | 
| 
       229 
221 
     | 
    
         
             
                end
         
     | 
| 
       230 
222 
     | 
    
         | 
| 
       231 
223 
     | 
    
         
             
                private
         
     | 
| 
       232 
224 
     | 
    
         | 
| 
       233 
     | 
    
         
            -
                # Ensure  
     | 
| 
      
 225 
     | 
    
         
            +
                # Ensure SSE initialization handshake has been performed
         
     | 
| 
       234 
226 
     | 
    
         
             
                def ensure_initialized
         
     | 
| 
       235 
227 
     | 
    
         
             
                  return if @initialized
         
     | 
| 
       236 
228 
     | 
    
         | 
| 
       237 
229 
     | 
    
         
             
                  connect
         
     | 
| 
       238 
230 
     | 
    
         
             
                  perform_initialize
         
     | 
| 
      
 231 
     | 
    
         
            +
             
     | 
| 
       239 
232 
     | 
    
         
             
                  @initialized = true
         
     | 
| 
       240 
233 
     | 
    
         
             
                end
         
     | 
| 
       241 
234 
     | 
    
         | 
| 
         @@ -265,51 +258,31 @@ module MCPClient 
     | 
|
| 
       265 
258 
     | 
    
         
             
                  return if @sse_thread&.alive?
         
     | 
| 
       266 
259 
     | 
    
         | 
| 
       267 
260 
     | 
    
         
             
                  @sse_thread = Thread.new do
         
     | 
| 
       268 
     | 
    
         
            -
                     
     | 
| 
       269 
     | 
    
         
            -
                     
     | 
| 
       270 
     | 
    
         
            -
             
     | 
| 
       271 
     | 
    
         
            -
                      sse_http = Net::HTTP.new(uri.host, uri.port)
         
     | 
| 
       272 
     | 
    
         
            -
             
     | 
| 
       273 
     | 
    
         
            -
                      if uri.scheme == 'https'
         
     | 
| 
       274 
     | 
    
         
            -
                        sse_http.use_ssl = true
         
     | 
| 
       275 
     | 
    
         
            -
                        sse_http.verify_mode = OpenSSL::SSL::VERIFY_PEER
         
     | 
| 
       276 
     | 
    
         
            -
                      end
         
     | 
| 
       277 
     | 
    
         
            -
             
     | 
| 
       278 
     | 
    
         
            -
                      sse_http.open_timeout = 10
         
     | 
| 
       279 
     | 
    
         
            -
                      sse_http.read_timeout = @read_timeout
         
     | 
| 
       280 
     | 
    
         
            -
                      sse_http.keep_alive_timeout = 60
         
     | 
| 
       281 
     | 
    
         
            -
             
     | 
| 
       282 
     | 
    
         
            -
                      sse_http.start do |http|
         
     | 
| 
       283 
     | 
    
         
            -
                        request = Net::HTTP::Get.new(uri)
         
     | 
| 
       284 
     | 
    
         
            -
                        @headers.each { |k, v| request[k] = v }
         
     | 
| 
       285 
     | 
    
         
            -
             
     | 
| 
       286 
     | 
    
         
            -
                        http.request(request) do |response|
         
     | 
| 
       287 
     | 
    
         
            -
                          unless response.is_a?(Net::HTTPSuccess) && response['content-type']&.start_with?('text/event-stream')
         
     | 
| 
       288 
     | 
    
         
            -
                            @mutex.synchronize do
         
     | 
| 
       289 
     | 
    
         
            -
                              @connection_established = false
         
     | 
| 
       290 
     | 
    
         
            -
                              @connection_cv.broadcast
         
     | 
| 
       291 
     | 
    
         
            -
                            end
         
     | 
| 
       292 
     | 
    
         
            -
                            raise MCPClient::Errors::ServerError, 'Server response not OK or not text/event-stream'
         
     | 
| 
       293 
     | 
    
         
            -
                          end
         
     | 
| 
      
 261 
     | 
    
         
            +
                    uri = URI.parse(@base_url)
         
     | 
| 
      
 262 
     | 
    
         
            +
                    sse_base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
         
     | 
| 
      
 263 
     | 
    
         
            +
                    sse_path = uri.request_uri
         
     | 
| 
       294 
264 
     | 
    
         | 
| 
       295 
     | 
    
         
            -
             
     | 
| 
       296 
     | 
    
         
            -
             
     | 
| 
       297 
     | 
    
         
            -
             
     | 
| 
      
 265 
     | 
    
         
            +
                    @sse_conn ||= Faraday.new(url: sse_base) do |f|
         
     | 
| 
      
 266 
     | 
    
         
            +
                      f.options.open_timeout = 10
         
     | 
| 
      
 267 
     | 
    
         
            +
                      f.options.timeout = nil
         
     | 
| 
      
 268 
     | 
    
         
            +
                      f.adapter Faraday.default_adapter
         
     | 
| 
      
 269 
     | 
    
         
            +
                    end
         
     | 
| 
       298 
270 
     | 
    
         | 
| 
       299 
     | 
    
         
            -
             
     | 
| 
       300 
     | 
    
         
            -
             
     | 
| 
       301 
     | 
    
         
            -
             
     | 
| 
       302 
     | 
    
         
            -
             
     | 
| 
       303 
     | 
    
         
            -
                         
     | 
| 
       304 
     | 
    
         
            -
                      end
         
     | 
| 
       305 
     | 
    
         
            -
                    rescue StandardError
         
     | 
| 
       306 
     | 
    
         
            -
                      nil
         
     | 
| 
       307 
     | 
    
         
            -
                    ensure
         
     | 
| 
       308 
     | 
    
         
            -
                      sse_http&.finish if sse_http&.started?
         
     | 
| 
       309 
     | 
    
         
            -
                      @mutex.synchronize do
         
     | 
| 
       310 
     | 
    
         
            -
                        @sse_connected = false
         
     | 
| 
      
 271 
     | 
    
         
            +
                    @sse_conn.get(sse_path) do |req|
         
     | 
| 
      
 272 
     | 
    
         
            +
                      @headers.each { |k, v| req.headers[k] = v }
         
     | 
| 
      
 273 
     | 
    
         
            +
                      req.options.on_data = proc do |chunk, _bytes|
         
     | 
| 
      
 274 
     | 
    
         
            +
                        @logger.debug("SSE chunk received: #{chunk.inspect}")
         
     | 
| 
      
 275 
     | 
    
         
            +
                        process_sse_chunk(chunk.dup)
         
     | 
| 
       311 
276 
     | 
    
         
             
                      end
         
     | 
| 
       312 
277 
     | 
    
         
             
                    end
         
     | 
| 
      
 278 
     | 
    
         
            +
                  rescue StandardError
         
     | 
| 
      
 279 
     | 
    
         
            +
                    # On any SSE thread error, signal connection established to unblock connect
         
     | 
| 
      
 280 
     | 
    
         
            +
                    @mutex.synchronize do
         
     | 
| 
      
 281 
     | 
    
         
            +
                      @connection_established = true
         
     | 
| 
      
 282 
     | 
    
         
            +
                      @connection_cv.broadcast
         
     | 
| 
      
 283 
     | 
    
         
            +
                    end
         
     | 
| 
      
 284 
     | 
    
         
            +
                  ensure
         
     | 
| 
      
 285 
     | 
    
         
            +
                    @mutex.synchronize { @sse_connected = false }
         
     | 
| 
       313 
286 
     | 
    
         
             
                  end
         
     | 
| 
       314 
287 
     | 
    
         
             
                end
         
     | 
| 
       315 
288 
     | 
    
         | 
| 
         @@ -339,14 +312,11 @@ module MCPClient 
     | 
|
| 
       339 
312 
     | 
    
         | 
| 
       340 
313 
     | 
    
         
             
                  case event[:event]
         
     | 
| 
       341 
314 
     | 
    
         
             
                  when 'endpoint'
         
     | 
| 
       342 
     | 
    
         
            -
                     
     | 
| 
       343 
     | 
    
         
            -
             
     | 
| 
       344 
     | 
    
         
            -
             
     | 
| 
       345 
     | 
    
         
            -
                      @ 
     | 
| 
       346 
     | 
    
         
            -
             
     | 
| 
       347 
     | 
    
         
            -
                        @connection_established = true
         
     | 
| 
       348 
     | 
    
         
            -
                        @connection_cv.broadcast
         
     | 
| 
       349 
     | 
    
         
            -
                      end
         
     | 
| 
      
 315 
     | 
    
         
            +
                    ep = event[:data]
         
     | 
| 
      
 316 
     | 
    
         
            +
                    @mutex.synchronize do
         
     | 
| 
      
 317 
     | 
    
         
            +
                      @rpc_endpoint = ep
         
     | 
| 
      
 318 
     | 
    
         
            +
                      @connection_established = true
         
     | 
| 
      
 319 
     | 
    
         
            +
                      @connection_cv.broadcast
         
     | 
| 
       350 
320 
     | 
    
         
             
                    end
         
     | 
| 
       351 
321 
     | 
    
         
             
                  when 'message'
         
     | 
| 
       352 
322 
     | 
    
         
             
                    begin
         
     | 
| 
         @@ -462,78 +432,56 @@ module MCPClient 
     | 
|
| 
       462 
432 
     | 
    
         
             
                def send_jsonrpc_request(request)
         
     | 
| 
       463 
433 
     | 
    
         
             
                  @logger.debug("Sending JSON-RPC request: #{request.to_json}")
         
     | 
| 
       464 
434 
     | 
    
         
             
                  uri = URI.parse(@base_url)
         
     | 
| 
       465 
     | 
    
         
            -
                   
     | 
| 
       466 
     | 
    
         
            -
             
     | 
| 
       467 
     | 
    
         
            -
             
     | 
| 
       468 
     | 
    
         
            -
             
     | 
| 
       469 
     | 
    
         
            -
                     
     | 
| 
      
 435 
     | 
    
         
            +
                  base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
         
     | 
| 
      
 436 
     | 
    
         
            +
                  rpc_ep = @mutex.synchronize { @rpc_endpoint }
         
     | 
| 
      
 437 
     | 
    
         
            +
             
     | 
| 
      
 438 
     | 
    
         
            +
                  @rpc_conn ||= Faraday.new(url: base) do |f|
         
     | 
| 
      
 439 
     | 
    
         
            +
                    f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
         
     | 
| 
      
 440 
     | 
    
         
            +
                    f.options.open_timeout = @read_timeout
         
     | 
| 
      
 441 
     | 
    
         
            +
                    f.options.timeout = @read_timeout
         
     | 
| 
      
 442 
     | 
    
         
            +
                    f.adapter Faraday.default_adapter
         
     | 
| 
       470 
443 
     | 
    
         
             
                  end
         
     | 
| 
       471 
444 
     | 
    
         | 
| 
       472 
     | 
    
         
            -
                   
     | 
| 
       473 
     | 
    
         
            -
             
     | 
| 
       474 
     | 
    
         
            -
             
     | 
| 
       475 
     | 
    
         
            -
             
     | 
| 
       476 
     | 
    
         
            -
             
     | 
| 
       477 
     | 
    
         
            -
             
     | 
| 
       478 
     | 
    
         
            -
             
     | 
| 
       479 
     | 
    
         
            -
             
     | 
| 
       480 
     | 
    
         
            -
             
     | 
| 
       481 
     | 
    
         
            -
             
     | 
| 
       482 
     | 
    
         
            -
             
     | 
| 
       483 
     | 
    
         
            -
             
     | 
| 
       484 
     | 
    
         
            -
                            end
         
     | 
| 
       485 
     | 
    
         
            -
             
     | 
| 
       486 
     | 
    
         
            -
                      uri = URI.parse(url)
         
     | 
| 
       487 
     | 
    
         
            -
                      http_request = Net::HTTP::Post.new(uri)
         
     | 
| 
       488 
     | 
    
         
            -
                      http_request.content_type = 'application/json'
         
     | 
| 
       489 
     | 
    
         
            -
                      http_request.body = request.to_json
         
     | 
| 
       490 
     | 
    
         
            -
             
     | 
| 
       491 
     | 
    
         
            -
                      headers = @mutex.synchronize { @headers.dup }
         
     | 
| 
       492 
     | 
    
         
            -
                      headers.except('Accept', 'Cache-Control')
         
     | 
| 
       493 
     | 
    
         
            -
                             .each { |k, v| http_request[k] = v }
         
     | 
| 
      
 445 
     | 
    
         
            +
                  response = @rpc_conn.post(rpc_ep) do |req|
         
     | 
| 
      
 446 
     | 
    
         
            +
                    req.headers['Content-Type'] = 'application/json'
         
     | 
| 
      
 447 
     | 
    
         
            +
                    req.headers['Accept'] = 'application/json'
         
     | 
| 
      
 448 
     | 
    
         
            +
                    (@headers.dup.tap do |h|
         
     | 
| 
      
 449 
     | 
    
         
            +
                      h.delete('Accept')
         
     | 
| 
      
 450 
     | 
    
         
            +
                      h.delete('Cache-Control')
         
     | 
| 
      
 451 
     | 
    
         
            +
                    end).each do |k, v|
         
     | 
| 
      
 452 
     | 
    
         
            +
                      req.headers[k] = v
         
     | 
| 
      
 453 
     | 
    
         
            +
                    end
         
     | 
| 
      
 454 
     | 
    
         
            +
                    req.body = request.to_json
         
     | 
| 
      
 455 
     | 
    
         
            +
                  end
         
     | 
| 
      
 456 
     | 
    
         
            +
                  @logger.debug("Received JSON-RPC response: #{response.status} #{response.body}")
         
     | 
| 
       494 
457 
     | 
    
         | 
| 
       495 
     | 
    
         
            -
             
     | 
| 
       496 
     | 
    
         
            -
             
     | 
| 
      
 458 
     | 
    
         
            +
                  unless response.success?
         
     | 
| 
      
 459 
     | 
    
         
            +
                    raise MCPClient::Errors::ServerError, "Server returned error: #{response.status} #{response.reason_phrase}"
         
     | 
| 
      
 460 
     | 
    
         
            +
                  end
         
     | 
| 
       497 
461 
     | 
    
         | 
| 
       498 
     | 
    
         
            -
             
     | 
| 
       499 
     | 
    
         
            -
             
     | 
| 
      
 462 
     | 
    
         
            +
                  if @use_sse
         
     | 
| 
      
 463 
     | 
    
         
            +
                    # Wait for result via SSE channel
         
     | 
| 
      
 464 
     | 
    
         
            +
                    request_id = request[:id]
         
     | 
| 
      
 465 
     | 
    
         
            +
                    start_time = Time.now
         
     | 
| 
      
 466 
     | 
    
         
            +
                    timeout = @read_timeout || 10
         
     | 
| 
      
 467 
     | 
    
         
            +
                    loop do
         
     | 
| 
      
 468 
     | 
    
         
            +
                      result = nil
         
     | 
| 
      
 469 
     | 
    
         
            +
                      @mutex.synchronize do
         
     | 
| 
      
 470 
     | 
    
         
            +
                        result = @sse_results.delete(request_id) if @sse_results.key?(request_id)
         
     | 
| 
       500 
471 
     | 
    
         
             
                      end
         
     | 
| 
      
 472 
     | 
    
         
            +
                      return result if result
         
     | 
| 
      
 473 
     | 
    
         
            +
                      break if Time.now - start_time > timeout
         
     | 
| 
       501 
474 
     | 
    
         | 
| 
       502 
     | 
    
         
            -
                       
     | 
| 
       503 
     | 
    
         
            -
             
     | 
| 
       504 
     | 
    
         
            -
             
     | 
| 
       505 
     | 
    
         
            -
             
     | 
| 
       506 
     | 
    
         
            -
             
     | 
| 
       507 
     | 
    
         
            -
             
     | 
| 
       508 
     | 
    
         
            -
             
     | 
| 
       509 
     | 
    
         
            -
             
     | 
| 
       510 
     | 
    
         
            -
             
     | 
| 
       511 
     | 
    
         
            -
                            if @sse_results[request_id]
         
     | 
| 
       512 
     | 
    
         
            -
                              result = @sse_results[request_id]
         
     | 
| 
       513 
     | 
    
         
            -
                              @sse_results.delete(request_id)
         
     | 
| 
       514 
     | 
    
         
            -
                            end
         
     | 
| 
       515 
     | 
    
         
            -
                          end
         
     | 
| 
       516 
     | 
    
         
            -
             
     | 
| 
       517 
     | 
    
         
            -
                          break if result || (Time.now - start_time > timeout)
         
     | 
| 
       518 
     | 
    
         
            -
             
     | 
| 
       519 
     | 
    
         
            -
                          sleep 0.1
         
     | 
| 
       520 
     | 
    
         
            -
                        end
         
     | 
| 
       521 
     | 
    
         
            -
             
     | 
| 
       522 
     | 
    
         
            -
                        return result if result
         
     | 
| 
       523 
     | 
    
         
            -
             
     | 
| 
       524 
     | 
    
         
            -
                        raise MCPClient::Errors::ToolCallError, "Timeout waiting for SSE result for request #{request_id}"
         
     | 
| 
       525 
     | 
    
         
            -
             
     | 
| 
       526 
     | 
    
         
            -
                      else
         
     | 
| 
       527 
     | 
    
         
            -
                        begin
         
     | 
| 
       528 
     | 
    
         
            -
                          data = JSON.parse(response.body)
         
     | 
| 
       529 
     | 
    
         
            -
                          return data['result']
         
     | 
| 
       530 
     | 
    
         
            -
                        rescue JSON::ParserError => e
         
     | 
| 
       531 
     | 
    
         
            -
                          raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
         
     | 
| 
       532 
     | 
    
         
            -
                        end
         
     | 
| 
       533 
     | 
    
         
            -
                      end
         
     | 
| 
      
 475 
     | 
    
         
            +
                      sleep 0.1
         
     | 
| 
      
 476 
     | 
    
         
            +
                    end
         
     | 
| 
      
 477 
     | 
    
         
            +
                    raise MCPClient::Errors::ToolCallError, "Timeout waiting for SSE result for request #{request_id}"
         
     | 
| 
      
 478 
     | 
    
         
            +
                  else
         
     | 
| 
      
 479 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 480 
     | 
    
         
            +
                      data = JSON.parse(response.body)
         
     | 
| 
      
 481 
     | 
    
         
            +
                      data['result']
         
     | 
| 
      
 482 
     | 
    
         
            +
                    rescue JSON::ParserError => e
         
     | 
| 
      
 483 
     | 
    
         
            +
                      raise MCPClient::Errors::TransportError, "Invalid JSON response from server: #{e.message}"
         
     | 
| 
       534 
484 
     | 
    
         
             
                    end
         
     | 
| 
       535 
     | 
    
         
            -
                  ensure
         
     | 
| 
       536 
     | 
    
         
            -
                    rpc_http.finish if rpc_http.started?
         
     | 
| 
       537 
485 
     | 
    
         
             
                  end
         
     | 
| 
       538 
486 
     | 
    
         
             
                end
         
     | 
| 
       539 
487 
     | 
    
         
             
              end
         
     | 
    
        data/lib/mcp_client/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | 
         @@ -1,15 +1,43 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: ruby-mcp-client
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 0. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 0.5.0
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Szymon Kurcab
         
     | 
| 
       8 
8 
     | 
    
         
             
            autorequire:
         
     | 
| 
       9 
9 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       10 
10 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       11 
     | 
    
         
            -
            date: 2025-04- 
     | 
| 
      
 11 
     | 
    
         
            +
            date: 2025-04-25 00:00:00.000000000 Z
         
     | 
| 
       12 
12 
     | 
    
         
             
            dependencies:
         
     | 
| 
      
 13 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 14 
     | 
    
         
            +
              name: faraday
         
     | 
| 
      
 15 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 16 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 17 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 18 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 19 
     | 
    
         
            +
                    version: '2.0'
         
     | 
| 
      
 20 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 21 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 22 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 23 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 24 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 25 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 26 
     | 
    
         
            +
                    version: '2.0'
         
     | 
| 
      
 27 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 28 
     | 
    
         
            +
              name: faraday-retry
         
     | 
| 
      
 29 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 30 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 31 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 32 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 33 
     | 
    
         
            +
                    version: '2.0'
         
     | 
| 
      
 34 
     | 
    
         
            +
              type: :runtime
         
     | 
| 
      
 35 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 36 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 37 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 38 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 39 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 40 
     | 
    
         
            +
                    version: '2.0'
         
     | 
| 
       13 
41 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
42 
     | 
    
         
             
              name: rdoc
         
     | 
| 
       15 
43 
     | 
    
         
             
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
         @@ -98,7 +126,7 @@ required_ruby_version: !ruby/object:Gem::Requirement 
     | 
|
| 
       98 
126 
     | 
    
         
             
              requirements:
         
     | 
| 
       99 
127 
     | 
    
         
             
              - - ">="
         
     | 
| 
       100 
128 
     | 
    
         
             
                - !ruby/object:Gem::Version
         
     | 
| 
       101 
     | 
    
         
            -
                  version: 2. 
     | 
| 
      
 129 
     | 
    
         
            +
                  version: 3.2.0
         
     | 
| 
       102 
130 
     | 
    
         
             
            required_rubygems_version: !ruby/object:Gem::Requirement
         
     | 
| 
       103 
131 
     | 
    
         
             
              requirements:
         
     | 
| 
       104 
132 
     | 
    
         
             
              - - ">="
         
     |