ruby_llm_swarm-mcp 0.8.0 → 0.8.1
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 +3 -44
- data/lib/generators/ruby_llm/mcp/install/templates/initializer.rb +4 -21
- data/lib/generators/ruby_llm/mcp/install/templates/mcps.yml +0 -20
- data/lib/ruby_llm/mcp/auth/browser/http_server.rb +3 -0
- data/lib/ruby_llm/mcp/auth/browser/opener.rb +2 -0
- data/lib/ruby_llm/mcp/auth/browser/pages.rb +32 -100
- data/lib/ruby_llm/mcp/auth/browser_oauth_provider.rb +6 -32
- data/lib/ruby_llm/mcp/auth/http_response_handler.rb +2 -0
- data/lib/ruby_llm/mcp/auth/memory_storage.rb +0 -18
- data/lib/ruby_llm/mcp/auth/oauth_provider.rb +3 -82
- data/lib/ruby_llm/mcp/auth/session_manager.rb +2 -0
- data/lib/ruby_llm/mcp/auth/url_builder.rb +2 -0
- data/lib/ruby_llm/mcp/client.rb +32 -119
- data/lib/ruby_llm/mcp/configuration.rb +6 -74
- data/lib/ruby_llm/mcp/coordinator.rb +304 -0
- data/lib/ruby_llm/mcp/elicitation.rb +6 -8
- data/lib/ruby_llm/mcp/errors.rb +0 -15
- data/lib/ruby_llm/mcp/notification_handler.rb +5 -21
- data/lib/ruby_llm/mcp/notifications/cancelled.rb +32 -0
- data/lib/ruby_llm/mcp/notifications/initialize.rb +24 -0
- data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +26 -0
- data/lib/ruby_llm/mcp/prompt.rb +7 -7
- data/lib/ruby_llm/mcp/protocol.rb +34 -0
- data/lib/ruby_llm/mcp/railtie.rb +6 -8
- data/lib/ruby_llm/mcp/requests/completion_prompt.rb +50 -0
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +50 -0
- data/lib/ruby_llm/mcp/requests/initialization.rb +34 -0
- data/lib/ruby_llm/mcp/requests/logging_set_level.rb +28 -0
- data/lib/ruby_llm/mcp/requests/ping.rb +24 -0
- data/lib/ruby_llm/mcp/requests/prompt_call.rb +32 -0
- data/lib/ruby_llm/mcp/requests/prompt_list.rb +31 -0
- data/lib/ruby_llm/mcp/requests/resource_list.rb +31 -0
- data/lib/ruby_llm/mcp/requests/resource_read.rb +30 -0
- data/lib/ruby_llm/mcp/requests/resource_template_list.rb +31 -0
- data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +30 -0
- data/lib/ruby_llm/mcp/requests/shared/meta.rb +32 -0
- data/lib/ruby_llm/mcp/requests/shared/pagination.rb +17 -0
- data/lib/ruby_llm/mcp/requests/tool_call.rb +35 -0
- data/lib/ruby_llm/mcp/requests/tool_list.rb +31 -0
- data/lib/ruby_llm/mcp/resource.rb +8 -6
- data/lib/ruby_llm/mcp/resource_template.rb +7 -7
- data/lib/ruby_llm/mcp/response_handler.rb +67 -0
- data/lib/ruby_llm/mcp/responses/elicitation.rb +33 -0
- data/lib/ruby_llm/mcp/responses/error.rb +33 -0
- data/lib/ruby_llm/mcp/responses/ping.rb +28 -0
- data/lib/ruby_llm/mcp/responses/roots_list.rb +31 -0
- data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +50 -0
- data/lib/ruby_llm/mcp/result.rb +4 -8
- data/lib/ruby_llm/mcp/roots.rb +4 -4
- data/lib/ruby_llm/mcp/sample.rb +2 -6
- data/lib/ruby_llm/mcp/tool.rb +9 -9
- data/lib/ruby_llm/mcp/transport.rb +151 -0
- data/lib/ruby_llm/mcp/transports/sse.rb +435 -0
- data/lib/ruby_llm/mcp/transports/stdio.rb +231 -0
- data/lib/ruby_llm/mcp/transports/streamable_http.rb +725 -0
- data/lib/ruby_llm/mcp/transports/support/http_client.rb +28 -0
- data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +47 -0
- data/lib/ruby_llm/mcp/transports/support/timeout.rb +34 -0
- data/lib/ruby_llm/mcp/version.rb +1 -1
- data/lib/ruby_llm/mcp.rb +7 -30
- metadata +38 -33
- data/lib/ruby_llm/mcp/adapters/base_adapter.rb +0 -179
- data/lib/ruby_llm/mcp/adapters/mcp_sdk_adapter.rb +0 -292
- data/lib/ruby_llm/mcp/adapters/mcp_transports/coordinator_stub.rb +0 -33
- data/lib/ruby_llm/mcp/adapters/mcp_transports/sse.rb +0 -52
- data/lib/ruby_llm/mcp/adapters/mcp_transports/stdio.rb +0 -52
- data/lib/ruby_llm/mcp/adapters/mcp_transports/streamable_http.rb +0 -86
- data/lib/ruby_llm/mcp/adapters/ruby_llm_adapter.rb +0 -92
- data/lib/ruby_llm/mcp/auth/transport_oauth_helper.rb +0 -107
- data/lib/ruby_llm/mcp/native/cancellable_operation.rb +0 -57
- data/lib/ruby_llm/mcp/native/client.rb +0 -387
- data/lib/ruby_llm/mcp/native/json_rpc.rb +0 -170
- data/lib/ruby_llm/mcp/native/messages/helpers.rb +0 -39
- data/lib/ruby_llm/mcp/native/messages/notifications.rb +0 -42
- data/lib/ruby_llm/mcp/native/messages/requests.rb +0 -206
- data/lib/ruby_llm/mcp/native/messages/responses.rb +0 -106
- data/lib/ruby_llm/mcp/native/messages.rb +0 -36
- data/lib/ruby_llm/mcp/native/notification.rb +0 -16
- data/lib/ruby_llm/mcp/native/protocol.rb +0 -36
- data/lib/ruby_llm/mcp/native/response_handler.rb +0 -110
- data/lib/ruby_llm/mcp/native/transport.rb +0 -88
- data/lib/ruby_llm/mcp/native/transports/sse.rb +0 -607
- data/lib/ruby_llm/mcp/native/transports/stdio.rb +0 -356
- data/lib/ruby_llm/mcp/native/transports/streamable_http.rb +0 -926
- data/lib/ruby_llm/mcp/native/transports/support/http_client.rb +0 -28
- data/lib/ruby_llm/mcp/native/transports/support/rate_limit.rb +0 -49
- data/lib/ruby_llm/mcp/native/transports/support/timeout.rb +0 -36
- data/lib/ruby_llm/mcp/native.rb +0 -12
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
module Responses
|
|
6
|
+
class SamplingCreateMessage
|
|
7
|
+
def initialize(coordinator, id:, message:, model:)
|
|
8
|
+
@coordinator = coordinator
|
|
9
|
+
@id = id
|
|
10
|
+
@message = message
|
|
11
|
+
@model = model
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def call
|
|
15
|
+
@coordinator.request(sampling_create_message_body, add_id: false, wait_for_response: false)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def sampling_create_message_body
|
|
21
|
+
{
|
|
22
|
+
jsonrpc: "2.0",
|
|
23
|
+
id: @id,
|
|
24
|
+
result: {
|
|
25
|
+
role: @message.role,
|
|
26
|
+
content: format_content(@message.content),
|
|
27
|
+
model: @model,
|
|
28
|
+
# TODO: We are going to assume it was a endTurn
|
|
29
|
+
# Look into getting RubyLLM to expose stopReason in message response
|
|
30
|
+
stopReason: "endTurn"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def format_content(content)
|
|
36
|
+
if content.is_a?(RubyLLM::Content)
|
|
37
|
+
if context.text.none? && content.attachments.any?
|
|
38
|
+
attachment = content.attachments.first
|
|
39
|
+
{ type: attachment.type, data: attachment.content, mime_type: attachment.mime_type }
|
|
40
|
+
else
|
|
41
|
+
{ type: "text", text: content.text }
|
|
42
|
+
end
|
|
43
|
+
else
|
|
44
|
+
{ type: "text", text: content }
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
data/lib/ruby_llm/mcp/result.rb
CHANGED
|
@@ -32,10 +32,6 @@ module RubyLLM
|
|
|
32
32
|
|
|
33
33
|
@result_is_error = response.dig("result", "isError") || false
|
|
34
34
|
@next_cursor = response.dig("result", "nextCursor")
|
|
35
|
-
|
|
36
|
-
# Track whether result/error keys exist (for JSON-RPC detection)
|
|
37
|
-
@has_result = response.key?("result")
|
|
38
|
-
@has_error = response.key?("error")
|
|
39
35
|
end
|
|
40
36
|
|
|
41
37
|
REQUEST_METHODS.each do |method_name, method_value|
|
|
@@ -69,7 +65,7 @@ module RubyLLM
|
|
|
69
65
|
end
|
|
70
66
|
|
|
71
67
|
def notification?
|
|
72
|
-
@
|
|
68
|
+
@method&.include?("notifications") || false
|
|
73
69
|
end
|
|
74
70
|
|
|
75
71
|
def next_cursor?
|
|
@@ -77,15 +73,15 @@ module RubyLLM
|
|
|
77
73
|
end
|
|
78
74
|
|
|
79
75
|
def request?
|
|
80
|
-
!@
|
|
76
|
+
!@method.nil? && !notification? && @result.none? && @error.none?
|
|
81
77
|
end
|
|
82
78
|
|
|
83
79
|
def response?
|
|
84
|
-
!@id.nil? &&
|
|
80
|
+
!@id.nil? && (@result || @error.any?) && !@method
|
|
85
81
|
end
|
|
86
82
|
|
|
87
83
|
def success?
|
|
88
|
-
|
|
84
|
+
!@result.empty?
|
|
89
85
|
end
|
|
90
86
|
|
|
91
87
|
def tool_success?
|
data/lib/ruby_llm/mcp/roots.rb
CHANGED
|
@@ -5,9 +5,9 @@ module RubyLLM
|
|
|
5
5
|
class Roots
|
|
6
6
|
attr_reader :paths
|
|
7
7
|
|
|
8
|
-
def initialize(paths: [],
|
|
8
|
+
def initialize(paths: [], coordinator: nil)
|
|
9
9
|
@paths = paths
|
|
10
|
-
@
|
|
10
|
+
@coordinator = coordinator
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
def active?
|
|
@@ -16,12 +16,12 @@ module RubyLLM
|
|
|
16
16
|
|
|
17
17
|
def add(path)
|
|
18
18
|
@paths << path
|
|
19
|
-
@
|
|
19
|
+
@coordinator.roots_list_change_notification
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def remove(path)
|
|
23
23
|
@paths.delete(path)
|
|
24
|
-
@
|
|
24
|
+
@coordinator.roots_list_change_notification
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def to_request
|
data/lib/ruby_llm/mcp/sample.rb
CHANGED
|
@@ -76,13 +76,9 @@ module RubyLLM
|
|
|
76
76
|
private
|
|
77
77
|
|
|
78
78
|
def callback_guard_success?
|
|
79
|
-
return true unless @coordinator.sampling_callback_enabled?
|
|
79
|
+
return true unless @coordinator.client.sampling_callback_enabled?
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
# If callback returns nil, it means no guard was configured - allow it
|
|
83
|
-
return true if callback_result.nil?
|
|
84
|
-
|
|
85
|
-
unless callback_result
|
|
81
|
+
unless @coordinator.client.on[:sampling].call(self)
|
|
86
82
|
@coordinator.error_response(id: @id, message: REJECTED_MESSAGE)
|
|
87
83
|
return false
|
|
88
84
|
end
|
data/lib/ruby_llm/mcp/tool.rb
CHANGED
|
@@ -25,11 +25,11 @@ module RubyLLM
|
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
class Tool < RubyLLM::Tool
|
|
28
|
-
attr_reader :name, :title, :description, :
|
|
28
|
+
attr_reader :name, :title, :description, :annotations, :coordinator, :tool_response, :with_prefix
|
|
29
29
|
|
|
30
|
-
def initialize(
|
|
30
|
+
def initialize(coordinator, tool_response, with_prefix: false)
|
|
31
31
|
super()
|
|
32
|
-
@
|
|
32
|
+
@coordinator = coordinator
|
|
33
33
|
|
|
34
34
|
@with_prefix = with_prefix
|
|
35
35
|
@name = format_name(tool_response["name"])
|
|
@@ -45,7 +45,7 @@ module RubyLLM
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def display_name
|
|
48
|
-
"#{@
|
|
48
|
+
"#{@coordinator.name}: #{@name}"
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def params_schema
|
|
@@ -53,7 +53,7 @@ module RubyLLM
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
def execute(**params)
|
|
56
|
-
result = @
|
|
56
|
+
result = @coordinator.execute_tool(
|
|
57
57
|
name: @mcp_name,
|
|
58
58
|
parameters: params
|
|
59
59
|
)
|
|
@@ -116,7 +116,7 @@ module RubyLLM
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
resource = Resource.new(
|
|
119
|
+
resource = Resource.new(coordinator, resource_data)
|
|
120
120
|
resource.to_content
|
|
121
121
|
when "resource_link"
|
|
122
122
|
resource_data = {
|
|
@@ -126,15 +126,15 @@ module RubyLLM
|
|
|
126
126
|
"mimeType" => content["mimeType"]
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
resource = Resource.new(
|
|
130
|
-
@
|
|
129
|
+
resource = Resource.new(coordinator, resource_data)
|
|
130
|
+
@coordinator.register_resource(resource)
|
|
131
131
|
resource.to_content
|
|
132
132
|
end
|
|
133
133
|
end
|
|
134
134
|
|
|
135
135
|
def format_name(name)
|
|
136
136
|
if @with_prefix
|
|
137
|
-
"#{@
|
|
137
|
+
"#{@coordinator.name}_#{name}"
|
|
138
138
|
else
|
|
139
139
|
name
|
|
140
140
|
end
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module MCP
|
|
5
|
+
class Transport
|
|
6
|
+
class << self
|
|
7
|
+
def transports
|
|
8
|
+
@transports ||= {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def register_transport(transport_type, transport_class)
|
|
12
|
+
transports[transport_type] = transport_class
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
extend Forwardable
|
|
17
|
+
|
|
18
|
+
register_transport(:sse, RubyLLM::MCP::Transports::SSE)
|
|
19
|
+
register_transport(:stdio, RubyLLM::MCP::Transports::Stdio)
|
|
20
|
+
register_transport(:streamable, RubyLLM::MCP::Transports::StreamableHTTP)
|
|
21
|
+
register_transport(:streamable_http, RubyLLM::MCP::Transports::StreamableHTTP)
|
|
22
|
+
|
|
23
|
+
attr_reader :transport_type, :coordinator, :config, :pid
|
|
24
|
+
|
|
25
|
+
def initialize(transport_type, coordinator, config:)
|
|
26
|
+
@transport_type = transport_type
|
|
27
|
+
@coordinator = coordinator
|
|
28
|
+
@config = config
|
|
29
|
+
@pid = Process.pid
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def_delegators :transport_protocol, :request, :alive?, :close, :start, :set_protocol_version
|
|
33
|
+
|
|
34
|
+
def transport_protocol
|
|
35
|
+
if @pid != Process.pid
|
|
36
|
+
@pid = Process.pid
|
|
37
|
+
@transport = build_transport
|
|
38
|
+
coordinator.restart_transport
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@transport_protocol ||= build_transport
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def build_transport
|
|
47
|
+
unless RubyLLM::MCP::Transport.transports.key?(transport_type)
|
|
48
|
+
supported_types = RubyLLM::MCP::Transport.transports.keys.join(", ")
|
|
49
|
+
message = "Invalid transport type: :#{transport_type}. Supported types are #{supported_types}"
|
|
50
|
+
raise Errors::InvalidTransportType.new(message: message)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
transport_config = prepare_transport_config
|
|
54
|
+
transport_klass = RubyLLM::MCP::Transport.transports[transport_type]
|
|
55
|
+
transport_klass.new(coordinator: coordinator, **transport_config)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def prepare_transport_config
|
|
59
|
+
transport_config = config.dup
|
|
60
|
+
oauth_provider = create_oauth_provider(transport_config) if oauth_config_present?(transport_config)
|
|
61
|
+
|
|
62
|
+
# Extract transport-specific parameters and consolidate into options
|
|
63
|
+
if %i[sse streamable streamable_http].include?(transport_type)
|
|
64
|
+
prepare_http_transport_config(transport_config, oauth_provider)
|
|
65
|
+
elsif transport_type == :stdio
|
|
66
|
+
prepare_stdio_transport_config(transport_config)
|
|
67
|
+
else
|
|
68
|
+
transport_config
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def prepare_http_transport_config(config, oauth_provider)
|
|
73
|
+
options = {
|
|
74
|
+
version: config.delete(:version) || config.delete("version"),
|
|
75
|
+
headers: config.delete(:headers) || config.delete("headers"),
|
|
76
|
+
oauth_provider: oauth_provider,
|
|
77
|
+
reconnection: config.delete(:reconnection) || config.delete("reconnection"),
|
|
78
|
+
reconnection_options: config.delete(:reconnection_options) || config.delete("reconnection_options"),
|
|
79
|
+
rate_limit: config.delete(:rate_limit) || config.delete("rate_limit"),
|
|
80
|
+
session_id: config.delete(:session_id) || config.delete("session_id")
|
|
81
|
+
}.compact
|
|
82
|
+
|
|
83
|
+
config[:options] = options
|
|
84
|
+
config
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def prepare_stdio_transport_config(config)
|
|
88
|
+
options = {
|
|
89
|
+
args: config.delete(:args) || config.delete("args"),
|
|
90
|
+
env: config.delete(:env) || config.delete("env")
|
|
91
|
+
}.compact
|
|
92
|
+
|
|
93
|
+
config[:options] = options unless options.empty?
|
|
94
|
+
config
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Check if OAuth configuration is present
|
|
98
|
+
def oauth_config_present?(config)
|
|
99
|
+
oauth_config = config[:oauth] || config["oauth"]
|
|
100
|
+
return false if oauth_config.nil?
|
|
101
|
+
|
|
102
|
+
# If it's an OAuth provider instance, it's present
|
|
103
|
+
return true if oauth_config.respond_to?(:access_token)
|
|
104
|
+
|
|
105
|
+
# If it's a hash, check if it's not empty
|
|
106
|
+
!oauth_config.empty?
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Create OAuth provider from configuration
|
|
110
|
+
# Accepts either a provider instance or a configuration hash
|
|
111
|
+
def create_oauth_provider(config)
|
|
112
|
+
oauth_config = config.delete(:oauth) || config.delete("oauth")
|
|
113
|
+
return nil unless oauth_config
|
|
114
|
+
|
|
115
|
+
# If provider key exists with an instance, use it
|
|
116
|
+
if oauth_config.is_a?(Hash) && (oauth_config[:provider] || oauth_config["provider"])
|
|
117
|
+
return oauth_config[:provider] || oauth_config["provider"]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# If oauth_config itself is a provider instance, use it directly
|
|
121
|
+
if oauth_config.respond_to?(:access_token) && oauth_config.respond_to?(:start_authorization_flow)
|
|
122
|
+
return oauth_config
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Otherwise create new provider from config hash
|
|
126
|
+
# Determine server URL based on transport type
|
|
127
|
+
server_url = determine_server_url(config)
|
|
128
|
+
return nil unless server_url
|
|
129
|
+
|
|
130
|
+
redirect_uri = oauth_config[:redirect_uri] || oauth_config["redirect_uri"] || "http://localhost:8080/callback"
|
|
131
|
+
scope = oauth_config[:scope] || oauth_config["scope"]
|
|
132
|
+
storage = oauth_config[:storage] || oauth_config["storage"]
|
|
133
|
+
grant_type = oauth_config[:grant_type] || oauth_config["grant_type"] || :authorization_code
|
|
134
|
+
|
|
135
|
+
RubyLLM::MCP::Auth::OAuthProvider.new(
|
|
136
|
+
server_url: server_url,
|
|
137
|
+
redirect_uri: redirect_uri,
|
|
138
|
+
scope: scope,
|
|
139
|
+
logger: MCP.logger,
|
|
140
|
+
storage: storage,
|
|
141
|
+
grant_type: grant_type
|
|
142
|
+
)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Determine server URL from transport config
|
|
146
|
+
def determine_server_url(config)
|
|
147
|
+
config[:url] || config["url"]
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|