ruby_llm-mcp 0.0.1 → 0.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0de5a83b2962398c602fe95d3b1c606bf3c0d1b8367209b70f9ef1f41a7f1e2
4
- data.tar.gz: 65c1a8c158efc387d9bec8ee67b701432cc605fd276647496d0825d0d83cafb3
3
+ metadata.gz: 131f358210388bfb593787a010a20459a2fc319f2249ee24594402ee92f78e5d
4
+ data.tar.gz: dbe0431413c8986bc76987cf748227c8e74bc201cfed23635d487430924d7f0a
5
5
  SHA512:
6
- metadata.gz: 416161bdd8d22711c144dd7bf3460e27f01cd57f764ebc5d024748bcdcc4fcf279898da28fefcba1ae31546f8b001d48798c7f9e48a0c1bc7b1ad555e2334fcd
7
- data.tar.gz: f9a6cbabf0e78aa89268c0cbbdaa2e07e9b33497cf47d1eab7282f60c6bfa161e4d5cbb7a1e6694de93ed18988318c1040ee0e3ffcd363b5d2d761e40158ce1b
6
+ metadata.gz: f4d4a86d99b955925457348b7a1ec1916b42ed8afda1549c311f1915fa7a0c106f51350819968679970366f9bdeaa393cf42f239b6709c40558065c2f8328484
7
+ data.tar.gz: a775771f2574f7a77627a1215bc60aaded76a287da65e13bd7504a0bb61a83f81494cddb5280e7cc960f989daef4149180a5e121d30d5da940488a0805183937
data/README.md CHANGED
@@ -87,6 +87,14 @@ response = chat.ask("Can you help me search for recent files in my project?")
87
87
  puts response
88
88
  ```
89
89
 
90
+ ### Support Complex Parameters
91
+
92
+ 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.
93
+
94
+ ```ruby
95
+ RubyLLM::MCP.support_complex_parameters!
96
+ ```
97
+
90
98
  ### Streaming Responses with Tool Calls
91
99
 
92
100
  ```ruby
@@ -4,6 +4,7 @@ module RubyLLM
4
4
  module MCP
5
5
  class Client
6
6
  PROTOCOL_VERSION = "2025-03-26"
7
+ attr_reader :name, :config, :transport_type, :transport, :request_timeout, :reverse_proxy_url
7
8
 
8
9
  def initialize(name:, transport_type:, request_timeout: 8000, reverse_proxy_url: nil, config: {})
9
10
  @name = name
@@ -13,9 +14,9 @@ module RubyLLM
13
14
  # TODO: Add streamable HTTP
14
15
  case @transport_type
15
16
  when :sse
16
- @transport = RubyLLM::MCP::Transport::SSE.new(config[:url])
17
+ @transport = RubyLLM::MCP::Transport::SSE.new(@config[:url])
17
18
  when :stdio
18
- @transport = RubyLLM::MCP::Transport::Stdio.new(config[:command], args: config[:args], env: config[:env])
19
+ @transport = RubyLLM::MCP::Transport::Stdio.new(@config[:command], args: @config[:args], env: @config[:env])
19
20
  else
20
21
  raise "Invalid transport type: #{transport_type}"
21
22
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ class Parameter < RubyLLM::Parameter
6
+ attr_accessor :items, :properties
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Providers
6
+ module Anthropic
7
+ module ComplexParameterSupport
8
+ def clean_parameters(parameters)
9
+ parameters.transform_values do |param|
10
+ format = {
11
+ type: param.type,
12
+ description: param.description
13
+ }.compact
14
+
15
+ if param.type == "array"
16
+ format[:items] = param.items
17
+ elsif param.type == "object"
18
+ format[:properties] = clean_parameters(param.properties)
19
+ end
20
+
21
+ format
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ RubyLLM::Providers::Anthropic.extend(RubyLLM::MCP::Providers::Anthropic::ComplexParameterSupport)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module MCP
5
+ module Providers
6
+ module OpenAI
7
+ module ComplexParameterSupport
8
+ def param_schema(param)
9
+ format = {
10
+ type: param.type,
11
+ description: param.description
12
+ }.compact
13
+
14
+ if param.type == "array"
15
+ format[:items] = param.items
16
+ elsif param.type == "object"
17
+ format[:properties] = param.properties.transform_values { |value| param_schema(value) }
18
+ end
19
+ format
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ RubyLLM::Providers::OpenAI.extend(RubyLLM::MCP::Providers::OpenAI::ComplexParameterSupport)
@@ -5,21 +5,6 @@ module RubyLLM
5
5
  class Tool < RubyLLM::Tool
