ruby-mcp-client 0.8.1 → 0.9.1

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,9 @@ 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
108
+ @roots_list_request_callback = nil # MCP 2025-06-18
109
+ @sampling_request_callback = nil # MCP 2025-06-18
101
110
  end
102
111
 
103
112
  # Stream tool call fallback for SSE transport (yields single result)
@@ -300,15 +309,11 @@ module MCPClient
300
309
  # Call a tool with the given parameters
301
310
  # @param tool_name [String] the name of the tool to call
302
311
  # @param parameters [Hash] the parameters to pass to the tool
303
- # @return [Object] the result of the tool invocation
312
+ # @return [Object] the result of the tool invocation (with string keys for backward compatibility)
304
313
  # @raise [MCPClient::Errors::ServerError] if server returns an error
305
314
  # @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
306
315
  # @raise [MCPClient::Errors::ToolCallError] for other errors during tool execution
307
316
  # @raise [MCPClient::Errors::ConnectionError] if server is disconnected
308
- # Call a tool with the given parameters
309
- # @param tool_name [String] the name of the tool to call
310
- # @param parameters [Hash] the parameters to pass to the tool
311
- # @return [Object] the result of the tool invocation (with string keys for backward compatibility)
312
317
  def call_tool(tool_name, parameters)
313
318
  rpc_request('tools/call', {
314
319
  name: tool_name,
@@ -322,6 +327,33 @@ module MCPClient
322
327
  raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
323
328
  end
324
329
 
330
+ # Request completion suggestions from the server (MCP 2025-06-18)
331
+ # @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
332
+ # @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
333
+ # @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
334
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
335
+ def complete(ref:, argument:)
336
+ result = rpc_request('completion/complete', { ref: ref, argument: argument })
337
+ result['completion'] || { 'values' => [] }
338
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
339
+ raise
340
+ rescue StandardError => e
341
+ raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
342
+ end
343
+
344
+ # Set the logging level on the server (MCP 2025-06-18)
345
+ # @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
346
+ # 'critical', 'alert', 'emergency')
347
+ # @return [Hash] empty result on success
348
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
349
+ def log_level=(level)
350
+ rpc_request('logging/setLevel', { level: level })
351
+ rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
352
+ raise
353
+ rescue StandardError => e
354
+ raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
355
+ end
356
+
325
357
  # Connect to the MCP server over HTTP/HTTPS with SSE
326
358
  # @return [Boolean] true if connection was successful
327
359
  # @raise [MCPClient::Errors::ConnectionError] if connection fails
@@ -409,6 +441,225 @@ module MCPClient
409
441
  end
410
442
  end
411
443
 
444
+ # Register a callback for elicitation requests (MCP 2025-06-18)
445
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
446
+ # @return [void]
447
+ def on_elicitation_request(&block)
448
+ @elicitation_request_callback = block
449
+ end
450
+
451
+ # Register a callback for roots/list requests (MCP 2025-06-18)
452
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
453
+ # @return [void]
454
+ def on_roots_list_request(&block)
455
+ @roots_list_request_callback = block
456
+ end
457
+
458
+ # Register a callback for sampling requests (MCP 2025-06-18)
459
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
460
+ # @return [void]
461
+ def on_sampling_request(&block)
462
+ @sampling_request_callback = block
463
+ end
464
+
465
+ # Handle incoming JSON-RPC request from server (MCP 2025-06-18)
466
+ # @param msg [Hash] the JSON-RPC request message
467
+ # @return [void]
468
+ def handle_server_request(msg)
469
+ request_id = msg['id']
470
+ method = msg['method']
471
+ params = msg['params'] || {}
472
+
473
+ @logger.debug("Received server request: #{method} (id: #{request_id})")
474
+
475
+ case method
476
+ when 'elicitation/create'
477
+ handle_elicitation_create(request_id, params)
478
+ when 'roots/list'
479
+ handle_roots_list(request_id, params)
480
+ when 'sampling/createMessage'
481
+ handle_sampling_create_message(request_id, params)
482
+ else
483
+ # Unknown request method, send error response
484
+ send_error_response(request_id, -32_601, "Method not found: #{method}")
485
+ end
486
+ rescue StandardError => e
487
+ @logger.error("Error handling server request: #{e.message}")
488
+ send_error_response(request_id, -32_603, "Internal error: #{e.message}")
489
+ end
490
+
491
+ # Handle elicitation/create request from server (MCP 2025-06-18)
492
+ # @param request_id [String, Integer] the JSON-RPC request ID
493
+ # @param params [Hash] the elicitation parameters
494
+ # @return [void]
495
+ def handle_elicitation_create(request_id, params)
496
+ # If no callback is registered, decline the request
497
+ unless @elicitation_request_callback
498
+ @logger.warn('Received elicitation request but no callback registered, declining')
499
+ send_elicitation_response(request_id, { 'action' => 'decline' })
500
+ return
501
+ end
502
+
503
+ # Call the registered callback
504
+ result = @elicitation_request_callback.call(request_id, params)
505
+
506
+ # Send the response back to the server
507
+ send_elicitation_response(request_id, result)
508
+ end
509
+
510
+ # Handle roots/list request from server (MCP 2025-06-18)
511
+ # @param request_id [String, Integer] the JSON-RPC request ID
512
+ # @param params [Hash] the request parameters
513
+ # @return [void]
514
+ def handle_roots_list(request_id, params)
515
+ # If no callback is registered, return empty roots list
516
+ unless @roots_list_request_callback
517
+ @logger.debug('Received roots/list request but no callback registered, returning empty list')
518
+ send_roots_list_response(request_id, { 'roots' => [] })
519
+ return
520
+ end
521
+
522
+ # Call the registered callback
523
+ result = @roots_list_request_callback.call(request_id, params)
524
+
525
+ # Send the response back to the server
526
+ send_roots_list_response(request_id, result)
527
+ end
528
+
529
+ # Send roots/list response back to server via HTTP POST (MCP 2025-06-18)
530
+ # @param request_id [String, Integer] the JSON-RPC request ID
531
+ # @param result [Hash] the roots list result
532
+ # @return [void]
533
+ def send_roots_list_response(request_id, result)
534
+ ensure_initialized
535
+
536
+ response = {
537
+ 'jsonrpc' => '2.0',
538
+ 'id' => request_id,
539
+ 'result' => result
540
+ }
541
+
542
+ # Send response via HTTP POST to the RPC endpoint
543
+ post_jsonrpc_response(response)
544
+ rescue StandardError => e
545
+ @logger.error("Error sending roots/list response: #{e.message}")
546
+ end
547
+
548
+ # Handle sampling/createMessage request from server (MCP 2025-06-18)
549
+ # @param request_id [String, Integer] the JSON-RPC request ID
550
+ # @param params [Hash] the sampling parameters
551
+ # @return [void]
552
+ def handle_sampling_create_message(request_id, params)
553
+ # If no callback is registered, return error
554
+ unless @sampling_request_callback
555
+ @logger.warn('Received sampling request but no callback registered, returning error')
556
+ send_error_response(request_id, -1, 'Sampling not supported')
557
+ return
558
+ end
559
+
560
+ # Call the registered callback
561
+ result = @sampling_request_callback.call(request_id, params)
562
+
563
+ # Send the response back to the server
564
+ send_sampling_response(request_id, result)
565
+ end
566
+
567
+ # Send sampling response back to server via HTTP POST (MCP 2025-06-18)
568
+ # @param request_id [String, Integer] the JSON-RPC request ID
569
+ # @param result [Hash] the sampling result (role, content, model, stopReason)
570
+ # @return [void]
571
+ def send_sampling_response(request_id, result)
572
+ ensure_initialized
573
+
574
+ # Check if result contains an error
575
+ if result.is_a?(Hash) && result['error']
576
+ send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
577
+ return
578
+ end
579
+
580
+ response = {
581
+ 'jsonrpc' => '2.0',
582
+ 'id' => request_id,
583
+ 'result' => result
584
+ }
585
+
586
+ # Send response via HTTP POST to the RPC endpoint
587
+ post_jsonrpc_response(response)
588
+ rescue StandardError => e
589
+ @logger.error("Error sending sampling response: #{e.message}")
590
+ end
591
+
592
+ # Send elicitation response back to server via HTTP POST (MCP 2025-06-18)
593
+ # @param request_id [String, Integer] the JSON-RPC request ID
594
+ # @param result [Hash] the elicitation result (action and optional content)
595
+ # @return [void]
596
+ def send_elicitation_response(request_id, result)
597
+ ensure_initialized
598
+
599
+ response = {
600
+ 'jsonrpc' => '2.0',
601
+ 'id' => request_id,
602
+ 'result' => result
603
+ }
604
+
605
+ # Send response via HTTP POST to the RPC endpoint
606
+ post_jsonrpc_response(response)
607
+ rescue StandardError => e
608
+ @logger.error("Error sending elicitation response: #{e.message}")
609
+ end
610
+
611
+ # Send error response back to server via HTTP POST (MCP 2025-06-18)
612
+ # @param request_id [String, Integer] the JSON-RPC request ID
613
+ # @param code [Integer] the error code
614
+ # @param message [String] the error message
615
+ # @return [void]
616
+ def send_error_response(request_id, code, message)
617
+ ensure_initialized
618
+
619
+ response = {
620
+ 'jsonrpc' => '2.0',
621
+ 'id' => request_id,
622
+ 'error' => {
623
+ 'code' => code,
624
+ 'message' => message
625
+ }
626
+ }
627
+
628
+ # Send response via HTTP POST to the RPC endpoint
629
+ post_jsonrpc_response(response)
630
+ rescue StandardError => e
631
+ @logger.error("Error sending error response: #{e.message}")
632
+ end
633
+
634
+ # Post a JSON-RPC response message to the server via HTTP
635
+ # @param response [Hash] the JSON-RPC response
636
+ # @return [void]
637
+ # @private
638
+ def post_jsonrpc_response(response)
639
+ unless @rpc_endpoint
640
+ @logger.error('Cannot send response: RPC endpoint not available')
641
+ return
642
+ end
643
+
644
+ # Use the same connection pattern as post_json_rpc_request
645
+ uri = URI.parse(@base_url)
646
+ base = "#{uri.scheme}://#{uri.host}:#{uri.port}"
647
+ @rpc_conn ||= create_json_rpc_connection(base)
648
+
649
+ json_body = JSON.generate(response)
650
+
651
+ @rpc_conn.post do |req|
652
+ req.url @rpc_endpoint
653
+ req.headers['Content-Type'] = 'application/json'
654
+ @headers.each { |k, v| req.headers[k] = v }
655
+ req.body = json_body
656
+ end
657
+
658
+ @logger.debug("Sent response via HTTP POST: #{json_body}")
659
+ rescue StandardError => e
660
+ @logger.error("Failed to send response via HTTP POST: #{e.message}")
661
+ end
662
+
412
663
  private
413
664
 
414
665
  # Start the SSE thread to listen for events
@@ -46,6 +46,9 @@ module MCPClient
46
46
  @retry_backoff = retry_backoff
47
47
  @read_timeout = read_timeout
48
48
  @env = env || {}
49
+ @elicitation_request_callback = nil # MCP 2025-06-18
50
+ @roots_list_request_callback = nil # MCP 2025-06-18
51
+ @sampling_request_callback = nil # MCP 2025-06-18
49
52
  end
50
53
 
51
54
  # Server info from the initialize response
@@ -95,12 +98,20 @@ module MCPClient
95
98
  def handle_line(line)
96
99
  msg = JSON.parse(line)
97
100
  @logger.debug("Received line: #{line.chomp}")
101
+
102
+ # Dispatch JSON-RPC requests from server (has id AND method) - MCP 2025-06-18
103
+ if msg['method'] && msg.key?('id')
104
+ handle_server_request(msg)
105
+ return
106
+ end
107
+
98
108
  # Dispatch JSON-RPC notifications (no id, has method)
99
109
  if msg['method'] && !msg.key?('id')
100
110
  @notification_callback&.call(msg['method'], msg['params'])
101
111
  return
102
112
  end
103
- # Handle standard JSON-RPC responses
113
+
114
+ # Handle standard JSON-RPC responses (has id, no method)
104
115
  id = msg['id']
105
116
  return unless id
106
117
 
@@ -331,6 +342,234 @@ module MCPClient
331
342
  raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
332
343
  end
333
344
 
345
+ # Request completion suggestions from the server (MCP 2025-06-18)
346
+ # @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
347
+ # @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
348
+ # @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
349
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
350
+ def complete(ref:, argument:)
351
+ ensure_initialized
352
+ req_id = next_id
353
+ req = {
354
+ 'jsonrpc' => '2.0',
355
+ 'id' => req_id,
356
+ 'method' => 'completion/complete',
357
+ 'params' => { 'ref' => ref, 'argument' => argument }
358
+ }
359
+ send_request(req)
360
+ res = wait_response(req_id)
361
+ if (err = res['error'])
362
+ raise MCPClient::Errors::ServerError, err['message']
363
+ end
364
+
365
+ res.dig('result', 'completion') || { 'values' => [] }
366
+ rescue StandardError => e
367
+ raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
368
+ end
369
+
370
+ # Set the logging level on the server (MCP 2025-06-18)
371
+ # @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
372
+ # 'critical', 'alert', 'emergency')
373
+ # @return [Hash] empty result on success
374
+ # @raise [MCPClient::Errors::ServerError] if server returns an error
375
+ def log_level=(level)
376
+ ensure_initialized
377
+ req_id = next_id
378
+ req = {
379
+ 'jsonrpc' => '2.0',
380
+ 'id' => req_id,
381
+ 'method' => 'logging/setLevel',
382
+ 'params' => { 'level' => level }
383
+ }
384
+ send_request(req)
385
+ res = wait_response(req_id)
386
+ if (err = res['error'])
387
+ raise MCPClient::Errors::ServerError, err['message']
388
+ end
389
+
390
+ res['result'] || {}
391
+ rescue StandardError => e
392
+ raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
393
+ end
394
+
395
+ # Register a callback for elicitation requests (MCP 2025-06-18)
396
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
397
+ # @return [void]
398
+ def on_elicitation_request(&block)
399
+ @elicitation_request_callback = block
400
+ end
401
+
402
+ # Register a callback for roots/list requests (MCP 2025-06-18)
403
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
404
+ # @return [void]
405
+ def on_roots_list_request(&block)
406
+ @roots_list_request_callback = block
407
+ end
408
+
409
+ # Register a callback for sampling requests (MCP 2025-06-18)
410
+ # @param block [Proc] callback that receives (request_id, params) and returns response hash
411
+ # @return [void]
412
+ def on_sampling_request(&block)
413
+ @sampling_request_callback = block
414
+ end
415
+
416
+ # Handle incoming JSON-RPC request from server (MCP 2025-06-18)
417
+ # @param msg [Hash] the JSON-RPC request message
418
+ # @return [void]
419
+ def handle_server_request(msg)
420
+ request_id = msg['id']
421
+ method = msg['method']
422
+ params = msg['params'] || {}
423
+
424
+ @logger.debug("Received server request: #{method} (id: #{request_id})")
425
+
426
+ case method
427
+ when 'elicitation/create'
428
+ handle_elicitation_create(request_id, params)
429
+ when 'roots/list'
430
+ handle_roots_list(request_id, params)
431
+ when 'sampling/createMessage'
432
+ handle_sampling_create_message(request_id, params)
433
+ else
434
+ # Unknown request method, send error response
435
+ send_error_response(request_id, -32_601, "Method not found: #{method}")
436
+ end
437
+ rescue StandardError => e
438
+ @logger.error("Error handling server request: #{e.message}")
439
+ send_error_response(request_id, -32_603, "Internal error: #{e.message}")
440
+ end
441
+
442
+ # Handle elicitation/create request from server (MCP 2025-06-18)
443
+ # @param request_id [String, Integer] the JSON-RPC request ID
444
+ # @param params [Hash] the elicitation parameters
445
+ # @return [void]
446
+ def handle_elicitation_create(request_id, params)
447
+ # If no callback is registered, decline the request
448
+ unless @elicitation_request_callback
449
+ @logger.warn('Received elicitation request but no callback registered, declining')
450
+ send_elicitation_response(request_id, { 'action' => 'decline' })
451
+ return
452
+ end
453
+
454
+ # Call the registered callback
455
+ result = @elicitation_request_callback.call(request_id, params)
456
+
457
+ # Send the response back to the server
458
+ send_elicitation_response(request_id, result)
459
+ end
460
+
461
+ # Handle roots/list request from server (MCP 2025-06-18)
462
+ # @param request_id [String, Integer] the JSON-RPC request ID
463
+ # @param params [Hash] the request parameters
464
+ # @return [void]
465
+ def handle_roots_list(request_id, params)
466
+ # If no callback is registered, return empty roots list
467
+ unless @roots_list_request_callback
468
+ @logger.debug('Received roots/list request but no callback registered, returning empty list')
469
+ send_roots_list_response(request_id, { 'roots' => [] })
470
+ return
471
+ end
472
+
473
+ # Call the registered callback
474
+ result = @roots_list_request_callback.call(request_id, params)
475
+
476
+ # Send the response back to the server
477
+ send_roots_list_response(request_id, result)
478
+ end
479
+
480
+ # Handle sampling/createMessage request from server (MCP 2025-06-18)
481
+ # @param request_id [String, Integer] the JSON-RPC request ID
482
+ # @param params [Hash] the sampling parameters
483
+ # @return [void]
484
+ def handle_sampling_create_message(request_id, params)
485
+ # If no callback is registered, return error
486
+ unless @sampling_request_callback
487
+ @logger.warn('Received sampling request but no callback registered, returning error')
488
+ send_error_response(request_id, -1, 'Sampling not supported')
489
+ return
490
+ end
491
+
492
+ # Call the registered callback
493
+ result = @sampling_request_callback.call(request_id, params)
494
+
495
+ # Send the response back to the server
496
+ send_sampling_response(request_id, result)
497
+ end
498
+
499
+ # Send roots/list response back to server (MCP 2025-06-18)
500
+ # @param request_id [String, Integer] the JSON-RPC request ID
501
+ # @param result [Hash] the roots list result
502
+ # @return [void]
503
+ def send_roots_list_response(request_id, result)
504
+ response = {
505
+ 'jsonrpc' => '2.0',
506
+ 'id' => request_id,
507
+ 'result' => result
508
+ }
509
+ send_message(response)
510
+ end
511
+
512
+ # Send sampling response back to server (MCP 2025-06-18)
513
+ # @param request_id [String, Integer] the JSON-RPC request ID
514
+ # @param result [Hash] the sampling result (role, content, model, stopReason)
515
+ # @return [void]
516
+ def send_sampling_response(request_id, result)
517
+ # Check if result contains an error
518
+ if result.is_a?(Hash) && result['error']
519
+ send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
520
+ return
521
+ end
522
+
523
+ response = {
524
+ 'jsonrpc' => '2.0',
525
+ 'id' => request_id,
526
+ 'result' => result
527
+ }
528
+ send_message(response)
529
+ end
530
+
531
+ # Send elicitation response back to server (MCP 2025-06-18)
532
+ # @param request_id [String, Integer] the JSON-RPC request ID
533
+ # @param result [Hash] the elicitation result (action and optional content)
534
+ # @return [void]
535
+ def send_elicitation_response(request_id, result)
536
+ response = {
537
+ 'jsonrpc' => '2.0',
538
+ 'id' => request_id,
539
+ 'result' => result
540
+ }
541
+ send_message(response)
542
+ end
543
+
544
+ # Send error response back to server (MCP 2025-06-18)
545
+ # @param request_id [String, Integer] the JSON-RPC request ID
546
+ # @param code [Integer] the error code
547
+ # @param message [String] the error message
548
+ # @return [void]
549
+ def send_error_response(request_id, code, message)
550
+ response = {
551
+ 'jsonrpc' => '2.0',
552
+ 'id' => request_id,
553
+ 'error' => {
554
+ 'code' => code,
555
+ 'message' => message
556
+ }
557
+ }
558
+ send_message(response)
559
+ end
560
+
561
+ # Send a JSON-RPC message to the server
562
+ # @param message [Hash] the message to send
563
+ # @return [void]
564
+ def send_message(message)
565
+ json = JSON.generate(message)
566
+ @stdin.puts(json)
567
+ @stdin.flush
568
+ @logger.debug("Sent message: #{json}")
569
+ rescue StandardError => e
570
+ @logger.error("Error sending message: #{e.message}")
571
+ end
572
+
334
573
  # Clean up the server connection
335
574
  # Closes all stdio handles and terminates any running processes and threads
336
575
  # @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')