ruby_llm-mcp 0.3.0 → 0.4.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +121 -2
  3. data/lib/ruby_llm/mcp/capabilities.rb +22 -2
  4. data/lib/ruby_llm/mcp/client.rb +104 -136
  5. data/lib/ruby_llm/mcp/configuration.rb +66 -0
  6. data/lib/ruby_llm/mcp/coordinator.rb +276 -0
  7. data/lib/ruby_llm/mcp/error.rb +34 -0
  8. data/lib/ruby_llm/mcp/errors.rb +38 -3
  9. data/lib/ruby_llm/mcp/logging.rb +16 -0
  10. data/lib/ruby_llm/mcp/parameter.rb +5 -2
  11. data/lib/ruby_llm/mcp/progress.rb +33 -0
  12. data/lib/ruby_llm/mcp/prompt.rb +20 -13
  13. data/lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb +7 -3
  14. data/lib/ruby_llm/mcp/providers/gemini/complex_parameter_support.rb +8 -4
  15. data/lib/ruby_llm/mcp/providers/openai/complex_parameter_support.rb +8 -4
  16. data/lib/ruby_llm/mcp/requests/base.rb +3 -3
  17. data/lib/ruby_llm/mcp/requests/cancelled_notification.rb +32 -0
  18. data/lib/ruby_llm/mcp/requests/completion_prompt.rb +3 -3
  19. data/lib/ruby_llm/mcp/requests/completion_resource.rb +3 -3
  20. data/lib/ruby_llm/mcp/requests/initialization.rb +24 -18
  21. data/lib/ruby_llm/mcp/requests/initialize_notification.rb +20 -0
  22. data/lib/ruby_llm/mcp/requests/logging_set_level.rb +28 -0
  23. data/lib/ruby_llm/mcp/requests/meta.rb +30 -0
  24. data/lib/ruby_llm/mcp/requests/ping.rb +20 -0
  25. data/lib/ruby_llm/mcp/requests/ping_response.rb +28 -0
  26. data/lib/ruby_llm/mcp/requests/prompt_call.rb +3 -3
  27. data/lib/ruby_llm/mcp/requests/prompt_list.rb +1 -1
  28. data/lib/ruby_llm/mcp/requests/resource_list.rb +1 -1
  29. data/lib/ruby_llm/mcp/requests/resource_read.rb +4 -4
  30. data/lib/ruby_llm/mcp/requests/resource_template_list.rb +1 -1
  31. data/lib/ruby_llm/mcp/requests/resources_subscribe.rb +30 -0
  32. data/lib/ruby_llm/mcp/requests/tool_call.rb +6 -3
  33. data/lib/ruby_llm/mcp/requests/tool_list.rb +17 -11
  34. data/lib/ruby_llm/mcp/resource.rb +28 -7
  35. data/lib/ruby_llm/mcp/resource_template.rb +17 -12
  36. data/lib/ruby_llm/mcp/result.rb +90 -0
  37. data/lib/ruby_llm/mcp/tool.rb +36 -10
  38. data/lib/ruby_llm/mcp/transport/sse.rb +82 -75
  39. data/lib/ruby_llm/mcp/transport/stdio.rb +33 -17
  40. data/lib/ruby_llm/mcp/transport/streamable_http.rb +647 -0
  41. data/lib/ruby_llm/mcp/version.rb +1 -1
  42. data/lib/ruby_llm/mcp.rb +18 -0
  43. data/lib/tasks/release.rake +23 -0
  44. metadata +22 -51
  45. data/lib/ruby_llm/mcp/requests/notification.rb +0 -14
  46. data/lib/ruby_llm/mcp/transport/streamable.rb +0 -299
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c414a0306ebefcb35dcb599406a0092e554e1389aebfa812597e7072e304cf88
4
- data.tar.gz: 25b384446ad37b3422bf64e1f9b15413bd8cd64636dbf50ad8ae861e4dc4589d
3
+ metadata.gz: 2dd0be50f01aa4fd126828c9c4f81d2e5a7b3cb35f4b32a4ed8e3e4deb731202
4
+ data.tar.gz: d6efbfbb3345544da2e201968ff26ff1e238a72efc95e7eb5cc2cb77adaaaf94
5
5
  SHA512:
