ruby_llm-mcp 0.4.1 → 0.5.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +313 -25
  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/chat.rb +2 -1
  8. data/lib/ruby_llm/mcp/client.rb +32 -13
  9. data/lib/ruby_llm/mcp/configuration.rb +123 -3
  10. data/lib/ruby_llm/mcp/coordinator.rb +108 -115
  11. data/lib/ruby_llm/mcp/errors.rb +3 -1
  12. data/lib/ruby_llm/mcp/notification_handler.rb +84 -0
  13. data/lib/ruby_llm/mcp/{requests/cancelled_notification.rb → notifications/cancelled.rb} +2 -2
  14. data/lib/ruby_llm/mcp/{requests/initialize_notification.rb → notifications/initialize.rb} +7 -3
  15. data/lib/ruby_llm/mcp/notifications/roots_list_change.rb +26 -0
  16. data/lib/ruby_llm/mcp/parameter.rb +19 -1
  17. data/lib/ruby_llm/mcp/progress.rb +3 -1
  18. data/lib/ruby_llm/mcp/prompt.rb +18 -0
  19. data/lib/ruby_llm/mcp/railtie.rb +20 -0
  20. data/lib/ruby_llm/mcp/requests/initialization.rb +8 -4
  21. data/lib/ruby_llm/mcp/requests/ping.rb +6 -2
  22. data/lib/ruby_llm/mcp/requests/prompt_list.rb +10 -2
  23. data/lib/ruby_llm/mcp/requests/resource_list.rb +12 -2
  24. data/lib/ruby_llm/mcp/requests/resource_template_list.rb +12 -2
  25. data/lib/ruby_llm/mcp/requests/shared/meta.rb +32 -0
  26. data/lib/ruby_llm/mcp/requests/shared/pagination.rb +17 -0
  27. data/lib/ruby_llm/mcp/requests/tool_call.rb +1 -1
  28. data/lib/ruby_llm/mcp/requests/tool_list.rb +10 -2
  29. data/lib/ruby_llm/mcp/resource.rb +17 -0
  30. data/lib/ruby_llm/mcp/response_handler.rb +58 -0
  31. data/lib/ruby_llm/mcp/responses/error.rb +33 -0
  32. data/lib/ruby_llm/mcp/{requests/ping_response.rb → responses/ping.rb} +2 -2
  33. data/lib/ruby_llm/mcp/responses/roots_list.rb +31 -0
  34. data/lib/ruby_llm/mcp/responses/sampling_create_message.rb +50 -0
  35. data/lib/ruby_llm/mcp/result.rb +21 -8
  36. data/lib/ruby_llm/mcp/roots.rb +45 -0
  37. data/lib/ruby_llm/mcp/sample.rb +148 -0
  38. data/lib/ruby_llm/mcp/{capabilities.rb → server_capabilities.rb} +1 -1
  39. data/lib/ruby_llm/mcp/tool.rb +35 -4
  40. data/lib/ruby_llm/mcp/transport.rb +58 -0
  41. data/lib/ruby_llm/mcp/transports/http_client.rb +26 -0
  42. data/lib/ruby_llm/mcp/{transport → transports}/sse.rb +25 -24
  43. data/lib/ruby_llm/mcp/{transport → transports}/stdio.rb +28 -26
  44. data/lib/ruby_llm/mcp/{transport → transports}/streamable_http.rb +25 -29
  45. data/lib/ruby_llm/mcp/transports/timeout.rb +32 -0
  46. data/lib/ruby_llm/mcp/version.rb +1 -1
  47. data/lib/ruby_llm/mcp.rb +60 -9
  48. metadata +27 -11
  49. data/lib/ruby_llm/mcp/requests/base.rb +0 -31
  50. data/lib/ruby_llm/mcp/requests/meta.rb +0 -30
@@ -1,14 +1,106 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+ require "yaml"
5
+ require "erb"
6
+
3
7
  module RubyLLM
4
8
  module MCP
5
9
  class Configuration
