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.
- checksums.yaml +4 -4
- data/README.md +226 -893
- data/lib/mcp_client/auth/browser_oauth.rb +424 -0
- data/lib/mcp_client/auth/oauth_provider.rb +131 -19
- data/lib/mcp_client/auth.rb +1 -1
- data/lib/mcp_client/client.rb +260 -4
- data/lib/mcp_client/errors.rb +3 -0
- data/lib/mcp_client/http_transport_base.rb +7 -1
- data/lib/mcp_client/json_rpc_common.rb +7 -1
- data/lib/mcp_client/root.rb +63 -0
- data/lib/mcp_client/server_factory.rb +6 -2
- data/lib/mcp_client/server_http.rb +39 -1
- data/lib/mcp_client/server_sse/sse_parser.rb +11 -0
- data/lib/mcp_client/server_sse.rb +256 -5
- data/lib/mcp_client/server_stdio.rb +240 -1
- data/lib/mcp_client/server_streamable_http/json_rpc_transport.rb +8 -1
- data/lib/mcp_client/server_streamable_http.rb +263 -7
- data/lib/mcp_client/tool.rb +40 -4
- data/lib/mcp_client/version.rb +2 -2
- data/lib/mcp_client.rb +317 -4
- metadata +4 -2
|
@@ -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
|
-
|
|
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
|
|
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')
|