ruby_llm_swarm-mcp 0.8.0 → 0.8.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.
Files changed (89) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -44
  3. data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +4 -21
  4. data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +0 -20
  5. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +3 -0
  6. data/lib/ruby_llm/mcp/auth/browser/opener.rb +2 -0
  7. data/lib/ruby_llm/mcp/auth/browser/pages.rb +32 -100
  8. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +6 -32
  9. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +2 -0
  10. data/lib/ruby_llm/mcp/auth/memory_storage.rb +0 -18
  11. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +3 -82
  12. data/lib/ruby_llm/mcp/auth/session_manager.rb +2 -0
  13. data/lib/ruby_llm/mcp/auth/url_builder.rb +2 -0
  14. data/lib/ruby_llm/mcp/client.rb +32 -119
  15. data/lib/ruby_llm/mcp/configuration.rb +6 -74
  16. data/lib/ruby_llm/mcp/coordinator.rb +304 -0
  17. data/lib/ruby_llm/mcp/elicitation.rb +6 -8
  18. data/lib/ruby_llm/mcp/errors.rb +0 -15
  19. data/lib/ruby_llm/mcp/notification_handler.rb +5 -21
  20. data/lib/ruby_llm/mcp/notifications/cancelled.rb +32 -0
  21. data/lib/ruby_llm/mcp/notifications/initialize.rb +24 -0
  22. data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +26 -0
  23. data/lib/ruby_llm/mcp/prompt.rb +7 -7
  24. data/lib/ruby_llm/mcp/protocol.rb +34 -0
  25. data/lib/ruby_llm/mcp/railtie.rb +6 -8
  26. data/lib/ruby_llm/mcp/requests/completion_prompt.rb +50 -0
  27. data/lib/ruby_llm/mcp/requests/completion_resource.rb +50 -0
  28. data/lib/ruby_llm/mcp/requests/initialization.rb +34 -0
  29. data/lib/ruby_llm/mcp/requests/logging_set_level.rb +28 -0
  30. data/lib/ruby_llm/mcp/requests/ping.rb +24 -0
  31. data/lib/ruby_llm/mcp/requests/prompt_call.rb +32 -0
  32. data/lib/ruby_llm/mcp/requests/prompt_list.rb +31 -0
  33. data/lib/ruby_llm/mcp/requests/resource_list.rb +31 -0
  34. data/lib/ruby_llm/mcp/requests/resource_read.rb +30 -0
  35. data/lib/ruby_llm/mcp/requests/resource_template_list.rb +31 -0
  36. data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +30 -0
  37. data/lib/ruby_llm/mcp/requests/shared/meta.rb +32 -0
  38. data/lib/ruby_llm/mcp/requests/shared/pagination.rb +17 -0
  39. data/lib/ruby_llm/mcp/requests/tool_call.rb +35 -0
  40. data/lib/ruby_llm/mcp/requests/tool_list.rb +31 -0
  41. data/lib/ruby_llm/mcp/resource.rb +8 -6
  42. data/lib/ruby_llm/mcp/resource_template.rb +7 -7
  43. data/lib/ruby_llm/mcp/response_handler.rb +67 -0
  44. data/lib/ruby_llm/mcp/responses/elicitation.rb +33 -0
  45. data/lib/ruby_llm/mcp/responses/error.rb +33 -0
  46. data/lib/ruby_llm/mcp/responses/ping.rb +28 -0
  47. data/lib/ruby_llm/mcp/responses/roots_list.rb +31 -0
  48. data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +50 -0
  49. data/lib/ruby_llm/mcp/result.rb +4 -8
  50. data/lib/ruby_llm/mcp/roots.rb +4 -4
  51. data/lib/ruby_llm/mcp/sample.rb +2 -6
  52. data/lib/ruby_llm/mcp/tool.rb +9 -9
  53. data/lib/ruby_llm/mcp/transport.rb +151 -0
  54. data/lib/ruby_llm/mcp/transports/sse.rb +435 -0
  55. data/lib/ruby_llm/mcp/transports/stdio.rb +231 -0
  56. data/lib/ruby_llm/mcp/transports/streamable_http.rb +725 -0
  57. data/lib/ruby_llm/mcp/transports/support/http_client.rb +28 -0
  58. data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +47 -0
  59. data/lib/ruby_llm/mcp/transports/support/timeout.rb +34 -0
  60. data/lib/ruby_llm/mcp/version.rb +1 -1
  61. data/lib/ruby_llm/mcp.rb +7 -30
  62. metadata +38 -33
  63. data/lib/ruby_llm/mcp/adapters/base_adapter.rb +0 -179
  64. data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +0 -292
  65. data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +0 -33
  66. data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +0 -52
  67. data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +0 -52
  68. data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +0 -86
  69. data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +0 -92
  70. data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +0 -107
  71. data/lib/ruby_llm/mcp/native/cancellable_operation.rb +0 -57
  72. data/lib/ruby_llm/mcp/native/client.rb +0 -387
  73. data/lib/ruby_llm/mcp/native/json_rpc.rb +0 -170
  74. data/lib/ruby_llm/mcp/native/messages/helpers.rb +0 -39
  75. data/lib/ruby_llm/mcp/native/messages/notifications.rb +0 -42
  76. data/lib/ruby_llm/mcp/native/messages/requests.rb +0 -206
  77. data/lib/ruby_llm/mcp/native/messages/responses.rb +0 -106
  78. data/lib/ruby_llm/mcp/native/messages.rb +0 -36
  79. data/lib/ruby_llm/mcp/native/notification.rb +0 -16
  80. data/lib/ruby_llm/mcp/native/protocol.rb +0 -36
  81. data/lib/ruby_llm/mcp/native/response_handler.rb +0 -110
  82. data/lib/ruby_llm/mcp/native/transport.rb +0 -88
  83. data/lib/ruby_llm/mcp/native/transports/sse.rb +0 -607
  84. data/lib/ruby_llm/mcp/native/transports/stdio.rb +0 -356
  85. data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +0 -926
  86. data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +0 -28
  87. data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +0 -49
  88. data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +0 -36
  89. data/lib/ruby_llm/mcp/native.rb +0 -12
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Responses
6
+ class SamplingCreateMessage
7
+ def initialize(coordinator, id:, message:, model:)
8
+ @coordinator = coordinator
9
+ @id = id
10
+ @message = message
11
+ @model = model
12
+ end
13
+
14
+ def call
15
+ @coordinator.request(sampling_create_message_body, add_id: false, wait_for_response: false)
16
+ end
17
+
18
+ private
19
+
20
+ def sampling_create_message_body
21
+ {
22
+ jsonrpc: "2.0",
23
+ id: @id,
24
+ result: {
25
+ role: @message.role,
26
+ content: format_content(@message.content),
27
+ model: @model,
28
+ # TODO: We are going to assume it was a endTurn
29
+ # Look into getting RubyLLM to expose stopReason in message response
30
+ stopReason: "endTurn"
31
+ }
32
+ }
33
+ end
34
+
35
+ def format_content(content)
36
+ if content.is_a?(RubyLLM::Content)
37
+ if context.text.none? && content.attachments.any?
38
+ attachment = content.attachments.first
39
+ { type: attachment.type, data: attachment.content, mime_type: attachment.mime_type }
40
+ else
41
+ { type: "text", text: content.text }
42
+ end
43
+ else
44
+ { type: "text", text: content }
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -32,10 +32,6 @@ module RubyLLM
32
32
 