6
- metadata.gz: abbca445c4ecb2ee7a3f2af4457f47aff5337ac981426746184cb0cb63e699b606116d5db983d8d4183557896675743a14bada2af4dd3d54d85066183fe34653
7
- data.tar.gz: d8007f3ee983eb3a8e2e76ad2b0daa8ead92bc6347e3352d8d569fab770056026a29237a76182c137459663b54cb4b364df93fb7e968a2d783d1c96fe98f4774
6
+ metadata.gz: 769aa222a4f5afd5dc124f1fa1bb62771fd2d2dd5d60211fa3f86e664aa030811600f7ee448ec7607a7d244d2d6f961a9ded088f3db7eaa611fff8041528b6c1
7
+ data.tar.gz: d3a494430dcce6c0ee9246f6378d682771458c0009779c3e7f34aba43309b2ba6b72722ddbbc9e8d2b84061358cc8375f5fedd19c76a187795fce5adfd49338b
data/README.md CHANGED
@@ -103,13 +103,39 @@ response = chat.ask("Can you help me search for recent files in my project?")
103
103
  puts response
104
104
  ```
105
105
 
106
+ ### Human in the Loop
107
+
108
+ You can use the `on_human_in_the_loop` callback to allow the human to intervene in the tool call. This is useful for tools that require human input or programic input to verify if the tool should be executed.
109
+
110
+ For tool calls that have access to do important operations, there SHOULD always be a human in the loop with the ability to deny tool invocations.
111
+
112
+ ```ruby
113
+ client.on_human_in_the_loop do |name, params|
114
+ name == "add" && params[:a] == 1 && params[:b] == 2
115
+ end
116
+
117
+ tool = client.tool("add")
118
+ result = tool.execute(a: 1, b: 2)
119
+ puts result # 3
120
+
121
+ # If the human in the loop returns false, the tool call will be cancelled
122
+ result = tool.execute(a: 2, b: 2)
123
+ puts result # Tool execution error: Tool call was cancelled by the client
124
+ ```
125
+
126
+ tool = client.tool("add")
127
+ result = tool.execute(a: 1, b: 2)
128
+ puts result
129
+
130
+ ````
131
+
106
132
  ### Support Complex Parameters
107
133
 
108
134
  If you want to support complex parameters, like an array of objects it currently requires a patch to RubyLLM itself. This is planned to be temporary until the RubyLLM is updated.
109
135
 
110
136
  ```ruby
111
137
  RubyLLM::MCP.support_complex_parameters!
112
- ```
138
+ ````
113
139
 
114
140
  ### Streaming Responses with Tool Calls
115
141
 
@@ -341,6 +367,14 @@ client.restart!
341
367
  client.stop
342
368
  ```
343
369
 
370
+ ### Ping
371
+
372
+ You can ping the MCP server to check if it is alive:
373
+
374
+ ```ruby
375
+ client.ping # => true or false
376
+ ```
377
+
344
378
  ## Refreshing Cached Data
345
379
 
346
380
  The client caches tools, resources, prompts, and resource templates list calls are cached to reduce round trips back to the MCP server. You can refresh this cache:
@@ -363,6 +397,71 @@ prompt = client.prompt("daily_greeting", refresh: true)
363
397
  template = client.resource_template("user_logs", refresh: true)
