ruby_llm-mcp 0.5.0 → 0.6.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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -620
  3. data/lib/generators/ruby_llm/mcp/install_generator.rb +27 -0
  4. data/lib/generators/ruby_llm/mcp/templates/README.txt +32 -0
  5. data/lib/generators/ruby_llm/mcp/templates/initializer.rb +42 -0
  6. data/lib/generators/ruby_llm/mcp/templates/mcps.yml +9 -0
  7. data/lib/ruby_llm/mcp/client.rb +56 -2
  8. data/lib/ruby_llm/mcp/completion.rb +3 -2
  9. data/lib/ruby_llm/mcp/configuration.rb +30 -1
  10. data/lib/ruby_llm/mcp/coordinator.rb +30 -6
  11. data/lib/ruby_llm/mcp/elicitation.rb +46 -0
  12. data/lib/ruby_llm/mcp/errors.rb +2 -0
  13. data/lib/ruby_llm/mcp/prompt.rb +4 -3
  14. data/lib/ruby_llm/mcp/protocol.rb +34 -0
  15. data/lib/ruby_llm/mcp/requests/completion_prompt.rb +13 -3
  16. data/lib/ruby_llm/mcp/requests/completion_resource.rb +13 -3
  17. data/lib/ruby_llm/mcp/resource.rb +1 -2
  18. data/lib/ruby_llm/mcp/resource_template.rb +4 -3
  19. data/lib/ruby_llm/mcp/response_handler.rb +10 -1
  20. data/lib/ruby_llm/mcp/responses/elicitation.rb +33 -0
  21. data/lib/ruby_llm/mcp/result.rb +2 -1
  22. data/lib/ruby_llm/mcp/tool.rb +33 -5
  23. data/lib/ruby_llm/mcp/transports/sse.rb +69 -25
  24. data/lib/ruby_llm/mcp/transports/stdio.rb +2 -2
  25. data/lib/ruby_llm/mcp/transports/streamable_http.rb +87 -19
  26. data/lib/ruby_llm/mcp/transports/support/http_client.rb +28 -0
  27. data/lib/ruby_llm/mcp/transports/support/rate_limit.rb +47 -0
  28. data/lib/ruby_llm/mcp/transports/support/timeout.rb +34 -0
  29. data/lib/ruby_llm/mcp/version.rb +1 -1
  30. data/lib/ruby_llm/mcp.rb +21 -9
  31. data/lib/tasks/release.rake +23 -0
  32. metadata +28 -8
  33. data/lib/ruby_llm/mcp/transports/http_client.rb +0 -26
  34. data/lib/ruby_llm/mcp/transports/timeout.rb +0 -32
@@ -36,6 +36,10 @@ module RubyLLM
36
36
  @mcp_name = tool_response["name"]
37
37
  @description = tool_response["description"].to_s
38
38
  @parameters = create_parameters(tool_response["inputSchema"])
39
+
40
+ @input_schema = tool_response["inputSchema"]
41
+ @output_schema = tool_response["outputSchema"]
42
+
39
43
  @annotations = tool_response["annotations"] ? Annotation.new(tool_response["annotations"]) : nil
40
44
  end
41
45
 
@@ -59,6 +63,15 @@ module RubyLLM
59
63
  return { error: "Tool execution error: #{text_values}" }
60
64
  end
61
65
 
66
+ if result.value.key?("structuredContent") && !@output_schema.nil?
67
+ is_valid = JSON::Validator.validate(@output_schema, result.value["structuredContent"])
68
+ unless is_valid
69
+ return { error: "Structued outputs was not invalid: #{result.value['structuredContent']}" }
70
+ end
71
+
72
+ return text_values
73
+ end
74
+
62
75
  if text_values.empty?
63
76
  create_content_for_message(result.value.dig("content", 0))
64
77
  else
@@ -79,12 +92,12 @@ module RubyLLM
79
92
 
80
93
  private
81
94
 
82
- def create_parameters(input_schema)
95
+ def create_parameters(schema)
83
96
  params = {}
84
- return params if input_schema["properties"].nil?
97
+ return params if schema["properties"].nil?
85
98
 
