Class: RubyLLM::MCP::Client

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/ruby_llm/mcp/client.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, transport_type:, sdk: nil, adapter: nil, start: true, request_timeout: MCP.config.request_timeout, config: {}) ⇒ Client

rubocop:disable Metrics/ParameterLists, Metrics/MethodLength



14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/ruby_llm/mcp/client.rb', line 14

def initialize(name:, transport_type:, sdk: nil, adapter: nil, start: true, # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
               request_timeout: MCP.config.request_timeout, config: {})
  @name = name
  @transport_type = transport_type.to_sym
  @adapter_type = adapter || sdk || MCP.config.default_adapter

  # Validate early
  MCP.config.adapter_config.validate!(
    adapter: @adapter_type,
    transport: @transport_type
  )

  @with_prefix = config.delete(:with_prefix) || false
  @resolved_protocol_version = config[:protocol_version] ||
                               config["protocol_version"] ||
                               MCP.config.protocol_version
  @resolved_extensions = Extensions::Registry.merge(
    MCP.config.extensions.to_h,
    config[:extensions] || config["extensions"]
  )

  @config = config.merge(
    request_timeout: request_timeout,
    protocol_version: @resolved_protocol_version,
    extensions: @resolved_extensions
  )
  @request_timeout = request_timeout

  # Store OAuth config for later use
  @oauth_config = config[:oauth] || config["oauth"]
  @oauth_provider = nil
  @oauth_storage = nil

  @on = {}
  @tools = {}
  @resources = {}
  @resource_templates = {}
  @prompts = {}

  @log_level = nil

  @linked_resources = []

  # Build adapter based on configuration
  @adapter = build_adapter

  setup_roots if @adapter.supports?(:roots)
  setup_sampling if @adapter.supports?(:sampling)
  setup_event_handlers
  sync_elicitation_handler_state

  @adapter.start if start
end

Instance Attribute Details

#adapterObject (readonly)

Returns the value of attribute adapter.



10
11
12
# File 'lib/ruby_llm/mcp/client.rb', line 10

def adapter
  @adapter
end

#configObject (readonly)

Returns the value of attribute config.



10
11
12
# File 'lib/ruby_llm/mcp/client.rb', line 10

def config
  @config
end

#linked_resourcesObject

Returns the value of attribute linked_resources.



12
13
14
# File 'lib/ruby_llm/mcp/client.rb', line 12

def linked_resources
  @linked_resources
end

#log_levelObject (readonly)

Returns the value of attribute log_level.



10
11
12
# File 'lib/ruby_llm/mcp/client.rb', line 10

def log_level
  @log_level
end

#nameObject (readonly)

Returns the value of attribute name.



10
11
12
# File 'lib/ruby_llm/mcp/client.rb', line 10

def name
  @name
end

#onObject (readonly)

Returns the value of attribute on.



10
11
12
# File 'lib/ruby_llm/mcp/client.rb', line 10

def on
  @on
end

#on_logging_levelObject (readonly)

Returns the value of attribute on_logging_level.



10
11
12
# File 'lib/ruby_llm/mcp/client.rb', line 10

def on_logging_level
  @on_logging_level
end

#request_timeoutObject (readonly)

Returns the value of attribute request_timeout.



10
11
12
# File 'lib/ruby_llm/mcp/client.rb', line 10

def request_timeout
  @request_timeout
end

#rootsObject (readonly)

Returns the value of attribute roots.



10
11
12
# File 'lib/ruby_llm/mcp/client.rb', line 10

def roots
  @roots
end

#transport_typeObject (readonly)

Returns the value of attribute transport_type.



10
11
12
# File 'lib/ruby_llm/mcp/client.rb', line 10

def transport_type
  @transport_type
end

Instance Method Details

#elicitation_callback_enabled?Boolean

Returns:

  • (Boolean)


342
343
344
# File 'lib/ruby_llm/mcp/client.rb', line 342

def elicitation_callback_enabled?
  @on.key?(:elicitation) && !@on[:elicitation].nil?
end

#elicitation_enabled?Boolean

