ruby-mcp-client 0.8.0 → 0.9.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.
@@ -11,6 +11,12 @@ require 'faraday/follow_redirects'
11
11
  module MCPClient
12
12
  # Implementation of MCP server that communicates via Server-Sent Events (SSE)
13
13
  # Useful for communicating with remote MCP servers over HTTP
14
+ #
15
+ # @note Elicitation Support (MCP 2025-06-18)
16
+ # This transport FULLY supports server-initiated elicitation requests via bidirectional
17
+ # JSON-RPC. The server sends elicitation/create requests via the SSE stream, and the
18
+ # client responds via HTTP POST to the RPC endpoint. This provides full elicitation
19
+ # capability for remote servers.
14
20
  class ServerSSE < ServerBase
15
21
  require_relative 'server_sse/sse_parser'
16
22
  require_relative 'server_sse/json_rpc_transport'
@@ -98,6 +104,7 @@ module MCPClient
98
104
  # Time of last activity
99
105
  @last_activity_time = Time.now
100
106
  @activity_timer_thread = nil
107
+ @elicitation_request_callback = nil # MCP 2025-06-18
101
108
  end
102
109
 
103
110
  # Stream tool call fallback for SSE transport (yields single result)
@@ -161,26 +168,34 @@ module MCPClient
161
168
  end
162
169
 
163
170
  # List all resources available from the MCP server
164
- # @return [Array<MCPClient::Resource>] list of available resources
171
+ # @param cursor [String, nil] optional cursor for pagination
172
+ # @return [Hash] result containing resources array and optional nextCursor
165
173
  # @raise [MCPClient::Errors::ServerError] if server returns an error
166
174
  # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
167
175
  # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
168
- def list_resources
176
+ def list_resources(cursor: nil)
169
177
  @mutex.synchronize do
170
- return @resources if @resources
178
+ return @resources_result if @resources_result && !cursor
171
179
  end
172
180
 
173
181
  begin
174
182
  ensure_initialized
175
183
 
176
- resources_data = request_resources_list
184
+ params = {}
185
+ params['cursor'] = cursor if cursor
186
+ result = rpc_request('resources/list', params)
187
+
188
+ resources = (result['resources'] || []).map do |resource_data|
189
+ MCPClient::Resource.from_json(resource_data, server: self)
190
+ end
191
+
192
+ resources_result = { 'resources' => resources, 'nextCursor' => result['nextCursor'] }
193
+
177
194
  @mutex.synchronize do
178
- @resources = resources_data.map do |resource_data|
179
- MCPClient::Resource.from_json(resource_data, server: self)
180
- end
195
+ @resources_result = resources_result unless cursor
181
196
  end
182
197
 
183
- @mutex.synchronize { @resources }
198
+ resources_result
184
199
  rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
185
200
  # Re-raise these errors directly
186
201
  raise
@@ -191,15 +206,15 @@ module MCPClient
191
206
 
192
207
  # Read a resource by its URI
193
208
  # @param uri [String] the URI of the resource to read
194
- # @return [Object] the resource contents
209
+ # @return [Array<MCPClient::ResourceContent>] array of resource contents
195
210
  # @raise [MCPClient::Errors::ServerError] if server returns an error
196
211
  # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
197
212
  # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
198
213
  # @raise [MCPClient::Errors::ConnectionError] if server is disconnected
199
214
  def read_resource(uri)
200
- rpc_request('resources/read', {
201
- uri: uri
202
- })
215
+ result = rpc_request('resources/read', { uri: uri })
216
+ contents = result['contents'] || []
217
+ contents.map { |content| MCPClient::ResourceContent.from_json(content) }
203
218
  rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
204
219
  # Re-raise connection/transport errors directly to match test expectations
205
220
  raise
@@ -208,6 +223,58 @@ module MCPClient
208
223
  raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
209
224
  end
210
225
 
