ruby-mcp-client 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +228 -1227
- data/lib/mcp_client/audio_content.rb +46 -0
- data/lib/mcp_client/client.rb +432 -37
- data/lib/mcp_client/elicitation_validator.rb +262 -0
- data/lib/mcp_client/errors.rb +9 -0
- data/lib/mcp_client/http_transport_base.rb +9 -1
- data/lib/mcp_client/json_rpc_common.rb +7 -3
- data/lib/mcp_client/resource.rb +8 -0
- data/lib/mcp_client/resource_link.rb +63 -0
- data/lib/mcp_client/root.rb +63 -0
- data/lib/mcp_client/server_factory.rb +4 -2
- data/lib/mcp_client/server_http.rb +46 -6
- data/lib/mcp_client/server_sse.rb +133 -5
- data/lib/mcp_client/server_stdio.rb +143 -0
- data/lib/mcp_client/server_streamable_http.rb +148 -5
- data/lib/mcp_client/task.rb +127 -0
- data/lib/mcp_client/tool.rb +73 -9
- data/lib/mcp_client/version.rb +2 -2
- data/lib/mcp_client.rb +344 -4
- metadata +9 -4
|
@@ -105,6 +105,8 @@ module MCPClient
|
|
|
105
105
|
@last_activity_time = Time.now
|
|
106
106
|
@activity_timer_thread = nil
|
|
107
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-11-25
|
|
108
110
|
end
|
|
109
111
|
|
|
110
112
|
# Stream tool call fallback for SSE transport (yields single result)
|
|
@@ -307,15 +309,11 @@ module MCPClient
|
|
|
307
309
|
# Call a tool with the given parameters
|
|
308
310
|
# @param tool_name [String] the name of the tool to call
|
|
309
311
|
# @param parameters [Hash] the parameters to pass to the tool
|
|
310
|
-
# @return [Object] the result of the tool invocation
|
|
312
|
+
# @return [Object] the result of the tool invocation (with string keys for backward compatibility)
|
|
311
313
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
312
314
|
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
|
313
315
|
# @raise [MCPClient::Errors::ToolCallError] for other errors during tool execution
|
|
314
316
|
# @raise [MCPClient::Errors::ConnectionError] if server is disconnected
|
|
315
|
-
# Call a tool with the given parameters
|
|
316
|
-
# @param tool_name [String] the name of the tool to call
|
|
317
|
-
# @param parameters [Hash] the parameters to pass to the tool
|
|
318
|
-
# @return [Object] the result of the tool invocation (with string keys for backward compatibility)
|
|
319
317
|
def call_tool(tool_name, parameters)
|
|
320
318
|
rpc_request('tools/call', {
|
|
321
319
|
name: tool_name,
|
|
@@ -329,6 +327,36 @@ module MCPClient
|
|
|
329
327
|
raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
|
|
330
328
|
end
|
|
331
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
|
+
# @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
|
|
334
|
+
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
335
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
336
|
+
def complete(ref:, argument:, context: nil)
|
|
337
|
+
params = { ref: ref, argument: argument }
|
|
338
|
+
params[:context] = context if context
|
|
339
|
+
result = rpc_request('completion/complete', params)
|
|
340
|
+
result['completion'] || { 'values' => [] }
|
|
341
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
342
|
+
raise
|
|
343
|
+
rescue StandardError => e
|
|
344
|
+
raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Set the logging level on the server (MCP 2025-06-18)
|
|
348
|
+
# @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
|
|
349
|
+
# 'critical', 'alert', 'emergency')
|
|
350
|
+
# @return [Hash] empty result on success
|
|
351
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
352
|
+
def log_level=(level)
|
|
353
|
+
rpc_request('logging/setLevel', { level: level })
|
|
354
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
355
|
+
raise
|
|
356
|
+
rescue StandardError => e
|
|
357
|
+
raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
|
|
358
|
+
end
|
|
359
|
+
|
|
332
360
|
# Connect to the MCP server over HTTP/HTTPS with SSE
|
|
333
361
|
# @return [Boolean] true if connection was successful
|
|
334
362
|
# @raise [MCPClient::Errors::ConnectionError] if connection fails
|
|
@@ -423,6 +451,20 @@ module MCPClient
|
|
|
423
451
|
@elicitation_request_callback = block
|
|
424
452
|
end
|
|
425
453
|
|
|
454
|
+
# Register a callback for roots/list requests (MCP 2025-06-18)
|
|
455
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
456
|
+
# @return [void]
|
|
457
|
+
def on_roots_list_request(&block)
|
|
458
|
+
@roots_list_request_callback = block
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# Register a callback for sampling requests (MCP 2025-11-25)
|
|
462
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
463
|
+
# @return [void]
|
|
464
|
+
def on_sampling_request(&block)
|
|
465
|
+
@sampling_request_callback = block
|
|
466
|
+
end
|
|
467
|
+
|
|
426
468
|
# Handle incoming JSON-RPC request from server (MCP 2025-06-18)
|
|
427
469
|
# @param msg [Hash] the JSON-RPC request message
|
|
428
470
|
# @return [void]
|
|
@@ -436,6 +478,10 @@ module MCPClient
|
|
|
436
478
|
case method
|
|
437
479
|
when 'elicitation/create'
|
|
438
480
|
handle_elicitation_create(request_id, params)
|
|
481
|
+
when 'roots/list'
|
|
482
|
+
handle_roots_list(request_id, params)
|
|
483
|
+
when 'sampling/createMessage'
|
|
484
|
+
handle_sampling_create_message(request_id, params)
|
|
439
485
|
else
|
|
440
486
|
# Unknown request method, send error response
|
|
441
487
|
send_error_response(request_id, -32_601, "Method not found: #{method}")
|
|
@@ -464,6 +510,88 @@ module MCPClient
|
|
|
464
510
|
send_elicitation_response(request_id, result)
|
|
465
511
|
end
|
|
466
512
|
|
|
513
|
+
# Handle roots/list request from server (MCP 2025-06-18)
|
|
514
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
515
|
+
# @param params [Hash] the request parameters
|
|
516
|
+
# @return [void]
|
|
517
|
+
def handle_roots_list(request_id, params)
|
|
518
|
+
# If no callback is registered, return empty roots list
|
|
519
|
+
unless @roots_list_request_callback
|
|
520
|
+
@logger.debug('Received roots/list request but no callback registered, returning empty list')
|
|
521
|
+
send_roots_list_response(request_id, { 'roots' => [] })
|
|
522
|
+
return
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Call the registered callback
|
|
526
|
+
result = @roots_list_request_callback.call(request_id, params)
|
|
527
|
+
|
|
528
|
+
# Send the response back to the server
|
|
529
|
+
send_roots_list_response(request_id, result)
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Send roots/list response back to server via HTTP POST (MCP 2025-06-18)
|
|
533
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
534
|
+
# @param result [Hash] the roots list result
|
|
535
|
+
# @return [void]
|
|
536
|
+
def send_roots_list_response(request_id, result)
|
|
537
|
+
ensure_initialized
|
|
538
|
+
|
|
539
|
+
response = {
|
|
540
|
+
'jsonrpc' => '2.0',
|
|
541
|
+
'id' => request_id,
|
|
542
|
+
'result' => result
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# Send response via HTTP POST to the RPC endpoint
|
|
546
|
+
post_jsonrpc_response(response)
|
|
547
|
+
rescue StandardError => e
|
|
548
|
+
@logger.error("Error sending roots/list response: #{e.message}")
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Handle sampling/createMessage request from server (MCP 2025-11-25)
|
|
552
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
553
|
+
# @param params [Hash] the sampling parameters
|
|
554
|
+
# @return [void]
|
|
555
|
+
def handle_sampling_create_message(request_id, params)
|
|
556
|
+
# If no callback is registered, return error
|
|
557
|
+
unless @sampling_request_callback
|
|
558
|
+
@logger.warn('Received sampling request but no callback registered, returning error')
|
|
559
|
+
send_error_response(request_id, -1, 'Sampling not supported')
|
|
560
|
+
return
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Call the registered callback
|
|
564
|
+
result = @sampling_request_callback.call(request_id, params)
|
|
565
|
+
|
|
566
|
+
# Send the response back to the server
|
|
567
|
+
send_sampling_response(request_id, result)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
# Send sampling response back to server via HTTP POST (MCP 2025-11-25)
|
|
571
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
572
|
+
# @param result [Hash] the sampling result (role, content, model, stopReason)
|
|
573
|
+
# @return [void]
|
|
574
|
+
def send_sampling_response(request_id, result)
|
|
575
|
+
ensure_initialized
|
|
576
|
+
|
|
577
|
+
# Check if result contains an error
|
|
578
|
+
if result.is_a?(Hash) && result['error']
|
|
579
|
+
send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
|
|
580
|
+
return
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
response = {
|
|
584
|
+
'jsonrpc' => '2.0',
|
|
585
|
+
'id' => request_id,
|
|
586
|
+
'result' => result
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
# Send response via HTTP POST to the RPC endpoint
|
|
590
|
+
post_jsonrpc_response(response)
|
|
591
|
+
rescue StandardError => e
|
|
592
|
+
@logger.error("Error sending sampling response: #{e.message}")
|
|
593
|
+
end
|
|
594
|
+
|
|
467
595
|
# Send elicitation response back to server via HTTP POST (MCP 2025-06-18)
|
|
468
596
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
469
597
|
# @param result [Hash] the elicitation result (action and optional content)
|
|
@@ -47,6 +47,8 @@ module MCPClient
|
|
|
47
47
|
@read_timeout = read_timeout
|
|
48
48
|
@env = env || {}
|
|
49
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-11-25
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
# Server info from the initialize response
|
|
@@ -340,6 +342,59 @@ module MCPClient
|
|
|
340
342
|
raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
|
|
341
343
|
end
|
|
342
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
|
+
# @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
|
|
349
|
+
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
350
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
351
|
+
def complete(ref:, argument:, context: nil)
|
|
352
|
+
ensure_initialized
|
|
353
|
+
req_id = next_id
|
|
354
|
+
params = { 'ref' => ref, 'argument' => argument }
|
|
355
|
+
params['context'] = context if context
|
|
356
|
+
req = {
|
|
357
|
+
'jsonrpc' => '2.0',
|
|
358
|
+
'id' => req_id,
|
|
359
|
+
'method' => 'completion/complete',
|
|
360
|
+
'params' => params
|
|
361
|
+
}
|
|
362
|
+
send_request(req)
|
|
363
|
+
res = wait_response(req_id)
|
|
364
|
+
if (err = res['error'])
|
|
365
|
+
raise MCPClient::Errors::ServerError, err['message']
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
res.dig('result', 'completion') || { 'values' => [] }
|
|
369
|
+
rescue StandardError => e
|
|
370
|
+
raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Set the logging level on the server (MCP 2025-06-18)
|
|
374
|
+
# @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
|
|
375
|
+
# 'critical', 'alert', 'emergency')
|
|
376
|
+
# @return [Hash] empty result on success
|
|
377
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
378
|
+
def log_level=(level)
|
|
379
|
+
ensure_initialized
|
|
380
|
+
req_id = next_id
|
|
381
|
+
req = {
|
|
382
|
+
'jsonrpc' => '2.0',
|
|
383
|
+
'id' => req_id,
|
|
384
|
+
'method' => 'logging/setLevel',
|
|
385
|
+
'params' => { 'level' => level }
|
|
386
|
+
}
|
|
387
|
+
send_request(req)
|
|
388
|
+
res = wait_response(req_id)
|
|
389
|
+
if (err = res['error'])
|
|
390
|
+
raise MCPClient::Errors::ServerError, err['message']
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
res['result'] || {}
|
|
394
|
+
rescue StandardError => e
|
|
395
|
+
raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
|
|
396
|
+
end
|
|
397
|
+
|
|
343
398
|
# Register a callback for elicitation requests (MCP 2025-06-18)
|
|
344
399
|
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
345
400
|
# @return [void]
|
|
@@ -347,6 +402,20 @@ module MCPClient
|
|
|
347
402
|
@elicitation_request_callback = block
|
|
348
403
|
end
|
|
349
404
|
|
|
405
|
+
# Register a callback for roots/list requests (MCP 2025-06-18)
|
|
406
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
407
|
+
# @return [void]
|
|
408
|
+
def on_roots_list_request(&block)
|
|
409
|
+
@roots_list_request_callback = block
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Register a callback for sampling requests (MCP 2025-11-25)
|
|
413
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
414
|
+
# @return [void]
|
|
415
|
+
def on_sampling_request(&block)
|
|
416
|
+
@sampling_request_callback = block
|
|
417
|
+
end
|
|
418
|
+
|
|
350
419
|
# Handle incoming JSON-RPC request from server (MCP 2025-06-18)
|
|
351
420
|
# @param msg [Hash] the JSON-RPC request message
|
|
352
421
|
# @return [void]
|
|
@@ -360,6 +429,10 @@ module MCPClient
|
|
|
360
429
|
case method
|
|
361
430
|
when 'elicitation/create'
|
|
362
431
|
handle_elicitation_create(request_id, params)
|
|
432
|
+
when 'roots/list'
|
|
433
|
+
handle_roots_list(request_id, params)
|
|
434
|
+
when 'sampling/createMessage'
|
|
435
|
+
handle_sampling_create_message(request_id, params)
|
|
363
436
|
else
|
|
364
437
|
# Unknown request method, send error response
|
|
365
438
|
send_error_response(request_id, -32_601, "Method not found: #{method}")
|
|
@@ -388,6 +461,76 @@ module MCPClient
|
|
|
388
461
|
send_elicitation_response(request_id, result)
|
|
389
462
|
end
|
|
390
463
|
|
|
464
|
+
# Handle roots/list request from server (MCP 2025-06-18)
|
|
465
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
466
|
+
# @param params [Hash] the request parameters
|
|
467
|
+
# @return [void]
|
|
468
|
+
def handle_roots_list(request_id, params)
|
|
469
|
+
# If no callback is registered, return empty roots list
|
|
470
|
+
unless @roots_list_request_callback
|
|
471
|
+
@logger.debug('Received roots/list request but no callback registered, returning empty list')
|
|
472
|
+
send_roots_list_response(request_id, { 'roots' => [] })
|
|
473
|
+
return
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
# Call the registered callback
|
|
477
|
+
result = @roots_list_request_callback.call(request_id, params)
|
|
478
|
+
|
|
479
|
+
# Send the response back to the server
|
|
480
|
+
send_roots_list_response(request_id, result)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Handle sampling/createMessage request from server (MCP 2025-11-25)
|
|
484
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
485
|
+
# @param params [Hash] the sampling parameters
|
|
486
|
+
# @return [void]
|
|
487
|
+
def handle_sampling_create_message(request_id, params)
|
|
488
|
+
# If no callback is registered, return error
|
|
489
|
+
unless @sampling_request_callback
|
|
490
|
+
@logger.warn('Received sampling request but no callback registered, returning error')
|
|
491
|
+
send_error_response(request_id, -1, 'Sampling not supported')
|
|
492
|
+
return
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
# Call the registered callback
|
|
496
|
+
result = @sampling_request_callback.call(request_id, params)
|
|
497
|
+
|
|
498
|
+
# Send the response back to the server
|
|
499
|
+
send_sampling_response(request_id, result)
|
|
500
|
+
end
|
|
501
|
+
|
|
502
|
+
# Send roots/list response back to server (MCP 2025-06-18)
|
|
503
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
504
|
+
# @param result [Hash] the roots list result
|
|
505
|
+
# @return [void]
|
|
506
|
+
def send_roots_list_response(request_id, result)
|
|
507
|
+
response = {
|
|
508
|
+
'jsonrpc' => '2.0',
|
|
509
|
+
'id' => request_id,
|
|
510
|
+
'result' => result
|
|
511
|
+
}
|
|
512
|
+
send_message(response)
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
# Send sampling response back to server (MCP 2025-11-25)
|
|
516
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
517
|
+
# @param result [Hash] the sampling result (role, content, model, stopReason)
|
|
518
|
+
# @return [void]
|
|
519
|
+
def send_sampling_response(request_id, result)
|
|
520
|
+
# Check if result contains an error
|
|
521
|
+
if result.is_a?(Hash) && result['error']
|
|
522
|
+
send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
|
|
523
|
+
return
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
response = {
|
|
527
|
+
'jsonrpc' => '2.0',
|
|
528
|
+
'id' => request_id,
|
|
529
|
+
'result' => result
|
|
530
|
+
}
|
|
531
|
+
send_message(response)
|
|
532
|
+
end
|
|
533
|
+
|
|
391
534
|
# Send elicitation response back to server (MCP 2025-06-18)
|
|
392
535
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
393
536
|
# @param result [Hash] the elicitation result (action and optional content)
|
|
@@ -97,6 +97,7 @@ module MCPClient
|
|
|
97
97
|
})
|
|
98
98
|
|
|
99
99
|
@read_timeout = opts[:read_timeout]
|
|
100
|
+
@faraday_config = opts[:faraday_config]
|
|
100
101
|
@tools = nil
|
|
101
102
|
@tools_data = nil
|
|
102
103
|
@prompts = nil
|
|
@@ -117,6 +118,8 @@ module MCPClient
|
|
|
117
118
|
@events_thread = nil
|
|
118
119
|
@buffer = '' # Buffer for partial SSE event data
|
|
119
120
|
@elicitation_request_callback = nil # MCP 2025-06-18
|
|
121
|
+
@roots_list_request_callback = nil # MCP 2025-06-18
|
|
122
|
+
@sampling_request_callback = nil # MCP 2025-11-25
|
|
120
123
|
end
|
|
121
124
|
|
|
122
125
|
# Connect to the MCP server over Streamable HTTP
|
|
@@ -216,6 +219,36 @@ module MCPClient
|
|
|
216
219
|
end
|
|
217
220
|
end
|
|
218
221
|
|
|
222
|
+
# Request completion suggestions from the server (MCP 2025-06-18)
|
|
223
|
+
# @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
|
|
224
|
+
# @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
|
|
225
|
+
# @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
|
|
226
|
+
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
227
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
228
|
+
def complete(ref:, argument:, context: nil)
|
|
229
|
+
params = { ref: ref, argument: argument }
|
|
230
|
+
params[:context] = context if context
|
|
231
|
+
result = rpc_request('completion/complete', params)
|
|
232
|
+
result['completion'] || { 'values' => [] }
|
|
233
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
234
|
+
raise
|
|
235
|
+
rescue StandardError => e
|
|
236
|
+
raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Set the logging level on the server (MCP 2025-06-18)
|
|
240
|
+
# @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
|
|
241
|
+
# 'critical', 'alert', 'emergency')
|
|
242
|
+
# @return [Hash] empty result on success
|
|
243
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
244
|
+
def log_level=(level)
|
|
245
|
+
rpc_request('logging/setLevel', { level: level })
|
|
246
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
247
|
+
raise
|
|
248
|
+
rescue StandardError => e
|
|
249
|
+
raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
|
|
250
|
+
end
|
|
251
|
+
|
|
219
252
|
# List all prompts available from the MCP server
|
|
220
253
|
# @return [Array<MCPClient::Prompt>] list of available prompts
|
|
221
254
|
# @raise [MCPClient::Errors::PromptGetError] if prompts list retrieval fails
|
|
@@ -363,10 +396,17 @@ module MCPClient
|
|
|
363
396
|
def apply_request_headers(req, request)
|
|
364
397
|
super
|
|
365
398
|
|
|
366
|
-
# Add session
|
|
367
|
-
if
|
|
368
|
-
|
|
369
|
-
|
|
399
|
+
# Add session and protocol version headers for non-initialize requests
|
|
400
|
+
if request['method'] != 'initialize'
|
|
401
|
+
if @session_id
|
|
402
|
+
req.headers['Mcp-Session-Id'] = @session_id
|
|
403
|
+
@logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
if @protocol_version
|
|
407
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version
|
|
408
|
+
@logger.debug("Adding protocol version header: Mcp-Protocol-Version: #{@protocol_version}")
|
|
409
|
+
end
|
|
370
410
|
end
|
|
371
411
|
|
|
372
412
|
# Add Last-Event-ID header for resumability (if available)
|
|
@@ -459,6 +499,20 @@ module MCPClient
|
|
|
459
499
|
@elicitation_request_callback = block
|
|
460
500
|
end
|
|
461
501
|
|
|
502
|
+
# Register a callback for roots/list requests (MCP 2025-06-18)
|
|
503
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
504
|
+
# @return [void]
|
|
505
|
+
def on_roots_list_request(&block)
|
|
506
|
+
@roots_list_request_callback = block
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Register a callback for sampling requests (MCP 2025-11-25)
|
|
510
|
+
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
511
|
+
# @return [void]
|
|
512
|
+
def on_sampling_request(&block)
|
|
513
|
+
@sampling_request_callback = block
|
|
514
|
+
end
|
|
515
|
+
|
|
462
516
|
private
|
|
463
517
|
|
|
464
518
|
def perform_initialize
|
|
@@ -483,7 +537,8 @@ module MCPClient
|
|
|
483
537
|
retry_backoff: 1,
|
|
484
538
|
name: nil,
|
|
485
539
|
logger: nil,
|
|
486
|
-
oauth_provider: nil
|
|
540
|
+
oauth_provider: nil,
|
|
541
|
+
faraday_config: nil
|
|
487
542
|
}
|
|
488
543
|
end
|
|
489
544
|
|
|
@@ -629,6 +684,9 @@ module MCPClient
|
|
|
629
684
|
end
|
|
630
685
|
end
|
|
631
686
|
|
|
687
|
+
# Apply user's Faraday customizations after defaults
|
|
688
|
+
@faraday_config&.call(conn)
|
|
689
|
+
|
|
632
690
|
@logger.debug("Establishing SSE events connection to #{@endpoint}") if @logger.level <= Logger::DEBUG
|
|
633
691
|
|
|
634
692
|
response = conn.get(@endpoint) do |req|
|
|
@@ -682,6 +740,7 @@ module MCPClient
|
|
|
682
740
|
def apply_events_headers(req)
|
|
683
741
|
@headers.each { |k, v| req.headers[k] = v }
|
|
684
742
|
req.headers['Mcp-Session-Id'] = @session_id if @session_id
|
|
743
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
|
|
685
744
|
end
|
|
686
745
|
|
|
687
746
|
# Process event chunks from the server
|
|
@@ -792,6 +851,7 @@ module MCPClient
|
|
|
792
851
|
response = conn.post(@endpoint) do |req|
|
|
793
852
|
@headers.each { |k, v| req.headers[k] = v }
|
|
794
853
|
req.headers['Mcp-Session-Id'] = @session_id if @session_id
|
|
854
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
|
|
795
855
|
req.body = pong_response.to_json
|
|
796
856
|
end
|
|
797
857
|
|
|
@@ -818,6 +878,10 @@ module MCPClient
|
|
|
818
878
|
case method
|
|
819
879
|
when 'elicitation/create'
|
|
820
880
|
handle_elicitation_create(request_id, params)
|
|
881
|
+
when 'roots/list'
|
|
882
|
+
handle_roots_list(request_id, params)
|
|
883
|
+
when 'sampling/createMessage'
|
|
884
|
+
handle_sampling_create_message(request_id, params)
|
|
821
885
|
else
|
|
822
886
|
# Unknown request method, send error response
|
|
823
887
|
send_error_response(request_id, -32_601, "Method not found: #{method}")
|
|
@@ -849,6 +913,84 @@ module MCPClient
|
|
|
849
913
|
send_elicitation_response(elicitation_id, result)
|
|
850
914
|
end
|
|
851
915
|
|
|
916
|
+
# Handle roots/list request from server (MCP 2025-06-18)
|
|
917
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
918
|
+
# @param params [Hash] the request parameters
|
|
919
|
+
# @return [void]
|
|
920
|
+
def handle_roots_list(request_id, params)
|
|
921
|
+
# If no callback is registered, return empty roots list
|
|
922
|
+
unless @roots_list_request_callback
|
|
923
|
+
@logger.debug('Received roots/list request but no callback registered, returning empty list')
|
|
924
|
+
send_roots_list_response(request_id, { 'roots' => [] })
|
|
925
|
+
return
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
# Call the registered callback
|
|
929
|
+
result = @roots_list_request_callback.call(request_id, params)
|
|
930
|
+
|
|
931
|
+
# Send the response back to the server
|
|
932
|
+
send_roots_list_response(request_id, result)
|
|
933
|
+
end
|
|
934
|
+
|
|
935
|
+
# Handle sampling/createMessage request from server (MCP 2025-11-25)
|
|
936
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
937
|
+
# @param params [Hash] the sampling parameters
|
|
938
|
+
# @return [void]
|
|
939
|
+
def handle_sampling_create_message(request_id, params)
|
|
940
|
+
# If no callback is registered, return error
|
|
941
|
+
unless @sampling_request_callback
|
|
942
|
+
@logger.warn('Received sampling request but no callback registered, returning error')
|
|
943
|
+
send_error_response(request_id, -1, 'Sampling not supported')
|
|
944
|
+
return
|
|
945
|
+
end
|
|
946
|
+
|
|
947
|
+
# Call the registered callback
|
|
948
|
+
result = @sampling_request_callback.call(request_id, params)
|
|
949
|
+
|
|
950
|
+
# Send the response back to the server
|
|
951
|
+
send_sampling_response(request_id, result)
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
# Send roots/list response back to server via HTTP POST (MCP 2025-06-18)
|
|
955
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
956
|
+
# @param result [Hash] the roots list result
|
|
957
|
+
# @return [void]
|
|
958
|
+
def send_roots_list_response(request_id, result)
|
|
959
|
+
response = {
|
|
960
|
+
'jsonrpc' => '2.0',
|
|
961
|
+
'id' => request_id,
|
|
962
|
+
'result' => result
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
# Send response via HTTP POST
|
|
966
|
+
post_jsonrpc_response(response)
|
|
967
|
+
rescue StandardError => e
|
|
968
|
+
@logger.error("Error sending roots/list response: #{e.message}")
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
# Send sampling response back to server via HTTP POST (MCP 2025-11-25)
|
|
972
|
+
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
973
|
+
# @param result [Hash] the sampling result (role, content, model, stopReason)
|
|
974
|
+
# @return [void]
|
|
975
|
+
def send_sampling_response(request_id, result)
|
|
976
|
+
# Check if result contains an error
|
|
977
|
+
if result.is_a?(Hash) && result['error']
|
|
978
|
+
send_error_response(request_id, result['error']['code'] || -1, result['error']['message'] || 'Sampling error')
|
|
979
|
+
return
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
response = {
|
|
983
|
+
'jsonrpc' => '2.0',
|
|
984
|
+
'id' => request_id,
|
|
985
|
+
'result' => result
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
# Send response via HTTP POST
|
|
989
|
+
post_jsonrpc_response(response)
|
|
990
|
+
rescue StandardError => e
|
|
991
|
+
@logger.error("Error sending sampling response: #{e.message}")
|
|
992
|
+
end
|
|
993
|
+
|
|
852
994
|
# Send elicitation response back to server via HTTP POST (MCP 2025-06-18)
|
|
853
995
|
# For streamable HTTP, this is sent as a JSON-RPC request (not response)
|
|
854
996
|
# because HTTP is unidirectional.
|
|
@@ -910,6 +1052,7 @@ module MCPClient
|
|
|
910
1052
|
resp = conn.post(@endpoint) do |req|
|
|
911
1053
|
@headers.each { |k, v| req.headers[k] = v }
|
|
912
1054
|
req.headers['Mcp-Session-Id'] = @session_id if @session_id
|
|
1055
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
|
|
913
1056
|
req.body = json_body
|
|
914
1057
|
end
|
|
915
1058
|
|