ruby_llm_swarm-mcp 0.8.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.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +277 -0
  4. data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
  5. data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
  6. data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
  7. data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
  8. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
  9. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
  10. data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
  11. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
  12. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
  13. data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
  14. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
  15. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
  16. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
  17. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
  18. data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
  19. data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
  20. data/lib/ruby_llm/chat.rb +34 -0
  21. data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
  22. data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
  23. data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
  24. data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
  25. data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
  26. data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
  27. data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
  28. data/lib/ruby_llm/mcp/attachment.rb +18 -0
  29. data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
  30. data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -0
  31. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
  32. data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
  33. data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
  34. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +280 -0
  35. data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
  36. data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
  37. data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
  38. data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -0
  39. data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
  40. data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
  41. data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
  42. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
  43. data/lib/ruby_llm/mcp/auth/memory_storage.rb +90 -0
  44. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -0
  45. data/lib/ruby_llm/mcp/auth/security.rb +44 -0
  46. data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
  47. data/lib/ruby_llm/mcp/auth/token_manager.rb +236 -0
  48. data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
  49. data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
  50. data/lib/ruby_llm/mcp/auth.rb +359 -0
  51. data/lib/ruby_llm/mcp/client.rb +401 -0
  52. data/lib/ruby_llm/mcp/completion.rb +16 -0
  53. data/lib/ruby_llm/mcp/configuration.rb +310 -0
  54. data/lib/ruby_llm/mcp/content.rb +28 -0
  55. data/lib/ruby_llm/mcp/elicitation.rb +48 -0
  56. data/lib/ruby_llm/mcp/error.rb +34 -0
  57. data/lib/ruby_llm/mcp/errors.rb +91 -0
  58. data/lib/ruby_llm/mcp/logging.rb +16 -0
  59. data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
  60. data/lib/ruby_llm/mcp/native/client.rb +387 -0
  61. data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
  62. data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
  63. data/lib/ruby_llm/mcp/native/messages/notifications.rb +42 -0
  64. data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
  65. data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
  66. data/lib/ruby_llm/mcp/native/messages.rb +36 -0
  67. data/lib/ruby_llm/mcp/native/notification.rb +16 -0
  68. data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
  69. data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
  70. data/lib/ruby_llm/mcp/native/transport.rb +88 -0
  71. data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
  72. data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
  73. data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
  74. data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
  75. data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +49 -0
  76. data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
  77. data/lib/ruby_llm/mcp/native.rb +12 -0
  78. data/lib/ruby_llm/mcp/notification_handler.rb +100 -0
  79. data/lib/ruby_llm/mcp/progress.rb +35 -0
  80. data/lib/ruby_llm/mcp/prompt.rb +132 -0
  81. data/lib/ruby_llm/mcp/railtie.rb +14 -0
  82. data/lib/ruby_llm/mcp/resource.rb +112 -0
  83. data/lib/ruby_llm/mcp/resource_template.rb +85 -0
  84. data/lib/ruby_llm/mcp/result.rb +108 -0
  85. data/lib/ruby_llm/mcp/roots.rb +45 -0
  86. data/lib/ruby_llm/mcp/sample.rb +152 -0
  87. data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
  88. data/lib/ruby_llm/mcp/tool.rb +228 -0
  89. data/lib/ruby_llm/mcp/version.rb +7 -0
  90. data/lib/ruby_llm/mcp.rb +125 -0
  91. data/lib/tasks/release.rake +23 -0
  92. metadata +184 -0