33
33
  @result_is_error = response.dig("result", "isError") || false
34
34
  @next_cursor = response.dig("result", "nextCursor")
35
-
36
- # Track whether result/error keys exist (for JSON-RPC detection)
37
- @has_result = response.key?("result")
38
- @has_error = response.key?("error")
39
35
  end
40
36
 
41
37
  REQUEST_METHODS.each do |method_name, method_value|
@@ -69,7 +65,7 @@ module RubyLLM
69
65
  end
70
66
 
71
67
  def notification?
72
- @id.nil? && !@method.nil?
68
+ @method&.include?("notifications") || false
73
69
  end
74
70
 
75
71
  def next_cursor?
@@ -77,15 +73,15 @@ module RubyLLM
77
73
  end
78
74
 
79
75
  def request?
80
- !@id.nil? && !@method.nil?
76
+ !@method.nil? && !notification? && @result.none? && @error.none?
81
77
  end
82
78
 
83
79
  def response?
84
- !@id.nil? && @method.nil? && (@has_result || @has_error)
80
+ !@id.nil? && (@result || @error.any?) && !@method
85
81
  end
86
82
 
87
83
  def success?
88
- @has_result
84
+ !@result.empty?
89
85
  end
90
86
 
91
87
  def tool_success?
@@ -5,9 +5,9 @@ module RubyLLM
5
5
  class Roots
6
6
  attr_reader :paths
7
7
 
8
- def initialize(paths: [], adapter: nil)
8
+ def initialize(paths: [], coordinator: nil)
9
9
  @paths = paths
10
- @adapter = adapter
10
+ @coordinator = coordinator
11
11
  end
12
12
 
13
13
  def active?
@@ -16,12 +16,12 @@ module RubyLLM
16
16
 
