ruby_llm-mcp 0.7.1 → 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.
Files changed (149) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +144 -162
  3. data/lib/generators/ruby_llm/mcp/{install_generator.rb → install/install_generator.rb} +4 -2
  4. data/lib/generators/ruby_llm/mcp/{templates → install/templates}/initializer.rb +21 -4
  5. data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
  6. data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
  7. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
  8. data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
  9. data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
  10. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
  11. data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
  12. data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
  13. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
  14. data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
  15. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
  16. data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
  17. data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
  18. data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
  19. data/lib/ruby_llm/mcp/adapters/base_adapter.rb +215 -0
  20. data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +413 -0
  21. data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +41 -0
  22. data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +56 -0
  23. data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +56 -0
  24. data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +90 -0
  25. data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +216 -0
  26. data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
  27. data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +36 -0
  28. data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
  29. data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
  30. data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
  31. data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +427 -0
  32. data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
  33. data/lib/ruby_llm/mcp/auth/discoverer.rb +255 -0
  34. data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +122 -0
  35. data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +67 -0
  36. data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
  37. data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
  38. data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
  39. data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
  40. data/lib/ruby_llm/mcp/auth/memory_storage.rb +91 -0
  41. data/lib/ruby_llm/mcp/auth/oauth_provider.rb +341 -0
  42. data/lib/ruby_llm/mcp/auth/security.rb +44 -0
  43. data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
  44. data/lib/ruby_llm/mcp/auth/token_manager.rb +307 -0
  45. data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
  46. data/lib/ruby_llm/mcp/auth/url_builder.rb +135 -0
  47. data/lib/ruby_llm/mcp/auth.rb +371 -0
  48. data/lib/ruby_llm/mcp/client.rb +312 -35
  49. data/lib/ruby_llm/mcp/configuration.rb +199 -24
  50. data/lib/ruby_llm/mcp/elicitation.rb +261 -14
  51. data/lib/ruby_llm/mcp/errors.rb +29 -0
  52. data/lib/ruby_llm/mcp/extensions/apps/constants.rb +28 -0
  53. data/lib/ruby_llm/mcp/extensions/apps/resource_metadata.rb +24 -0
  54. data/lib/ruby_llm/mcp/extensions/apps/tool_metadata.rb +45 -0
  55. data/lib/ruby_llm/mcp/extensions/configuration.rb +72 -0
  56. data/lib/ruby_llm/mcp/extensions/constants.rb +16 -0
  57. data/lib/ruby_llm/mcp/extensions/registry.rb +85 -0
  58. data/lib/ruby_llm/mcp/handlers/approval_decision.rb +90 -0
  59. data/lib/ruby_llm/mcp/handlers/async_response.rb +181 -0
  60. data/lib/ruby_llm/mcp/handlers/concerns/approval_actions.rb +42 -0
  61. data/lib/ruby_llm/mcp/handlers/concerns/async_execution.rb +80 -0
  62. data/lib/ruby_llm/mcp/handlers/concerns/elicitation_actions.rb +42 -0
  63. data/lib/ruby_llm/mcp/handlers/concerns/error_handling.rb +29 -0
  64. data/lib/ruby_llm/mcp/handlers/concerns/guard_checks.rb +72 -0
  65. data/lib/ruby_llm/mcp/handlers/concerns/lifecycle.rb +84 -0
  66. data/lib/ruby_llm/mcp/handlers/concerns/logging.rb +19 -0
  67. data/lib/ruby_llm/mcp/handlers/concerns/model_filtering.rb +36 -0
  68. data/lib/ruby_llm/mcp/handlers/concerns/options.rb +83 -0
  69. data/lib/ruby_llm/mcp/handlers/concerns/registry_integration.rb +54 -0
  70. data/lib/ruby_llm/mcp/handlers/concerns/sampling_actions.rb +84 -0
  71. data/lib/ruby_llm/mcp/handlers/concerns/timeouts.rb +52 -0
  72. data/lib/ruby_llm/mcp/handlers/concerns/tool_filtering.rb +50 -0
  73. data/lib/ruby_llm/mcp/handlers/elicitation_handler.rb +58 -0
  74. data/lib/ruby_llm/mcp/handlers/elicitation_registry.rb +203 -0
  75. data/lib/ruby_llm/mcp/handlers/human_in_the_loop_handler.rb +93 -0
  76. data/lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb +271 -0
  77. data/lib/ruby_llm/mcp/handlers/promise.rb +192 -0
  78. data/lib/ruby_llm/mcp/handlers/sampling_handler.rb +64 -0
  79. data/lib/ruby_llm/mcp/handlers.rb +14 -0
  80. data/lib/ruby_llm/mcp/native/cancellable_operation.rb +94 -0
  81. data/lib/ruby_llm/mcp/native/client.rb +551 -0
  82. data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
  83. data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
  84. data/lib/ruby_llm/mcp/native/messages/notifications.rb +60 -0
  85. data/lib/ruby_llm/mcp/native/messages/requests.rb +267 -0
  86. data/lib/ruby_llm/mcp/native/messages/responses.rb +114 -0
  87. data/lib/ruby_llm/mcp/native/messages.rb +43 -0
  88. data/lib/ruby_llm/mcp/native/notification.rb +16 -0
  89. data/lib/ruby_llm/mcp/native/protocol.rb +79 -0
  90. data/lib/ruby_llm/mcp/native/response_handler.rb +220 -0
  91. data/lib/ruby_llm/mcp/native/task_registry.rb +62 -0
  92. data/lib/ruby_llm/mcp/native/transport.rb +88 -0
  93. data/lib/ruby_llm/mcp/native/transports/sse.rb +655 -0
  94. data/lib/ruby_llm/mcp/native/transports/stdio.rb +367 -0
  95. data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +1024 -0
  96. data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
  97. data/lib/ruby_llm/mcp/native/transports/support/rate_limiter.rb +49 -0
  98. data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
  99. data/lib/ruby_llm/mcp/native.rb +12 -0
  100. data/lib/ruby_llm/mcp/notification_handler.rb +43 -5
  101. data/lib/ruby_llm/mcp/prompt.rb +7 -7
  102. data/lib/ruby_llm/mcp/railtie.rb +7 -13
  103. data/lib/ruby_llm/mcp/resource.rb +17 -8
  104. data/lib/ruby_llm/mcp/resource_template.rb +8 -7
  105. data/lib/ruby_llm/mcp/result.rb +8 -4
  106. data/lib/ruby_llm/mcp/roots.rb +4 -4
  107. data/lib/ruby_llm/mcp/sample.rb +83 -13
  108. data/lib/ruby_llm/mcp/schema_validator.rb +33 -0
  109. data/lib/ruby_llm/mcp/server_capabilities.rb +41 -0
  110. data/lib/ruby_llm/mcp/task.rb +65 -0
  111. data/lib/ruby_llm/mcp/tool.rb +33 -27
  112. data/lib/ruby_llm/mcp/version.rb +1 -1
  113. data/lib/ruby_llm/mcp.rb +37 -7
  114. data/lib/tasks/smoke.rake +66 -0
  115. metadata +115 -39
  116. data/lib/generators/ruby_llm/mcp/templates/mcps.yml +0 -9
  117. data/lib/ruby_llm/mcp/coordinator.rb +0 -293
  118. data/lib/ruby_llm/mcp/notifications/cancelled.rb +0 -32
  119. data/lib/ruby_llm/mcp/notifications/initialize.rb +0 -24
  120. data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +0 -26
  121. data/lib/ruby_llm/mcp/protocol.rb +0 -34
  122. data/lib/ruby_llm/mcp/requests/completion_prompt.rb +0 -50
  123. data/lib/ruby_llm/mcp/requests/completion_resource.rb +0 -50
  124. data/lib/ruby_llm/mcp/requests/initialization.rb +0 -34
  125. data/lib/ruby_llm/mcp/requests/logging_set_level.rb +0 -28
  126. data/lib/ruby_llm/mcp/requests/ping.rb +0 -24
  127. data/lib/ruby_llm/mcp/requests/prompt_call.rb +0 -32
  128. data/lib/ruby_llm/mcp/requests/prompt_list.rb +0 -31
  129. data/lib/ruby_llm/mcp/requests/resource_list.rb +0 -31
  130. data/lib/ruby_llm/mcp/requests/resource_read.rb +0 -30
  131. data/lib/ruby_llm/mcp/requests/resource_template_list.rb +0 -31
  132. data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +0 -30
  133. data/lib/ruby_llm/mcp/requests/shared/meta.rb +0 -32
  134. data/lib/ruby_llm/mcp/requests/shared/pagination.rb +0 -17
  135. data/lib/ruby_llm/mcp/requests/tool_call.rb +0 -35
  136. data/lib/ruby_llm/mcp/requests/tool_list.rb +0 -31
  137. data/lib/ruby_llm/mcp/response_handler.rb +0 -67
  138. data/lib/ruby_llm/mcp/responses/elicitation.rb +0 -33
  139. data/lib/ruby_llm/mcp/responses/error.rb +0 -33
  140. data/lib/ruby_llm/mcp/responses/ping.rb +0 -28
  141. data/lib/ruby_llm/mcp/responses/roots_list.rb +0 -31
  142. data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +0 -50
  143. data/lib/ruby_llm/mcp/transport.rb +0 -58
  144. data/lib/ruby_llm/mcp/transports/sse.rb +0 -341
  145. data/lib/ruby_llm/mcp/transports/stdio.rb +0 -230
  146. data/lib/ruby_llm/mcp/transports/streamable_http.rb +0 -723
  147. data/lib/ruby_llm/mcp/transports/support/http_client.rb +0 -28
  148. data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +0 -47
  149. data/lib/ruby_llm/mcp/transports/support/timeout.rb +0 -34