364
398
  ```
365
399
 
400
+ ## Notifications
401
+
402
+ MCPs can produce notifications that happen in an async nature outside normal calls to the MCP server.
403
+
404
+ ### Subscribing to a Resource Update
405
+
406
+ By default, the client will look for any resource cha to resource updates and refresh the resource content when it changes.
407
+
408
+ ### Logging Notifications
409
+
410
+ MCPs can produce logging notifications for long-running tool operations. Logging notifications allow tools to send real-time updates about their execution status.
411
+
412
+ ```ruby
413
+ client.on_logging do |logging|
414
+ puts "Logging: #{logging.level} - #{logging.message}"
415
+ end
416
+
417
+ # Execute a tool that supports logging notifications
418
+ tool = client.tool("long_running_operation")
419
+ result = tool.execute(operation: "data_processing")
420
+
421
+ # Logging: info - Processing data...
422
+ # Logging: info - Processing data...
423
+ # Logging: warning - Something went wrong but not major...
424
+ ```
425
+
426
+ Different levels of logging are supported:
427
+
428
+ ```ruby
429
+ client.on_logging(RubyLLM::MCP::Logging::WARNING) do |logging|
430
+ puts "Logging: #{logging.level} - #{logging.message}"
431
+ end
432
+
433
+ # Execute a tool that supports logging notifications
434
+ tool = client.tool("long_running_operation")
435
+ result = tool.execute(operation: "data_processing")
436
+
437
+ # Logging: warning - Something went wrong but not major...
438
+ ```
439
+
440
+ ### Progress Notifications
441
+
442
+ MCPs can produce progress notifications for long-running tool operations. Progress notifications allow tools to send real-time updates about their execution status.
443
+
444
+ **Note:** that we only support progress notifications for tool calls today.
445
+
446
+ ```ruby
447
+ # Set up progress tracking
448
+ client.on_progress do |progress|
449
+ puts "Progress: #{progress.progress}% - #{progress.message}"
450
+ end
451
+
452
+ # Execute a tool that supports progress notifications
453
+ tool = client.tool("long_running_operation")
454
+ result = tool.execute(operation: "data_processing")
455
+
456
+ # Progress 25% - Processing data...
457
+ # Progress 50% - Processing data...
458
+ # Progress 75% - Processing data...
459
+ # Progress 100% - Processing data...
460
+ puts result
461
+
462
+ # Result: { status: "success", data: "Processed data" }
463
+ ```
464
+
366
465
  ## Transport Types
367
466
 
368
467
  ### SSE (Server-Sent Events)
@@ -411,7 +510,27 @@ client = RubyLLM::MCP.client(
411
510
  )
412
511
  ```
413
512
 
414
- ## Configuration Options
513
+ ## RubyLLM::MCP and Client Configuration Options
514
+
515
+ MCP comes with some common configuration options that can be set on the client.
516
+
517
+ ```ruby
518
+ RubyLLM::MCP.configure do |config|
519
+ # Set the progress handler
520
+ config.support_complex_parameters!
521
+
522
+ # Set parameters on the built in logger
523
+ config.log_file = $stdout
524
+ config.log_level = Logger::ERROR
525
+
526
+ # Or add a custom logger
527
+ config.logger = Logger.new(STDOUT)
528
+ end
529
+ ```
530
+
531
+ ### MCP Client Options
532
+
533
+ MCP client options are set on the client itself.
415
534
 
416
535
  - `name`: A unique identifier for your MCP client
417
536
  - `transport_type`: Either `:sse`, `:streamable`, or `:stdio`
@@ -9,7 +9,11 @@ module RubyLLM
9
9
  @capabilities = capabilities
10
10
  end
11
11
 
12
- def resources_list_changed?
12
+ def resources_list?
13
+ !@capabilities["resources"].nil?
14
+ end
15
+
16
+ def resources_list_changes?
13
17
  @capabilities.dig("resources", "listChanged") || false
14
18
  end
15
19
 
@@ -17,13 +21,29 @@ module RubyLLM
17
21
  @capabilities.dig("resources", "subscribe") || false
18
22
  end
19
23
 
20
- def tools_list_changed?
24
+ def tools_list?
25
+ !@capabilities["tools"].nil?
26
+ end
27
+
28
+ def tools_list_changes?
21
29
  @capabilities.dig("tools", "listChanged") || false
22
30
  end
23
31
 
32
+ def prompt_list?
33
+ !@capabilities["prompts"].nil?
34
+ end
35
+
36
+ def prompt_list_changes?
37
+ @capabilities.dig("prompts", "listChanged") || false
38
+ end
39
+
24
40
  def completion?
25
41
  !@capabilities["completions"].nil?
26
42
  end
43
+
44
+ def logging?
45
+ !@capabilities["logging"].nil?
46
+ end
27
47
  end
28
48
  end
29
49
  end
@@ -1,223 +1,191 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "forwardable"
4
+
3
5
  module RubyLLM
4
6
  module MCP
5
7
  class Client
6
- PROTOCOL_VERSION = "2025-03-26"
7
- PV_2024_11_05 = "2024-11-05"
8
+ extend Forwardable
8
9
 
9
- attr_reader :name, :config, :transport_type, :transport, :request_timeout, :reverse_proxy_url, :protocol_version,
10
- :capabilities
10
+ attr_reader :name, :config, :transport_type, :request_timeout, :log_level, :on
11
11
 