6
- attr_accessor :request_timeout, :log_file, :log_level, :has_support_complex_parameters
7
- attr_writer :logger
10
+ class Sampling
11
+ attr_accessor :enabled
12
+ attr_writer :preferred_model
13
+
14
+ def initialize
15
+ set_defaults
16
+ end
17
+
18
+ def reset!
19
+ set_defaults
20
+ end
21
+
22
+ def guard(&block)
23
+ @guard = block if block_given?
24
+ @guard
25
+ end
26
+
27
+ def preferred_model(&block)
28
+ @preferred_model = block if block_given?
29
+ @preferred_model
30
+ end
31
+
32
+ def enabled?
33
+ @enabled
34
+ end
35
+
36
+ private
37
+
38
+ def set_defaults
39
+ @enabled = false
40
+ @preferred_model = nil
41
+ @guard = nil
42
+ end
43
+ end
44
+
45
+ class ConfigFile
46
+ attr_reader :file_path
47
+
48
+ def initialize(file_path)
49
+ @file_path = file_path
50
+ end
51
+
52
+ def parse
53
+ @parse ||= if @file_path && File.exist?(@file_path)
54
+ config = parse_config_file
55
+ load_mcps_config(config)
56
+ else
57
+ []
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def parse_config_file
64
+ output = ERB.new(File.read(@file_path)).result
65
+
66
+ if [".yaml", ".yml"].include?(File.extname(@file_path))
67
+ YAML.safe_load(output, symbolize_names: true)
68
+ else
69
+ JSON.parse(output, symbolize_names: true)
70
+ end
71
+ end
72
+
73
+ def load_mcps_config(config)
74
+ return [] unless config.key?(:mcp_servers)
75
+
76
+ config[:mcp_servers].map do |name, configuration|
77
+ {
78
+ name: name,
79
+ transport_type: configuration.delete(:transport_type),
80
+ start: false,
81
+ config: configuration
82
+ }
83
+ end
84
+ end
85
+ end
86
+
87
+ attr_accessor :request_timeout,
88
+ :log_file,
89
+ :log_level,
90
+ :has_support_complex_parameters,
91
+ :roots,
92
+ :sampling,
93
+ :max_connections,
94
+ :pool_timeout,
95
+ :config_path,
96
+ :launch_control
97
+
98
+ attr_writer :logger, :mcp_configuration
8
99
 
9
100
  REQUEST_TIMEOUT_DEFAULT = 8000
10
101
 
11
102
  def initialize
103
+ @sampling = Sampling.new
12
104
  set_defaults
13
105
  end
14
106
 
@@ -31,6 +123,10 @@ module RubyLLM
31
123
  )
32
124
  end
33
125
 
126
+ def mcp_configuration
127
+ @mcp_configuration + load_mcps_config
128
+ end
129
+
34
130
  def inspect
35
131
  redacted = lambda do |name, value|
36
132
  if name.match?(/_id|_key|_secret|_token$/)
@@ -51,15 +147,39 @@ module RubyLLM
51
147
 
52
148
  private
53
149
 
150
+ def load_mcps_config
151
+ @config_file ||= ConfigFile.new(config_path)
152
+ @config_file.parse
153
+ end
154
+
54
155
  def set_defaults
55
156
  # Connection configuration
56
157
  @request_timeout = REQUEST_TIMEOUT_DEFAULT
57
158
 
159
+ # Connection Pool
160
+ @max_connections = Float::INFINITY
161
+ @pool_timeout = 5
162
+
58
163
  # Logging configuration
59
164
  @log_file = $stdout
60
165
  @log_level = ENV["RUBYLLM_MCP_DEBUG"] ? Logger::DEBUG : Logger::INFO
61
- @has_support_complex_parameters = false
62
166
  @logger = nil
167
+
168
+ # Complex parameters support
169
+ @has_support_complex_parameters = false
170
+
171
+ # MCPs configuration
172
+ @mcps_config_path = nil
173
+ @mcp_configuration = []
174
+
175
+ # Rails specific configuration
176
+ @launch_control = :automatic
177
+
178
+ # Roots configuration
179
+ @roots = []
180
+
181
+ # Sampling configuration
182
+ @sampling.reset!
63
183
  end
64
184
  end
65
185
  end
@@ -8,8 +8,7 @@ module RubyLLM
8
8
  PROTOCOL_VERSION = "2025-03-26"
9
9
  PV_2024_11_05 = "2024-11-05"
10
10
 
11
- attr_reader :client, :transport_type, :config, :request_timeout, :headers, :transport, :initialize_response,
12
- :capabilities, :protocol_version
11
+ attr_reader :client, :transport_type, :config, :capabilities, :protocol_version
13
12
 
14
13
  def initialize(client, transport_type:, config: {})