@@ -0,0 +1,215 @@
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 supports_extension_negotiation?
46
+ false
47
+ end
48
+
49
+ def extension_mode
50
+ :none
51
+ end
52
+
53
+ def build_client_extensions_capabilities(protocol_version:) # rubocop:disable Lint/UnusedMethodArgument
54
+ {}
55
+ end
56
+
57
+ def validate_transport!(transport_type)
58
+ unless self.class.transport_supported?(transport_type)
59
+ raise Errors::UnsupportedTransport.new(
60
+ message: <<~MSG.strip
61
+ Transport '#{transport_type}' is not supported by #{self.class.name}.
62
+ Supported transports: #{self.class.supported_transports.join(', ')}
63
+ MSG
64
+ )
65
+ end
66
+ end
67
+
68
+ def start
69
+ raise NotImplementedError, "#{self.class.name} must implement #start"
70
+ end
71
+
72
+ def stop
73
+ raise NotImplementedError, "#{self.class.name} must implement #stop"
74
+ end
75
+
76
+ def restart!
77
+ raise NotImplementedError, "#{self.class.name} must implement #restart!"
78
+ end
79
+
80
+ def alive?
81
+ raise NotImplementedError, "#{self.class.name} must implement #alive?"
82
+ end
83
+
84
+ def ping
85
+ raise NotImplementedError, "#{self.class.name} must implement #ping"
86
+ end
87
+
88
+ def capabilities
89
+ raise NotImplementedError, "#{self.class.name} must implement #capabilities"
90
+ end
91
+
92
+ def client_capabilities
93
+ raise NotImplementedError, "#{self.class.name} must implement #client_capabilities"
94
+ end
95
+
96
+ def tool_list(cursor: nil)
97
+ raise NotImplementedError, "#{self.class.name} must implement #tool_list"
98
+ end
99
+
100
+ def execute_tool(name:, parameters:)
101
+ raise NotImplementedError, "#{self.class.name} must implement #execute_tool"
102
+ end
103
+
104
+ def resource_list(cursor: nil)
105
+ raise NotImplementedError, "#{self.class.name} must implement #resource_list"
106
+ end
107
+
108
+ def resource_read(uri:)
109
+ raise NotImplementedError, "#{self.class.name} must implement #resource_read"
110
+ end
111
+
112
+ def prompt_list(cursor: nil)
113
+ raise NotImplementedError, "#{self.class.name} must implement #prompt_list"
114
+ end
115
+
116
+ def execute_prompt(name:, arguments:)
117
+ raise NotImplementedError, "#{self.class.name} must implement #execute_prompt"
118
+ end
119
+
120
+ def resource_template_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
121
+ raise_unsupported_feature(:resource_templates)
122
+ end
123
+
124
+ def completion_resource(uri:, argument:, value:, context: nil) # rubocop:disable Lint/UnusedMethodArgument
125
+ raise_unsupported_feature(:completions)
126
+ end
127
+
128
+ def completion_prompt(name:, argument:, value:, context: nil) # rubocop:disable Lint/UnusedMethodArgument
129
+ raise_unsupported_feature(:completions)
130
+ end
131
+
132
+ def set_logging(level:) # rubocop:disable Lint/UnusedMethodArgument
133
+ raise_unsupported_feature(:logging)
134
+ end
135
+
136
+ def resources_subscribe(uri:) # rubocop:disable Lint/UnusedMethodArgument
137
+ raise_unsupported_feature(:subscriptions)
138
+ end
139
+
140
+ def resources_unsubscribe(uri:) # rubocop:disable Lint/UnusedMethodArgument
141
+ raise_unsupported_feature(:subscriptions)
142
+ end
143
+
144
+ def tasks_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
145
+ raise_unsupported_feature(:tasks)
146
+ end
147
+
148
+ def task_get(task_id:) # rubocop:disable Lint/UnusedMethodArgument
149
+ raise_unsupported_feature(:tasks)
150
+ end
151
+
152
+ def task_result(task_id:) # rubocop:disable Lint/UnusedMethodArgument
153
+ raise_unsupported_feature(:tasks)
154
+ end
155
+
156
+ def task_cancel(task_id:) # rubocop:disable Lint/UnusedMethodArgument
157
+ raise_unsupported_feature(:tasks)
158
+ end
159
+
160
+ def task_status_notification(task:) # rubocop:disable Lint/UnusedMethodArgument
161
+ raise_unsupported_feature(:tasks)
162
+ end
163
+
164
+ def initialize_notification
165
+ raise_unsupported_feature(:notifications)
166
+ end
167
+
168
+ def cancelled_notification(reason:, request_id:) # rubocop:disable Lint/UnusedMethodArgument
169
+ raise_unsupported_feature(:notifications)
170
+ end
171
+
172
+ def roots_list_change_notification
173
+ raise_unsupported_feature(:notifications)
174
+ end
175
+
176
+ def ping_response(id:) # rubocop:disable Lint/UnusedMethodArgument
177
+ raise_unsupported_feature(:responses)
178
+ end
179
+
180
+ def roots_list_response(id:) # rubocop:disable Lint/UnusedMethodArgument
181
+ raise_unsupported_feature(:responses)
182
+ end
183
+
184
+ def sampling_create_message_response(id:, model:, message:, **_options) # rubocop:disable Lint/UnusedMethodArgument
185
+ raise_unsupported_feature(:sampling)
186
+ end
187
+
188
+ def error_response(id:, message:, code: -32_000) # rubocop:disable Lint/UnusedMethodArgument
189
+ raise_unsupported_feature(:responses)
190
+ end
191
+
192
+ def elicitation_response(id:, elicitation:) # rubocop:disable Lint/UnusedMethodArgument
193
+ raise_unsupported_feature(:elicitation)
194
+ end
195
+
196
+ def register_resource(_resource)
197
+ raise_unsupported_feature(:resource_registration)
198
+ end
199
+
200
+ private
201
+
202
+ def raise_unsupported_feature(feature)
203
+ raise Errors::UnsupportedFeature.new(
204
+ message: <<~MSG.strip
205
+ Feature '#{feature}' is not supported by #{self.class.name}.
206
+
207
+ This feature requires the :ruby_llm adapter.
208
+ Change your configuration to use adapter: :ruby_llm
209
+ MSG
210
+ )
211
+ end
212
+ end
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,413 @@
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
+ supports :tools, :resources, :prompts, :resource_templates, :logging
14
+
15
+ # Supported transports:
16
+ # - stdio: Via custom wrapper using native stdio transport ✓ FULLY TESTED
17
+ # - sse: Via custom wrapper using native SSE transport ✓ FUNCTIONAL
18
+ # - http: Via MCP::Client::HTTP (for simple JSON-only HTTP servers)
19
+ supports_transport :stdio, :http, :sse, :streamable, :streamable_http
20
+
21
+ attr_reader :transport_type, :config, :mcp_client
22
+
23
+ def initialize(client, transport_type:, config: {})
24
+ validate_transport!(transport_type)
25
+ require_mcp_gem!
26
+ super
27
+
28
+ @mcp_client = nil
29
+ @notification_handler = NotificationHandler.new(client)
30
+ warn_passive_extension_support! if configured_extensions?
31
+ end
32
+
33
+ def start
34
+ return if @mcp_client
35
+
36
+ transport = build_transport
37
+ transport.start if transport.respond_to?(:start)
38
+
39
+ @mcp_client = ::MCP::Client.new(transport: transport)
40
+ set_logging(level: client.on_logging_level) if client.logging_handler_enabled?
41
+ end
42
+
43
+ def stop
44
+ if @mcp_client && @mcp_client.transport.respond_to?(:close)
45
+ @mcp_client.transport.close
46
+ end
47
+ @mcp_client = nil
48
+ end
49
+
50
+ def restart!
51
+ stop
52
+ start
53
+ end
54
+
55
+ def alive?
56
+ !@mcp_client.nil?
57
+ end
58
+
59
+ def ping # rubocop:disable Naming/PredicateMethod
60
+ ensure_started
61
+ alive?
62
+ end
63
+
64
+ def capabilities
65
+ # Return minimal capabilities for official SDK
66
+ @capabilities ||= ServerCapabilities.new(
67
+ "tools" => {},
68
+ "resources" => {},
69
+ "prompts" => {},
70
+ "logging" => {}
71
+ )
72
+ end
73
+
74
+ def client_capabilities
75
+ {} # Official SDK handles this internally
76
+ end
77
+
78
+ def supports_extension_negotiation?
79
+ false
80
+ end
81
+
82
+ def extension_mode
83
+ :passive
84
+ end
85
+
86
+ def build_client_extensions_capabilities(protocol_version:) # rubocop:disable Lint/UnusedMethodArgument
87
+ {}
88
+ end
89
+
90
+ def tool_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
91
+ ensure_started
92
+ @mcp_client.tools.map { |tool| transform_tool(tool) }
93
+ end
94
+
95
+ def execute_tool(name:, parameters:)
96
+ ensure_started
97
+ tool = find_tool(name)
98
+ result = @mcp_client.call_tool(tool: tool, arguments: parameters)
99
+ transform_tool_result(result)
100
+ rescue RubyLLM::MCP::Errors::TimeoutError => e
101
+ native_transport = @mcp_client&.transport&.native_transport
102
+ if native_transport&.alive? && !e.request_id.nil?
103
+ cancelled_notification(reason: "Request timed out", request_id: e.request_id)
104
+ end
105
+ raise e
106
+ end
107
+
108
+ def resource_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
109
+ ensure_started
110
+ @mcp_client.resources.map { |resource| transform_resource(resource) }
111
+ end
112
+
113
+ def resource_read(uri:)
114
+ ensure_started
115
+ result = @mcp_client.read_resource(uri: uri)
116
+ transform_resource_content(result)
117
+ end
118
+
119
+ def prompt_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
120
+ ensure_started
121
+ @mcp_client.prompts.map { |prompt| transform_prompt(prompt) }
122
+ end
123
+
124
+ def execute_prompt(name:, arguments:)
125
+ ensure_started
126
+ response = @mcp_client.transport.send_request(
127
+ request: {
128
+ jsonrpc: "2.0",
129
+ id: SecureRandom.uuid,
130
+ method: "prompts/get",
131
+ params: {
132
+ name: name,
133
+ arguments: arguments
134
+ }
135
+ }
136
+ )
137
+
138
+ transform_prompt_result(response)
139
+ end
140
+
141
+ def resource_template_list(cursor: nil) # rubocop:disable Lint/UnusedMethodArgument
142
+ ensure_started
143
+ @mcp_client.resource_templates.map { |resource_template| transform_resource_template(resource_template) }
144
+ end
145
+
146
+ def set_logging(level:)
147
+ ensure_started
148
+ @mcp_client.transport.send_request(
149
+ request: {
150
+ jsonrpc: "2.0",
151
+ id: SecureRandom.uuid,
152
+ method: "logging/setLevel",
153
+ params: { level: level }
154
+ }
155
+ )
156
+ end
157
+
158
+ def cancelled_notification(reason:, request_id:)
159
+ return unless @mcp_client&.transport.respond_to?(:native_transport)
160
+
161
+ native_transport = @mcp_client.transport.native_transport
162
+ return unless native_transport
163
+
164
+ body = RubyLLM::MCP::Native::Messages::Notifications.cancelled(
165
+ request_id: request_id,
166
+ reason: reason
167
+ )
168
+ native_transport.request(body, wait_for_response: false)
169
+ end
170
+
171
+ # These methods remain as NotImplementedError from base class:
172
+ # - completion_resource
173
+ # - completion_prompt
174
+ # - resources_subscribe
175
+ # - initialize_notification
176
+ # - roots_list_change_notification
177
+ # - ping_response
178
+ # - roots_list_response
179
+ # - sampling_create_message_response
180
+ # - error_response
181
+ # - elicitation_response
182
+ # - register_resource
183
+
184
+ private
185
+
186
+ def ensure_started
187
+ start unless @mcp_client
188
+ end
189
+
190
+ def require_mcp_gem!
191
+ require "mcp"
192
+ if Gem::Version.new(::MCP::VERSION) < Gem::Version.new("0.7.0")
193
+ raise Errors::AdapterConfigurationError.new(message: <<~MSG)
194
+ The official MCP SDK version 0.7 or higher is required to use the :mcp_sdk adapter.
195
+ MSG
196
+ end
197
+ rescue LoadError
198
+ raise LoadError, <<~MSG
199
+ The official MCP SDK is required to use the :mcp_sdk adapter.
200
+
201
+ Add to your Gemfile:
202
+ gem 'mcp', '~> 0.7'
203
+
204
+ Then run: bundle install
205
+ MSG
206
+ end
207
+
208
+ def build_transport # rubocop:disable Metrics/MethodLength
209
+ protocol_version = @config[:protocol_version] || RubyLLM::MCP.config.protocol_version
210
+ notification_callback = lambda do |notification|
211
+ @notification_handler.execute(notification)
212
+ end
213
+
214
+ case @transport_type
215
+ when :http
216
+ # MCP::Client::HTTP is for simple JSON-only HTTP servers
217
+ # Use :streamable for servers that support the streamable HTTP/SSE protocol
218
+ ::MCP::Client::HTTP.new(
219
+ url: @config[:url],
220
+ headers: @config[:headers] || {}
221
+ )
222
+ when :stdio
223
+ MCPTransports::Stdio.new(
224
+ command: @config[:command],
225
+ args: @config[:args] || [],
226
+ env: @config[:env] || {},
227
+ request_timeout: @config[:request_timeout] || 10_000,
228
+ protocol_version: protocol_version,
229
+ notification_callback: notification_callback
230
+ )
231
+ when :sse
232
+ MCPTransports::SSE.new(
233
+ url: @config[:url],
234
+ headers: @config[:headers] || {},
235
+ version: @config[:version] || :http2,
236
+ request_timeout: @config[:request_timeout] || 10_000,
237
+ protocol_version: protocol_version,
238
+ notification_callback: notification_callback
239
+ )
240
+ when :streamable, :streamable_http
241
+ config_copy = @config.dup
242
+ oauth_provider = Auth::TransportOauthHelper.create_oauth_provider(config_copy) if Auth::TransportOauthHelper.oauth_config_present?(config_copy)
243
+
244
+ MCPTransports::StreamableHTTP.new(
245
+ url: @config[:url],
246
+ headers: @config[:headers] || {},
247
+ version: @config[:version] || :http2,
248
+ request_timeout: @config[:request_timeout] || 10_000,
249
+ reconnection: @config[:reconnection] || {},
250
+ oauth_provider: oauth_provider,
251
+ rate_limit: @config[:rate_limit],
252
+ session_id: @config[:session_id],
253
+ protocol_version: protocol_version,
254
+ notification_callback: notification_callback
255
+ )
256
+ end
257
+ end
258
+
259
+ def find_tool(name)
260
+ @mcp_client.tools.find { |t| t.name == name } ||
261
+ raise(Errors::ResponseError.new(
262
+ message: "Tool '#{name}' not found",
263
+ error: { "code" => -32_602, "message" => "Tool not found" }
264
+ ))
265
+ end
266
+
267
+ # Transform methods to normalize official SDK objects
268
+ def transform_tool(tool)
269
+ {
270
+ "name" => tool.name,
271
+ "description" => tool.description,
272
+ "inputSchema" => tool.input_schema,
273
+ "outputSchema" => tool.output_schema,
274
+ "_meta" => extract_tool_meta(tool)
275
+ }.compact
276
+ end
277
+
278
+ def transform_resource(resource)
279
+ {
280
+ "name" => resource["name"],
281
+ "uri" => resource["uri"],
282
+ "title" => resource["title"],
283
+ "description" => resource["description"],
284
+ "mimeType" => resource["mimeType"],
285
+ "annotations" => resource["annotations"],
286
+ "icons" => resource["icons"],
287
+ "_meta" => resource["_meta"]
288
+ }.compact
289
+ end
290
+
291
+ def transform_prompt(prompt)
292
+ {
293
+ "name" => prompt["name"],
294
+ "title" => prompt["title"],
295
+ "description" => prompt["description"],
296
+ "arguments" => prompt["arguments"]
297
+ }.compact
298
+ end
299
+
300
+ def transform_resource_template(resource_template)
301
+ {
302
+ "name" => resource_template["name"],
303
+ "uriTemplate" => resource_template["uriTemplate"],
304
+ "title" => resource_template["title"],
305
+ "description" => resource_template["description"],
306
+ "mimeType" => resource_template["mimeType"],
307
+ "annotations" => resource_template["annotations"],
308
+ "icons" => resource_template["icons"],
309
+ "_meta" => resource_template["_meta"]
310
+ }.compact
311
+ end
312
+
313
+ def transform_tool_result(result)
314
+ # The MCP gem returns the full JSON-RPC response
315
+ # Extract the content from result["result"]["content"]
316
+ content = if result.is_a?(Hash) && result["result"] && result["result"]["content"]
317
+ result["result"]["content"]
318
+ elsif result.is_a?(Array)
319
+ result.map { |item| transform_content_item(item) }
320
+ else
321
+ [{ "type" => "text", "text" => result.to_s }]
322
+ end
323
+
324
+ is_error = if result.is_a?(Hash) && result["result"]
325
+ result["result"]["isError"]
326
+ end
327
+
328
+ result_data = { "content" => content }
329
+ result_data["isError"] = is_error unless is_error.nil?
330
+
331
+ Result.new({
332
+ "result" => result_data
333
+ })
334
+ end
335
+
336
+ def transform_content_item(item)
337
+ case item
338
+ when String
339
+ { "type" => "text", "text" => item }
340
+ when Hash
341
+ item
342
+ else
343
+ { "type" => "text", "text" => item.to_s }
344
+ end
345
+ end
346
+
347
+ def transform_resource_content(result)
348
+ contents = if result.is_a?(Array)
349
+ result.map { |r| transform_single_resource_content(r) }
350
+ else
351
+ [transform_single_resource_content(result)]
352
+ end
353
+
354
+ Result.new({
355
+ "result" => {
356
+ "contents" => contents
357
+ }
358
+ })
359
+ end
360
+
361
+ def transform_single_resource_content(result)
362
+ {
363
+ "uri" => result["uri"],
364
+ "mimeType" => result["mimeType"],
365
+ "text" => result["text"],
366
+ "blob" => result["blob"]
367
+ }
368
+ end
369
+
370
+ def transform_prompt_result(result)
371
+ if result.is_a?(Hash) && (result.key?("result") || result.key?("error"))
372
+ Result.new(result)
373
+ else
374
+ Result.new({
375
+ "result" => result || {}
376
+ })
377
+ end
378
+ end
379
+
380
+ def configured_extensions?
381
+ !Extensions::Registry.normalize_map(@config[:extensions]).empty?
382
+ end
383
+
384
+ def warn_passive_extension_support!
385
+ self.class.warn_passive_extension_support_once
386
+ end
387
+
388
+ def extract_tool_meta(tool)
389
+ return tool["_meta"] if tool.respond_to?(:[]) && tool["_meta"]
390
+ return tool.meta if tool.respond_to?(:meta)
391
+ return tool.instance_variable_get(:@meta) if tool.instance_variable_defined?(:@meta)
392
+
393
+ nil
394
+ end
395
+
396
+ class << self
397
+ def warn_passive_extension_support_once
398
+ @extensions_warning_mutex ||= Mutex.new
399
+
400
+ @extensions_warning_mutex.synchronize do
401
+ return if @extensions_warning_emitted
402
+
403
+ RubyLLM::MCP.logger.warn(
404
+ "MCP SDK adapter extension configuration is passive: extensions are accepted but not advertised."
405
+ )
406
+ @extensions_warning_emitted = true
407
+ end
408
+ end
409
+ end
410
+ end
411
+ end
412
+ end
413
+ end
@@ -0,0 +1,41 @@
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(protocol_version:, notification_callback: nil)
13
+ @name = "MCP-SDK-Adapter"
14
+ @protocol_version = protocol_version
15
+ @transport = nil
16
+ @notification_callback = notification_callback
17
+ end
18
+
19
+ def process_result(result)
20
+ if result&.notification?
21
+ @notification_callback&.call(result.notification)
22
+ return nil
23
+ end
24
+
25
+ return nil if result&.request?
26
+
27
+ result
28
+ end
29
+
30
+ def client_capabilities
31
+ {} # MCP SDK doesn't provide client capabilities
32
+ end
33
+
34
+ def request(body, **options)
35
+ # For notifications (cancelled, etc), we need to send them through the transport
36
+ return nil unless @transport
37
+
38
+ @transport.request(body, **options)
39
+ end
40
+ end
41
+ end