ruby-mcp-client 0.6.1 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa54ff918901d9386e2d395408c4ae23707cdc7b0f7162f348e31d24c163bc39
4
- data.tar.gz: 77b384d45849e900e11b49647602cbd681511c39ae4a03cabdbfc3571f4cf38d
3
+ metadata.gz: 0f797750d8f5a6f1b742411418f77c8352cc89e8879e77ba51bcb7257f9de6db
4
+ data.tar.gz: 104e26ec1506bebae21b5b4e70508db0159d3ef8b9b69148720fed202633d7e8
5
5
  SHA512:
6
- metadata.gz: 8702eef3f53b5a77f5323d215d8be67678cb5148163443b94f986c03933366b23b1eeac5428e0ccbe4448aa9bf3a322f6556a29383c9c22c697685bcd483143e
7
- data.tar.gz: 108332fb14663b1c65363b4b78766fee383a0b48ee8ff5f621dabf0b340accaf435e790246713f23e1cb0653b3852b2d3bbe9725e321202933b2d8ecf6fa1d3b
6
+ metadata.gz: 20aa894d62ed87e70d50f92b7ce4b60839855f7ce9697f476c7822dcf99167f62030bfbd90f1d5c50d535cc78a075e35f25086e0b3f09d57c3d0bd9f4c8ff35e
7
+ data.tar.gz: dd252c97f599834a3f1e4f9a3acf0c7e98489c247c4601a3d038f8af646c431ef0245ed00b33cb61ea387c296ee5c63c99a8487bf8b9b3ed68be5bfd4e5a5f55
data/README.md CHANGED
@@ -30,6 +30,8 @@ via different transport mechanisms:
30
30
 
31
31
  - **Standard I/O**: Local processes implementing the MCP protocol
32
32
  - **Server-Sent Events (SSE)**: Remote MCP servers over HTTP with streaming support
33
+ - **HTTP**: Remote MCP servers over HTTP request/response (non-streaming Streamable HTTP)
34
+ - **Streamable HTTP**: Remote MCP servers that use HTTP POST with Server-Sent Event formatted responses
33
35
 
34
36
  The core client resides in `MCPClient::Client` and provides helper methods for integrating
35
37
  with popular AI services with built-in conversions:
@@ -56,7 +58,7 @@ client = MCPClient.create_client(
56
58
  MCPClient.sse_config(
57
59
  base_url: 'https://api.example.com/sse',
58
60
  headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
59
- name: 'api', # Optional name for this server
61
+ name: 'sse_api', # Optional name for this server
60
62
  read_timeout: 30, # Optional timeout in seconds (default: 30)
61
63
  ping: 10, # Optional ping interval in seconds of inactivity (default: 10)
62
64
  # Connection closes automatically after inactivity (2.5x ping interval)
@@ -64,7 +66,19 @@ client = MCPClient.create_client(
64
66
  retry_backoff: 1, # Optional backoff delay in seconds (default: 1)
65
67
  # Native support for tool streaming via call_tool_streaming method
66
68
  logger: Logger.new($stdout, level: Logger::INFO) # Optional logger for this server
67
- ) ],
69
+ ),
70
+ # Remote HTTP server (request/response without streaming)
71
+ MCPClient.http_config(
72
+ base_url: 'https://api.example.com',
73
+ endpoint: '/rpc', # Optional JSON-RPC endpoint path (default: '/rpc')
74
+ headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
75
+ name: 'http_api', # Optional name for this server
76
+ read_timeout: 30, # Optional timeout in seconds (default: 30)
77
+ retries: 3, # Optional number of retry attempts (default: 3)
78
+ retry_backoff: 1, # Optional backoff delay in seconds (default: 1)
79
+ logger: Logger.new($stdout, level: Logger::INFO) # Optional logger for this server
80
+ )
81
+ ],
68
82
  # Optional logger for the client and all servers without explicit loggers
69
83
  logger: Logger.new($stdout, level: Logger::WARN)
70
84
  )