86
- input_schema["properties"].each_key do |key|
87
- param_data = input_schema.dig("properties", key)
99
+ schema["properties"].each_key do |key|
100
+ param_data = schema.dig("properties", key)
88
101
 
89
102
  param = if param_data.key?("oneOf") || param_data.key?("anyOf") || param_data.key?("allOf")
90
103
  process_union_parameter(key, param_data)
@@ -152,10 +165,25 @@ module RubyLLM
152
165
  "name" => name,
153
166
  "description" => description,
154
167
  "uri" => content.dig("resource", "uri"),
155
- "content" => content["resource"]
168
+ "mimeType" => content.dig("resource", "mimeType"),
169
+ "content_response" => {
170
+ "text" => content.dig("resource", "text"),
171
+ "blob" => content.dig("resource", "blob")
172
+ }
173
+ }
174
+
175
+ resource = Resource.new(coordinator, resource_data)
176
+ resource.to_content
177
+ when "resource_link"
178
+ resource_data = {
179
+ "name" => content["name"],
180
+ "uri" => content["uri"],
181
+ "description" => content["description"],
182
+ "mimeType" => content["mimeType"]
156
183
  }
157
184
 
158
185
  resource = Resource.new(coordinator, resource_data)
186
+ @coordinator.register_resource(resource)
159
187
  resource.to_content
160
188
  end
161
189
  end
@@ -10,15 +10,16 @@ module RubyLLM
10
10
  module MCP
11
11
  module Transports
12
12
  class SSE
13
- include Timeout
13
+ include Support::Timeout
14
14
 
15
15
  attr_reader :headers, :id, :coordinator
16
16
 
17
- def initialize(url:, coordinator:, request_timeout:, headers: {})
17
+ def initialize(url:, coordinator:, request_timeout:, version: :http2, headers: {})
18
18
  @event_url = url
19
19
  @messages_url = nil
20
20
  @coordinator = coordinator
21
21
  @request_timeout = request_timeout
22
+ @version = version
22
23
 
23
24
  uri = URI.parse(url)
24
25
  @root_url = "#{uri.scheme}://#{uri.host}"
@@ -44,7 +45,7 @@ module RubyLLM
44
45
  RubyLLM::MCP.logger.info "Initializing SSE transport to #{@event_url} with client ID #{@client_id}"
45
46
  end
46
47
 
47
- def request(body, add_id: true, wait_for_response: true) # rubocop:disable Metrics/MethodLength
48
+ def request(body, add_id: true, wait_for_response: true)
48
49
  if add_id
49
50
  @id_mutex.synchronize { @id_counter += 1 }
50
51
  request_id = @id_counter
@@ -59,34 +60,20 @@ module RubyLLM
59
60
  end
60
61
 
61
62
  begin
62
- http_client = HTTPClient.connection.with(timeout: { request_timeout: @request_timeout / 1000 },
63
- headers: @headers)
64
- response = http_client.post(@messages_url, body: JSON.generate(body))
65
-
66
- unless response.status == 200
67
- @pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
68
- RubyLLM::MCP.logger.error "SSE request failed: #{response.status} - #{response.body}"
69
- raise Errors::TransportError.new(
70
- message: "Failed to request #{@messages_url}: #{response.status} - #{response.body}",
71
- code: response.status
72
- )
73
- end
74
- rescue StandardError => e
63
+ send_request(body, request_id)
64
+ rescue Errors::TransportError, Errors::TimeoutError => e
75
65
  @pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
76
- RubyLLM::MCP.logger.error "SSE request error (ID: #{request_id}): #{e.message}"
77
- raise RubyLLM::MCP::Errors::TransportError.new(
78
- message: e.message,
79
- code: -1,
80
- error: e
81
- )
66
+ RubyLLM::MCP.logger.error "Request error (ID: #{request_id}): #{e.message}"
67
+ raise e
82
68
  end
69
+
83
70
  return unless wait_for_response
84
71
 
85
72
  begin
86
73
  with_timeout(@request_timeout / 1000, request_id: request_id) do
87
74
  response_queue.pop