Returns:

  • (Boolean)


338
339
340
# File 'lib/ruby_llm/mcp/client.rb', line 338

def elicitation_enabled?
  @adapter_type == :ruby_llm && MCP.config.elicitation.enabled?
end

#human_in_the_loop?Boolean

Returns:

  • (Boolean)


265
266
267
# File 'lib/ruby_llm/mcp/client.rb', line 265

def human_in_the_loop?
  @on.key?(:human_in_the_loop) && !@on[:human_in_the_loop].nil?
end

#inspectObject



396
397
398
# File 'lib/ruby_llm/mcp/client.rb', line 396

def inspect
  "#<#{self.class.name}:0x#{object_id.to_s(16)} #{to_h.map { |k, v| "#{k}: #{v}" }.join(', ')}>"
end

#logging_enabled?Boolean

Returns:

  • (Boolean)


293
294
295
# File 'lib/ruby_llm/mcp/client.rb', line 293

def logging_enabled?
  !@log_level.nil?
end

#logging_handler_enabled?Boolean

Returns:

  • (Boolean)


289
290
291
# File 'lib/ruby_llm/mcp/client.rb', line 289

def logging_handler_enabled?
  @on.key?(:logging) && !@on[:logging].nil?
end

#oauth(type: :standard, **options) ⇒ OAuthProvider, BrowserOAuthProvider

Get or create OAuth provider for this client

Parameters:

  • type (Symbol) (defaults to: :standard)

    OAuth provider type (:standard or :browser, defaults to :standard)

  • options (Hash)

    additional options passed to provider

Returns:

  • (OAuthProvider, BrowserOAuthProvider)

    OAuth provider instance



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/ruby_llm/mcp/client.rb', line 88

def oauth(type: :standard, **options)
  # Return existing provider if already created
  return @oauth_provider if @oauth_provider

  # Get provider from transport if it already exists
  transport_oauth = transport_oauth_provider
  return transport_oauth if transport_oauth

  # Create new provider lazily
  server_url = @config[:url] || @config["url"]
  unless server_url
    raise Errors::ConfigurationError.new(
      message: "Cannot create OAuth provider without server URL in config"
    )
  end

  oauth_options = {
    server_url: server_url,
    scope: @oauth_config&.dig(:scope) || @oauth_config&.dig("scope"),
    storage: oauth_storage,
    **options
  }

  @oauth_provider = Auth.create_oauth(
    server_url,
    type: type,
    **oauth_options
  )
end

#on_elicitation(handler_class = nil, **options, &block) ⇒ Object



346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/ruby_llm/mcp/client.rb', line 346

def on_elicitation(handler_class = nil, **options, &block)
  require_feature!(:elicitation)

  if handler_class
    # Validate handler class
    validate_handler_class!(handler_class, :execute)

    # Handler class provided
    @on[:elicitation] = if options.any?
                          lambda do |elicitation|
                            handler_class.new(
                              elicitation: elicitation,
                              coordinator: @adapter.native_client,
                              **options
                            ).call
                          end
                        else
                          handler_class
                        end
  elsif block_given?
    # Block provided (backward compatible)
    @on[:elicitation] = block
  else
    # Clear handler when called without arguments
    @on[:elicitation] = nil
  end

  sync_elicitation_handler_state

  self
end

#on_human_in_the_loop(handler_class = nil, **options) ⇒ Object



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/ruby_llm/mcp/client.rb', line 269

def on_human_in_the_loop(handler_class = nil, **options)
  require_feature!(:human_in_the_loop)

  if block_given?
    raise ArgumentError, "Block-based human-in-the-loop callbacks are no longer supported. Use a handler class."
  end

  if handler_class
    # Validate handler class
    validate_handler_class!(handler_class, :execute)

    @on[:human_in_the_loop] = { class: handler_class, options: options }
  else
    # Clear handler when called without arguments
    @on[:human_in_the_loop] = nil
  end

  self
end

#on_logging(level: Logging::WARNING, &block) ⇒ Object



297
298
299
300
301
302
303
304
305
306
# File 'lib/ruby_llm/mcp/client.rb', line 297