@@ -78,11 +92,12 @@ client = MCPClient.create_client(
78
92
  # MCP server configuration JSON format can be:
79
93
  # 1. A single server object:
80
94
  # { "type": "sse", "url": "http://example.com/sse" }
95
+ # { "type": "http", "url": "http://example.com", "endpoint": "/rpc" }
81
96
  # 2. An array of server objects:
82
- # [{ "type": "stdio", "command": "npx server" }, { "type": "sse", "url": "http://..." }]
97
+ # [{ "type": "stdio", "command": "npx server" }, { "type": "sse", "url": "http://..." }, { "type": "http", "url": "http://..." }]
83
98
  # 3. An object with "mcpServers" key containing named servers:
84
- # { "mcpServers": { "server1": { "type": "sse", "url": "http://..." } } }
85
- # Note: When using this format, server1 will be accessible by name
99
+ # { "mcpServers": { "server1": { "type": "sse", "url": "http://..." }, "server2": { "type": "http", "url": "http://..." } } }
100
+ # Note: When using this format, server1/server2 will be accessible by name
86
101
 
87
102
  # List available tools
88
103
  tools = client.list_tools
@@ -143,6 +158,114 @@ client.clear_cache
143
158
  client.cleanup
144
159
  ```
145
160
 
161
+ ### HTTP Transport Example
162
+
163
+ The HTTP transport provides simple request/response communication with MCP servers:
164
+
165
+ ```ruby
166
+ require 'mcp_client'
167
+ require 'logger'
168
+
169
+ # Optional logger for debugging
170
+ logger = Logger.new($stdout)
171
+ logger.level = Logger::INFO
172
+
173
+ # Create an MCP client that connects to an HTTP MCP server
174
+ http_client = MCPClient.create_client(
175
+ mcp_server_configs: [
176
+ MCPClient.http_config(
177
+ base_url: 'https://api.example.com',
178
+ endpoint: '/mcp', # JSON-RPC endpoint path
179
+ headers: {
180
+ 'Authorization' => 'Bearer YOUR_API_TOKEN',
181
+ 'X-Custom-Header' => 'custom-value'
182
+ },
183
+ read_timeout: 30, # Timeout in seconds for HTTP requests
184
+ retries: 3, # Number of retry attempts on transient errors
185
+ retry_backoff: 1, # Base delay in seconds for exponential backoff
186
+ logger: logger # Optional logger for debugging HTTP requests
187
+ )
188
+ ]
189
+ )
190
+
191
+ # List available tools
192
+ tools = http_client.list_tools
193
+
194
+ # Call a tool
195
+ result = http_client.call_tool('analyze_data', {
196
+ dataset: 'sales_2024',
197
+ metrics: ['revenue', 'conversion_rate']
198
+ })
199
+
200
+ # HTTP transport also supports streaming (though implemented as single response)
201
+ # This provides API compatibility with SSE transport
202
+ http_client.call_tool_streaming('process_batch', { batch_id: 123 }).each do |result|
203
+ puts "Processing result: #{result}"
204
+ end
205
+
206
+ # Send custom JSON-RPC requests
207
+ custom_result = http_client.send_rpc('custom_method', params: { key: 'value' })
208
+
209
+ # Send notifications (fire-and-forget)
210
+ http_client.send_notification('status_update', params: { status: 'processing' })
211
+
212
+ # Test connectivity
213
+ ping_result = http_client.ping
214
+ puts "Server is responsive: #{ping_result.inspect}"
215
+
216
+ # Clean up
217
+ http_client.cleanup
218
+ ```
219
+
220
+ ### Streamable HTTP Transport Example
221
+
222
+ The Streamable HTTP transport is designed for servers that use HTTP POST requests but return Server-Sent Event formatted responses. This is commonly used by services like Zapier's MCP implementation:
223
+
224
+ ```ruby
225
+ require 'mcp_client'
226
+ require 'logger'
227
+
228
+ # Optional logger for debugging
229
+ logger = Logger.new($stdout)
230
+ logger.level = Logger::INFO
231
+
232
+ # Create an MCP client that connects to a Streamable HTTP MCP server
233
+ streamable_client = MCPClient.create_client(
234
+ mcp_server_configs: [
235
+ MCPClient.streamable_http_config(
236
+ base_url: 'https://mcp.zapier.com/api/mcp/s/YOUR_SESSION_ID/mcp',
237
+ headers: {
238
+ 'Authorization' => 'Bearer YOUR_ZAPIER_TOKEN'
239
+ },
240
+ read_timeout: 60, # Timeout in seconds for HTTP requests
241
+ retries: 3, # Number of retry attempts on transient errors
242
+ retry_backoff: 2, # Base delay in seconds for exponential backoff
243
+ logger: logger # Optional logger for debugging requests
244
+ )
245
+ ]
246
+ )
247
+
248
+ # List available tools (server responds with SSE-formatted JSON)
249
+ tools = streamable_client.list_tools
250
+ puts "Found #{tools.size} tools:"
251
+ tools.each { |tool| puts "- #{tool.name}: #{tool.description}" }
252
+
253
+ # Call a tool (response will be in SSE format)
254
+ result = streamable_client.call_tool('google_calendar_find_event', {
255
+ instructions: 'Find today\'s meetings',
256
+ calendarid: 'primary'
257
+ })
258
+
259
+ # The client automatically parses SSE responses like:
260
+ # event: message
261
+ # data: {"jsonrpc":"2.0","id":1,"result":{"content":[...]}}
262
+
263
+ puts "Tool result: #{result.inspect}"
264
+
265
+ # Clean up
266
+ streamable_client.cleanup
267
+ ```
268
+
146
269
  ### Server-Sent Events (SSE) Example
147
270
 
148
271
  The SSE transport provides robust connection handling for remote MCP servers:
@@ -313,6 +436,15 @@ You can define MCP server configurations in JSON files for easier management:
313
436
  "Authorization": "Bearer TOKEN"
314
437
  }
315
438
  },
439
+ "api_server": {
440
+ "type": "http",
441
+ "url": "https://api.example.com",
442
+ "endpoint": "/mcp",
443
+ "headers": {
444
+ "Authorization": "Bearer API_TOKEN",
445
+ "X-Custom-Header": "value"
446
+ }
447
+ },
316
448
  "filesystem": {
317
449
  "type": "stdio",
318
450
  "command": "npx",
@@ -346,20 +478,82 @@ client = MCPClient.create_client(server_definition_file: 'path/to/definition.jso
346
478
  ```
347
479
 
348
480
  The JSON format supports:
349
- 1. A single server object: `{ "type": "sse", "url": "..." }`
350
- 2. An array of server objects: `[{ "type": "stdio", ... }, { "type": "sse", ... }]`
481
+ 1. A single server object: `{ "type": "sse", "url": "..." }` or `{ "type": "http", "url": "..." }`
482
+ 2. An array of server objects: `[{ "type": "stdio", ... }, { "type": "sse", ... }, { "type": "http", ... }]`
351
483
  3. An object with named servers under `mcpServers` key (as shown above)
352
484
 
353
485
  Special configuration options:
354
486
  - `comment` and `description` are reserved keys that are ignored during parsing and can be used for documentation
355
- - Server type can be inferred from the presence of either `command` (for stdio) or `url` (for SSE)
487
+ - Server type can be inferred from the presence of either `command` (for stdio) or `url` (for SSE/HTTP)
488
+ - For HTTP servers, `endpoint` specifies the JSON-RPC endpoint path (defaults to '/rpc' if not specified)
356
489
  - All string values in arrays (like `args`) are automatically converted to strings
357
490
 
491
+ ## Session-Based MCP Protocol Support
492
+
493
+ Both HTTP and Streamable HTTP transports now support session-based MCP servers that require session continuity:
494
+
495
+ ### Session Management Features
496
+
497
+ - **Automatic Session Management**: Captures session IDs from `initialize` response headers
498
+ - **Session Header Injection**: Automatically includes `Mcp-Session-Id` header in subsequent requests
499
+ - **Session Termination**: Sends HTTP DELETE requests to properly terminate sessions during cleanup
500
+ - **Session Validation**: Validates session ID format for security (8-128 alphanumeric characters with hyphens/underscores)
501
+ - **Backward Compatibility**: Works with both session-based and stateless MCP servers
502
+ - **Session Cleanup**: Properly cleans up session state during connection teardown
503
+
504
+ ### Resumability and Redelivery (Streamable HTTP)
505
+
506
+ The Streamable HTTP transport provides additional resumability features for reliable message delivery:
507
+
508
+ - **Event ID Tracking**: Automatically tracks event IDs from SSE responses
509
+ - **Last-Event-ID Header**: Includes `Last-Event-ID` header in requests for resuming from disconnection points
510
+ - **Message Replay**: Enables servers to replay missed messages from the last received event
511
+ - **Connection Recovery**: Maintains message continuity even with unstable network connections
512
+
513
+ ### Security Features
514
+
515
+ Both transports implement security best practices:
516
+
517
+ - **URL Validation**: Validates server URLs to ensure only HTTP/HTTPS protocols are used
518
+ - **Session ID Validation**: Enforces secure session ID formats to prevent malicious injection
519
+ - **Security Warnings**: Logs warnings for potentially insecure configurations (e.g., 0.0.0.0 binding)
520
+ - **Header Sanitization**: Properly handles and validates all session-related headers
521
+
522
+ ### Usage
523
+
524
+ The session support is transparent to the user - no additional configuration is required. The client will automatically detect and handle session-based servers by:
525
+
526
+ 1. **Session Initialization**: Capturing the `Mcp-Session-Id` header from the `initialize` response
527
+ 2. **Session Persistence**: Including this header in all subsequent requests (except `initialize`)
528
+ 3. **Session Termination**: Sending HTTP DELETE request with session ID during cleanup
529
+ 4. **Resumability** (Streamable HTTP): Tracking event IDs and including `Last-Event-ID` for message replay
530
+ 5. **Security Validation**: Validating session IDs and server URLs for security
531
+ 6. **Logging**: Comprehensive logging of session activity for debugging purposes
532
+
533
+ Example of automatic session termination:
534
+
535
+ ```ruby
536
+ # Session is automatically terminated when client is cleaned up
537
+ client = MCPClient.create_client(
538
+ mcp_server_configs: [
539
+ MCPClient.http_config(base_url: 'https://api.example.com/mcp')
540
+ ]
541
+ )
542
+
543
+ # Use the client...
544
+ tools = client.list_tools
545
+
546
+ # Session automatically terminated with HTTP DELETE request
547
+ client.cleanup
548
+ ```
549
+
550
+ This enables compatibility with MCP servers that maintain state between requests and require session identification.
551
+
358
552
  ## Key Features
359
553
 
360
554
  ### Client Features
361
555
 
362
- - **Multiple transports** - Support for both stdio and SSE transports
556
+ - **Multiple transports** - Support for stdio, SSE, HTTP, and Streamable HTTP transports
363
557
  - **Multiple servers** - Connect to multiple MCP servers simultaneously
364
558
  - **Named servers** - Associate names with servers and find/reference them by name
365
559
  - **Server lookup** - Find servers by name using `find_server`
@@ -404,6 +598,47 @@ The SSE client implementation provides these key features:
404
598
  - **URL normalization**: Consistent URL handling that respects user-provided formats
405
599
  - **Server connectivity check**: Built-in `ping` method to test server connectivity and health
406
600
 
601
+ ### HTTP Transport Implementation
602
+
603
+ The HTTP transport provides a simpler, stateless communication mechanism for MCP servers:
604
+
605
+ - **Request/Response Model**: Standard HTTP request/response cycle for each JSON-RPC call
606
+ - **JSON-Only Responses**: Accepts only `application/json` responses (no SSE support)
607
+ - **Session Support**: Automatic session header (`Mcp-Session-Id`) capture and injection for session-based MCP servers
608
+ - **Session Termination**: Proper session cleanup with HTTP DELETE requests during connection teardown
609
+ - **Session Validation**: Security validation of session IDs to prevent malicious injection
610
+ - **Stateless & Stateful**: Supports both stateless servers and session-based servers that require state continuity
611
+ - **HTTP Headers Support**: Full support for custom headers including authorization, API keys, and other metadata
612
+ - **Reliable Error Handling**: Comprehensive HTTP status code handling with appropriate error mapping
613
+ - **Configurable Retries**: Exponential backoff retry logic for transient network failures
614
+ - **Connection Pooling**: Uses Faraday's connection pooling for efficient HTTP connections
615
+ - **Timeout Management**: Configurable timeouts for both connection establishment and request completion
616
+ - **JSON-RPC over HTTP**: Full JSON-RPC 2.0 implementation over HTTP POST requests
617
+ - **MCP Protocol Compliance**: Supports all standard MCP methods (initialize, tools/list, tools/call)
618
+ - **Custom RPC Methods**: Send any custom JSON-RPC method or notification
619
+ - **Thread Safety**: All operations are thread-safe for concurrent usage
620
+ - **Streaming API Compatibility**: Provides `call_tool_streaming` method for API compatibility (returns single response)
621
+ - **Graceful Degradation**: Simple fallback behavior when complex features aren't needed
622
+
623
+ ### Streamable HTTP Transport Implementation
624
+
625
+ The Streamable HTTP transport bridges HTTP and Server-Sent Events, designed for servers that use HTTP POST but return SSE-formatted responses:
626
+
627
+ - **Hybrid Communication**: HTTP POST requests with Server-Sent Event formatted responses
628
+ - **SSE Response Parsing**: Automatically parses `event:` and `data:` lines from SSE responses
629
+ - **Session Support**: Automatic session header (`Mcp-Session-Id`) capture and injection for session-based MCP servers
630
+ - **Session Termination**: Proper session cleanup with HTTP DELETE requests during connection teardown
631
+ - **Resumability**: Event ID tracking and `Last-Event-ID` header support for message replay after disconnections
632
+ - **Session Validation**: Security validation of session IDs to prevent malicious injection
633
+ - **HTTP Semantics**: Maintains standard HTTP request/response model for client compatibility
634
+ - **Streaming Format Support**: Handles complex SSE responses with multiple fields (event, id, retry, etc.)
635
+ - **Error Handling**: Comprehensive error handling for both HTTP and SSE parsing failures
636
+ - **Headers Optimization**: Includes SSE-compatible headers (`Accept: text/event-stream, application/json`, `Cache-Control: no-cache`)
637
+ - **JSON-RPC Compliance**: Full JSON-RPC 2.0 support over the hybrid HTTP/SSE transport
638
+ - **Retry Logic**: Exponential backoff for both connection and parsing failures
639
+ - **Thread Safety**: All operations are thread-safe for concurrent usage
640
+ - **Malformed Response Handling**: Graceful handling of invalid SSE format or missing data lines
641
+
407
642
  ## Requirements
408
643
 
409
644
  - Ruby >= 3.2.0
@@ -413,7 +648,7 @@ The SSE client implementation provides these key features:
413
648
 
414
649
  To implement a compatible MCP server you must:
415
650
 
416
- - Listen on your chosen transport (JSON-RPC stdio, or HTTP SSE)
651
+ - Listen on your chosen transport (JSON-RPC stdio, HTTP SSE, HTTP, or Streamable HTTP)
417
652
  - Respond to `list_tools` requests with a JSON list of tools
418
653
  - Respond to `call_tool` requests by executing the specified tool
419
654
  - Return results (or errors) in JSON format
@@ -70,7 +70,7 @@ module MCPClient
70
70
  if tools.empty? && !servers.empty?
71
71
  raise connection_errors.first if connection_errors.any?
72
72
 
73
- raise MCPClient::Errors::ToolCallError, 'Failed to retrieve tools from any server'
73
+ @logger.warn('No tools found from any server.')
74
74
  end
75
75
 
76
76
  tools
@@ -87,6 +87,10 @@ module MCPClient
87
87
  parse_stdio_config(clean, config, server_name)
88
88
  when 'sse'
89
89
  return nil unless parse_sse_config(clean, config, server_name)
90
+ when 'streamable_http'
91
+ return nil unless parse_streamable_http_config(clean, config, server_name)
92
+ when 'http'
93
+ return nil unless parse_http_config(clean, config, server_name)
90
94
  else
91
95
  @logger.warn("Unrecognized type '#{type}' for server '#{server_name}'; skipping.")
92
96
  return nil
@@ -106,7 +110,9 @@ module MCPClient
106
110
  inferred_type = if config.key?('command') || config.key?('args') || config.key?('env')
107
111
  'stdio'
108
112
  elsif config.key?('url')
109
- 'sse'
113
+ # Default to streamable_http unless URL contains "sse"
114
+ url = config['url'].to_s.downcase
115
+ url.include?('sse') ? 'sse' : 'streamable_http'
110
116
  end
111
117
 
112
118
  if inferred_type
@@ -181,6 +187,72 @@ module MCPClient
181
187
  true
182
188
  end
183
189
 
190
+ # Parse Streamable HTTP-specific configuration
191
+ # @param clean [Hash] clean configuration hash to update
192
+ # @param config [Hash] raw configuration from JSON
193
+ # @param server_name [String] name of the server for error reporting
194
+ # @return [Boolean] true if parsing succeeded, false if required elements are missing
195
+ def parse_streamable_http_config(clean, config, server_name)
196
+ # URL is required
197
+ source = config['url']
198
+ unless source
199
+ @logger.warn("Streamable HTTP server '#{server_name}' is missing required 'url' property; skipping.")
200
+ return false
201
+ end
202
+
203
+ unless source.is_a?(String)
204
+ @logger.warn("'url' for server '#{server_name}' is not a string; converting to string.")
205
+ source = source.to_s
206
+ end
207
+
208
+ # Headers are optional
209
+ headers = config['headers']
210
+ headers = headers.is_a?(Hash) ? headers.transform_keys(&:to_s) : {}
211
+
212
+ # Endpoint is optional (defaults to '/rpc' in the transport)
213
+ endpoint = config['endpoint']
214
+ endpoint = endpoint.to_s if endpoint && !endpoint.is_a?(String)
215
+
216
+ # Update clean config
217
+ clean[:url] = source
218
+ clean[:headers] = headers
219
+ clean[:endpoint] = endpoint if endpoint
220
+ true
221
+ end
222
+
223
+ # Parse HTTP-specific configuration
224
+ # @param clean [Hash] clean configuration hash to update
225
+ # @param config [Hash] raw configuration from JSON
226
+ # @param server_name [String] name of the server for error reporting
227
+ # @return [Boolean] true if parsing succeeded, false if required elements are missing
228
+ def parse_http_config(clean, config, server_name)
229
+ # URL is required
230
+ source = config['url']
231
+ unless source
232
+ @logger.warn("HTTP server '#{server_name}' is missing required 'url' property; skipping.")
233
+ return false
234
+ end
235
+
236
+ unless source.is_a?(String)
237
+ @logger.warn("'url' for server '#{server_name}' is not a string; converting to string.")
238
+ source = source.to_s
239
+ end
240
+
241
+ # Headers are optional
242
+ headers = config['headers']
243
+ headers = headers.is_a?(Hash) ? headers.transform_keys(&:to_s) : {}
244
+
245
+ # Endpoint is optional (defaults to '/rpc' in the transport)
246
+ endpoint = config['endpoint']
247
+ endpoint = endpoint.to_s if endpoint && !endpoint.is_a?(String)
248
+
249
+ # Update clean config
250
+ clean[:url] = source
251
+ clean[:headers] = headers
252
+ clean[:endpoint] = endpoint if endpoint
253
+ true
254
+ end
255
+
184
256
  # Filter out reserved keys from configuration objects
185
257
  # @param data [Hash] configuration data
186
258
  # @return [Hash] filtered configuration data