6
6
  attr_reader :name, :description, :parameters, :mcp_client, :tool_response
7
7
 
8
- # @tool_response = {
9
- # name: string; // Unique identifier for the tool
10
- # description?: string; // Human-readable description
11
- # inputSchema: { // JSON Schema for the tool's parameters
12
- # type: "object",
13
- # properties: { ... } // Tool-specific parameters
14
- # },
15
- # annotations?: { // Optional hints about tool behavior
16
- # title?: string; // Human-readable title for the tool
17
- # readOnlyHint?: boolean; // If true, the tool does not modify its environment
18
- # destructiveHint?: boolean; // If true, the tool may perform destructive updates
19
- # idempotentHint?: boolean; // If true, repeated calls with same args have no additional effect
20
- # openWorldHint?: boolean; // If true, tool interacts with external entities
21
- # }
22
- # }
23
8
  def initialize(mcp_client, tool_response)
24
9
  super()
25
10
  @mcp_client = mcp_client
@@ -41,13 +26,20 @@ module RubyLLM
41
26
  def create_parameters(input_schema)
42
27
  params = {}
43
28
  input_schema["properties"].each_key do |key|
44
- param = RubyLLM::Parameter.new(
29
+ param = RubyLLM::MCP::Parameter.new(
45
30
  key,
46
31
  type: input_schema["properties"][key]["type"],
47
32
  desc: input_schema["properties"][key]["description"],
48
33
  required: input_schema["properties"][key]["required"]
49
34
  )
50
35
 
36
+ if param.type == "array"
37
+ param.items = input_schema["properties"][key]["items"]
38
+ elsif param.type == "object"
39
+ properties = create_parameters(input_schema["properties"][key]["properties"])
40
+ param.properties = properties
41
+ end
42
+
51
43
  params[key] = param
52
44
  end
53
45
 
@@ -14,10 +14,9 @@ module RubyLLM
14
14
  def initialize(command, args: [], env: {})
15
15
  @command = command
16
16
  @args = args
17
- @env = env
17
+ @env = env || {}
18
18
  @client_id = SecureRandom.uuid
19
19
 
20
- # Initialize state variables
21
20
  @id_counter = 0
22
21
  @id_mutex = Mutex.new
23
22
  @pending_requests = {}
@@ -25,17 +24,14 @@ module RubyLLM
25
24
  @running = true
26
25
  @reader_thread = nil
27
26
 
28
- # Start the process
29
27
  start_process
30
28
  end
31
29
 
32
30
  def request(body, wait_for_response: true)
33
- # Generate a unique request ID
34
31
  @id_mutex.synchronize { @id_counter += 1 }
35
32
  request_id = @id_counter
36
33
  body["id"] = request_id
37
34
 
38
- # Create a queue for this request's response
39
35
  response_queue = Queue.new
40
36
  if wait_for_response
41
37
  @pending_mutex.synchronize do
@@ -43,7 +39,6 @@ module RubyLLM
43
39
  end
44
40
  end
45
41
 
46
- # Send the request to the process
47
42
  begin
48
43
  @stdin.puts(JSON.generate(body))
49
44
  @stdin.flush
@@ -55,7 +50,6 @@ module RubyLLM
55
50
 
56
51
  return unless wait_for_response
57
52
 
58
- # Wait for the response with matching ID using a timeout
59
53
  begin
60
54
  Timeout.timeout(30) do
61
55
  response_queue.pop
@@ -69,21 +63,18 @@ module RubyLLM
69
63
  def close
70
64
  @running = false
71
65
 
72
- # Close stdin to signal the process to exit
73
66
  begin
74
67
  @stdin&.close
75
68
  rescue StandardError
76
69
  nil
77
70
  end
78
71
 
79
- # Wait for process to exit
80
72
  begin
81
73
  @wait_thread&.join(1)
82
74
  rescue StandardError
83
75
  nil
84
76
  end
85
77
 
86
- # Close remaining IO streams
87
78
  begin
88
79
  @stdout&.close
89
80
  rescue StandardError
@@ -95,7 +86,6 @@ module RubyLLM
95
86
  nil
96
87
  end
97
88
 
98
- # Wait for reader thread to finish
99
89
  begin
100
90
  @reader_thread&.join(1)
101
91
  rescue StandardError
@@ -112,13 +102,14 @@ module RubyLLM
112
102
  private
113
103
 
114
104
  def start_process
115
- # Close any existing process
116
105
  close if @stdin || @stdout || @stderr || @wait_thread
117
106
 
118
- # Start a new process
119
- @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(@env, @command, *@args)
107
+ @stdin, @stdout, @stderr, @wait_thread = if @env.empty?
108
+ Open3.popen3(@command, *@args)
109
+ else
110
+ Open3.popen3(environment_string, @command, *@args)
111
+ end
120
112
 
121
- # Start a thread to read responses
122
113
  start_reader_thread
123
114
  end
124
115
 
@@ -137,13 +128,9 @@ module RubyLLM
137
128
  next
138
129
  end
139
130
 
140
- # Read a line from the process
141
131
  line = @stdout.gets
142
-
143
- # Skip empty lines
144
132
  next unless line && !line.strip.empty?
145
133
 
146
- # Process the response
147
134
  process_response(line.strip)
148
135
  rescue IOError, Errno::EPIPE => e
149
136
  puts "Reader error: #{e.message}. Restarting in 1 second..."
@@ -160,7 +147,6 @@ module RubyLLM
160
147
  end
161
148
 
162
149
  def process_response(line)
163
- # Try to parse the response as JSON
164
150
  response = begin
165
151
  JSON.parse(line)
166
152
  rescue JSON::ParserError => e
@@ -168,11 +154,8 @@ module RubyLLM
168
154
  puts "Raw response: #{line}"
169
155
  return
170
156
  end
171
-
172
- # Extract the request ID
173
157
  request_id = response["id"]&.to_s
174
158
 
175
- # Find and fulfill the matching request
176
159
  @pending_mutex.synchronize do
177
160
  if request_id && @pending_requests.key?(request_id)
178
161
  response_queue = @pending_requests.delete(request_id)
@@ -180,6 +163,10 @@ module RubyLLM
180
163
  end
181
164
  end
182
165
  end
166
+
167
+ def environment_string
168
+ @env.map { |key, value| "#{key}=#{value}" }.join(" ")
169
+ end
183
170
  end
184
171
  end
185
172
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  module MCP
5
- VERSION = "0.0.1"
5
+ VERSION = "0.0.2"
6
6
  end
7
7
  end
data/lib/ruby_llm/mcp.rb CHANGED
@@ -10,8 +10,15 @@ loader.setup
10
10
 
11
11
  module RubyLLM
12
12
  module MCP
13
- def self.client(*args, **kwargs)
13
+ module_function
14
+
15
+ def client(*args, **kwargs)
14
16
  @client ||= Client.new(*args, **kwargs)
15
17
  end
18
+
19
+ def support_complex_parameters!
20
+ require_relative "providers/open_ai/complex_parameter_support"
21
+ require_relative "providers/anthropic/complex_parameter_support"
22
+ end
16
23
  end
17
24
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ module ToolsComplexParametersSupport
5
+ def param_schema(param)
6
+ format = {
7
+ type: param.type,
8
+ description: param.description
9
+ }.compact
10
+
11
+ if param.type == "array"
12
+ format[:items] = param.items
13
+ elsif param.type == "object"
14
+ format[:properties] = param.properties
15
+ end
16
+ format
17
+ end
18
+ end
19
+ end
20
+
21
+ RubyLLM::Providers::OpenAI.extend(RubyLLM::ToolsComplexParametersSupport)
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.0.1
4
+ version: 0.0.2
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-05-22 00:00:00.000000000 Z
11
+ date: 2025-05-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -110,6 +110,9 @@ files:
110
110
  - lib/ruby_llm/mcp.rb
111
111
  - lib/ruby_llm/mcp/client.rb
112
112
  - lib/ruby_llm/mcp/errors.rb
113
+ - lib/ruby_llm/mcp/parameter.rb
114
+ - lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb
115
+ - lib/ruby_llm/mcp/providers/open_ai/complex_parameter_support.rb
113
116
  - lib/ruby_llm/mcp/requests/base.rb
114
117
  - lib/ruby_llm/mcp/requests/initialization.rb
115
118
  - lib/ruby_llm/mcp/requests/notification.rb
@@ -120,6 +123,7 @@ files:
120
123
  - lib/ruby_llm/mcp/transport/stdio.rb
121
124
  - lib/ruby_llm/mcp/transport/streamable.rb
122
125
  - lib/ruby_llm/mcp/version.rb
126
+ - lib/ruby_llm/overrides.rb
123
127
  homepage: https://github.com/patvice/ruby_llm-mcp
124
128
  licenses:
125
129
  - MIT