def on_logging(level: Logging::WARNING, &block)
  require_feature!(:logging)
  @on_logging_level = level
  if alive?
    @adapter.set_logging(level: level)
  end

  @on[:logging] = block
  self
end

#on_progress(&block) ⇒ Object



255
256
257
258
259
260
261
262
263
# File 'lib/ruby_llm/mcp/client.rb', line 255

def on_progress(&block)
  require_feature!(:progress_tracking)
  if alive?
    @adapter.set_progress_tracking(enabled: true)
  end

  @on[:progress] = block
  self
end

#on_sampling(handler_class = nil, **options, &block) ⇒ Object



312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/ruby_llm/mcp/client.rb', line 312

def on_sampling(handler_class = nil, **options, &block)
  require_feature!(:sampling)

  if handler_class
    # Validate handler class
    validate_handler_class!(handler_class, :execute)

    # Handler class provided
    @on[:sampling] = if options.any?
                       lambda do |sample|
                         handler_class.new(sample: sample, coordinator: @adapter.native_client, **options).call
                       end
                     else
                       handler_class
                     end
  elsif block_given?
    # Block provided (backward compatible)
    @on[:sampling] = block
  else
    # Clear handler when called without arguments
    @on[:sampling] = nil
  end

  self
end

#prompt(name, refresh: false) ⇒ Object



211
212
213
214
215
# File 'lib/ruby_llm/mcp/client.rb', line 211

def prompt(name, refresh: false)
  prompts(refresh: refresh)

  @prompts[name]
end

#prompts(refresh: false) ⇒ Object



199
200
201
202
203
204
205
206
207
208
209
# File 'lib/ruby_llm/mcp/client.rb', line 199

def prompts(refresh: false)
  require_feature!(:prompts)
  return [] unless capabilities.prompt_list?

  fetch(:prompts, refresh) do
    prompts = @adapter.prompt_list
    build_map(prompts, MCP::Prompt)
  end

  @prompts.values
end

#reset_prompts!Object



217
218
219
# File 'lib/ruby_llm/mcp/client.rb', line 217

def reset_prompts!
  @prompts = {}
end

#reset_resource_templates!Object



195
196
197
# File 'lib/ruby_llm/mcp/client.rb', line 195

def reset_resource_templates!
  @resource_templates = {}
end

#reset_resources!Object



173
174
175
# File 'lib/ruby_llm/mcp/client.rb', line 173

def reset_resources!
  @resources = {}
end

#reset_tools!Object



136
137
138
# File 'lib/ruby_llm/mcp/client.rb', line 136

def reset_tools!
  @tools = {}
end

#resource(name, refresh: false) ⇒ Object



153
154
155
156
157
# File 'lib/ruby_llm/mcp/client.rb', line 153

def resource(name, refresh: false)
  resources(refresh: refresh)

  @resources[name]
end

#resource_template(name, refresh: false) ⇒ Object



189
190
191
192
193
# File 'lib/ruby_llm/mcp/client.rb', line 189

def resource_template(name, refresh: false)
  resource_templates(refresh: refresh)

  @resource_templates[name]
end

#resource_templates(refresh: false) ⇒ Object



177
178
179
180
181
182
183
184
185
186
187
# File 'lib/ruby_llm/mcp/client.rb', line 177

def resource_templates(refresh: false)
  require_feature!(:resource_templates)
  return [] unless capabilities.resources_list?

  fetch(:resource_templates, refresh) do
    resource_templates = @adapter.resource_template_list
    build_map(resource_templates, MCP::ResourceTemplate)
  end

  @resource_templates.values
end

#resources(refresh: false) ⇒ Object



140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/ruby_llm/mcp/client.rb', line 140

def resources(refresh: false)
  require_feature!(:resources)
  return [] unless capabilities.resources_list?

  fetch(:resources, refresh) do
    resources = @adapter.resource_list
    resources = build_map(resources, MCP::Resource)
    include_linked_resources(resources)
  end

  @resources.values
end

#restart!Object