12
- def initialize(name:, transport_type:, start: true, request_timeout: 8000, reverse_proxy_url: nil, config: {}) # rubocop:disable Metrics/ParameterLists
12
+ def initialize(name:, transport_type:, start: true, request_timeout: MCP.config.request_timeout, config: {})
13
13
  @name = name
14
- @config = config
15
- @protocol_version = PROTOCOL_VERSION
16
- @headers = config[:headers] || {}
17
-
14
+ @config = config.merge(request_timeout: request_timeout)
18
15
  @transport_type = transport_type.to_sym
19
- @transport = nil
16
+ @request_timeout = request_timeout
20
17
 
21
- @capabilities = nil
18
+ @coordinator = setup_coordinator
22
19
 
23
- @request_timeout = request_timeout
24
- @reverse_proxy_url = reverse_proxy_url
20
+ @on = {}
21
+ @tools = {}
22
+ @resources = {}
23
+ @resource_templates = {}
24
+ @prompts = {}
25
25
 
26
- if start
27
- self.start
28
- end
29
- end
26
+ @log_level = nil
30
27
 
31
- def request(body, **options)
32
- @transport.request(body, **options)
28
+ @coordinator.start_transport if start
33
29
  end
34
30
 
35
- def start
36
- case @transport_type
37
- when :sse
38
- @transport = RubyLLM::MCP::Transport::SSE.new(@config[:url], request_timeout: @request_timeout,
39
- headers: @headers)
40
- when :stdio
41
- @transport = RubyLLM::MCP::Transport::Stdio.new(@config[:command], request_timeout: @request_timeout,
42
- args: @config[:args], env: @config[:env])
43
- when :streamable
44
- @transport = RubyLLM::MCP::Transport::Streamable.new(@config[:url], request_timeout: @request_timeout,
45
- headers: @headers)
46
- else
47
- raise "Invalid transport type: #{transport_type}"
48
- end
31
+ def_delegators :@coordinator, :alive?, :capabilities, :ping
49
32
 
50
- @initialize_response = initialize_request
51
- @capabilities = RubyLLM::MCP::Capabilities.new(@initialize_response["result"]["capabilities"])
52
- notification_request
33
+ def start
34
+ @coordinator.start_transport
53
35
  end
54
36
 
55
37
  def stop
56
- @transport&.close
57
- @transport = nil
38
+ @coordinator.stop_transport
58
39
  end
59
40
 
60
41
  def restart!
61
- stop
62
- start
63
- end
64
-
65
- def alive?
66
- !!@transport&.alive?
42
+ @coordinator.restart_transport
67
43
  end
68
44
 
69
45
  def tools(refresh: false)
70
- @tools = nil if refresh
71
- @tools ||= fetch_and_create_tools
46
+ return [] unless capabilities.tools_list?
47
+
48
+ fetch(:tools, refresh) do
49
+ tools = @coordinator.tool_list
50
+ build_map(tools, MCP::Tool)
51
+ end
52
+
72
53
  @tools.values
73
54
  end
74
55
 
75
56
  def tool(name, refresh: false)
76
- @tools = nil if refresh
77
- @tools ||= fetch_and_create_tools
57
+ tools(refresh: refresh)
78
58
 
79
59
  @tools[name]
80
60
  end
81
61
 
62
+ def reset_tools!
63
+ @tools = {}
64
+ end
65
+
82
66
  def resources(refresh: false)
83
- @resources = nil if refresh
84
- @resources ||= fetch_and_create_resources
67
+ return [] unless capabilities.resources_list?
68
+
69
+ fetch(:resources, refresh) do
70
+ resources = @coordinator.resource_list
71
+ build_map(resources, MCP::Resource)
72
+ end
73
+
85
74
  @resources.values
86
75
  end
87
76
 
88
77
  def resource(name, refresh: false)
89
- @resources = nil if refresh
90
- @resources ||= fetch_and_create_resources
78
+ resources(refresh: refresh)
91
79
 
92
80
  @resources[name]
93
81
  end
94
82
 
83
+ def reset_resources!
84
+ @resources = {}
85
+ end
86
+
95
87
  def resource_templates(refresh: false)