15
14
  @client = client
@@ -17,23 +16,46 @@ module RubyLLM
17
16
  @config = config
18
17
 
19
18
  @protocol_version = PROTOCOL_VERSION
20
- @headers = config[:headers] || {}
21
19
 
22
20
  @transport = nil
23
21
  @capabilities = nil
24
22
  end
25
23
 
24
+ def name
25
+ client.name
26
+ end
27
+
26
28
  def request(body, **options)
27
- @transport.request(body, **options)
29
+ transport.request(body, **options)
28
30
  rescue RubyLLM::MCP::Errors::TimeoutError => e
29
- if @transport.alive?
31
+ if transport&.alive?
30
32
  cancelled_notification(reason: "Request timed out", request_id: e.request_id)
31
33
  end
32
34
  raise e
33
35
  end
34
36
 
37
+ def process_result(result)
38
+ if result.notification?
39
+ process_notification(result)
40
+ return nil
41
+ end
42
+
43
+ if result.request?
44
+ process_request(result) if alive?
45
+ return nil
46
+ end
47
+
48
+ if result.response?
49
+ return result
50
+ end
51
+
52
+ nil
53
+ end
54
+
35
55
  def start_transport
36
- build_transport
56
+ return unless capabilities.nil?
57
+
58
+ transport.start
37
59
 
38
60
  initialize_response = initialize_request
39
61
  initialize_response.raise_error! if initialize_response.error?
@@ -47,16 +69,19 @@ module RubyLLM
47
69
  @transport.set_protocol_version(@protocol_version)
48
70
  end
49
71
 
50
- @capabilities = RubyLLM::MCP::Capabilities.new(initialize_response.value["capabilities"])
72
+ @capabilities = RubyLLM::MCP::ServerCapabilities.new(initialize_response.value["capabilities"])
51
73
  initialize_notification
52
74
  end
53
75
 
54
76
  def stop_transport
55
77
  @transport&.close
78
+ @capabilities = nil
56
79
  @transport = nil
80
+ @protocol_version = PROTOCOL_VERSION
57
81
  end
58
82
 
59
83
  def restart_transport
84
+ @initialize_response = nil
60
85
  stop_transport
61
86
  start_transport
62
87
  end
@@ -70,7 +95,7 @@ module RubyLLM
70
95
  if alive?
71
96
  result = ping_request.call
72
97
  else
73
- build_transport
98
+ transport.start
74
99
 
75
100
  result = ping_request.call
76
101
  @transport = nil
@@ -83,50 +108,26 @@ module RubyLLM
83
108
 
84
109
  def process_notification(result)
85
110
  notification = result.notification
86
-
87
- case notification.type
88
- when "notifications/tools/list_changed"
89
- client.reset_tools!
90
- when "notifications/resources/list_changed"
91
- client.reset_resources!
92
- when "notifications/resources/updated"
93
- uri = notification.params["uri"]
94
- resource = client.resources.find { |r| r.uri == uri }
95
- resource&.reset_content!
96
- when "notifications/prompts/list_changed"
97
- client.reset_prompts!
98
- when "notifications/message"
99
- process_logging_message(notification)
100
- when "notifications/progress"
101
- process_progress_message(notification)
102
- when "notifications/cancelled"
103
- # TODO: - do nothing at the moment until we support client operations
104
- else
105
- message = "Unknown notification type: #{notification.type} params:#{notification.params.to_h}"
106
- raise Errors::UnknownNotification.new(message: message)
107
- end
111
+ NotificationHandler.new(self).execute(notification)
108
112
  end
109
113
 
110
114
  def process_request(result)
111
- if result.ping?
112
- ping_response(id: result.id)
113
- return
114
- end
115
-
116
- # Handle server-initiated requests
117
- # Currently, we do not support any client operations but will
118
- raise RubyLLM::MCP::Errors::UnknownRequest.new(message: "Unknown request type: #{result.inspect}")
115
+ ResponseHandler.new(self).execute(result)
119
116
  end
120
117
 
121
118
  def initialize_request
122
119
  RubyLLM::MCP::Requests::Initialization.new(self).call
123
120
  end
124
121
 
125
- def tool_list
126
- result = RubyLLM::MCP::Requests::ToolList.new(self).call
122
+ def tool_list(cursor: nil)
123
+ result = RubyLLM::MCP::Requests::ToolList.new(self, cursor: cursor).call
127
124
  result.raise_error! if result.error?