226
+ # List all resource templates available from the MCP server
227
+ # @param cursor [String, nil] optional cursor for pagination
228
+ # @return [Hash] result containing resourceTemplates array and optional nextCursor
229
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
230
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource template listing
231
+ def list_resource_templates(cursor: nil)
232
+ ensure_initialized
233
+ params = {}
234
+ params['cursor'] = cursor if cursor
235
+ result = rpc_request('resources/templates/list', params)
236
+
237
+ templates = (result['resourceTemplates'] || []).map do |template_data|
238
+ MCPClient::ResourceTemplate.from_json(template_data, server: self)
239
+ end
240
+
241
+ { 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
242
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
243
+ raise
244
+ rescue StandardError => e
245
+ raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
246
+ end
247
+
248
+ # Subscribe to resource updates
249
+ # @param uri [String] the URI of the resource to subscribe to
250
+ # @return [Boolean] true if subscription successful
251
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
252
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during subscription
253
+ def subscribe_resource(uri)
254
+ ensure_initialized
255
+ rpc_request('resources/subscribe', { uri: uri })
256
+ true
257
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
258
+ raise
259
+ rescue StandardError => e
260
+ raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
261
+ end
262
+
263
+ # Unsubscribe from resource updates
264
+ # @param uri [String] the URI of the resource to unsubscribe from
265
+ # @return [Boolean] true if unsubscription successful
266
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
267
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during unsubscription
268
+ def unsubscribe_resource(uri)
269
+ ensure_initialized
270
+ rpc_request('resources/unsubscribe', { uri: uri })
271
+ true
272
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
273
+ raise
274
+ rescue StandardError => e
275
+ raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
276
+ end
277
+
211
278
  # List all tools available from the MCP server
212
279
  # @return [Array<MCPClient::Tool>] list of available tools
213
280
  # @raise [MCPClient::Errors::ServerError] if server returns an error
@@ -349,6 +416,125 @@ module MCPClient
349
416
  end
350
417
  end
351
418
 
419
+ # Register a callback for elicitation requests (MCP 2025-06-18)
420
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
421
+ # @return [void]
422
+ def on_elicitation_request(&block)
423
+ @elicitation_request_callback = block
424
+ end
425
+
426
+ # Handle incoming JSON-RPC request from server (MCP 2025-06-18)
427
+ # @param msg [Hash] the JSON-RPC request message
428
+ # @return [void]
429
+ def handle_server_request(msg)
430
+ request_id = msg['id']
431
+ method = msg['method']
432
+ params = msg['params'] || {}
433
+
434
+ @logger.debug("Received server request: #{method} (id: #{request_id})")
435
+
436
+ case method
437
+ when 'elicitation/create'
438
+ handle_elicitation_create(request_id, params)
439
+ else
440
+ # Unknown request method, send error response
441
+ send_error_response(request_id, -32_601, "Method not found: #{method}")
442
+ end
443
+ rescue StandardError => e
444
+ @logger.error("Error handling server request: #{e.message}")
445
+ send_error_response(request_id, -32_603, "Internal error: #{e.message}")
446
+ end
447
+
448
+ # Handle elicitation/create request from server (MCP 2025-06-18)
449
+ # @param request_id [String, Integer] the JSON-RPC request ID
450
+ # @param params [Hash] the elicitation parameters
451
+ # @return [void]
452
+ def handle_elicitation_create(request_id, params)
453
+ # If no callback is registered, decline the request
454
+ unless @elicitation_request_callback
455
+ @logger.warn('Received elicitation request but no callback registered, declining')
456
+ send_elicitation_response(request_id, { 'action' => 'decline' })
457
+ return
458
+ end
459
+
460
+ # Call the registered callback
461
+ result = @elicitation_request_callback.call(request_id, params)
462
+
463
+ # Send the response back to the server
464
+ send_elicitation_response(request_id, result)
465
+ end
466
+
467
+ # Send elicitation response back to server via HTTP POST (MCP 2025-06-18)
468
+ # @param request_id [String, Integer] the JSON-RPC request ID
469
+ # @param result [Hash] the elicitation result (action and optional content)
470
+ # @return [void]
471
+ def send_elicitation_response(request_id, result)
472
+ ensure_initialized
473
+
474
+ response = {
475
+ 'jsonrpc' => '2.0',
476
+ 'id' => request_id,
477
+ 'result' => result
478
+ }
479
+
480
+ # Send response via HTTP POST to the RPC endpoint
481
+ post_jsonrpc_response(response)
482
+ rescue StandardError => e
483
+ @logger.error("Error sending elicitation response: #{e.message}")
484
+ end
485
+
486
+ # Send error response back to server via HTTP POST (MCP 2025-06-18)
487
+ # @param request_id [String, Integer] the JSON-RPC request ID
488
+ # @param code [Integer] the error code
489
+ # @param message [String] the error message
490
+ # @return [void]
491
+ def send_error_response(request_id, code, message)
492
+ ensure_initialized
493
+
494
+ response = {
495
+ 'jsonrpc' => '2.0',
496
+ 'id' => request_id,
497
+ 'error' => {
498
+ 'code' => code,
499
+ 'message' => message
500
+ }
501
+ }
502
+
503
+ # Send response via HTTP POST to the RPC endpoint
504
+ post_jsonrpc_response(response)
505
+ rescue StandardError => e
506
+ @logger.error("Error sending error response: #{e.message}")
507
+ end
508
+
509
+ # Post a JSON-RPC response message to the server via HTTP
510
+ # @param response [Hash] the JSON-RPC response
511
+ # @return [void]
512
+ # @private
513
+ def post_jsonrpc_response(response)
514
+ unless @rpc_endpoint
515
+ @logger.error('Cannot send response: RPC endpoint not available')
516
+ return
517
+ end
518
+
519
+ # Use the same connection pattern as post_json_rpc_request
520
+ uri = URI.parse(@base_url)
521
+ base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
522
+ @rpc_conn ||= create_json_rpc_connection(base)
523
+
524
+ json_body = JSON.generate(response)
525
+
526
+ @rpc_conn.post do |req|
527
+ req.url @rpc_endpoint
528
+ req.headers['Content-Type'] = 'application/json'
529
+ @headers.each { |k, v| req.headers[k] = v }
530
+ req.body = json_body
531
+ end
532
+
533
+ @logger.debug("Sent response via HTTP POST: #{json_body}")
534
+ rescue StandardError => e
535
+ @logger.error("Failed to send response via HTTP POST: #{e.message}")
536
+ end
537
+
352
538
  private
353
539
 
354
540
  # Start the SSE thread to listen for events
@@ -34,6 +34,11 @@ module MCPClient
34
34
  raise MCPClient::Errors::ConnectionError, "Initialize failed: #{err['message']}"
35
35
  end
36
36
 
37
+ # Store server info and capabilities
38
+ result = res['result'] || {}
39
+ @server_info = result['serverInfo']
40
+ @capabilities = result['capabilities']
41
+
37
42
  # Send initialized notification
38
43
  notif = build_jsonrpc_notification('notifications/initialized', {})
39
44
  @stdin.puts(notif.to_json)
@@ -39,13 +39,24 @@ module MCPClient
39
39
  @next_id = 1
40
40
  @pending = {}
41
41
  @initialized = false
42
+ @server_info = nil
43
+ @capabilities = nil
42
44
  initialize_logger(logger)
43
45
  @max_retries = retries
44
46
  @retry_backoff = retry_backoff
45
47
  @read_timeout = read_timeout
46
48
  @env = env || {}
49
+ @elicitation_request_callback = nil # MCP 2025-06-18
47
50
  end
48
51
 
52
+ # Server info from the initialize response
53
+ # @return [Hash, nil] Server information
54
+ attr_reader :server_info
55
+
56
+ # Server capabilities from the initialize response
57
+ # @return [Hash, nil] Server capabilities
58
+ attr_reader :capabilities
59
+
49
60
  # Connect to the MCP server by launching the command process via stdin/stdout
50
61
  # @return [Boolean] true if connection was successful
51
62
  # @raise [MCPClient::Errors::ConnectionError] if connection fails
@@ -85,12 +96,20 @@ module MCPClient
85
96
  def handle_line(line)
86
97
  msg = JSON.parse(line)
87
98
  @logger.debug("Received line: #{line.chomp}")
99
+
100
+ # Dispatch JSON-RPC requests from server (has id AND method) - MCP 2025-06-18
101
+ if msg['method'] && msg.key?('id')
102
+ handle_server_request(msg)
103
+ return
104
+ end
105
+
88
106
  # Dispatch JSON-RPC notifications (no id, has method)
89
107
  if msg['method'] && !msg.key?('id')
90
108
  @notification_callback&.call(msg['method'], msg['params'])
91
109
  return
92
110
  end
93
- # Handle standard JSON-RPC responses
111
+
112
+ # Handle standard JSON-RPC responses (has id, no method)
94
113
  id = msg['id']
95
114
  return unless id
96
115
 
@@ -149,27 +168,32 @@ module MCPClient
149
168
  end
150
169
 
151
170
  # List all resources available from the MCP server
152
- # @return [Array<MCPClient::Resource>] list of available resources
171
+ # @param cursor [String, nil] optional cursor for pagination
172
+ # @return [Hash] result containing resources array and optional nextCursor
153
173
  # @raise [MCPClient::Errors::ServerError] if server returns an error
154
174
  # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
155
- def list_resources
175
+ def list_resources(cursor: nil)
156
176
  ensure_initialized
157
177
  req_id = next_id
158
- req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/list', 'params' => {} }
178
+ params = {}
179
+ params['cursor'] = cursor if cursor
180
+ req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/list', 'params' => params }
159
181
  send_request(req)
160
182
  res = wait_response(req_id)
161
183
  if (err = res['error'])
162
184
  raise MCPClient::Errors::ServerError, err['message']
163
185
  end
164
186
 
165
- (res.dig('result', 'resources') || []).map { |td| MCPClient::Resource.from_json(td, server: self) }
187
+ result = res['result'] || {}
188
+ resources = (result['resources'] || []).map { |td| MCPClient::Resource.from_json(td, server: self) }
189
+ { 'resources' => resources, 'nextCursor' => result['nextCursor'] }
166
190
  rescue StandardError => e
167
191
  raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
168
192
  end
169
193
 
170
194
  # Read a resource by its URI
171
195
  # @param uri [String] the URI of the resource to read
172
- # @return [Object] the resource contents
196
+ # @return [Array<MCPClient::ResourceContent>] array of resource contents
173
197
  # @raise [MCPClient::Errors::ServerError] if server returns an error
174
198
  # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
175
199
  def read_resource(uri)
@@ -188,11 +212,87 @@ module MCPClient
188
212
  raise MCPClient::Errors::ServerError, err['message']
189
213
  end
190
214
 
191
- res['result']
215
+ result = res['result'] || {}
216
+ contents = result['contents'] || []
217
+ contents.map { |content| MCPClient::ResourceContent.from_json(content) }
192
218
  rescue StandardError => e
193
219
  raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
194
220
  end
195
221
 
222
+ # List all resource templates available from the MCP server
223
+ # @param cursor [String, nil] optional cursor for pagination
224
+ # @return [Hash] result containing resourceTemplates array and optional nextCursor
225
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
226
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during resource template listing
227
+ def list_resource_templates(cursor: nil)
228
+ ensure_initialized
229
+ req_id = next_id
230
+ params = {}
231
+ params['cursor'] = cursor if cursor
232
+ req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/templates/list', 'params' => params }
233
+ send_request(req)
234
+ res = wait_response(req_id)
235
+ if (err = res['error'])
236
+ raise MCPClient::Errors::ServerError, err['message']
237
+ end
238
+
239
+ result = res['result'] || {}
240
+ templates = (result['resourceTemplates'] || []).map { |td| MCPClient::ResourceTemplate.from_json(td, server: self) }
241
+ { 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
242
+ rescue StandardError => e
243
+ raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
244
+ end
245
+
246
+ # Subscribe to resource updates
247
+ # @param uri [String] the URI of the resource to subscribe to
248
+ # @return [Boolean] true if subscription successful
249
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
250
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during subscription
251
+ def subscribe_resource(uri)
252
+ ensure_initialized
253
+ req_id = next_id
254
+ req = {
255
+ 'jsonrpc' => '2.0',
256
+ 'id' => req_id,
257
+ 'method' => 'resources/subscribe',
258
+ 'params' => { 'uri' => uri }
259
+ }
260
+ send_request(req)
261
+ res = wait_response(req_id)
262
+ if (err = res['error'])
263
+ raise MCPClient::Errors::ServerError, err['message']
264
+ end
265
+
266
+ true
267
+ rescue StandardError => e
268
+ raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
269
+ end
270
+
271
+ # Unsubscribe from resource updates
272
+ # @param uri [String] the URI of the resource to unsubscribe from
273
+ # @return [Boolean] true if unsubscription successful
274
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
275
+ # @raise [MCPClient::Errors::ResourceReadError] for other errors during unsubscription
276
+ def unsubscribe_resource(uri)
277
+ ensure_initialized
278
+ req_id = next_id
279
+ req = {
280
+ 'jsonrpc' => '2.0',
281
+ 'id' => req_id,
282
+ 'method' => 'resources/unsubscribe',
283
+ 'params' => { 'uri' => uri }
284
+ }
285
+ send_request(req)
286
+ res = wait_response(req_id)
287
+ if (err = res['error'])
288
+ raise MCPClient::Errors::ServerError, err['message']
289
+ end
290
+
291
+ true
292
+ rescue StandardError => e
293
+ raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
294
+ end
295
+
196
296
  # List all tools available from the MCP server
197
297
  # @return [Array<MCPClient::Tool>] list of available tools
198
298
  # @raise [MCPClient::Errors::ServerError] if server returns an error
@@ -240,6 +340,96 @@ module MCPClient
240
340
  raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
241
341
  end
242
342
 
343
+ # Register a callback for elicitation requests (MCP 2025-06-18)
344
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
345
+ # @return [void]
346
+ def on_elicitation_request(&block)
347
+ @elicitation_request_callback = block
348
+ end
349
+
350
+ # Handle incoming JSON-RPC request from server (MCP 2025-06-18)
351
+ # @param msg [Hash] the JSON-RPC request message
352
+ # @return [void]
353
+ def handle_server_request(msg)
354
+ request_id = msg['id']
355
+ method = msg['method']
356
+ params = msg['params'] || {}
357
+
358
+ @logger.debug("Received server request: #{method} (id: #{request_id})")
359
+
360
+ case method
361
+ when 'elicitation/create'
362
+ handle_elicitation_create(request_id, params)
363
+ else
364
+ # Unknown request method, send error response
365
+ send_error_response(request_id, -32_601, "Method not found: #{method}")
366
+ end
367
+ rescue StandardError => e
368
+ @logger.error("Error handling server request: #{e.message}")
369
+ send_error_response(request_id, -32_603, "Internal error: #{e.message}")
370
+ end
371
+
372
+ # Handle elicitation/create request from server (MCP 2025-06-18)
373
+ # @param request_id [String, Integer] the JSON-RPC request ID
374
+ # @param params [Hash] the elicitation parameters
375
+ # @return [void]
376
+ def handle_elicitation_create(request_id, params)
377
+ # If no callback is registered, decline the request
378
+ unless @elicitation_request_callback
379
+ @logger.warn('Received elicitation request but no callback registered, declining')
380
+ send_elicitation_response(request_id, { 'action' => 'decline' })
381
+ return
382
+ end
383
+
384
+ # Call the registered callback
385
+ result = @elicitation_request_callback.call(request_id, params)
386
+
387
+ # Send the response back to the server
388
+ send_elicitation_response(request_id, result)
389
+ end
390
+
391
+ # Send elicitation response back to server (MCP 2025-06-18)
392
+ # @param request_id [String, Integer] the JSON-RPC request ID
393
+ # @param result [Hash] the elicitation result (action and optional content)
394
+ # @return [void]
395
+ def send_elicitation_response(request_id, result)
396
+ response = {
397
+ 'jsonrpc' => '2.0',
398
+ 'id' => request_id,
399
+ 'result' => result
400
+ }
401
+ send_message(response)
402
+ end
403
+
404
+ # Send error response back to server (MCP 2025-06-18)
405
+ # @param request_id [String, Integer] the JSON-RPC request ID
406
+ # @param code [Integer] the error code
407
+ # @param message [String] the error message
408
+ # @return [void]
409
+ def send_error_response(request_id, code, message)
410
+ response = {
411
+ 'jsonrpc' => '2.0',
412
+ 'id' => request_id,
413
+ 'error' => {
414
+ 'code' => code,
415
+ 'message' => message
416
+ }
417
+ }
418
+ send_message(response)
419
+ end
420
+
421
+ # Send a JSON-RPC message to the server
422
+ # @param message [Hash] the message to send
423
+ # @return [void]
424
+ def send_message(message)
425
+ json = JSON.generate(message)
426
+ @stdin.puts(json)
427
+ @stdin.flush
428
+ @logger.debug("Sent message: #{json}")
429
+ rescue StandardError => e
430
+ @logger.error("Error sending message: #{e.message}")
431
+ end
432
+
243
433
  # Clean up the server connection
244
434
  # Closes all stdio handles and terminates any running processes and threads
245
435
  # @return [void]
@@ -2,6 +2,9 @@
2
2
 
3
3
  require_relative '../http_transport_base'
4
4
 
5
+ require 'zlib'
6
+ require 'stringio'
7
+
5
8
  module MCPClient
6
9
  class ServerStreamableHTTP
7
10
  # JSON-RPC request/notification plumbing for Streamable HTTP transport
@@ -23,8 +26,12 @@ module MCPClient
23
26
  # @raise [MCPClient::Errors::TransportError] if parsing fails
24
27
  # @raise [MCPClient::Errors::ServerError] if the response contains an error
25
28
  def parse_response(response)
26
- body = response.body.strip
29
+ body = response.body
27
30
  content_type = response.headers['content-type'] || response.headers['Content-Type'] || ''
31
+ content_encoding = response.headers['content-encoding'] || response.headers['Content-Encoding'] || ''
32
+
33
+ body = Zlib::GzipReader.new(StringIO.new(body)).read if content_encoding.include?('gzip')
34
+ body = body&.strip
28
35
 
29
36
  # Determine response format based on Content-Type header per MCP 2025 spec
30
37
  data = if content_type.include?('text/event-stream')