96
- @resource_templates = nil if refresh
97
- @resource_templates ||= fetch_and_create_resource_templates
88
+ return [] unless capabilities.resources_list?
89
+
90
+ fetch(:resource_templates, refresh) do
91
+ resource_templates = @coordinator.resource_template_list
92
+ build_map(resource_templates, MCP::ResourceTemplate)
93
+ end
94
+
98
95
  @resource_templates.values
99
96
  end
100
97
 
101
98
  def resource_template(name, refresh: false)
102
- @resource_templates = nil if refresh
103
- @resource_templates ||= fetch_and_create_resource_templates
99
+ resource_templates(refresh: refresh)
104
100
 
105
101
  @resource_templates[name]
106
102
  end
107
103
 
108
- def prompts(refresh: false)
109
- @prompts = nil if refresh
110
- @prompts ||= fetch_and_create_prompts
111
- @prompts.values
104
+ def reset_resource_templates!
105
+ @resource_templates = {}
112
106
  end
113
107
 
114
- def prompt(name, refresh: false)
115
- @prompts = nil if refresh
116
- @prompts ||= fetch_and_create_prompts
117
-
118
- @prompts[name]
119
- end
108
+ def prompts(refresh: false)
109
+ return [] unless capabilities.prompt_list?
120
110
 
121
- def execute_tool(**args)
122
- RubyLLM::MCP::Requests::ToolCall.new(self, **args).call
123
- end
111
+ fetch(:prompts, refresh) do
112
+ prompts = @coordinator.prompt_list
113
+ build_map(prompts, MCP::Prompt)
114
+ end
124
115
 
125
- def resource_read_request(**args)
126
- RubyLLM::MCP::Requests::ResourceRead.new(self, **args).call
116
+ @prompts.values
127
117
  end
128
118
 
129
- def completion_resource(**args)
130
- RubyLLM::MCP::Requests::CompletionResource.new(self, **args).call
131
- end
119
+ def prompt(name, refresh: false)
120
+ prompts(refresh: refresh)
132
121
 
133
- def completion_prompt(**args)
134
- RubyLLM::MCP::Requests::CompletionPrompt.new(self, **args).call
122
+ @prompts[name]
135
123
  end
136
124
 
137
- def execute_prompt(**args)
138
- RubyLLM::MCP::Requests::PromptCall.new(self, **args).call
125
+ def reset_prompts!
126
+ @prompts = {}
139
127
  end
140
128
 
141
- private
142
-
143
- def initialize_request
144
- RubyLLM::MCP::Requests::Initialization.new(self).call
129
+ def tracking_progress?
130
+ @on.key?(:progress) && !@on[:progress].nil?
145
131
  end
146
132
 
147
- def notification_request
148
- RubyLLM::MCP::Requests::Notification.new(self).call
133
+ def on_progress(&block)
134
+ @on[:progress] = block
135
+ self
149
136
  end
150
137
 
151
- def tool_list_request
152
- RubyLLM::MCP::Requests::ToolList.new(self).call
138
+ def human_in_the_loop?
139
+ @on.key?(:human_in_the_loop) && !@on[:human_in_the_loop].nil?
153
140
  end
154
141
 
155
- def resources_list_request
156
- RubyLLM::MCP::Requests::ResourceList.new(self).call
142
+ def on_human_in_the_loop(&block)
143
+ @on[:human_in_the_loop] = block
144
+ self
157
145
  end
158
146
 
159
- def resource_template_list_request
160
- RubyLLM::MCP::Requests::ResourceTemplateList.new(self).call
147
+ def logging_handler_enabled?
148
+ @on.key?(:logging) && !@on[:logging].nil?
161
149
  end
162
150
 
163
- def prompt_list_request
164
- RubyLLM::MCP::Requests::PromptList.new(self).call
151
+ def logging_enabled?
152
+ !@log_level.nil?
165
153
  end
166
154
 
167
- def fetch_and_create_tools
168
- tools_response = tool_list_request
169
- tools_response = tools_response["result"]["tools"]
170
-
171
- tools = {}
172
- tools_response.each do |tool|
173
- new_tool = RubyLLM::MCP::Tool.new(self, tool)
174
- tools[new_tool.name] = new_tool
175
- end
155
+ def on_logging(level: Logging::WARNING, logger: nil, &block)
156
+ @coordinator.set_logging(level: level)
176
157
 
