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 +4 -4
- data/README.md +8 -0
- data/lib/ruby_llm/mcp/client.rb +3 -2
- data/lib/ruby_llm/mcp/parameter.rb +9 -0
- data/lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb +30 -0
- data/lib/ruby_llm/mcp/providers/open_ai/complex_parameter_support.rb +27 -0
- data/lib/ruby_llm/mcp/tool.rb +8 -16
- data/lib/ruby_llm/mcp/transport/stdio.rb +10 -23
- data/lib/ruby_llm/mcp/version.rb +1 -1
- data/lib/ruby_llm/mcp.rb +8 -1
- data/lib/ruby_llm/overrides.rb +21 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 131f358210388bfb593787a010a20459a2fc319f2249ee24594402ee92f78e5d
|
4
|
+
data.tar.gz: dbe0431413c8986bc76987cf748227c8e74bc201cfed23635d487430924d7f0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/ruby_llm/mcp/client.rb
CHANGED
@@ -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,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)
|
data/lib/ruby_llm/mcp/tool.rb
CHANGED
@@ -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
|
-
|
119
|
-
|
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
|
data/lib/ruby_llm/mcp/version.rb
CHANGED
data/lib/ruby_llm/mcp.rb
CHANGED
@@ -10,8 +10,15 @@ loader.setup
|
|
10
10
|
|
11
11
|
module RubyLLM
|
12
12
|
module MCP
|
13
|
-
|
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.
|
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-
|
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
|