88
75
  end
89
- rescue RubyLLM::MCP::Errors::TimeoutError => e
76
+ rescue Errors::TimeoutError => e
90
77
  @pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
91
78
  RubyLLM::MCP.logger.error "SSE request timeout (ID: #{request_id}) after #{@request_timeout / 1000} seconds"
92
79
  raise e
@@ -117,6 +104,23 @@ module RubyLLM
117
104
 
118
105
  private
119
106
 
107
+ def send_request(body, request_id)
108
+ http_client = Support::HTTPClient.connection.with(timeout: { request_timeout: @request_timeout / 1000 },
109
+ headers: @headers)
110
+ response = http_client.post(@messages_url, body: JSON.generate(body))
111
+ handle_httpx_error_response!(response,
112
+ context: { location: "message endpoint request", request_id: request_id })
113
+
114
+ unless [200, 202].include?(response.status)
115
+ message = "Failed to have a successful request to #{@messages_url}: #{response.status} - #{response.body}"
116
+ RubyLLM::MCP.logger.error(message)
117
+ raise Errors::TransportError.new(
118
+ message: message,
119
+ code: response.status
120
+ )
121
+ end
122
+ end
123
+
120
124
  def start_sse_listener
121
125
  @connection_mutex.synchronize do
122
126
  return if sse_thread_running?
@@ -167,15 +171,36 @@ module RubyLLM
167
171
  sse_client = sse_client.with(
168
172
  headers: @headers
169
173
  )
174
+
175
+ if @version == :http1
176
+ sse_client = sse_client.with(
177
+ ssl: { alpn_protocols: ["http/1.1"] }
178
+ )
179
+ end
180
+
170
181
  response = sse_client.get(@event_url, stream: true)
182
+
183
+ event_buffer = []
171
184
  response.each_line do |event_line|
172
185
  unless @running
173
186
  response.body.close
174
187
  next
175
188
  end
176
189
 
177
- event = parse_event(event_line)
178
- process_event(event)
190
+ # Strip the line and check if it's empty (indicates end of event)
191
+ line = event_line.strip
192
+
193
+ if line.empty?
194
+ # End of event - process the accumulated buffer
195
+ if event_buffer.any?
196
+ event = parse_event(event_buffer.join("\n"))
197
+ process_event(event)
198
+ event_buffer.clear
199
+ end
200
+ else
201
+ # Accumulate the line for the current event
202
+ event_buffer << line
203
+ end
179
204
  end
180
205
  end
181
206
 
@@ -187,6 +212,25 @@ module RubyLLM
187
212
  sleep 1
188
213
  end
189
214
 
215
+ def handle_httpx_error_response!(response, context:)
216
+ return false unless response.is_a?(HTTPX::ErrorResponse)
217
+
218
+ error = response.error
219
+
220
+ if error.is_a?(HTTPX::ReadTimeoutError)
221
+ raise Errors::TimeoutError.new(
222
+ message: "Request timed out after #{@request_timeout / 1000} seconds"
223
+ )
224
+ end
225
+
226
+ error_message = response.error&.message || "Request failed"
227
+
228
+ raise Errors::TransportError.new(
229
+ code: nil,
230
+ message: "Request Error #{context}: #{error_message}"
231
+ )
232
+ end
233
+
190
234
  def process_event(raw_event)
191
235
  # Return if we believe that are getting a partial event
192
236
  return if raw_event[:data].nil?
@@ -9,11 +9,11 @@ module RubyLLM
9
9
  module MCP
10
10
  module Transports
11
11
  class Stdio
12
- include Timeout
12
+ include Support::Timeout
13
13
 
14
14
  attr_reader :command, :stdin, :stdout, :stderr, :id, :coordinator
15
15
 
16
- def initialize(command:, request_timeout:, coordinator:, args: [], env: {})
16
+ def initialize(command:, coordinator:, request_timeout:, args: [], env: {})
17
17
  @request_timeout = request_timeout
18
18
  @command = command
19
19
  @coordinator = coordinator
@@ -27,6 +27,21 @@ module RubyLLM
27
27
  end
