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.
- checksums.yaml +4 -4
- data/README.md +144 -162
- data/lib/generators/ruby_llm/mcp/{install_generator.rb → install/install_generator.rb} +4 -2
- data/lib/generators/ruby_llm/mcp/{templates → install/templates}/initializer.rb +21 -4
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +29 -0
- data/lib/generators/ruby_llm/mcp/oauth/install_generator.rb +354 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/mcp_token_storage.rb.tt +114 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/concerns/user_mcp_oauth_concern.rb.tt +90 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/controllers/mcp_connections_controller.rb.tt +239 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/cleanup_expired_oauth_states_job.rb.tt +27 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/jobs/example_job.rb.tt +78 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/lib/mcp_client.rb.tt +68 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_credentials.rb.tt +19 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/migrations/create_mcp_oauth_states.rb.tt +21 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_credential.rb.tt +54 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/models/mcp_oauth_state.rb.tt +30 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/index.html.erb +646 -0
- data/lib/generators/ruby_llm/mcp/oauth/templates/views/show.html.erb +560 -0
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +215 -0
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +413 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +41 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +56 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +56 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +90 -0
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +216 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +36 -0
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +112 -0
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +39 -0
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +607 -0
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +427 -0
- data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
- data/lib/ruby_llm/mcp/auth/discoverer.rb +255 -0
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +122 -0
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +67 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/authorization_code.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/base.rb +31 -0
- data/lib/ruby_llm/mcp/auth/grant_strategies/client_credentials.rb +31 -0
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +63 -0
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +91 -0
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +341 -0
- data/lib/ruby_llm/mcp/auth/security.rb +44 -0
- data/lib/ruby_llm/mcp/auth/session_manager.rb +54 -0
- data/lib/ruby_llm/mcp/auth/token_manager.rb +307 -0
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +135 -0
- data/lib/ruby_llm/mcp/auth.rb +371 -0
- data/lib/ruby_llm/mcp/client.rb +312 -35
- data/lib/ruby_llm/mcp/configuration.rb +199 -24
- data/lib/ruby_llm/mcp/elicitation.rb +261 -14
- data/lib/ruby_llm/mcp/errors.rb +29 -0
- data/lib/ruby_llm/mcp/extensions/apps/constants.rb +28 -0
- data/lib/ruby_llm/mcp/extensions/apps/resource_metadata.rb +24 -0
- data/lib/ruby_llm/mcp/extensions/apps/tool_metadata.rb +45 -0
- data/lib/ruby_llm/mcp/extensions/configuration.rb +72 -0
- data/lib/ruby_llm/mcp/extensions/constants.rb +16 -0
- data/lib/ruby_llm/mcp/extensions/registry.rb +85 -0
- data/lib/ruby_llm/mcp/handlers/approval_decision.rb +90 -0
- data/lib/ruby_llm/mcp/handlers/async_response.rb +181 -0
- data/lib/ruby_llm/mcp/handlers/concerns/approval_actions.rb +42 -0
- data/lib/ruby_llm/mcp/handlers/concerns/async_execution.rb +80 -0
- data/lib/ruby_llm/mcp/handlers/concerns/elicitation_actions.rb +42 -0
- data/lib/ruby_llm/mcp/handlers/concerns/error_handling.rb +29 -0
- data/lib/ruby_llm/mcp/handlers/concerns/guard_checks.rb +72 -0
- data/lib/ruby_llm/mcp/handlers/concerns/lifecycle.rb +84 -0
- data/lib/ruby_llm/mcp/handlers/concerns/logging.rb +19 -0
- data/lib/ruby_llm/mcp/handlers/concerns/model_filtering.rb +36 -0
- data/lib/ruby_llm/mcp/handlers/concerns/options.rb +83 -0
- data/lib/ruby_llm/mcp/handlers/concerns/registry_integration.rb +54 -0
- data/lib/ruby_llm/mcp/handlers/concerns/sampling_actions.rb +84 -0
- data/lib/ruby_llm/mcp/handlers/concerns/timeouts.rb +52 -0
- data/lib/ruby_llm/mcp/handlers/concerns/tool_filtering.rb +50 -0
- data/lib/ruby_llm/mcp/handlers/elicitation_handler.rb +58 -0
- data/lib/ruby_llm/mcp/handlers/elicitation_registry.rb +203 -0
- data/lib/ruby_llm/mcp/handlers/human_in_the_loop_handler.rb +93 -0
- data/lib/ruby_llm/mcp/handlers/human_in_the_loop_registry.rb +271 -0
- data/lib/ruby_llm/mcp/handlers/promise.rb +192 -0
- data/lib/ruby_llm/mcp/handlers/sampling_handler.rb +64 -0
- data/lib/ruby_llm/mcp/handlers.rb +14 -0
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +94 -0
- data/lib/ruby_llm/mcp/native/client.rb +551 -0
- data/lib/ruby_llm/mcp/native/json_rpc.rb +170 -0
- data/lib/ruby_llm/mcp/native/messages/helpers.rb +39 -0
- data/lib/ruby_llm/mcp/native/messages/notifications.rb +60 -0
- data/lib/ruby_llm/mcp/native/messages/requests.rb +267 -0
- data/lib/ruby_llm/mcp/native/messages/responses.rb +114 -0
- data/lib/ruby_llm/mcp/native/messages.rb +43 -0
- data/lib/ruby_llm/mcp/native/notification.rb +16 -0
- data/lib/ruby_llm/mcp/native/protocol.rb +79 -0
- data/lib/ruby_llm/mcp/native/response_handler.rb +220 -0
- data/lib/ruby_llm/mcp/native/task_registry.rb +62 -0
- data/lib/ruby_llm/mcp/native/transport.rb +88 -0
- data/lib/ruby_llm/mcp/native/transports/sse.rb +655 -0
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +367 -0
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +1024 -0
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/native/transports/support/rate_limiter.rb +49 -0
- data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +36 -0
- data/lib/ruby_llm/mcp/native.rb +12 -0
- data/lib/ruby_llm/mcp/notification_handler.rb +43 -5
- data/lib/ruby_llm/mcp/prompt.rb +7 -7
- data/lib/ruby_llm/mcp/railtie.rb +7 -13
- data/lib/ruby_llm/mcp/resource.rb +17 -8
- data/lib/ruby_llm/mcp/resource_template.rb +8 -7
- data/lib/ruby_llm/mcp/result.rb +8 -4
- data/lib/ruby_llm/mcp/roots.rb +4 -4
- data/lib/ruby_llm/mcp/sample.rb +83 -13
- data/lib/ruby_llm/mcp/schema_validator.rb +33 -0
- data/lib/ruby_llm/mcp/server_capabilities.rb +41 -0
- data/lib/ruby_llm/mcp/task.rb +65 -0
- data/lib/ruby_llm/mcp/tool.rb +33 -27
- data/lib/ruby_llm/mcp/version.rb +1 -1
- data/lib/ruby_llm/mcp.rb +37 -7
- data/lib/tasks/smoke.rake +66 -0
- metadata +115 -39
- data/lib/generators/ruby_llm/mcp/templates/mcps.yml +0 -9
- data/lib/ruby_llm/mcp/coordinator.rb +0 -293
- data/lib/ruby_llm/mcp/notifications/cancelled.rb +0 -32
- data/lib/ruby_llm/mcp/notifications/initialize.rb +0 -24
- data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +0 -26
- data/lib/ruby_llm/mcp/protocol.rb +0 -34
- data/lib/ruby_llm/mcp/requests/completion_prompt.rb +0 -50
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +0 -50
- data/lib/ruby_llm/mcp/requests/initialization.rb +0 -34
- data/lib/ruby_llm/mcp/requests/logging_set_level.rb +0 -28
- data/lib/ruby_llm/mcp/requests/ping.rb +0 -24
- data/lib/ruby_llm/mcp/requests/prompt_call.rb +0 -32
- data/lib/ruby_llm/mcp/requests/prompt_list.rb +0 -31
- data/lib/ruby_llm/mcp/requests/resource_list.rb +0 -31
- data/lib/ruby_llm/mcp/requests/resource_read.rb +0 -30
- data/lib/ruby_llm/mcp/requests/resource_template_list.rb +0 -31
- data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +0 -30
- data/lib/ruby_llm/mcp/requests/shared/meta.rb +0 -32
- data/lib/ruby_llm/mcp/requests/shared/pagination.rb +0 -17
- data/lib/ruby_llm/mcp/requests/tool_call.rb +0 -35
- data/lib/ruby_llm/mcp/requests/tool_list.rb +0 -31
- data/lib/ruby_llm/mcp/response_handler.rb +0 -67
- data/lib/ruby_llm/mcp/responses/elicitation.rb +0 -33
- data/lib/ruby_llm/mcp/responses/error.rb +0 -33
- data/lib/ruby_llm/mcp/responses/ping.rb +0 -28
- data/lib/ruby_llm/mcp/responses/roots_list.rb +0 -31
- data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +0 -50
- data/lib/ruby_llm/mcp/transport.rb +0 -58
- data/lib/ruby_llm/mcp/transports/sse.rb +0 -341
- data/lib/ruby_llm/mcp/transports/stdio.rb +0 -230
- data/lib/ruby_llm/mcp/transports/streamable_http.rb +0 -723
- data/lib/ruby_llm/mcp/transports/support/http_client.rb +0 -28
- data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +0 -47
- 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
|