17
17
  def add(path)
18
18
  @paths << path
19
- @adapter.roots_list_change_notification
19
+ @coordinator.roots_list_change_notification
20
20
  end
21
21
 
22
22
  def remove(path)
23
23
  @paths.delete(path)
24
- @adapter.roots_list_change_notification
24
+ @coordinator.roots_list_change_notification
25
25
  end
26
26
 
27
27
  def to_request
@@ -76,13 +76,9 @@ module RubyLLM
76
76
  private
77
77
 
78
78
  def callback_guard_success?
79
- return true unless @coordinator.sampling_callback_enabled?
79
+ return true unless @coordinator.client.sampling_callback_enabled?
80
80
 
81
- callback_result = @coordinator.sampling_callback&.call(self)
82
- # If callback returns nil, it means no guard was configured - allow it
83
- return true if callback_result.nil?
84
-
85
- unless callback_result
81
+ unless @coordinator.client.on[:sampling].call(self)
86
82
  @coordinator.error_response(id: @id, message: REJECTED_MESSAGE)
87
83
  return false
88
84
  end
@@ -25,11 +25,11 @@ module RubyLLM
25
25
  end
26
26
 
27
27
  class Tool < RubyLLM::Tool
28
- attr_reader :name, :title, :description, :adapter, :annotations, :tool_response, :with_prefix
28
+ attr_reader :name, :title, :description, :annotations, :coordinator, :tool_response, :with_prefix
29
29
 
30
- def initialize(adapter, tool_response, with_prefix: false)
30
+ def initialize(coordinator, tool_response, with_prefix: false)
31
31
  super()
32
- @adapter = adapter
32
+ @coordinator = coordinator
33
33
 
34
34
  @with_prefix = with_prefix
35
35
  @name = format_name(tool_response["name"])
@@ -45,7 +45,7 @@ module RubyLLM
45
45
  end
46
46
 
47
47
  def display_name
48
- "#{@adapter.client.name}: #{@name}"
48
+ "#{@coordinator.name}: #{@name}"
49
49
  end
50
50
 
51
51
  def params_schema
@@ -53,7 +53,7 @@ module RubyLLM
53
53
  end
54
54
 
55
55
  def execute(**params)
56
- result = @adapter.execute_tool(
56
+ result = @coordinator.execute_tool(
57
57
  name: @mcp_name,
58
58
  parameters: params
59
59
  )
@@ -116,7 +116,7 @@ module RubyLLM
116
116
  }
117
117
  }
118
118
 
119
- resource = Resource.new(adapter, resource_data)
119
+ resource = Resource.new(coordinator, resource_data)
120
120
  resource.to_content
121
121
  when "resource_link"
122
122
  resource_data = {
@@ -126,15 +126,15 @@ module RubyLLM
126
126
  "mimeType" => content["mimeType"]
127
127
  }
128
128
 
129
- resource = Resource.new(adapter, resource_data)
130
- @adapter.register_resource(resource)
129
+ resource = Resource.new(coordinator, resource_data)
130
+ @coordinator.register_resource(resource)
131
131
  resource.to_content
132
132
  end
133
133
  end
134
134
 
135
135
  def format_name(name)
136
136
  if @with_prefix
137
- "#{@adapter.client.name}_#{name}"
137
+ "#{@coordinator.name}_#{name}"
138
138
  else
139
139
  name
140
140
  end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class Transport