28
28
  end
29
29
 
30
+ class OAuthOptions
31
+ attr_reader :issuer, :client_id, :client_secret, :scope
32
+
33
+ def initialize(issuer:, client_id:, client_secret:, scopes:)
34
+ @issuer = issuer
35
+ @client_id = client_id
36
+ @client_secret = client_secret
37
+ @scope = scopes
38
+ end
39
+
40
+ def enabled?
41
+ @issuer && @client_id && @client_secret && @scope
42
+ end
43
+ end
44
+
30
45
  # Options for starting SSE connections
31
46
  class StartSSEOptions
32
47
  attr_reader :resumption_token, :on_resumption_token, :replay_message_id
@@ -40,7 +55,7 @@ module RubyLLM
40
55
 
41
56
  # Main StreamableHTTP transport class
42
57
  class StreamableHTTP
43
- include Timeout
58
+ include Support::Timeout
44
59
 
45
60
  attr_reader :session_id, :protocol_version, :coordinator
46
61
 
@@ -49,6 +64,10 @@ module RubyLLM
49
64
  request_timeout:,
50
65
  coordinator:,
51
66
  headers: {},
67
+ reconnection: {},
68
+ version: :http2,
69
+ oauth: nil,
70
+ rate_limit: nil,
52
71
  reconnection_options: nil,
53
72
  session_id: nil
54
73
  )
@@ -57,11 +76,19 @@ module RubyLLM
57
76
  @request_timeout = request_timeout
58
77
  @headers = headers || {}
59
78
  @session_id = session_id
79
+
80
+ @version = version
60
81
  @reconnection_options = reconnection_options || ReconnectionOptions.new
61
82
  @protocol_version = nil
83
+ @session_id = session_id
84
+
62
85
  @resource_metadata_url = nil
63
86
  @client_id = SecureRandom.uuid
64
87
 
88
+ @reconnection_options = ReconnectionOptions.new(**reconnection)
89
+ @oauth_options = OAuthOptions.new(**oauth) unless oauth.nil?
90
+ @rate_limiter = Support::RateLimiter.new(**rate_limit) if rate_limit
91
+
65
92
  @id_counter = 0
66
93
  @id_mutex = Mutex.new
67
94
  @pending_requests = {}
@@ -79,6 +106,11 @@ module RubyLLM
79
106
  end
80
107
 
81
108
  def request(body, add_id: true, wait_for_response: true)
109
+ if @rate_limiter&.exceeded?
110
+ sleep(1) while @rate_limiter&.exceeded?
111
+ end
112
+ @rate_limiter&.add
113
+
82
114
  # Generate a unique request ID for requests
83
115
  if add_id && body.is_a?(Hash) && !body.key?("id")
84
116
  @id_mutex.synchronize { @id_counter += 1 }
@@ -202,7 +234,7 @@ module RubyLLM
202
234
  end
203
235
 
204
236
  def create_connection
205
- client = HTTPClient.connection.with(
237
+ client = Support::HTTPClient.connection.with(
206
238
  timeout: {
207
239
  connect_timeout: 10,
208
240
  read_timeout: @request_timeout / 1000,
@@ -210,6 +242,18 @@ module RubyLLM
210
242
  operation_timeout: @request_timeout / 1000
211
243
  }
212
244
  )
245
+
246
+ if @oauth_options&.enabled?
247
+ client = client.plugin(:oauth).oauth_auth(
248
+ issuer: @oauth_options.issuer,
249
+ client_id: @oauth_options.client_id,
250
+ client_secret: @oauth_options.client_secret,
251
+ scope: @oauth_options.scope
252
+ )
253
+
254
+ client.with_access_token
255
+ end
256
+
213
257
  register_client(client)
214
258
  end
215
259
 
@@ -219,6 +263,7 @@ module RubyLLM
219
263
  headers["mcp-session-id"] = @session_id if @session_id
220
264
  headers["mcp-protocol-version"] = @protocol_version if @protocol_version
221
265
  headers["X-CLIENT-ID"] = @client_id
266
+ headers["Origin"] = @uri.to_s
222
267
 
223
268
  headers
224
269
  end
@@ -259,7 +304,8 @@ module RubyLLM
259
304
  def create_connection_with_streaming_callbacks(request_id)
260
305
  buffer = +""
261
306
 
262
- client = HTTPClient.connection.plugin(:callbacks).on_response_body_chunk do |request, _response, chunk|
307
+ client = Support::HTTPClient.connection.plugin(:callbacks)
308
+ .on_response_body_chunk do |request, _response, chunk|
263
309
  next unless @running && !@abort_controller
264
310
 
265
311
  RubyLLM::MCP.logger.debug "Received chunk: #{chunk.bytesize} bytes for #{request.uri}"
@@ -274,6 +320,17 @@ module RubyLLM
274
320
  operation_timeout: @request_timeout / 1000
275
321
  }
