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.
@@ -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 = 30
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(30) do
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(message: "Request timed out after 30 seconds")
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
- # rubocop:enable Metrics/MethodLength
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(30) do
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(message: "Request timed out after 30 seconds")
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 close
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(environment_string, @command, *@args)
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 process_response(line)
152
- response = begin
153
- JSON.parse(line)
154
- rescue JSON::ParserError => e
155
- raise "Error parsing response as JSON: #{e.message}\nRaw response: #{line}"
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
- end
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 = 300
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(30) do
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(message: "Request timed out after 30 seconds")
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  module MCP
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.1"
6
6
  end
7
7
  end
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.2.1
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-17 00:00:00.000000000 Z
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/completion.rb
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/notification.rb
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