6
+ class << self
7
+ def transports
8
+ @transports ||= {}
9
+ end
10
+
11
+ def register_transport(transport_type, transport_class)
12
+ transports[transport_type] = transport_class
13
+ end
14
+ end
15
+
16
+ extend Forwardable
17
+
18
+ register_transport(:sse, RubyLLM::MCP::Transports::SSE)
19
+ register_transport(:stdio, RubyLLM::MCP::Transports::Stdio)
20
+ register_transport(:streamable, RubyLLM::MCP::Transports::StreamableHTTP)
21
+ register_transport(:streamable_http, RubyLLM::MCP::Transports::StreamableHTTP)
22
+
23
+ attr_reader :transport_type, :coordinator, :config, :pid
24
+
25
+ def initialize(transport_type, coordinator, config:)
26
+ @transport_type = transport_type
27
+ @coordinator = coordinator
28
+ @config = config
29
+ @pid = Process.pid
30
+ end
31
+
32
+ def_delegators :transport_protocol, :request, :alive?, :close, :start, :set_protocol_version
33
+
34
+ def transport_protocol
35
+ if @pid != Process.pid
36
+ @pid = Process.pid
37
+ @transport = build_transport
38
+ coordinator.restart_transport
39
+ end
40
+
41
+ @transport_protocol ||= build_transport
42
+ end
43
+
44
+ private
45
+
46
+ def build_transport
47
+ unless RubyLLM::MCP::Transport.transports.key?(transport_type)
48
+ supported_types = RubyLLM::MCP::Transport.transports.keys.join(", ")
49
+ message = "Invalid transport type: :#{transport_type}. Supported types are #{supported_types}"
50
+ raise Errors::InvalidTransportType.new(message: message)
51
+ end
52
+
53
+ transport_config = prepare_transport_config
54
+ transport_klass = RubyLLM::MCP::Transport.transports[transport_type]
55
+ transport_klass.new(coordinator: coordinator, **transport_config)
56
+ end
57
+
58
+ def prepare_transport_config
59
+ transport_config = config.dup
60
+ oauth_provider = create_oauth_provider(transport_config) if oauth_config_present?(transport_config)
61
+
62
+ # Extract transport-specific parameters and consolidate into options
63
+ if %i[sse streamable streamable_http].include?(transport_type)
64
+ prepare_http_transport_config(transport_config, oauth_provider)
65
+ elsif transport_type == :stdio
66
+ prepare_stdio_transport_config(transport_config)
67
+ else
68
+ transport_config
69
+ end
70
+ end
71
+
72
+ def prepare_http_transport_config(config, oauth_provider)
73
+ options = {
74
+ version: config.delete(:version) || config.delete("version"),
75
+ headers: config.delete(:headers) || config.delete("headers"),
76
+ oauth_provider: oauth_provider,
77
+ reconnection: config.delete(:reconnection) || config.delete("reconnection"),
78
+ reconnection_options: config.delete(:reconnection_options) || config.delete("reconnection_options"),
79
+ rate_limit: config.delete(:rate_limit) || config.delete("rate_limit"),
80
+ session_id: config.delete(:session_id) || config.delete("session_id")
81
+ }.compact
82
+
83
+ config[:options] = options
84
+ config
85
+ end
86
+
87
+ def prepare_stdio_transport_config(config)
88
+ options = {
89
+ args: config.delete(:args) || config.delete("args"),
90
+ env: config.delete(:env) || config.delete("env")
91
+ }.compact
92
+
93
+ config[:options] = options unless options.empty?
94
+ config
95
+ end
96
+
97
+ # Check if OAuth configuration is present
98
+ def oauth_config_present?(config)
99
+ oauth_config = config[:oauth] || config["oauth"]
100
+ return false if oauth_config.nil?
101
+
102
+ # If it's an OAuth provider instance, it's present
103
+ return true if oauth_config.respond_to?(:access_token)
104
+
105
+ # If it's a hash, check if it's not empty
106
+ !oauth_config.empty?
107
+ end
108
+
109
+ # Create OAuth provider from configuration
110
+ # Accepts either a provider instance or a configuration hash
111
+ def create_oauth_provider(config)
112
+ oauth_config = config.delete(:oauth) || config.delete("oauth")
113
+ return nil unless oauth_config
114
+
115
+ # If provider key exists with an instance, use it
116
+ if oauth_config.is_a?(Hash) && (oauth_config[:provider] || oauth_config["provider"])
117
+ return oauth_config[:provider] || oauth_config["provider"]
118
+ end
119
+
120
+ # If oauth_config itself is a provider instance, use it directly
121
+ if oauth_config.respond_to?(:access_token) && oauth_config.respond_to?(:start_authorization_flow)
122
+ return oauth_config
123
+ end
124
+
125
+ # Otherwise create new provider from config hash
126
+ # Determine server URL based on transport type
127
+ server_url = determine_server_url(config)
128
+ return nil unless server_url
129
+
130
+ redirect_uri = oauth_config[:redirect_uri] || oauth_config["redirect_uri"] || "http://localhost:8080/callback"
131
+ scope = oauth_config[:scope] || oauth_config["scope"]
132
+ storage = oauth_config[:storage] || oauth_config["storage"]
133
+ grant_type = oauth_config[:grant_type] || oauth_config["grant_type"] || :authorization_code
134
+
135
+ RubyLLM::MCP::Auth::OAuthProvider.new(
136
+ server_url: server_url,
137
+ redirect_uri: redirect_uri,
138
+ scope: scope,
139
+ logger: MCP.logger,
140
+ storage: storage,
141
+ grant_type: grant_type
142
+ )
143
+ end
144
+
145
+ # Determine server URL from transport config
146
+ def determine_server_url(config)
147
+ config[:url] || config["url"]
148
+ end
149
+ end
150
+ end
151
+ end