ruby_llm-mcp 0.2.1 → 0.3.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 +112 -22
- data/lib/ruby_llm/chat.rb +8 -2
- data/lib/ruby_llm/mcp/capabilities.rb +3 -3
- data/lib/ruby_llm/mcp/client.rb +57 -99
- data/lib/ruby_llm/mcp/coordinator.rb +112 -0
- data/lib/ruby_llm/mcp/errors.rb +5 -3
- data/lib/ruby_llm/mcp/parameter.rb +12 -1
- data/lib/ruby_llm/mcp/prompt.rb +25 -14
- data/lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb +26 -6
- data/lib/ruby_llm/mcp/providers/gemini/complex_parameter_support.rb +62 -0
- data/lib/ruby_llm/mcp/providers/openai/complex_parameter_support.rb +20 -5
- data/lib/ruby_llm/mcp/requests/{completion.rb → completion_prompt.rb} +3 -13
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +40 -0
- data/lib/ruby_llm/mcp/requests/{notification.rb → initialize_notification.rb} +1 -1
- data/lib/ruby_llm/mcp/resource.rb +24 -46
- data/lib/ruby_llm/mcp/resource_template.rb +79 -0
- data/lib/ruby_llm/mcp/tool.rb +67 -21
- data/lib/ruby_llm/mcp/transport/sse.rb +12 -7
- data/lib/ruby_llm/mcp/transport/stdio.rb +51 -14
- data/lib/ruby_llm/mcp/transport/streamable.rb +11 -4
- data/lib/ruby_llm/mcp/version.rb +1 -1
- metadata +8 -4
@@ -12,9 +12,10 @@ module RubyLLM
|
|
12
12
|
class SSE
|
13
13
|
attr_reader :headers, :id
|
14
14
|
|
15
|
-
def initialize(url, headers: {})
|
15
|
+
def initialize(url, headers: {}, request_timeout: 8000)
|
16
16
|
@event_url = url
|
17
17
|
@messages_url = nil
|
18
|
+
@request_timeout = request_timeout
|
18
19
|
|
19
20
|
uri = URI.parse(url)
|
20
21
|
@root_url = "#{uri.scheme}://#{uri.host}"
|
@@ -40,8 +41,7 @@ module RubyLLM
|
|
40
41
|
start_sse_listener
|
41
42
|
end
|
42
43
|
|
43
|
-
# rubocop:disable Metrics/MethodLength
|
44
|
-
def request(body, add_id: true, wait_for_response: true)
|
44
|
+
def request(body, add_id: true, wait_for_response: true) # rubocop:disable Metrics/MethodLength
|
45
45
|
# Generate a unique request ID
|
46
46
|
if add_id
|
47
47
|
@id_mutex.synchronize { @id_counter += 1 }
|
@@ -60,7 +60,7 @@ module RubyLLM
|
|
60
60
|
# Send the request using Faraday
|
61
61
|
begin
|
62
62
|
conn = Faraday.new do |f|
|
63
|
-
f.options.timeout =
|
63
|
+
f.options.timeout = @request_timeout / 1000
|
64
64
|
f.options.open_timeout = 5
|
65
65
|
end
|
66
66
|
|
@@ -83,15 +83,20 @@ module RubyLLM
|
|
83
83
|
return unless wait_for_response
|
84
84
|
|
85
85
|
begin
|
86
|
-
Timeout.timeout(
|
86
|
+
Timeout.timeout(@request_timeout / 1000) do
|
87
87
|
response_queue.pop
|
88
88
|
end
|
89
89
|
rescue Timeout::Error
|
90
90
|
@pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
|
91
|
-
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
91
|
+
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
92
|
+
message: "Request timed out after #{@request_timeout / 1000} seconds"
|
93
|
+
)
|
92
94
|
end
|
93
95
|
end
|
94
|
-
|
96
|
+
|
97
|
+
def alive?
|
98
|
+
@running
|
99
|
+
end
|
95
100
|
|
96
101
|
def close
|
97
102
|
@running = false
|
@@ -11,7 +11,8 @@ module RubyLLM
|
|
11
11
|
class Stdio
|
12
12
|
attr_reader :command, :stdin, :stdout, :stderr, :id
|
13
13
|
|
14
|
-
def initialize(command, args: [], env: {})
|
14
|
+
def initialize(command, request_timeout:, args: [], env: {})
|
15
|
+
@request_timeout = request_timeout
|
15
16
|
@command = command
|
16
17
|
@args = args
|
17
18
|
@env = env || {}
|
@@ -23,6 +24,7 @@ module RubyLLM
|
|
23
24
|
@pending_mutex = Mutex.new
|
24
25
|
@running = true
|
25
26
|
@reader_thread = nil
|
27
|
+
@stderr_thread = nil
|
26
28
|
|
27
29
|
start_process
|
28
30
|
end
|
@@ -53,16 +55,22 @@ module RubyLLM
|
|
53
55
|
return unless wait_for_response
|
54
56
|
|
55
57
|
begin
|
56
|
-
Timeout.timeout(
|
58
|
+
Timeout.timeout(@request_timeout / 1000) do
|
57
59
|
response_queue.pop
|
58
60
|
end
|
59
61
|
rescue Timeout::Error
|
60
62
|
@pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
|
61
|
-
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
63
|
+
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
64
|
+
message: "Request timed out after #{@request_timeout / 1000} seconds"
|
65
|
+
)
|
62
66
|
end
|
63
67
|
end
|
64
68
|
|
65
|
-
def
|
69
|
+
def alive?
|
70
|
+
@running
|
71
|
+
end
|
72
|
+
|
73
|
+
def close # rubocop:disable Metrics/MethodLength
|
66
74
|
@running = false
|
67
75
|
|
68
76
|
begin
|
@@ -82,6 +90,7 @@ module RubyLLM
|
|
82
90
|
rescue StandardError
|
83
91
|
nil
|
84
92
|
end
|
93
|
+
|
85
94
|
begin
|
86
95
|
@stderr&.close
|
87
96
|
rescue StandardError
|
@@ -94,11 +103,18 @@ module RubyLLM
|
|
94
103
|
nil
|
95
104
|
end
|
96
105
|
|
106
|
+
begin
|
107
|
+
@stderr_thread&.join(1)
|
108
|
+
rescue StandardError
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
|
97
112
|
@stdin = nil
|
98
113
|
@stdout = nil
|
99
114
|
@stderr = nil
|
100
115
|
@wait_thread = nil
|
101
116
|
@reader_thread = nil
|
117
|
+
@stderr_thread = nil
|
102
118
|
end
|
103
119
|
|
104
120
|
private
|
@@ -109,10 +125,11 @@ module RubyLLM
|
|
109
125
|
@stdin, @stdout, @stderr, @wait_thread = if @env.empty?
|
110
126
|
Open3.popen3(@command, *@args)
|
111
127
|
else
|
112
|
-
Open3.popen3(
|
128
|
+
Open3.popen3(@env, @command, *@args)
|
113
129
|
end
|
114
130
|
|
115
131
|
start_reader_thread
|
132
|
+
start_stderr_thread
|
116
133
|
end
|
117
134
|
|
118
135
|
def restart_process
|
@@ -148,12 +165,34 @@ module RubyLLM
|
|
148
165
|
@reader_thread.abort_on_exception = true
|
149
166
|
end
|
150
167
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
168
|
+
def start_stderr_thread
|
169
|
+
@stderr_thread = Thread.new do
|
170
|
+
while @running
|
171
|
+
begin
|
172
|
+
if @stderr.closed? || @wait_thread.nil? || !@wait_thread.alive?
|
173
|
+
sleep 1
|
174
|
+
next
|
175
|
+
end
|
176
|
+
|
177
|
+
line = @stderr.gets
|
178
|
+
next unless line && !line.strip.empty?
|
179
|
+
|
180
|
+
puts "STDERR: #{line.strip}"
|
181
|
+
rescue IOError, Errno::EPIPE => e
|
182
|
+
puts "Stderr reader error: #{e.message}"
|
183
|
+
sleep 1
|
184
|
+
rescue StandardError => e
|
185
|
+
puts "Error in stderr thread: #{e.message}"
|
186
|
+
sleep 1
|
187
|
+
end
|
188
|
+
end
|
156
189
|
end
|
190
|
+
|
191
|
+
@stderr_thread.abort_on_exception = true
|
192
|
+
end
|
193
|
+
|
194
|
+
def process_response(line)
|
195
|
+
response = JSON.parse(line)
|
157
196
|
request_id = response["id"]&.to_s
|
158
197
|
|
159
198
|
@pending_mutex.synchronize do
|
@@ -162,10 +201,8 @@ module RubyLLM
|
|
162
201
|
response_queue&.push(response)
|
163
202
|
end
|
164
203
|
end
|
165
|
-
|
166
|
-
|
167
|
-
def environment_string
|
168
|
-
@env.map { |key, value| "#{key}=#{value}" }.join(" ")
|
204
|
+
rescue JSON::ParserError => e
|
205
|
+
RubyLLM.logger.error("Error parsing response as JSON: #{e.message}\nRaw response: #{line}")
|
169
206
|
end
|
170
207
|
end
|
171
208
|
end
|
@@ -12,8 +12,9 @@ module RubyLLM
|
|
12
12
|
class Streamable
|
13
13
|
attr_reader :headers, :id, :session_id
|
14
14
|
|
15
|
-
def initialize(url, headers: {})
|
15
|
+
def initialize(url, request_timeout:, headers: {})
|
16
16
|
@url = url
|
17
|
+
@request_timeout = request_timeout
|
17
18
|
@client_id = SecureRandom.uuid
|
18
19
|
@session_id = nil
|
19
20
|
@base_headers = headers.merge({
|
@@ -55,6 +56,10 @@ module RubyLLM
|
|
55
56
|
handle_response(response, request_id, response_queue, wait_for_response)
|
56
57
|
end
|
57
58
|
|
59
|
+
def alive?
|
60
|
+
@running
|
61
|
+
end
|
62
|
+
|
58
63
|
def close
|
59
64
|
@running = false
|
60
65
|
@sse_mutex.synchronize do
|
@@ -83,7 +88,7 @@ module RubyLLM
|
|
83
88
|
|
84
89
|
def create_connection
|
85
90
|
Faraday.new(url: @url) do |f|
|
86
|
-
f.options.timeout =
|
91
|
+
f.options.timeout = @request_timeout / 1000
|
87
92
|
f.options.open_timeout = 10
|
88
93
|
end
|
89
94
|
end
|
@@ -279,12 +284,14 @@ module RubyLLM
|
|
279
284
|
end
|
280
285
|
|
281
286
|
def wait_for_response_with_timeout(request_id, response_queue)
|
282
|
-
Timeout.timeout(
|
287
|
+
Timeout.timeout(@request_timeout / 1000) do
|
283
288
|
response_queue.pop
|
284
289
|
end
|
285
290
|
rescue Timeout::Error
|
286
291
|
@pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
|
287
|
-
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
292
|
+
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
293
|
+
message: "Request timed out after #{@request_timeout / 1000} seconds"
|
294
|
+
)
|
288
295
|
end
|
289
296
|
end
|
290
297
|
end
|
data/lib/ruby_llm/mcp/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby_llm-mcp
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Patrick Vice
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-06-
|
11
|
+
date: 2025-06-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -114,15 +114,18 @@ files:
|
|
114
114
|
- lib/ruby_llm/mcp/client.rb
|
115
115
|
- lib/ruby_llm/mcp/completion.rb
|
116
116
|
- lib/ruby_llm/mcp/content.rb
|
117
|
+
- lib/ruby_llm/mcp/coordinator.rb
|
117
118
|
- lib/ruby_llm/mcp/errors.rb
|
118
119
|
- lib/ruby_llm/mcp/parameter.rb
|
119
120
|
- lib/ruby_llm/mcp/prompt.rb
|
120
121
|
- lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb
|
122
|
+
- lib/ruby_llm/mcp/providers/gemini/complex_parameter_support.rb
|
121
123
|
- lib/ruby_llm/mcp/providers/openai/complex_parameter_support.rb
|
122
124
|
- lib/ruby_llm/mcp/requests/base.rb
|
123
|
-
- lib/ruby_llm/mcp/requests/
|
125
|
+
- lib/ruby_llm/mcp/requests/completion_prompt.rb
|
126
|
+
- lib/ruby_llm/mcp/requests/completion_resource.rb
|
124
127
|
- lib/ruby_llm/mcp/requests/initialization.rb
|
125
|
-
- lib/ruby_llm/mcp/requests/
|
128
|
+
- lib/ruby_llm/mcp/requests/initialize_notification.rb
|
126
129
|
- lib/ruby_llm/mcp/requests/prompt_call.rb
|
127
130
|
- lib/ruby_llm/mcp/requests/prompt_list.rb
|
128
131
|
- lib/ruby_llm/mcp/requests/resource_list.rb
|
@@ -131,6 +134,7 @@ files:
|
|
131
134
|
- lib/ruby_llm/mcp/requests/tool_call.rb
|
132
135
|
- lib/ruby_llm/mcp/requests/tool_list.rb
|
133
136
|
- lib/ruby_llm/mcp/resource.rb
|
137
|
+
- lib/ruby_llm/mcp/resource_template.rb
|
134
138
|
- lib/ruby_llm/mcp/tool.rb
|
135
139
|
- lib/ruby_llm/mcp/transport/sse.rb
|
136
140
|
- lib/ruby_llm/mcp/transport/stdio.rb
|