177
- tools
158
+ @on[:logging] = if block_given?
159
+ block
160
+ else
161
+ lambda do |notification|
162
+ @coordinator.default_process_logging_message(notification, logger: logger)
163
+ end
164
+ end
165
+ self
178
166
  end
179
167
 
180
- def fetch_and_create_resources
181
- resources_response = resources_list_request
182
- resources_response = resources_response["result"]["resources"]
183
-
184
- resources = {}
185
- resources_response.each do |resource|
186
- new_resource = RubyLLM::MCP::Resource.new(self, resource)
187
- resources[new_resource.name] = new_resource
188
- end
168
+ private
189
169
 
190
- resources
170
+ def setup_coordinator
171
+ Coordinator.new(self,
172
+ transport_type: @transport_type,
173
+ config: @config)
191
174
  end
192
175
 
193
- def fetch_and_create_resource_templates
194
- resource_templates_response = resource_template_list_request
195
- resource_templates_response = resource_templates_response["result"]["resourceTemplates"]
196
-
197
- resource_templates = {}
198
- resource_templates_response.each do |resource_template|
199
- new_resource_template = RubyLLM::MCP::ResourceTemplate.new(self, resource_template)
200
- resource_templates[new_resource_template.name] = new_resource_template
176
+ def fetch(cache_key, refresh)
177
+ instance_variable_set("@#{cache_key}", {}) if refresh
178
+ if instance_variable_get("@#{cache_key}").empty?
179
+ instance_variable_set("@#{cache_key}", yield)
201
180
  end
202
-
203
- resource_templates
181
+ instance_variable_get("@#{cache_key}")
204
182
  end
205
183
 
206
- def fetch_and_create_prompts
207
- prompts_response = prompt_list_request
208
- prompts_response = prompts_response["result"]["prompts"]
209
-
210
- prompts = {}
211
- prompts_response.each do |prompt|
212
- new_prompt = RubyLLM::MCP::Prompt.new(self,
213
- name: prompt["name"],
214
- description: prompt["description"],
215
- arguments: prompt["arguments"])
216
-
217
- prompts[new_prompt.name] = new_prompt
184
+ def build_map(raw_data, klass)
185
+ raw_data.each_with_object({}) do |item, acc|
186
+ instance = klass.new(@coordinator, item)
187
+ acc[instance.name] = instance
218
188
  end
219
-
220
- prompts
221
189
  end
222
190
  end
223
191
  end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class Configuration
6
+ attr_accessor :request_timeout, :log_file, :log_level, :has_support_complex_parameters
7
+ attr_writer :logger
8
+
9
+ REQUEST_TIMEOUT_DEFAULT = 8000
10
+
11
+ def initialize
12
+ set_defaults
13
+ end
14
+
15
+ def reset!
16
+ set_defaults
17
+ end
18
+
19
+ def support_complex_parameters!
20
+ return if @has_support_complex_parameters
21
+
22
+ @has_support_complex_parameters = true
23
+ RubyLLM::MCP.support_complex_parameters!
24
+ end
25
+
26
+ def logger
27
+ @logger ||= Logger.new(
28
+ log_file,
29
+ progname: "RubyLLM::MCP",
30
+ level: log_level
31
+ )
32
+ end
33
+
34
+ def inspect
35
+ redacted = lambda do |name, value|
36
+ if name.match?(/_id|_key|_secret|_token$/)
37
+ value.nil? ? "nil" : "[FILTERED]"
38
+ else
39
+ value
40
+ end
41
+ end
42
+
43
+ inspection = instance_variables.map do |ivar|
44
+ name = ivar.to_s.delete_prefix("@")
45
+ value = redacted[name, instance_variable_get(ivar)]
46
+ "#{name}: #{value}"
47
+ end.join(", ")
48
+
49
+ "#<#{self.class}:0x#{object_id.to_s(16)} #{inspection}>"
50
+ end
51
+
52
+ private
53
+
54
+ def set_defaults
55
+ # Connection configuration
56
+ @request_timeout = REQUEST_TIMEOUT_DEFAULT
57
+
58
+ # Logging configuration
59
+ @log_file = $stdout
60
+ @log_level = ENV["RUBYLLM_MCP_DEBUG"] ? Logger::DEBUG : Logger::INFO
61
+ @has_support_complex_parameters = false
62
+ @logger = nil
63
+ end
64
+ end
65
+ end
66
+ end