@@ -0,0 +1,179 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Adapters
6
+ class BaseAdapter
7
+ class << self
8
+ def supported_features
9
+ @supported_features ||= {}
10
+ end
11
+
12
+ def supports(*features)
13
+ features.each { |f| supported_features[f] = true }
14
+ end
15
+
16
+ def support?(feature)
17
+ supported_features[feature] || false
18
+ end
19
+
20
+ def supported_transports
21
+ @supported_transports ||= []
22
+ end
23
+
24
+ def supports_transport(*transports)
25
+ @supported_transports = transports
26
+ end
27
+
28
+ def transport_supported?(transport)
29
+ supported_transports.include?(transport.to_sym)
30
+ end
31
+ end
32
+
33
+ attr_reader :client
34
+
35
+ def initialize(client, transport_type:, config: {})
36
+ @client = client
37
+ @transport_type = transport_type
38
+ @config = config
39
+ end
40
+
41
+ def supports?(feature)
42
+ self.class.support?(feature)
43
+ end
44
+
45
+ def validate_transport!(transport_type)
46
+ unless self.class.transport_supported?(transport_type)
47
+ raise Errors::UnsupportedTransport.new(
48
+ message: <<~MSG.strip
49
+ Transport '#{transport_type}' is not supported by #{self.class.name}.
50
+ Supported transports: #{self.class.supported_transports.join(', ')}
51
+ MSG
52
+ )
53
+ end
54
+ end
55
+
56
+ def start
57
+ raise NotImplementedError, "#{self.class.name} must implement #start"
58
+ end
59
+
60
+ def stop
61
+ raise NotImplementedError, "#{self.class.name} must implement #stop"
62
+ end
63
+
64
+ def restart!
65
+ raise NotImplementedError, "#{self.class.name} must implement #restart!"
66
+ end
67
+
68
+ def alive?
69
+ raise NotImplementedError, "#{self.class.name} must implement #alive?"
70
+ end
71
+
72
+ def ping
73
+ raise NotImplementedError, "#{self.class.name} must implement #ping"
74
+ end
75
+
76
+ def capabilities
77
+ raise NotImplementedError, "#{self.class.name} must implement #capabilities"
78
+ end
79
+
80
+ def client_capabilities
81
+ raise NotImplementedError, "#{self.class.name} must implement #client_capabilities"
82
+ end
83
+
84
+ def tool_list(cursor: nil)
85
+ raise NotImplementedError, "#{self.class.name} must implement #tool_list"
86
+ end
87
+
88
+ def execute_tool(name:, parameters:)
89
+ raise NotImplementedError, "#{self.class.name} must implement #execute_tool"
90
+ end
91
+
92
+ def resource_list(cursor: nil)
93
+ raise NotImplementedError, "#{self.class.name} must implement #resource_list"
94
+ end
95
+
96
+ def resource_read(uri:)
97
+ raise NotImplementedError, "#{self.class.name} must implement #resource_read"
98
+ end
99
+
100
+ def prompt_list(cursor: nil)
101
+ raise NotImplementedError, "#{self.class.name} must implement #prompt_list"
102
+ end
103
+
104
+ def execute_prompt(name:, arguments:)
105
+ raise NotImplementedError, "#{self.class.name} must implement #execute_prompt"
106
+ end
107
+
108
+ def resource_template_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
109
+ raise_unsupported_feature(:resource_templates)
110
+ end
111
+
112
+ def completion_resource(uri:, argument:, value:, context: nil) # rubocop:disable Lint/UnusedMethodArgument
113
+ raise_unsupported_feature(:completions)
114
+ end
115
+
116
+ def completion_prompt(name:, argument:, value:, context: nil) # rubocop:disable Lint/UnusedMethodArgument
117
+ raise_unsupported_feature(:completions)
118
+ end
119
+
120
+ def set_logging(level:) # rubocop:disable Lint/UnusedMethodArgument
121
+ raise_unsupported_feature(:logging)
122
+ end
123
+
124
+ def resources_subscribe(uri:) # rubocop:disable Lint/UnusedMethodArgument
125
+ raise_unsupported_feature(:subscriptions)
126
+ end
127
+
128
+ def initialize_notification
129
+ raise_unsupported_feature(:notifications)
130
+ end
131
+
132
+ def cancelled_notification(reason:, request_id:) # rubocop:disable Lint/UnusedMethodArgument
133
+ raise_unsupported_feature(:notifications)
134
+ end
135
+
136
+ def roots_list_change_notification
137
+ raise_unsupported_feature(:notifications)
138
+ end
139
+
140
+ def ping_response(id:) # rubocop:disable Lint/UnusedMethodArgument
141
+ raise_unsupported_feature(:responses)
142
+ end
143
+
144
+ def roots_list_response(id:) # rubocop:disable Lint/UnusedMethodArgument
145
+ raise_unsupported_feature(:responses)
146
+ end
147
+
148
+ def sampling_create_message_response(id:, model:, message:, **_options) # rubocop:disable Lint/UnusedMethodArgument
149
+ raise_unsupported_feature(:sampling)
150
+ end
151
+
152
+ def error_response(id:, message:, code: -32_000) # rubocop:disable Lint/UnusedMethodArgument
153
+ raise_unsupported_feature(:responses)
154
+ end
155
+
156
+ def elicitation_response(id:, elicitation:) # rubocop:disable Lint/UnusedMethodArgument
157
+ raise_unsupported_feature(:elicitation)
158
+ end
159
+
160
+ def register_resource(_resource)
161
+ raise_unsupported_feature(:resource_registration)
162
+ end
163
+
164
+ private
165
+
166
+ def raise_unsupported_feature(feature)
167
+ raise Errors::UnsupportedFeature.new(
168
+ message: <<~MSG.strip
169
+ Feature '#{feature}' is not supported by #{self.class.name}.
170
+
171
+ This feature requires the :ruby_llm adapter.
172
+ Change your configuration to use adapter: :ruby_llm
173
+ MSG
174
+ )
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "mcp_transports/coordinator_stub"
4
+ require_relative "mcp_transports/stdio"
5
+ require_relative "mcp_transports/sse"
6
+ require_relative "mcp_transports/streamable_http"
7
+
8
+ module RubyLLM
9
+ module MCP
10
+ module Adapters
11
+ class MCPSdkAdapter < BaseAdapter
12
+ # Only declare features the official MCP SDK supports
13
+ # Note: The MCP gem (as of v0.4) does NOT support prompts or resource templates
14
+ supports :tools, :resources
15
+
16
+ # Supported transports:
17
+ # - stdio: Via custom wrapper using native stdio transport ✓ FULLY TESTED
18
+ # - sse: Via custom wrapper using native SSE transport ✓ FUNCTIONAL
19
+ # - http: Via MCP::Client::HTTP (for simple JSON-only HTTP servers)
20
+ supports_transport :stdio, :http, :sse, :streamable, :streamable_http
21
+
22
+ attr_reader :transport_type, :config
23
+
24
+ def initialize(client, transport_type:, config: {})
25
+ validate_transport!(transport_type)
26
+ require_mcp_gem!
27
+ super
28
+
29
+ @mcp_client = nil
30
+ end
31
+
32
+ def start
33
+ return if @mcp_client
34
+
35
+ transport = build_transport
36
+ transport.start if transport.respond_to?(:start)
37
+
38
+ @mcp_client = ::MCP::Client.new(transport: transport)
39
+ end
40
+
41
+ def stop
42
+ if @mcp_client && @mcp_client.transport.respond_to?(:close)
43
+ @mcp_client.transport.close
44
+ end
45
+ @mcp_client = nil
46
+ end
47
+
48
+ def restart!
49
+ stop
50
+ start
51
+ end
52
+
53
+ def alive?
54
+ !@mcp_client.nil?
55
+ end
56
+
57
+ def ping # rubocop:disable Naming/PredicateMethod
58
+ ensure_started
59
+ alive?
60
+ end
61
+
62
+ def capabilities
63
+ # Return minimal capabilities for official SDK
64
+ # Note: Prompts are not supported by the MCP gem
65
+ ServerCapabilities.new({
66
+ "tools" => {},
67
+ "resources" => {}
68
+ })
69
+ end
70
+
71
+ def client_capabilities
72
+ {} # Official SDK handles this internally
73
+ end
74
+
75
+ def tool_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
76
+ ensure_started
77
+ @mcp_client.tools.map { |tool| transform_tool(tool) }
78
+ end
79
+
80
+ def execute_tool(name:, parameters:)
81
+ ensure_started
82
+ tool = find_tool(name)
83
+ result = @mcp_client.call_tool(tool: tool, arguments: parameters)
84
+ transform_tool_result(result)
85
+ rescue RubyLLM::MCP::Errors::TimeoutError => e
86
+ native_transport = @mcp_client&.transport&.native_transport
87
+ if native_transport&.alive? && !e.request_id.nil?
88
+ cancelled_notification(reason: "Request timed out", request_id: e.request_id)
89
+ end
90
+ raise e
91
+ end
92
+
93
+ def resource_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
94
+ ensure_started
95
+ @mcp_client.resources.map { |resource| transform_resource(resource) }
96
+ end
97
+
98
+ def resource_read(uri:)
99
+ ensure_started
100
+ result = @mcp_client.read_resource(uri: uri)
101
+ transform_resource_content(result)
102
+ end
103
+
104
+ def prompt_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
105
+ []
106
+ end
107
+
108
+ def execute_prompt(name:, arguments:)
109
+ raise NotImplementedError, "Prompts are not supported by the MCP SDK (gem 'mcp')"
110
+ end
111
+
112
+ def resource_template_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
113
+ []
114
+ end
115
+
116
+ def cancelled_notification(reason:, request_id:)
117
+ return unless @mcp_client&.transport.respond_to?(:native_transport)
118
+
119
+ native_transport = @mcp_client.transport.native_transport
120
+ return unless native_transport
121
+
122
+ body = RubyLLM::MCP::Native::Messages::Notifications.cancelled(
123
+ request_id: request_id,
124
+ reason: reason
125
+ )
126
+ native_transport.request(body, wait_for_response: false)
127
+ end
128
+
129
+ # These methods remain as NotImplementedError from base class:
130
+ # - completion_resource
131
+ # - completion_prompt
132
+ # - set_logging
133
+ # - resources_subscribe
134
+ # - initialize_notification
135
+ # - roots_list_change_notification
136
+ # - ping_response
137
+ # - roots_list_response
138
+ # - sampling_create_message_response
139
+ # - error_response
140
+ # - elicitation_response
141
+ # - register_resource
142
+
143
+ private
144
+
145
+ def ensure_started
146
+ start unless @mcp_client
147
+ end
148
+
149
+ def require_mcp_gem!
150
+ require "mcp"
151
+ if ::MCP::VERSION < "0.4"
152
+ raise Errors::AdapterConfigurationError.new(message: <<~MSG)
153
+ The official MCP SDK version 0.4 or higher is required to use the :mcp_sdk adapter.
154
+ MSG
155
+ end
156
+ rescue LoadError
157
+ raise LoadError, <<~MSG
158
+ The official MCP SDK is required to use the :mcp_sdk adapter.
159
+
160
+ Add to your Gemfile:
161
+ gem 'mcp', '~> 0.4'
162
+
163
+ Then run: bundle install
164
+ MSG
165
+ end
166
+
167
+ def build_transport
168
+ case @transport_type
169
+ when :http
170
+ # MCP::Client::HTTP is for simple JSON-only HTTP servers
171
+ # Use :streamable for servers that support the streamable HTTP/SSE protocol
172
+ ::MCP::Client::HTTP.new(
173
+ url: @config[:url],
174
+ headers: @config[:headers] || {}
175
+ )
176
+ when :stdio
177
+ MCPTransports::Stdio.new(
178
+ command: @config[:command],
179
+ args: @config[:args] || [],
180
+ env: @config[:env] || {},
181
+ request_timeout: @config[:request_timeout] || 10_000
182
+ )
183
+ when :sse
184
+ MCPTransports::SSE.new(
185
+ url: @config[:url],
186
+ headers: @config[:headers] || {},
187
+ version: @config[:version] || :http2,
188
+ request_timeout: @config[:request_timeout] || 10_000
189
+ )
190
+ when :streamable, :streamable_http
191
+ config_copy = @config.dup
192
+ oauth_provider = Auth::TransportOauthHelper.create_oauth_provider(config_copy) if Auth::TransportOauthHelper.oauth_config_present?(config_copy)
193
+
194
+ MCPTransports::StreamableHTTP.new(
195
+ url: @config[:url],
196
+ headers: @config[:headers] || {},
197
+ version: @config[:version] || :http2,
198
+ request_timeout: @config[:request_timeout] || 10_000,
199
+ reconnection: @config[:reconnection] || {},
200
+ oauth_provider: oauth_provider,
201
+ rate_limit: @config[:rate_limit],
202
+ session_id: @config[:session_id]
203
+ )
204
+ end
205
+ end
206
+
207
+ def find_tool(name)
208
+ @mcp_client.tools.find { |t| t.name == name } ||
209
+ raise(Errors::ResponseError.new(
210
+ message: "Tool '#{name}' not found",
211
+ error: { "code" => -32_602, "message" => "Tool not found" }
212
+ ))
213
+ end
214
+
215
+ # Transform methods to normalize official SDK objects
216
+ def transform_tool(tool)
217
+ {
218
+ "name" => tool.name,
219
+ "description" => tool.description,
220
+ "inputSchema" => tool.input_schema
221
+ }
222
+ end
223
+
224
+ def transform_resource(resource)
225
+ {
226
+ "name" => resource["name"],
227
+ "uri" => resource["uri"],
228
+ "description" => resource["description"],
229
+ "mimeType" => resource["mimeType"]
230
+ }
231
+ end
232
+
233
+ def transform_tool_result(result)
234
+ # The MCP gem returns the full JSON-RPC response
235
+ # Extract the content from result["result"]["content"]
236
+ content = if result.is_a?(Hash) && result["result"] && result["result"]["content"]
237
+ result["result"]["content"]
238
+ elsif result.is_a?(Array)
239
+ result.map { |item| transform_content_item(item) }
240
+ else
241
+ [{ "type" => "text", "text" => result.to_s }]
242
+ end
243
+
244
+ is_error = if result.is_a?(Hash) && result["result"]
245
+ result["result"]["isError"]
246
+ end
247
+
248
+ result_data = { "content" => content }
249
+ result_data["isError"] = is_error unless is_error.nil?
250
+
251
+ Result.new({
252
+ "result" => result_data
253
+ })
254
+ end
255
+
256
+ def transform_content_item(item)
257
+ case item
258
+ when String
259
+ { "type" => "text", "text" => item }
260
+ when Hash
261
+ item
262
+ else
263
+ { "type" => "text", "text" => item.to_s }
264
+ end
265
+ end
266
+
267
+ def transform_resource_content(result)
268
+ contents = if result.is_a?(Array)
269
+ result.map { |r| transform_single_resource_content(r) }
270
+ else
271
+ [transform_single_resource_content(result)]
272
+ end
273
+
274
+ Result.new({
275
+ "result" => {
276
+ "contents" => contents
277
+ }
278
+ })
279
+ end
280
+
281
+ def transform_single_resource_content(result)
282
+ {
283
+ "uri" => result["uri"],
284
+ "mimeType" => result["mimeType"],
285
+ "text" => result["text"],
286
+ "blob" => result["blob"]
287
+ }
288
+ end
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM::MCP::Adapters::MCPTransports
4
+ # Minimal coordinator stub for MCP transports
5
+ # The native transports expect a coordinator object, but for the MCP SDK adapter
6
+ # we don't need to process results (just pass them through)
7
+ # as MCP SDK adapter doesn't methods that requires responsing to the MCP server as of yet.
8
+ class CoordinatorStub
9
+ attr_reader :name, :protocol_version
10
+ attr_accessor :transport
11
+
12
+ def initialize
13
+ @name = "MCP-SDK-Adapter"
14
+ @protocol_version = RubyLLM::MCP::Native::Protocol.default_negotiated_version
15
+ @transport = nil
16
+ end
17
+
18
+ def process_result(result)
19
+ result
20
+ end
21
+
22
+ def client_capabilities
23
+ {} # MCP SDK doesn't provide client capabilities
24
+ end
25
+
26
+ def request(body, **options)
27
+ # For notifications (cancelled, etc), we need to send them through the transport
28
+ return nil unless @transport
29
+
30
+ @transport.request(body, **options)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM::MCP::Adapters::MCPTransports
4
+ # Custom SSE transport for MCP SDK adapter
5
+ # Wraps the native SSE transport to provide the interface expected by MCP::Client
6
+ class SSE
7
+ attr_reader :native_transport
8
+
9
+ def initialize(url:, headers: {}, version: :http2, request_timeout: 10_000)
10
+ # Create a minimal coordinator-like object for the native transport
11
+ @coordinator = CoordinatorStub.new
12
+
13
+ @native_transport = RubyLLM::MCP::Native::Transports::SSE.new(
14
+ url: url,
15
+ coordinator: @coordinator,
16
+ request_timeout: request_timeout,
17
+ options: {
18
+ headers: headers,
19
+ version: version
20
+ }
21
+ )
22
+ end
23
+
24
+ def start
25
+ @native_transport.start
26
+ end
27
+
28
+ def close
29
+ @native_transport.close
30
+ end
31
+
32
+ # Send a JSON-RPC request and return the response
33
+ # This is the interface expected by MCP::Client
34
+ #
35
+ # @param request [Hash] A JSON-RPC request object
36
+ # @return [Hash] A JSON-RPC response object
37
+ def send_request(request:)
38
+ start unless @native_transport.alive?
39
+
40
+ unless request["id"] || request[:id]
41
+ request["id"] = SecureRandom.uuid
42
+ end
43
+ result = @native_transport.request(request, wait_for_response: true)
44
+
45
+ if result.is_a?(RubyLLM::MCP::Result)
46
+ result.response
47
+ else
48
+ result
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM::MCP::Adapters::MCPTransports
4
+ # Custom Stdio transport for MCP SDK adapter
5
+ # Wraps the native Stdio transport to provide the interface expected by MCP::Client
6
+ class Stdio
7
+ attr_reader :native_transport
8
+
9
+ def initialize(command:, args: [], env: {}, request_timeout: 10_000)
10
+ # Create a minimal coordinator-like object for the native transport
11
+ @coordinator = CoordinatorStub.new
12
+
13
+ @native_transport = RubyLLM::MCP::Native::Transports::Stdio.new(
14
+ command: command,
15
+ args: args,
16
+ env: env,
17
+ coordinator: @coordinator,
18
+ request_timeout: request_timeout
19
+ )
20
+
21
+ @coordinator.transport = @native_transport
22
+ end
23
+
24
+ def start
25
+ @native_transport.start
26
+ end
27
+
28
+ def close
29
+ @native_transport.close
30
+ end
31
+
32
+ # Send a JSON-RPC request and return the response
33
+ # This is the interface expected by MCP::Client
34
+ #
35
+ # @param request [Hash] A JSON-RPC request object
36
+ # @return [Hash] A JSON-RPC response object
37
+ def send_request(request:)
38
+ start unless @native_transport.alive?
39
+
40
+ unless request["id"] || request[:id]
41
+ request["id"] = SecureRandom.uuid
42
+ end
43
+ result = @native_transport.request(request, wait_for_response: true)
44
+
45
+ if result.is_a?(RubyLLM::MCP::Result)
46
+ result.response
47
+ else
48
+ result
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM::MCP::Adapters::MCPTransports
4
+ # Custom Streamable HTTP transport for MCP SDK adapter
5
+ # Wraps the native StreamableHTTP transport to provide the interface expected by MCP::Client
6
+ class StreamableHTTP
7
+ attr_reader :native_transport
8
+
9
+ def initialize(url:, headers: {}, version: :http2, request_timeout: 10_000, # rubocop:disable Metrics/ParameterLists
10
+ reconnection: {}, oauth_provider: nil, rate_limit: nil, session_id: nil)
11
+ # Create a minimal coordinator-like object for the native transport
12
+ @coordinator = CoordinatorStub.new
13
+ @initialized = false
14
+
15
+ @native_transport = RubyLLM::MCP::Native::Transports::StreamableHTTP.new(
16
+ url: url,
17
+ headers: headers,
18
+ version: version,
19
+ coordinator: @coordinator,
20
+ request_timeout: request_timeout,
21
+ reconnection: reconnection,
22
+ oauth_provider: oauth_provider,
23
+ rate_limit: rate_limit,
24
+ session_id: session_id
25
+ )
26
+
27
+ @coordinator.transport = @native_transport
28
+ end
29
+
30
+ def start
31
+ @native_transport.start
32
+ end
33
+
34
+ def close
35
+ @initialized = false
36
+ @native_transport.close
37
+ end
38
+
39
+ # Send a JSON-RPC request and return the response
40
+ # This is the interface expected by MCP::Client
41
+ #
42
+ # @param request [Hash] A JSON-RPC request object
43
+ # @return [Hash] A JSON-RPC response object
44
+ def send_request(request:)
45
+ # Auto-initialize on first non-initialize request
46
+ # Streamable HTTP servers require initialization before other requests
47
+ unless @initialized || request[:method] == "initialize" || request["method"] == "initialize"
48
+ perform_initialization
49
+ end
50
+
51
+ unless request["id"] || request[:id]
52
+ request["id"] = SecureRandom.uuid
53
+ end
54
+ result = @native_transport.request(request, wait_for_response: true)
55
+
56
+ if result.is_a?(RubyLLM::MCP::Result)
57
+ result.response
58
+ else
59
+ result
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def perform_initialization
66
+ # Send initialization request
67
+ init_request = RubyLLM::MCP::Native::Messages::Requests.initialize(
68
+ protocol_version: @coordinator.protocol_version,
69
+ capabilities: @coordinator.client_capabilities
70
+ )
71
+ result = @native_transport.request(init_request, wait_for_response: true)
72
+
73
+ if result.is_a?(RubyLLM::MCP::Result) && result.error?
74
+ raise RubyLLM::MCP::Errors::TransportError.new(
75
+ message: "Initialization failed: #{result.error}",
76
+ error: result.error
77
+ )
78
+ end
79
+
80
+ initialized_notification = RubyLLM::MCP::Native::Messages::Notifications.initialized
81
+ @native_transport.request(initialized_notification, wait_for_response: false)
82
+
83
+ @initialized = true
84
+ end
85
+ end
86
+ end