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
data/lib/mcp_client/client.rb
CHANGED
|
@@ -16,12 +16,17 @@ module MCPClient
|
|
|
16
16
|
# @return [Hash<String, MCPClient::Resource>] cache of resources by composite key (server_id:uri)
|
|
17
17
|
# @!attribute [r] logger
|
|
18
18
|
# @return [Logger] logger for client operations
|
|
19
|
-
|
|
19
|
+
# @!attribute [r] roots
|
|
20
|
+
# @return [Array<MCPClient::Root>] list of MCP roots (MCP 2025-06-18)
|
|
21
|
+
attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger, :roots
|
|
20
22
|
|
|
21
23
|
# Initialize a new MCPClient::Client
|
|
22
24
|
# @param mcp_server_configs [Array<Hash>] configurations for MCP servers
|
|
23
25
|
# @param logger [Logger, nil] optional logger, defaults to STDOUT
|
|
24
|
-
|
|
26
|
+
# @param elicitation_handler [Proc, nil] optional handler for elicitation requests (MCP 2025-06-18)
|
|
27
|
+
# @param roots [Array<MCPClient::Root, Hash>, nil] optional list of roots (MCP 2025-06-18)
|
|
28
|
+
# @param sampling_handler [Proc, nil] optional handler for sampling requests (MCP 2025-06-18)
|
|
29
|
+
def initialize(mcp_server_configs: [], logger: nil, elicitation_handler: nil, roots: nil, sampling_handler: nil)
|
|
25
30
|
@logger = logger || Logger.new($stdout, level: Logger::WARN)
|
|
26
31
|
@logger.progname = self.class.name
|
|
27
32
|
@logger.formatter = proc { |severity, _datetime, progname, msg| "#{severity} [#{progname}] #{msg}\n" }
|
|
@@ -34,6 +39,12 @@ module MCPClient
|
|
|
34
39
|
@resource_cache = {}
|
|
35
40
|
# JSON-RPC notification listeners
|
|
36
41
|
@notification_listeners = []
|
|
42
|
+
# Elicitation handler (MCP 2025-06-18)
|
|
43
|
+
@elicitation_handler = elicitation_handler
|
|
44
|
+
# Sampling handler (MCP 2025-06-18)
|
|
45
|
+
@sampling_handler = sampling_handler
|
|
46
|
+
# Roots (MCP 2025-06-18)
|
|
47
|
+
@roots = normalize_roots(roots)
|
|
37
48
|
# Register default and user-defined notification handlers on each server
|
|
38
49
|
@servers.each do |server|
|
|
39
50
|
server.on_notification do |method, params|
|
|
@@ -42,6 +53,14 @@ module MCPClient
|
|
|
42
53
|
# Invoke user-defined listeners
|
|
43
54
|
@notification_listeners.each { |cb| cb.call(server, method, params) }
|
|
44
55
|
end
|
|
56
|
+
# Register elicitation handler on each server
|
|
57
|
+
if server.respond_to?(:on_elicitation_request)
|
|
58
|
+
server.on_elicitation_request(&method(:handle_elicitation_request))
|
|
59
|
+
end
|
|
60
|
+
# Register roots list handler on each server (MCP 2025-06-18)
|
|
61
|
+
server.on_roots_list_request(&method(:handle_roots_list_request)) if server.respond_to?(:on_roots_list_request)
|
|
62
|
+
# Register sampling handler on each server (MCP 2025-06-18)
|
|
63
|
+
server.on_sampling_request(&method(:handle_sampling_request)) if server.respond_to?(:on_sampling_request)
|
|
45
64
|
end
|
|
46
65
|
end
|
|
47
66
|
|
|
@@ -323,6 +342,16 @@ module MCPClient
|
|
|
323
342
|
@notification_listeners << block
|
|
324
343
|
end
|
|
325
344
|
|
|
345
|
+
# Set the roots for this client (MCP 2025-06-18)
|
|
346
|
+
# When roots are changed, a notification is sent to all connected servers
|
|
347
|
+
# @param new_roots [Array<MCPClient::Root, Hash>] the new roots to set
|
|
348
|
+
# @return [void]
|
|
349
|
+
def roots=(new_roots)
|
|
350
|
+
@roots = normalize_roots(new_roots)
|
|
351
|
+
# Notify servers that roots have changed
|
|
352
|
+
notify_roots_changed
|
|
353
|
+
end
|
|
354
|
+
|
|
326
355
|
# Find a server by name
|
|
327
356
|
# @param name [String] the name of the server to find
|
|
328
357
|
# @return [MCPClient::ServerBase, nil] the server with the given name, or nil if not found
|
|
@@ -346,8 +375,10 @@ module MCPClient
|
|
|
346
375
|
end
|
|
347
376
|
|
|
348
377
|
# Call multiple tools in batch
|
|
349
|
-
# @param calls [Array<Hash>] array of
|
|
350
|
-
#
|
|
378
|
+
# @param calls [Array<Hash>] array of call hashes with keys:
|
|
379
|
+
# - name: tool name (required)
|
|
380
|
+
# - parameters: tool parameters (optional, default empty hash)
|
|
381
|
+
# - server: server name for routing (optional)
|
|
351
382
|
# @return [Array<Object>] array of results for each tool invocation
|
|
352
383
|
def call_tools(calls)
|
|
353
384
|
calls.map do |call|
|
|
@@ -452,6 +483,28 @@ module MCPClient
|
|
|
452
483
|
srv.rpc_notify(method, params)
|
|
453
484
|
end
|
|
454
485
|
|
|
486
|
+
# Request completion suggestions from a server (MCP 2025-06-18)
|
|
487
|
+
# @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
|
|
488
|
+
# @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
|
|
489
|
+
# @param server [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
|
|
490
|
+
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
491
|
+
# @raise [MCPClient::Errors::ServerNotFound] if no server is available
|
|
492
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
493
|
+
def complete(ref:, argument:, server: nil)
|
|
494
|
+
srv = select_server(server)
|
|
495
|
+
srv.complete(ref: ref, argument: argument)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
# Set the logging level on all connected servers (MCP 2025-06-18)
|
|
499
|
+
# To set on a specific server, use: client.find_server('name').log_level = 'debug'
|
|
500
|
+
# @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
|
|
501
|
+
# 'critical', 'alert', 'emergency')
|
|
502
|
+
# @return [Array<Hash>] results from servers
|
|
503
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
504
|
+
def log_level=(level)
|
|
505
|
+
@servers.map { |srv| srv.log_level = level }
|
|
506
|
+
end
|
|
507
|
+
|
|
455
508
|
private
|
|
456
509
|
|
|
457
510
|
# Process incoming JSON-RPC notifications with default handlers
|
|
@@ -473,12 +526,43 @@ module MCPClient
|
|
|
473
526
|
when 'notifications/resources/list_changed'
|
|
474
527
|
logger.warn("[#{server_id}] Resource list has changed, clearing resource cache")
|
|
475
528
|
@resource_cache.clear
|
|
529
|
+
when 'notifications/message'
|
|
530
|
+
# MCP 2025-06-18: Handle logging messages from server
|
|
531
|
+
handle_log_message(server_id, params)
|
|
476
532
|
else
|
|
477
533
|
# Log unknown notification types for debugging purposes
|
|
478
534
|
logger.debug("[#{server_id}] Received unknown notification: #{method} - #{params}")
|
|
479
535
|
end
|
|
480
536
|
end
|
|
481
537
|
|
|
538
|
+
# Handle logging message notification from server (MCP 2025-06-18)
|
|
539
|
+
# @param server_id [String] server identifier for log prefix
|
|
540
|
+
# @param params [Hash] log message params (level, logger, data)
|
|
541
|
+
# @return [void]
|
|
542
|
+
def handle_log_message(server_id, params)
|
|
543
|
+
level = params['level'] || 'info'
|
|
544
|
+
logger_name = params['logger']
|
|
545
|
+
data = params['data']
|
|
546
|
+
|
|
547
|
+
# Format the message
|
|
548
|
+
prefix = logger_name ? "[#{server_id}:#{logger_name}]" : "[#{server_id}]"
|
|
549
|
+
message = data.is_a?(String) ? data : data.inspect
|
|
550
|
+
|
|
551
|
+
# Map MCP log levels to Ruby Logger levels
|
|
552
|
+
case level.to_s.downcase
|
|
553
|
+
when 'debug'
|
|
554
|
+
logger.debug("#{prefix} #{message}")
|
|
555
|
+
when 'info', 'notice'
|
|
556
|
+
logger.info("#{prefix} #{message}")
|
|
557
|
+
when 'warning'
|
|
558
|
+
logger.warn("#{prefix} #{message}")
|
|
559
|
+
when 'error', 'critical', 'alert', 'emergency'
|
|
560
|
+
logger.error("#{prefix} #{message}")
|
|
561
|
+
else
|
|
562
|
+
logger.info("#{prefix} [#{level}] #{message}")
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
482
566
|
# Select a server based on index, name, type, or instance
|
|
483
567
|
# @param server_arg [Integer, String, Symbol, MCPClient::ServerBase, nil] server selector
|
|
484
568
|
# @return [MCPClient::ServerBase]
|
|
@@ -598,5 +682,177 @@ module MCPClient
|
|
|
598
682
|
"Error reading resource '#{uri}': #{e.message} (Server: #{server_id})"
|
|
599
683
|
end
|
|
600
684
|
end
|
|
685
|
+
|
|
686
|
+
# Handle elicitation request from server (MCP 2025-06-18)
|
|
687
|
+
# @param _request_id [String, Integer] the JSON-RPC request ID (unused at client layer)
|
|
688
|
+
# @param params [Hash] the elicitation parameters
|
|
689
|
+
# @return [Hash] the elicitation response
|
|
690
|
+
def handle_elicitation_request(_request_id, params)
|
|
691
|
+
# If no handler is configured, decline the request
|
|
692
|
+
unless @elicitation_handler
|
|
693
|
+
@logger.warn('Received elicitation request but no handler configured, declining')
|
|
694
|
+
return { 'action' => 'decline' }
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
message = params['message']
|
|
698
|
+
schema = params['schema'] || params['requestedSchema']
|
|
699
|
+
metadata = params['metadata']
|
|
700
|
+
|
|
701
|
+
begin
|
|
702
|
+
# Call the user-defined handler
|
|
703
|
+
result = case @elicitation_handler.arity
|
|
704
|
+
when 0
|
|
705
|
+
@elicitation_handler.call
|
|
706
|
+
when 1
|
|
707
|
+
@elicitation_handler.call(message)
|
|
708
|
+
when 2, -1
|
|
709
|
+
@elicitation_handler.call(message, schema)
|
|
710
|
+
else
|
|
711
|
+
@elicitation_handler.call(message, schema, metadata)
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# Validate and format response
|
|
715
|
+
case result
|
|
716
|
+
when Hash
|
|
717
|
+
if result['action']
|
|
718
|
+
normalised_action_response(result)
|
|
719
|
+
elsif result[:action]
|
|
720
|
+
# Convert symbol keys to strings
|
|
721
|
+
{
|
|
722
|
+
'action' => result[:action].to_s,
|
|
723
|
+
'content' => result[:content]
|
|
724
|
+
}.compact.then { |payload| normalised_action_response(payload) }
|
|
725
|
+
else
|
|
726
|
+
# Assume it's content for an accept action
|
|
727
|
+
{ 'action' => 'accept', 'content' => result }
|
|
728
|
+
end
|
|
729
|
+
when nil
|
|
730
|
+
{ 'action' => 'cancel' }
|
|
731
|
+
else
|
|
732
|
+
{ 'action' => 'accept', 'content' => result }
|
|
733
|
+
end
|
|
734
|
+
rescue StandardError => e
|
|
735
|
+
@logger.error("Elicitation handler error: #{e.message}")
|
|
736
|
+
@logger.debug(e.backtrace.join("\n"))
|
|
737
|
+
{ 'action' => 'decline' }
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# Ensure the action value conforms to MCP spec (accept, decline, cancel)
|
|
742
|
+
# Falls back to accept for unknown action values.
|
|
743
|
+
def normalised_action_response(result)
|
|
744
|
+
action = result['action']
|
|
745
|
+
return result if %w[accept decline cancel].include?(action)
|
|
746
|
+
|
|
747
|
+
@logger.warn("Unknown elicitation action '#{action}', defaulting to accept")
|
|
748
|
+
result.merge('action' => 'accept')
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
# Normalize roots array - convert Hashes to Root objects (MCP 2025-06-18)
|
|
752
|
+
# @param roots [Array<MCPClient::Root, Hash>, nil] the roots to normalize
|
|
753
|
+
# @return [Array<MCPClient::Root>] normalized roots array
|
|
754
|
+
def normalize_roots(roots)
|
|
755
|
+
return [] if roots.nil?
|
|
756
|
+
|
|
757
|
+
roots.map do |root|
|
|
758
|
+
case root
|
|
759
|
+
when MCPClient::Root
|
|
760
|
+
root
|
|
761
|
+
when Hash
|
|
762
|
+
MCPClient::Root.from_json(root)
|
|
763
|
+
else
|
|
764
|
+
raise ArgumentError, "Invalid root type: #{root.class}. Expected MCPClient::Root or Hash."
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# Handle roots/list request from server (MCP 2025-06-18)
|
|
770
|
+
# @param _request_id [String, Integer] the JSON-RPC request ID (unused, kept for callback signature)
|
|
771
|
+
# @param _params [Hash] the request parameters (unused)
|
|
772
|
+
# @return [Hash] the roots list response
|
|
773
|
+
def handle_roots_list_request(_request_id, _params)
|
|
774
|
+
{ 'roots' => @roots.map(&:to_h) }
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# Send notification to all servers that roots have changed (MCP 2025-06-18)
|
|
778
|
+
# @return [void]
|
|
779
|
+
def notify_roots_changed
|
|
780
|
+
@servers.each do |server|
|
|
781
|
+
server.rpc_notify('notifications/roots/list_changed', {})
|
|
782
|
+
rescue StandardError => e
|
|
783
|
+
server_id = server.name ? "#{server.class}[#{server.name}]" : server.class
|
|
784
|
+
@logger.warn("[#{server_id}] Failed to send roots/list_changed notification: #{e.message}")
|
|
785
|
+
end
|
|
786
|
+
end
|
|
787
|
+
|
|
788
|
+
# Handle sampling/createMessage request from server (MCP 2025-06-18)
|
|
789
|
+
# @param _request_id [String, Integer] the JSON-RPC request ID (unused, kept for callback signature)
|
|
790
|
+
# @param params [Hash] the sampling parameters (messages, modelPreferences, systemPrompt, maxTokens)
|
|
791
|
+
# @return [Hash] the sampling response (role, content, model, stopReason)
|
|
792
|
+
def handle_sampling_request(_request_id, params)
|
|
793
|
+
# If no handler is configured, return an error
|
|
794
|
+
unless @sampling_handler
|
|
795
|
+
@logger.warn('Received sampling request but no handler configured')
|
|
796
|
+
return { 'error' => { 'code' => -1, 'message' => 'Sampling not supported' } }
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
messages = params['messages'] || []
|
|
800
|
+
model_preferences = params['modelPreferences']
|
|
801
|
+
system_prompt = params['systemPrompt']
|
|
802
|
+
max_tokens = params['maxTokens']
|
|
803
|
+
|
|
804
|
+
begin
|
|
805
|
+
# Call the user-defined handler
|
|
806
|
+
result = case @sampling_handler.arity
|
|
807
|
+
when 0
|
|
808
|
+
@sampling_handler.call
|
|
809
|
+
when 1
|
|
810
|
+
@sampling_handler.call(messages)
|
|
811
|
+
when 2
|
|
812
|
+
@sampling_handler.call(messages, model_preferences)
|
|
813
|
+
when 3
|
|
814
|
+
@sampling_handler.call(messages, model_preferences, system_prompt)
|
|
815
|
+
else
|
|
816
|
+
@sampling_handler.call(messages, model_preferences, system_prompt, max_tokens)
|
|
817
|
+
end
|
|
818
|
+
|
|
819
|
+
# Validate and format response
|
|
820
|
+
validate_sampling_response(result)
|
|
821
|
+
rescue StandardError => e
|
|
822
|
+
@logger.error("Sampling handler error: #{e.message}")
|
|
823
|
+
@logger.debug(e.backtrace.join("\n"))
|
|
824
|
+
{ 'error' => { 'code' => -1, 'message' => "Sampling error: #{e.message}" } }
|
|
825
|
+
end
|
|
826
|
+
end
|
|
827
|
+
|
|
828
|
+
# Validate sampling response from handler (MCP 2025-06-18)
|
|
829
|
+
# @param result [Hash] the result from the sampling handler
|
|
830
|
+
# @return [Hash] validated sampling response
|
|
831
|
+
def validate_sampling_response(result)
|
|
832
|
+
return { 'error' => { 'code' => -1, 'message' => 'Sampling rejected' } } if result.nil?
|
|
833
|
+
|
|
834
|
+
# Convert symbol keys to string keys
|
|
835
|
+
result = result.transform_keys(&:to_s) if result.is_a?(Hash) && result.keys.first.is_a?(Symbol)
|
|
836
|
+
|
|
837
|
+
# Ensure required fields are present
|
|
838
|
+
unless result.is_a?(Hash) && result['content']
|
|
839
|
+
return {
|
|
840
|
+
'role' => 'assistant',
|
|
841
|
+
'content' => { 'type' => 'text', 'text' => result.to_s },
|
|
842
|
+
'model' => 'unknown',
|
|
843
|
+
'stopReason' => 'endTurn'
|
|
844
|
+
}
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
# Set defaults for missing fields
|
|
848
|
+
result['role'] ||= 'assistant'
|
|
849
|
+
result['model'] ||= 'unknown'
|
|
850
|
+
result['stopReason'] ||= 'endTurn'
|
|
851
|
+
|
|
852
|
+
# Normalize content if it's a string
|
|
853
|
+
result['content'] = { 'type' => 'text', 'text' => result['content'] } if result['content'].is_a?(String)
|
|
854
|
+
|
|
855
|
+
result
|
|
856
|
+
end
|
|
601
857
|
end
|
|
602
858
|
end
|
data/lib/mcp_client/errors.rb
CHANGED
|
@@ -47,5 +47,8 @@ module MCPClient
|
|
|
47
47
|
|
|
48
48
|
# Raised when multiple resources with the same URI exist across different servers
|
|
49
49
|
class AmbiguousResourceURI < MCPError; end
|
|
50
|
+
|
|
51
|
+
# Raised when transport type cannot be determined from target URL/command
|
|
52
|
+
class TransportDetectionError < MCPError; end
|
|
50
53
|
end
|
|
51
54
|
end
|
|
@@ -256,14 +256,20 @@ module MCPClient
|
|
|
256
256
|
end
|
|
257
257
|
|
|
258
258
|
# Create a Faraday connection for HTTP requests
|
|
259
|
+
# Applies default configuration first, then allows user customization via @faraday_config block
|
|
259
260
|
# @return [Faraday::Connection] the configured connection
|
|
260
261
|
def create_http_connection
|
|
261
|
-
Faraday.new(url: @base_url) do |f|
|
|
262
|
+
conn = Faraday.new(url: @base_url) do |f|
|
|
262
263
|
f.request :retry, max: @max_retries, interval: @retry_backoff, backoff_factor: 2
|
|
263
264
|
f.options.open_timeout = @read_timeout
|
|
264
265
|
f.options.timeout = @read_timeout
|
|
265
266
|
f.adapter Faraday.default_adapter
|
|
266
267
|
end
|
|
268
|
+
|
|
269
|
+
# Apply user's Faraday customizations after defaults
|
|
270
|
+
@faraday_config&.call(conn)
|
|
271
|
+
|
|
272
|
+
conn
|
|
267
273
|
end
|
|
268
274
|
|
|
269
275
|
# Log HTTP response (to be overridden by specific transports)
|
|
@@ -62,9 +62,15 @@ module MCPClient
|
|
|
62
62
|
# Generate initialization parameters for MCP protocol
|
|
63
63
|
# @return [Hash] the initialization parameters
|
|
64
64
|
def initialization_params
|
|
65
|
+
capabilities = {
|
|
66
|
+
'elicitation' => {}, # MCP 2025-06-18: Support for server-initiated user interactions
|
|
67
|
+
'roots' => { 'listChanged' => true }, # MCP 2025-06-18: Support for roots
|
|
68
|
+
'sampling' => {} # MCP 2025-06-18: Support for server-initiated LLM sampling
|
|
69
|
+
}
|
|
70
|
+
|
|
65
71
|
{
|
|
66
72
|
'protocolVersion' => MCPClient::PROTOCOL_VERSION,
|
|
67
|
-
'capabilities' =>
|
|
73
|
+
'capabilities' => capabilities,
|
|
68
74
|
'clientInfo' => { 'name' => 'ruby-mcp-client', 'version' => MCPClient::VERSION }
|
|
69
75
|
}
|
|
70
76
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
# Represents an MCP Root - a URI that defines a boundary where servers can operate
|
|
5
|
+
# Roots are declared by clients to inform servers about relevant resources and their locations
|
|
6
|
+
class Root
|
|
7
|
+
attr_reader :uri, :name
|
|
8
|
+
|
|
9
|
+
# Create a new Root
|
|
10
|
+
# @param uri [String] The URI for the root (typically file:// URI)
|
|
11
|
+
# @param name [String, nil] Optional human-readable name for display purposes
|
|
12
|
+
def initialize(uri:, name: nil)
|
|
13
|
+
@uri = uri
|
|
14
|
+
@name = name
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Create a Root from a JSON hash
|
|
18
|
+
# @param json [Hash] The JSON hash with 'uri' and optional 'name' keys
|
|
19
|
+
# @return [Root]
|
|
20
|
+
def self.from_json(json)
|
|
21
|
+
new(
|
|
22
|
+
uri: json['uri'] || json[:uri],
|
|
23
|
+
name: json['name'] || json[:name]
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Convert to JSON-serializable hash
|
|
28
|
+
# @return [Hash]
|
|
29
|
+
def to_h
|
|
30
|
+
result = { 'uri' => @uri }
|
|
31
|
+
result['name'] = @name if @name
|
|
32
|
+
result
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Convert to JSON string
|
|
36
|
+
# @return [String]
|
|
37
|
+
def to_json(*)
|
|
38
|
+
to_h.to_json(*)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check equality
|
|
42
|
+
def ==(other)
|
|
43
|
+
return false unless other.is_a?(Root)
|
|
44
|
+
|
|
45
|
+
uri == other.uri && name == other.name
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
alias eql? ==
|
|
49
|
+
|
|
50
|
+
def hash
|
|
51
|
+
[uri, name].hash
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# String representation
|
|
55
|
+
def to_s
|
|
56
|
+
name ? "#{name} (#{uri})" : uri
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def inspect
|
|
60
|
+
"#<MCPClient::Root uri=#{uri.inspect} name=#{name.inspect}>"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -76,7 +76,9 @@ module MCPClient
|
|
|
76
76
|
retries: config[:retries] || 3,
|
|
77
77
|
retry_backoff: config[:retry_backoff] || 1,
|
|
78
78
|
name: config[:name],
|
|
79
|
-
logger: logger
|
|
79
|
+
logger: logger,
|
|
80
|
+
oauth_provider: config[:oauth_provider],
|
|
81
|
+
faraday_config: config[:faraday_config]
|
|
80
82
|
)
|
|
81
83
|
end
|
|
82
84
|
|
|
@@ -95,7 +97,9 @@ module MCPClient
|
|
|
95
97
|
retries: config[:retries] || 3,
|
|
96
98
|
retry_backoff: config[:retry_backoff] || 1,
|
|
97
99
|
name: config[:name],
|
|
98
|
-
logger: logger
|
|
100
|
+
logger: logger,
|
|
101
|
+
oauth_provider: config[:oauth_provider],
|
|
102
|
+
faraday_config: config[:faraday_config]
|
|
99
103
|
)
|
|
100
104
|
end
|
|
101
105
|
|
|
@@ -12,6 +12,14 @@ module MCPClient
|
|
|
12
12
|
# Implementation of MCP server that communicates via HTTP requests/responses
|
|
13
13
|
# Useful for communicating with MCP servers that support HTTP-based transport
|
|
14
14
|
# without Server-Sent Events streaming
|
|
15
|
+
#
|
|
16
|
+
# @note Elicitation Support (MCP 2025-06-18)
|
|
17
|
+
# This transport does NOT support server-initiated elicitation requests.
|
|
18
|
+
# The HTTP transport uses a pure request-response architecture where only the client
|
|
19
|
+
# can initiate requests. For elicitation support, use one of these transports instead:
|
|
20
|
+
# - ServerStdio: Full bidirectional JSON-RPC over stdin/stdout
|
|
21
|
+
# - ServerSSE: Server requests via SSE stream, client responses via HTTP POST
|
|
22
|
+
# - ServerStreamableHTTP: Server requests via SSE-formatted responses, client responses via HTTP POST
|
|
15
23
|
class ServerHTTP < ServerBase
|
|
16
24
|
require_relative 'server_http/json_rpc_transport'
|
|
17
25
|
|
|
@@ -47,6 +55,7 @@ module MCPClient
|
|
|
47
55
|
# @option options [String, nil] :name Optional name for this server
|
|
48
56
|
# @option options [Logger, nil] :logger Optional logger
|
|
49
57
|
# @option options [MCPClient::Auth::OAuthProvider, nil] :oauth_provider Optional OAuth provider
|
|
58
|
+
# @option options [Proc, nil] :faraday_config Optional block to customize the Faraday connection
|
|
50
59
|
def initialize(base_url:, **options)
|
|
51
60
|
opts = default_options.merge(options)
|
|
52
61
|
super(name: opts[:name])
|
|
@@ -91,6 +100,7 @@ module MCPClient
|
|
|
91
100
|
})
|
|
92
101
|
|
|
93
102
|
@read_timeout = opts[:read_timeout]
|
|
103
|
+
@faraday_config = opts[:faraday_config]
|
|
94
104
|
@tools = nil
|
|
95
105
|
@tools_data = nil
|
|
96
106
|
@request_id = 0
|
|
@@ -312,6 +322,33 @@ module MCPClient
|
|
|
312
322
|
raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
|
|
313
323
|
end
|
|
314
324
|
|
|
325
|
+
# Request completion suggestions from the server (MCP 2025-06-18)
|
|
326
|
+
# @param ref [Hash] reference to complete (prompt or resource)
|
|
327
|
+
# @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
|
|
328
|
+
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
329
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
330
|
+
def complete(ref:, argument:)
|
|
331
|
+
result = rpc_request('completion/complete', { ref: ref, argument: argument })
|
|
332
|
+
result['completion'] || { 'values' => [] }
|
|
333
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
334
|
+
raise
|
|
335
|
+
rescue StandardError => e
|
|
336
|
+
raise MCPClient::Errors::ServerError, "Error requesting completion: #{e.message}"
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Set the logging level on the server (MCP 2025-06-18)
|
|
340
|
+
# @param level [String] the log level ('debug', 'info', 'notice', 'warning', 'error',
|
|
341
|
+
# 'critical', 'alert', 'emergency')
|
|
342
|
+
# @return [Hash] empty result on success
|
|
343
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
344
|
+
def log_level=(level)
|
|
345
|
+
rpc_request('logging/setLevel', { level: level })
|
|
346
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
347
|
+
raise
|
|
348
|
+
rescue StandardError => e
|
|
349
|
+
raise MCPClient::Errors::ServerError, "Error setting log level: #{e.message}"
|
|
350
|
+
end
|
|
351
|
+
|
|
315
352
|
# List all resource templates available from the MCP server
|
|
316
353
|
# @param cursor [String, nil] optional cursor for pagination
|
|
317
354
|
# @return [Hash] result containing resourceTemplates array and optional nextCursor
|
|
@@ -412,7 +449,8 @@ module MCPClient
|
|
|
412
449
|
retry_backoff: 1,
|
|
413
450
|
name: nil,
|
|
414
451
|
logger: nil,
|
|
415
|
-
oauth_provider: nil
|
|
452
|
+
oauth_provider: nil,
|
|
453
|
+
faraday_config: nil
|
|
416
454
|
}
|
|
417
455
|
end
|
|
418
456
|
|
|
@@ -31,6 +31,7 @@ module MCPClient
|
|
|
31
31
|
data = JSON.parse(event[:data])
|
|
32
32
|
|
|
33
33
|
return if process_error_in_message(data)
|
|
34
|
+
return if process_server_request?(data)
|
|
34
35
|
return if process_notification?(data)
|
|
35
36
|
|
|
36
37
|
process_response?(data)
|
|
@@ -58,6 +59,16 @@ module MCPClient
|
|
|
58
59
|
true
|
|
59
60
|
end
|
|
60
61
|
|
|
62
|
+
# Process a JSON-RPC request from server (has both id AND method)
|
|
63
|
+
# @param data [Hash] the parsed JSON payload
|
|
64
|
+
# @return [Boolean] true if we saw & handled a server request
|
|
65
|
+
def process_server_request?(data)
|
|
66
|
+
return false unless data['method'] && data.key?('id')
|
|
67
|
+
|
|
68
|
+
handle_server_request(data)
|
|
69
|
+
true
|
|
70
|
+
end
|
|
71
|
+
|
|
61
72
|
# Process a JSON-RPC notification (no id => notification)
|
|
62
73
|
# @param data [Hash] the parsed JSON payload
|
|
63
74
|
# @return [Boolean] true if we saw & handled a notification
|