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,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
class ResponseHandler
|
|
7
|
+
attr_reader :coordinator
|
|
8
|
+
|
|
9
|
+
def initialize(coordinator)
|
|
10
|
+
@coordinator = coordinator
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def execute(result)
|
|
14
|
+
operation = CancellableOperation.new(result.id)
|
|
15
|
+
coordinator.register_in_flight_request(result.id, operation)
|
|
16
|
+
|
|
17
|
+
begin
|
|
18
|
+
# Execute in a separate thread that can be terminated on cancellation
|
|
19
|
+
operation.execute do
|
|
20
|
+
if result.ping?
|
|
21
|
+
coordinator.ping_response(id: result.id)
|
|
22
|
+
true
|
|
23
|
+
elsif result.roots?
|
|
24
|
+
handle_roots_response(result)
|
|
25
|
+
true
|
|
26
|
+
elsif result.sampling?
|
|
27
|
+
handle_sampling_response(result)
|
|
28
|
+
true
|
|
29
|
+
elsif result.elicitation?
|
|
30
|
+
handle_elicitation_response(result)
|
|
31
|
+
true
|
|
32
|
+
else
|
|
33
|
+
handle_unknown_request(result)
|
|
34
|
+
RubyLLM::MCP.logger.error("MCP client was sent unknown method type and \
|
|
35
|
+
could not respond: #{result.inspect}.")
|
|
36
|
+
false
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
rescue Errors::RequestCancelled => e
|
|
40
|
+
RubyLLM::MCP.logger.info("Request #{result.id} was cancelled: #{e.message}")
|
|
41
|
+
# Don't send response - cancellation means result is unused
|
|
42
|
+
true
|
|
43
|
+
ensure
|
|
44
|
+
coordinator.unregister_in_flight_request(result.id)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def handle_roots_response(result)
|
|
51
|
+
RubyLLM::MCP.logger.info("Roots request: #{result.inspect}")
|
|
52
|
+
roots_paths = coordinator.roots_paths
|
|
53
|
+
if roots_paths&.any?
|
|
54
|
+
coordinator.roots_list_response(id: result.id)
|
|
55
|
+
else
|
|
56
|
+
coordinator.error_response(
|
|
57
|
+
id: result.id,
|
|
58
|
+
message: "Roots are not enabled",
|
|
59
|
+
code: Native::JsonRpc::ErrorCodes::SERVER_ERROR
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
RubyLLM::MCP.logger.error("Error in roots request: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
64
|
+
coordinator.error_response(
|
|
65
|
+
id: result.id,
|
|
66
|
+
message: "Internal error processing roots request",
|
|
67
|
+
code: Native::JsonRpc::ErrorCodes::INTERNAL_ERROR,
|
|
68
|
+
data: { detail: e.message }
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def handle_sampling_response(result)
|
|
73
|
+
unless MCP.config.sampling.enabled?
|
|
74
|
+
RubyLLM::MCP.logger.info("Sampling is disabled, yet server requested sampling")
|
|
75
|
+
coordinator.error_response(
|
|
76
|
+
id: result.id,
|
|
77
|
+
message: "Sampling is disabled",
|
|
78
|
+
code: Native::JsonRpc::ErrorCodes::SERVER_ERROR
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
RubyLLM::MCP.logger.info("Sampling request: #{result.inspect}")
|
|
84
|
+
Sample.new(result, coordinator).execute
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
RubyLLM::MCP.logger.error("Error in sampling request: #{e.message}\n#{e.backtrace.join("\n")}")
|
|
87
|
+
coordinator.error_response(
|
|
88
|
+
id: result.id,
|
|
89
|
+
message: "Internal error processing sampling request",
|
|
90
|
+
code: Native::JsonRpc::ErrorCodes::INTERNAL_ERROR,
|
|
91
|
+
data: { detail: e.message }
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def handle_elicitation_response(result)
|
|
96
|
+
RubyLLM::MCP.logger.info("Elicitation request: #{result.inspect}")
|
|
97
|
+
Elicitation.new(coordinator, result).execute
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def handle_unknown_request(result)
|
|
101
|
+
coordinator.error_response(
|
|
102
|
+
id: result.id,
|
|
103
|
+
message: "Method not found: #{result.method}",
|
|
104
|
+
code: Native::JsonRpc::ErrorCodes::METHOD_NOT_FOUND
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Native
|
|
6
|
+
class Transport
|
|
7
|
+
class << self
|
|
8
|
+
def transports
|
|
9
|
+
@transports ||= {}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def register_transport(transport_type, transport_class)
|
|
13
|
+
transports[transport_type] = transport_class
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
extend Forwardable
|
|
18
|
+
|
|
19
|
+
register_transport(:sse, RubyLLM::MCP::Native::Transports::SSE)
|
|
20
|
+
register_transport(:stdio, RubyLLM::MCP::Native::Transports::Stdio)
|
|
21
|
+
register_transport(:streamable, RubyLLM::MCP::Native::Transports::StreamableHTTP)
|
|
22
|
+
register_transport(:streamable_http, RubyLLM::MCP::Native::Transports::StreamableHTTP)
|
|
23
|
+
|
|
24
|
+
attr_reader :transport_type, :coordinator, :config, :pid
|
|
25
|
+
|
|
26
|
+
def initialize(transport_type, coordinator, config:)
|
|
27
|
+
@transport_type = transport_type
|
|
28
|
+
@coordinator = coordinator
|
|
29
|
+
@config = config
|
|
30
|
+
@pid = Process.pid
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def_delegators :transport_protocol, :request, :alive?, :close, :start, :set_protocol_version
|
|
34
|
+
|
|
35
|
+
def transport_protocol
|
|
36
|
+
if @pid != Process.pid
|
|
37
|
+
@pid = Process.pid
|
|
38
|
+
@transport_protocol = nil
|
|
39
|
+
@transport_protocol = build_transport
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
@transport_protocol ||= build_transport
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def build_transport
|
|
48
|
+
unless RubyLLM::MCP::Native::Transport.transports.key?(transport_type)
|
|
49
|
+
supported_types = RubyLLM::MCP::Native::Transport.transports.keys.join(", ")
|
|
50
|
+
message = "Invalid transport type: :#{transport_type}. Supported types are #{supported_types}"
|
|
51
|
+
raise Errors::InvalidTransportType.new(message: message)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Extract and merge options if present (from OAuth helper preparation)
|
|
55
|
+
transport_config = config.dup
|
|
56
|
+
if transport_config[:options]
|
|
57
|
+
options = transport_config.delete(:options)
|
|
58
|
+
transport_config.merge!(options)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Handle SSE transport specially - it uses options hash pattern
|
|
62
|
+
if transport_type == :sse
|
|
63
|
+
url = transport_config.delete(:url) || transport_config.delete("url")
|
|
64
|
+
request_timeout = transport_config.delete(:request_timeout) ||
|
|
65
|
+
transport_config.delete("request_timeout") ||
|
|
66
|
+
MCP.config.request_timeout
|
|
67
|
+
# Everything else goes into options
|
|
68
|
+
options_hash = transport_config.dup
|
|
69
|
+
transport_config.clear
|
|
70
|
+
transport_config[:url] = url
|
|
71
|
+
transport_config[:request_timeout] = request_timeout
|
|
72
|
+
transport_config[:options] = options_hash
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Remove OAuth-specific params from transports that don't support them
|
|
76
|
+
# This allows other arbitrary params (like timeout) to pass through for testing
|
|
77
|
+
unless %i[streamable streamable_http sse].include?(transport_type)
|
|
78
|
+
transport_config.delete(:oauth_provider)
|
|
79
|
+
transport_config.delete(:oauth)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
transport_klass = RubyLLM::MCP::Native::Transport.transports[transport_type]
|
|
83
|
+
transport_klass.new(coordinator: coordinator, **transport_config)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|