128
125
 
129
- result.value["tools"]
126
+ if result.next_cursor?
127
+ result.value["tools"] + tool_list(cursor: result.next_cursor)
128
+ else
129
+ result.value["tools"]
130
+ end
130
131
  end
131
132
 
132
133
  def execute_tool(**args)
@@ -149,33 +150,45 @@ module RubyLLM
149
150
  RubyLLM::MCP::Requests::ToolCall.new(self, **args).call
150
151
  end
151
152
 
152
- def resource_list
153
- result = RubyLLM::MCP::Requests::ResourceList.new(self).call
153
+ def resource_list(cursor: nil)
154
+ result = RubyLLM::MCP::Requests::ResourceList.new(self, cursor: cursor).call
154
155
  result.raise_error! if result.error?
155
156
 
156
- result.value["resources"]
157
+ if result.next_cursor?
158
+ result.value["resources"] + resource_list(cursor: result.next_cursor)
159
+ else
160
+ result.value["resources"]
161
+ end
157
162
  end
158
163
 
159
164
  def resource_read(**args)
160
165
  RubyLLM::MCP::Requests::ResourceRead.new(self, **args).call
161
166
  end
162
167
 
163
- def resource_template_list
164
- result = RubyLLM::MCP::Requests::ResourceTemplateList.new(self).call
168
+ def resource_template_list(cursor: nil)
169
+ result = RubyLLM::MCP::Requests::ResourceTemplateList.new(self, cursor: cursor).call
165
170
  result.raise_error! if result.error?
166
171
 
167
- result.value["resourceTemplates"]
172
+ if result.next_cursor?
173
+ result.value["resourceTemplates"] + resource_template_list(cursor: result.next_cursor)
174
+ else
175
+ result.value["resourceTemplates"]
176
+ end
168
177
  end
169
178
 
170
179
  def resources_subscribe(**args)
171
180
  RubyLLM::MCP::Requests::ResourcesSubscribe.new(self, **args).call
172
181
  end
173
182
 
174
- def prompt_list
175
- result = RubyLLM::MCP::Requests::PromptList.new(self).call
183
+ def prompt_list(cursor: nil)
184
+ result = RubyLLM::MCP::Requests::PromptList.new(self, cursor: cursor).call
176
185
  result.raise_error! if result.error?
177
186
 
178
- result.value["prompts"]
187
+ if result.next_cursor?
188
+ result.value["prompts"] + prompt_list(cursor: result.next_cursor)
189
+ else
190
+ result.value["prompts"]
191
+ end
179
192
  end
180
193
 
181
194
  def execute_prompt(**args)
@@ -190,86 +203,66 @@ module RubyLLM
190
203
  RubyLLM::MCP::Requests::CompletionPrompt.new(self, **args).call
191
204
  end
192
205
 
206
+ def set_logging(**args)
207
+ RubyLLM::MCP::Requests::LoggingSetLevel.new(self, **args).call
208
+ end
209
+
210
+ ## Notifications
211
+ #
193
212
  def initialize_notification
194
- RubyLLM::MCP::Requests::InitializeNotification.new(self).call
213
+ RubyLLM::MCP::Notifications::Initialize.new(self).call
195
214
  end
196
215
 
197
216
  def cancelled_notification(**args)
198
- RubyLLM::MCP::Requests::CancelledNotification.new(self, **args).call
199
- end
200
-
201
- def ping_response(id: nil)
202
- RubyLLM::MCP::Requests::PingResponse.new(self, id: id).call
203
- end
204
-
205
- def set_logging(level:)
206
- RubyLLM::MCP::Requests::LoggingSetLevel.new(self, level: level).call
207
- end
208
-
209
- def build_transport
210
- case @transport_type
211
- when :sse
212
- @transport = RubyLLM::MCP::Transport::SSE.new(@config[:url],
213
- request_timeout: @config[:request_timeout],
214
- headers: @headers,
215
- coordinator: self)
216
- when :stdio
217
- @transport = RubyLLM::MCP::Transport::Stdio.new(@config[:command],
218
- request_timeout: @config[:request_timeout],
219
- args: @config[:args],
220
- env: @config[:env],
221
- coordinator: self)
222
- when :streamable
223
- @transport = RubyLLM::MCP::Transport::StreamableHTTP.new(@config[:url],
224
- request_timeout: @config[:request_timeout],
225
- headers: @headers,
226
- coordinator: self)
227
- else
228
- message = "Invalid transport type: :#{transport_type}. Supported types are :sse, :stdio, :streamable"
229
- raise Errors::InvalidTransportType.new(message: message)
230
- end
217
+ RubyLLM::MCP::Notifications::Cancelled.new(self, **args).call
231
218
  end