80
81
82
# File 'lib/ruby_llm/mcp/client.rb', line 80

def restart!
  @adapter.restart!
end

#sampling_callback_enabled?Boolean

Returns:

  • (Boolean)


308
309
310
# File 'lib/ruby_llm/mcp/client.rb', line 308

def sampling_callback_enabled?
  @on.key?(:sampling) && !@on[:sampling].nil?
end

#startObject



72
73
74
# File 'lib/ruby_llm/mcp/client.rb', line 72

def start
  @adapter.start
end

#stopObject



76
77
78
# File 'lib/ruby_llm/mcp/client.rb', line 76

def stop
  @adapter.stop
end

#task_cancel(task_id) ⇒ Object



240
241
242
243
244
245
246
247
248
249
# File 'lib/ruby_llm/mcp/client.rb', line 240

def task_cancel(task_id)
  require_feature!(:tasks)
  unless capabilities.tasks_cancel?
    message = "Task cancellation is not available for this MCP server"
    raise Errors::Capabilities::TaskCancelNotAvailable.new(message: message)
  end

  result = @adapter.task_cancel(task_id: task_id)
  MCP::Task.new(@adapter, result.value)
end

#task_get(task_id) ⇒ Object



228
229
230
231
232
# File 'lib/ruby_llm/mcp/client.rb', line 228

def task_get(task_id)
  require_feature!(:tasks)
  result = @adapter.task_get(task_id: task_id)
  MCP::Task.new(@adapter, result.value)
end

#task_result(task_id) ⇒ Object



234
235
236
237
238
# File 'lib/ruby_llm/mcp/client.rb', line 234

def task_result(task_id)
  require_feature!(:tasks)
  result = @adapter.task_result(task_id: task_id)
  result.value
end

#tasks_listObject



221
222
223
224
225
226
# File 'lib/ruby_llm/mcp/client.rb', line 221

def tasks_list
  require_feature!(:tasks)
  return [] unless capabilities.tasks? && capabilities.tasks_list?

  @adapter.tasks_list.map { |task| MCP::Task.new(@adapter, task) }
end

#to_hObject Also known as: as_json



378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
# File 'lib/ruby_llm/mcp/client.rb', line 378

def to_h
  {
    name: @name,
    transport_type: @transport_type,
    request_timeout: @request_timeout,
    start: @start,
    config: @config,
    on: @on,
    tools: @tools,
    resources: @resources,
    resource_templates: @resource_templates,
    prompts: @prompts,
    log_level: @log_level
  }
end

#tool(name, refresh: false) ⇒ Object



130
131
132
133
134
# File 'lib/ruby_llm/mcp/client.rb', line 130

def tool(name, refresh: false)
  tools(refresh: refresh)

  @tools[name]
end

#tools(refresh: false) ⇒ Object



118
119
120
121
122
123
124
125
126
127
128
# File 'lib/ruby_llm/mcp/client.rb', line 118

def tools(refresh: false)
  require_feature!(:tools)
  return [] unless capabilities.tools_list?

  fetch(:tools, refresh) do
    tools = @adapter.tool_list
    build_map(tools, MCP::Tool, with_prefix: @with_prefix)
  end

  @tools.values
end

#tracking_progress?Boolean

Returns:

  • (Boolean)


251
252
253
# File 'lib/ruby_llm/mcp/client.rb', line 251

def tracking_progress?
  @on.key?(:progress) && !@on[:progress].nil?
end

#unsubscribe_from_resource(resource_or_uri) ⇒ Object

rubocop:disable Naming/PredicateMethod



159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/ruby_llm/mcp/client.rb', line 159

def unsubscribe_from_resource(resource_or_uri) # rubocop:disable Naming/PredicateMethod
  require_feature!(:subscriptions)

  uri = if resource_or_uri.respond_to?(:uri)
          resource_or_uri.uri
        else
          resource_or_uri.to_s
        end
  @adapter.resources_unsubscribe(uri: uri)
  resource = @resources.values.find { |existing| existing.uri == uri }
  resource&.instance_variable_set(:@subscribed, false)
  true
end