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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +277 -0
- data/lib/generators/ruby_llm/mcp/install/install_generator.rb +42 -0
- data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +56 -0
- 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/chat.rb +34 -0
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +179 -0
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +292 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +33 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +52 -0
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +86 -0
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +92 -0
- data/lib/ruby_llm/mcp/attachment.rb +18 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_handler.rb +71 -0
- data/lib/ruby_llm/mcp/auth/browser/callback_server.rb +30 -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 +280 -0
- data/lib/ruby_llm/mcp/auth/client_registrar.rb +170 -0
- data/lib/ruby_llm/mcp/auth/discoverer.rb +124 -0
- data/lib/ruby_llm/mcp/auth/flows/authorization_code_flow.rb +105 -0
- data/lib/ruby_llm/mcp/auth/flows/client_credentials_flow.rb +66 -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 +90 -0
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +305 -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 +236 -0
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +107 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +76 -0
- data/lib/ruby_llm/mcp/auth.rb +359 -0
- data/lib/ruby_llm/mcp/client.rb +401 -0
- data/lib/ruby_llm/mcp/completion.rb +16 -0
- data/lib/ruby_llm/mcp/configuration.rb +310 -0
- data/lib/ruby_llm/mcp/content.rb +28 -0
- data/lib/ruby_llm/mcp/elicitation.rb +48 -0
- data/lib/ruby_llm/mcp/error.rb +34 -0
- data/lib/ruby_llm/mcp/errors.rb +91 -0
- data/lib/ruby_llm/mcp/logging.rb +16 -0
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +57 -0
- data/lib/ruby_llm/mcp/native/client.rb +387 -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 +42 -0
- data/lib/ruby_llm/mcp/native/messages/requests.rb +206 -0
- data/lib/ruby_llm/mcp/native/messages/responses.rb +106 -0
- data/lib/ruby_llm/mcp/native/messages.rb +36 -0
- data/lib/ruby_llm/mcp/native/notification.rb +16 -0
- data/lib/ruby_llm/mcp/native/protocol.rb +36 -0
- data/lib/ruby_llm/mcp/native/response_handler.rb +110 -0
- data/lib/ruby_llm/mcp/native/transport.rb +88 -0
- data/lib/ruby_llm/mcp/native/transports/sse.rb +607 -0
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +356 -0
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +926 -0
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/native/transports/support/rate_limit.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 +100 -0
- data/lib/ruby_llm/mcp/progress.rb +35 -0
- data/lib/ruby_llm/mcp/prompt.rb +132 -0
- data/lib/ruby_llm/mcp/railtie.rb +14 -0
- data/lib/ruby_llm/mcp/resource.rb +112 -0
- data/lib/ruby_llm/mcp/resource_template.rb +85 -0
- data/lib/ruby_llm/mcp/result.rb +108 -0
- data/lib/ruby_llm/mcp/roots.rb +45 -0
- data/lib/ruby_llm/mcp/sample.rb +152 -0
- data/lib/ruby_llm/mcp/server_capabilities.rb +49 -0
- data/lib/ruby_llm/mcp/tool.rb +228 -0
- data/lib/ruby_llm/mcp/version.rb +7 -0
- data/lib/ruby_llm/mcp.rb +125 -0
- data/lib/tasks/release.rake +23 -0
- 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
|