232
219
 
233
- def process_logging_message(notification)
234
- if client.logging_handler_enabled?
235
- client.on[:logging].call(notification)
236
- else
237
- default_process_logging_message(notification)
238
- end
220
+ def roots_list_change_notification
221
+ RubyLLM::MCP::Notifications::RootsListChange.new(self).call
222
+ end
223
+
224
+ ## Responses
225
+ #
226
+ def ping_response(**args)
227
+ RubyLLM::MCP::Responses::Ping.new(self, **args).call
228
+ end
229
+
230
+ def roots_list_response(**args)
231
+ RubyLLM::MCP::Responses::RootsList.new(self, **args).call
232
+ end
233
+
234
+ def sampling_create_message_response(**args)
235
+ RubyLLM::MCP::Responses::SamplingCreateMessage.new(self, **args).call
239
236
  end
240
237
 
241
- def default_process_logging_message(notification, logger: RubyLLM::MCP.logger)
242
- level = notification.params["level"]
243
- logger_message = notification.params["logger"]
244
- message = notification.params["data"]
245
-
246
- message = "#{logger_message}: #{message}"
247
-
248
- case level
249
- when "debug"
250
- logger.debug(message["message"])
251
- when "info", "notice"
252
- logger.info(message["message"])
253
- when "warning"
254
- logger.warn(message["message"])
255
- when "error", "critical"
256
- logger.error(message["message"])
257
- when "alert", "emergency"
258
- logger.fatal(message["message"])
238
+ def error_response(**args)
239
+ RubyLLM::MCP::Responses::Error.new(self, **args).call
240
+ end
241
+
242
+ def client_capabilities
243
+ capabilities = {}
244
+
245
+ if client.roots.active?
246
+ capabilities[:roots] = {
247
+ listChanged: true
248
+ }
259
249
  end
250
+
251
+ if sampling_enabled?
252
+ capabilities[:sampling] = {}
253
+ end
254
+
255
+ capabilities
260
256
  end
261
257
 
262
- def name
263
- client.name
258
+ def transport
259
+ @transport ||= RubyLLM::MCP::Transport.new(@transport_type, self, config: @config)
264
260
  end
265
261
 
266
262
  private
267
263
 
268
- def process_progress_message(notification)
269
- progress_obj = RubyLLM::MCP::Progress.new(self, client.on[:progress], notification.params)
270
- if client.tracking_progress?
271
- progress_obj.execute_progress_handler
272
- end
264
+ def sampling_enabled?
265
+ MCP.config.sampling.enabled?
273
266
  end
274
267
  end
275
268
  end
@@ -17,6 +17,8 @@ module RubyLLM
17
17
  class ResourceSubscribeNotAvailable < BaseError; end
18
18
  end
19
19
 
20
+ class InvalidFormatError < BaseError; end
21
+
20
22
  class InvalidProtocolVersionError < BaseError; end
21
23
 
22
24
  class InvalidTransportType < BaseError; end
@@ -39,7 +41,7 @@ module RubyLLM
39
41
  class TimeoutError < BaseError
40
42
  attr_reader :request_id
41
43
 
42
- def initialize(message:, request_id:)
44
+ def initialize(message:, request_id: nil)
43
45
  @request_id = request_id
44
46
  super(message: message)
45
47
  end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class NotificationHandler