276
322
  )
323
+
324
+ if @oauth_options&.enabled?
325
+ client = client.plugin(:oauth).oauth_auth(
326
+ issuer: @oauth_options.issuer,
327
+ client_id: @oauth_options.client_id,
328
+ client_secret: @oauth_options.client_secret,
329
+ scope: @oauth_options.scope
330
+ )
331
+
332
+ client.with_access_token
333
+ end
277
334
  register_client(client)
278
335
  end
279
336
 
@@ -422,8 +479,8 @@ module RubyLLM
422
479
  end
423
480
 
424
481
  # Set up SSE streaming connection with callbacks
425
- connection = create_connection_with_sse_callbacks(options)
426
- response = connection.get(@url, headers: headers)
482
+ connection = create_connection_with_sse_callbacks(options, headers)
483
+ response = connection.get(@url)
427
484
 
428
485
  # Handle HTTPX error responses first
429
486
  error_result = handle_httpx_error_response!(response, context: { location: "SSE connection" },
@@ -463,12 +520,32 @@ module RubyLLM
463
520
  end
464
521
  end
465
522
 
466
- def create_connection_with_sse_callbacks(options)
467
- buffer = +""
523
+ def create_connection_with_sse_callbacks(options, headers)
524
+ client = HTTPX.plugin(:callbacks)
525
+ client = add_on_response_body_chunk_callback(client, options)
526
+
527
+ client = client.with(
528
+ timeout: {
529
+ connect_timeout: 10,
530
+ read_timeout: @request_timeout / 1000,
531
+ write_timeout: @request_timeout / 1000,
532
+ operation_timeout: @request_timeout / 1000
533
+ },
534
+ headers: headers
535
+ )
468
536
 
469
- client = HTTPX
470
- .plugin(:callbacks)
471
- .on_response_body_chunk do |request, response, chunk|
537
+ if @version == :http1
538
+ client = client.with(
539
+ ssl: { alpn_protocols: ["http/1.1"] }
540
+ )
541
+ end
542
+
543
+ register_client(client)
544
+ end
545
+
546
+ def add_on_response_body_chunk_callback(client, options)
547
+ buffer = +""
548
+ client.on_response_body_chunk do |request, response, chunk|
472
549
  # Only process chunks for text/event-stream and if still running
473
550
  next unless @running && !@abort_controller
474
551
 
@@ -495,15 +572,6 @@ module RubyLLM
495
572
  end
496
573
  end
497
574
  end
498
- .with(
499
- timeout: {
500
- connect_timeout: 10,
501
- read_timeout: @request_timeout / 1000,
502
- write_timeout: @request_timeout / 1000,
503
- operation_timeout: @request_timeout / 1000
504
- }
505
- )
506
- register_client(client)
507
575
  end
508
576
 
509
577
  def calculate_reconnection_delay(attempt)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httpx"
4
+
5
+ module RubyLLM
6
+ module MCP
7
+ module Transports
8
+ module Support
9
+ class HTTPClient
10
+ CONNECTION_KEY = :ruby_llm_mcp_client_connection
11
+
12
+ def self.connection
13
+ Thread.current[CONNECTION_KEY] ||= build_connection
14
+ end
15
+
16
+ def self.build_connection
17
+ HTTPX.with(
18
+ pool_options: {
19
+ max_connections: RubyLLM::MCP.config.max_connections,
20
+ pool_timeout: RubyLLM::MCP.config.pool_timeout
21
+ }
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Transports
6
+ module Support
7
+ class RateLimit
8
+ def initialize(limit: 10, interval: 1000)
9
+ @limit = limit
10
+ @interval = interval
11
+ @timestamps = []
12
+ @mutex = Mutex.new
13
+ end
14
+
15
+ def exceeded?
16
+ now = current_time
17
+
18
+ @mutex.synchronize do
19
+ purge_old(now)
20
+ @timestamps.size >= @limit
21
+ end
22
+ end
23
+
24
+ def add
25
+ now = current_time
26
+
27
+ @mutex.synchronize do
28
+ purge_old(now)
29
+ @timestamps << now
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def current_time
36
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
37
+ end
38
+
39
+ def purge_old(now)
40
+ cutoff = now - @interval
41
+ @timestamps.reject! { |t| t < cutoff }
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Transports
6
+ module Support
7
+ module Timeout
8
+ def with_timeout(seconds, request_id: nil)
9
+ result = nil
10
+ exception = nil
11
+
12
+ worker = Thread.new do
13
+ result = yield
14
+ rescue StandardError => e
15
+ exception = e
16
+ end
17
+
18
+ if worker.join(seconds)
19
+ raise exception if exception
20
+
21
+ result
22
+ else
23
+ worker.kill # stop the thread (can still have some risk if shared resources)
24
+ raise RubyLLM::MCP::Errors::TimeoutError.new(
25
+ message: "Request timed out after #{seconds} seconds",
26
+ request_id: request_id
27
+ )
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  module MCP
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
data/lib/ruby_llm/mcp.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "ruby_llm"
4
+ require "json-schema"
4
5
  require "zeitwerk"
5
6
  require_relative "chat"
6
7
 
@@ -9,20 +10,21 @@ module RubyLLM
9
10
  module_function
10
11
 
11
12
  def clients(config = RubyLLM::MCP.config.mcp_configuration)
12
- @clients ||= {}
13
- config.map do |options|
14
- @clients[options[:name]] ||= Client.new(**options)
13
+ if @clients.nil?
14
+ @clients = {}
15
+ config.map do |options|
16
+ @clients[options[:name]] ||= Client.new(**options)
17
+ end
15
18
  end
19
+ @clients
16
20
  end
17
21
 
18
22
  def add_client(options)
19
- @clients ||= {}
20
- @clients[options[:name]] ||= Client.new(**options)
23
+ clients[options[:name]] ||= Client.new(**options)
21
24
  end
22
25
 
23
26
  def remove_client(name)
24
- @clients ||= {}
25
- client = @clients.delete(name)
27
+ client = clients.delete(name)
26
28
  client&.stop
27
29
  client
28
30
  end
@@ -33,8 +35,18 @@ module RubyLLM
33
35
 
34
36
  def establish_connection(&)
35
37
  clients.each(&:start)
36
- yield clients
37
- ensure
38
+ if block_given?
39
+ begin
40
+ yield clients
41
+ ensure
42
+ close_connection
43
+ end
44
+ else
45
+ clients
46
+ end
47
+ end
48
+
49
+ def close_connection
38
50
  clients.each do |client|
39
51
  client.stop if client.alive?
40
52
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :release do
4
+ desc "Release a new version of the gem"
5
+ task :version do
6
+ # Load the current version from version.rb
7
+ require_relative "../../lib/ruby_llm/schema/version"
8
+ version = RubyLlm::Schema::VERSION
9
+
10
+ puts "Releasing version #{version}..."
11
+
12
+ # Make sure we are on the main branch
13
+ system "git checkout main"
14
+ system "git pull origin main"
15
+
16
+ # Create a new tag for the version
17
+ system "git tag -a v#{version} -m 'Release version #{version}'"
18
+ system "git push origin v#{version}"
19
+
20
+ system "gem build ruby_llm-mcp.gemspec"
21
+ system "gem push ruby_llm-mcp-#{version}.gem"
22
+ end
23
+ end