6
+ attr_reader :coordinator, :client
7
+
8
+ def initialize(coordinator)
9
+ @coordinator = coordinator
10
+ @client = coordinator.client
11
+ end
12
+
13
+ def execute(notification)
14
+ case notification.type
15
+ when "notifications/tools/list_changed"
16
+ client.reset_tools!
17
+ when "notifications/resources/list_changed"
18
+ client.reset_resources!
19
+ when "notifications/resources/updated"
20
+ process_resource_updated(notification)
21
+ when "notifications/prompts/list_changed"
22
+ client.reset_prompts!
23
+ when "notifications/message"
24
+ process_logging_message(notification)
25
+ when "notifications/progress"
26
+ process_progress_message(notification)
27
+ when "notifications/cancelled"
28
+ # TODO: - do nothing at the moment until we support client operations
29
+ else
30
+ process_unknown_notification(notification)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def process_resource_updated(notification)
37
+ uri = notification.params["uri"]
38
+ resource = client.resources.find { |r| r.uri == uri }
39
+ resource&.reset_content!
40
+ end
41
+
42
+ def process_logging_message(notification)
43
+ if client.logging_handler_enabled?
44
+ client.on[:logging].call(notification)
45
+ else
46
+ default_process_logging_message(notification)
47
+ end
48
+ end
49
+
50
+ def process_progress_message(notification)
51
+ if client.tracking_progress?
52
+ progress_obj = RubyLLM::MCP::Progress.new(self, client.on[:progress], notification.params)
53
+ progress_obj.execute_progress_handler
54
+ end
55
+ end
56
+
57
+ def default_process_logging_message(notification, logger: RubyLLM::MCP.logger)
58
+ level = notification.params["level"]
59
+ logger_message = notification.params["logger"]
60
+ message = notification.params["data"]
61
+
62
+ message = "#{logger_message}: #{message}"
63
+
64
+ case level
65
+ when "debug"
66
+ logger.debug(message["message"])
67
+ when "info", "notice"
68
+ logger.info(message["message"])
69
+ when "warning"
70
+ logger.warn(message["message"])
71
+ when "error", "critical"
72
+ logger.error(message["message"])
73
+ when "alert", "emergency"
74
+ logger.fatal(message["message"])
75
+ end
76
+ end
77
+
78
+ def process_unknown_notification(notification)
79
+ message = "Unknown notification type: #{notification.type} params: #{notification.params.to_h}"
80
+ RubyLLM::MCP.logger.error(message)
81
+ end
82
+ end
83
+ end
84
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module RubyLLM
4
4
  module MCP
5
- module Requests
6
- class CancelledNotification
5
+ module Notifications
6
+ class Cancelled
7
7
  def initialize(coordinator, request_id:, reason:)
8
8
  @coordinator = coordinator
9
9
  @request_id = request_id
@@ -2,10 +2,14 @@
2
2
 
3
3
  module RubyLLM
4
4
  module MCP
5
- module Requests
6
- class InitializeNotification < RubyLLM::MCP::Requests::Base
5
+ module Notifications
6
+ class Initialize
7
+ def initialize(coordinator)
8
+ @coordinator = coordinator
9
+ end
10
+
7
11
  def call
8
- coordinator.request(notification_body, add_id: false, wait_for_response: false)
12
+ @coordinator.request(notification_body, add_id: false, wait_for_response: false)
9
13
  end
10
14
 
11
15
  def notification_body
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Notifications
6
+ class RootsListChange
7
+ def initialize(coordinator)
8
+ @coordinator = coordinator
9
+ end
10
+
11
+ def call
12
+ @coordinator.request(roots_list_change_notification_body, add_id: false, wait_for_response: false)
13
+ end
14
+
15
+ private
16
+
17
+ def roots_list_change_notification_body
18
+ {
19
+ jsonrpc: "2.0",
20
+ method: "notifications/roots/list_changed"
21
+ }
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -15,7 +15,25 @@ module RubyLLM
15
15
  end
16
16
 
17
17
  def item_type
18
- @items["type"].to_sym
18
+ @items&.dig("type")&.to_sym
19
+ end
20
+
21
+ def as_json(*_args)
22
+ to_h
23
+ end
24
+
25
+ def to_h
26
+ {
27
+ name: @name,
28
+ type: @type,
29
+ description: @desc,
30
+ required: @required,
31
+ default: @default,
32
+ union_type: @union_type,
33
+ items: @items&.to_h,
34
+ properties: @properties&.values,
35
+ enum: @enum
36
+ }
19
37
  end
20
38
  end
21
39
  end
@@ -17,7 +17,7 @@ module RubyLLM
17
17
  end
18
18
 
19
19
  def execute_progress_handler
20
- @progress_handler&.call(self)
20
+ @progress_handler.call(self)
21
21
  end
22
22
 
23
23
  def to_h
@@ -28,6 +28,8 @@ module RubyLLM
28
28
  message: @message
29
29
  }
30
30
  end
31
+
32
+ alias to_json to_h
31
33
  end
32
34
  end
33
35
  end