ruby_llm-mcp 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/README.md +112 -22
- data/lib/ruby_llm/chat.rb +8 -2
- data/lib/ruby_llm/mcp/capabilities.rb +3 -3
- data/lib/ruby_llm/mcp/client.rb +98 -20
- data/lib/ruby_llm/mcp/parameter.rb +11 -1
- data/lib/ruby_llm/mcp/prompt.rb +17 -6
- data/lib/ruby_llm/mcp/providers/anthropic/complex_parameter_support.rb +25 -6
- data/lib/ruby_llm/mcp/providers/gemini/complex_parameter_support.rb +61 -0
- data/lib/ruby_llm/mcp/providers/openai/complex_parameter_support.rb +19 -5
- data/lib/ruby_llm/mcp/requests/{completion.rb → completion_prompt.rb} +3 -13
- data/lib/ruby_llm/mcp/requests/completion_resource.rb +40 -0
- data/lib/ruby_llm/mcp/resource.rb +22 -44
- data/lib/ruby_llm/mcp/resource_template.rb +79 -0
- data/lib/ruby_llm/mcp/tool.rb +62 -17
- data/lib/ruby_llm/mcp/transport/sse.rb +10 -6
- data/lib/ruby_llm/mcp/transport/stdio.rb +51 -14
- data/lib/ruby_llm/mcp/transport/streamable.rb +11 -4
- data/lib/ruby_llm/mcp/version.rb +1 -1
- metadata +6 -3
@@ -10,21 +10,35 @@ module RubyLLM
|
|
10
10
|
def param_schema(param)
|
11
11
|
properties = case param.type
|
12
12
|
when :array
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
13
|
+
if param.item_type == :object
|
14
|
+
{
|
15
|
+
type: param.type,
|
16
|
+
items: {
|
17
|
+
type: param.item_type,
|
18
|
+
properties: param.properties.transform_values { |value| param_schema(value) }
|
19
|
+
}
|
20
|
+
}
|
21
|
+
else
|
22
|
+
{
|
23
|
+
type: param.type,
|
24
|
+
items: { type: param.item_type, enum: param.enum }.compact
|
25
|
+
}
|
26
|
+
end
|
17
27
|
when :object
|
18
28
|
{
|
19
29
|
type: param.type,
|
20
30
|
properties: param.properties.transform_values { |value| param_schema(value) },
|
21
31
|
required: param.properties.select { |_, p| p.required }.keys
|
22
32
|
}
|
33
|
+
when :union
|
34
|
+
{
|
35
|
+
param.union_type => param.properties.map { |property| param_schema(property) }
|
36
|
+
}
|
23
37
|
else
|
24
38
|
{
|
25
39
|
type: param.type,
|
26
40
|
description: param.description
|
27
|
-
}
|
41
|
+
}.compact
|
28
42
|
end
|
29
43
|
|
30
44
|
properties.compact
|
@@ -3,10 +3,9 @@
|
|
3
3
|
module RubyLLM
|
4
4
|
module MCP
|
5
5
|
module Requests
|
6
|
-
class
|
7
|
-
def initialize(client,
|
6
|
+
class CompletionPrompt
|
7
|
+
def initialize(client, name:, argument:, value:)
|
8
8
|
@client = client
|
9
|
-
@type = type
|
10
9
|
@name = name
|
11
10
|
@argument = argument
|
12
11
|
@value = value
|
@@ -25,7 +24,7 @@ module RubyLLM
|
|
25
24
|
method: "completion/complete",
|
26
25
|
params: {
|
27
26
|
ref: {
|
28
|
-
type:
|
27
|
+
type: "ref/prompt",
|
29
28
|
name: @name
|
30
29
|
},
|
31
30
|
argument: {
|
@@ -35,15 +34,6 @@ module RubyLLM
|
|
35
34
|
}
|
36
35
|
}
|
37
36
|
end
|
38
|
-
|
39
|
-
def ref_type
|
40
|
-
case @type
|
41
|
-
when :prompt
|
42
|
-
"ref/prompt"
|
43
|
-
when :resource
|
44
|
-
"ref/resource"
|
45
|
-
end
|
46
|
-
end
|
47
37
|
end
|
48
38
|
end
|
49
39
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module MCP
|
5
|
+
module Requests
|
6
|
+
class CompletionResource
|
7
|
+
def initialize(client, uri:, argument:, value:)
|
8
|
+
@client = client
|
9
|
+
@uri = uri
|
10
|
+
@argument = argument
|
11
|
+
@value = value
|
12
|
+
end
|
13
|
+
|
14
|
+
def call
|
15
|
+
@client.request(request_body)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def request_body
|
21
|
+
{
|
22
|
+
jsonrpc: "2.0",
|
23
|
+
id: 1,
|
24
|
+
method: "completion/complete",
|
25
|
+
params: {
|
26
|
+
ref: {
|
27
|
+
type: "ref/resource",
|
28
|
+
uri: @uri
|
29
|
+
},
|
30
|
+
argument: {
|
31
|
+
name: @argument,
|
32
|
+
value: @value
|
33
|
+
}
|
34
|
+
}
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -3,37 +3,26 @@
|
|
3
3
|
module RubyLLM
|
4
4
|
module MCP
|
5
5
|
class Resource
|
6
|
-
attr_reader :uri, :name, :description, :mime_type, :mcp_client
|
6
|
+
attr_reader :uri, :name, :description, :mime_type, :mcp_client
|
7
7
|
|
8
|
-
def initialize(mcp_client, resource
|
8
|
+
def initialize(mcp_client, resource)
|
9
9
|
@mcp_client = mcp_client
|
10
10
|
@uri = resource["uri"]
|
11
11
|
@name = resource["name"]
|
12
12
|
@description = resource["description"]
|
13
13
|
@mime_type = resource["mimeType"]
|
14
|
-
|
15
|
-
|
14
|
+
if resource.key?("content_response")
|
15
|
+
@content_response = resource["content_response"]
|
16
|
+
@content = @content_response["text"] || @content_response["blob"]
|
17
|
+
end
|
16
18
|
end
|
17
19
|
|
18
|
-
def content
|
19
|
-
return @content
|
20
|
-
|
21
|
-
response = if template?
|
22
|
-
templated_uri = apply_template(@uri, arguments)
|
23
|
-
|
24
|
-
read_response(uri: templated_uri)
|
25
|
-
else
|
26
|
-
read_response
|
27
|
-
end
|
28
|
-
|
29
|
-
content = response.dig("result", "contents", 0)
|
30
|
-
@type = if content.key?("type")
|
31
|
-
content["type"]
|
32
|
-
else
|
33
|
-
"text"
|
34
|
-
end
|
20
|
+
def content
|
21
|
+
return @content unless @content.nil?
|
35
22
|
|
36
|
-
|
23
|
+
response = read_response
|
24
|
+
@content_response = response.dig("result", "contents", 0)
|
25
|
+
@content = @content_response["text"] || @content_response["blob"]
|
37
26
|
end
|
38
27
|
|
39
28
|
def include(chat, **args)
|
@@ -45,35 +34,30 @@ module RubyLLM
|
|
45
34
|
chat.add_message(message)
|
46
35
|
end
|
47
36
|
|
48
|
-
def to_content
|
49
|
-
content = content
|
37
|
+
def to_content
|
38
|
+
content = self.content
|
50
39
|
|
51
|
-
case
|
40
|
+
case content_type
|
52
41
|
when "text"
|
53
|
-
MCP::Content.new(content)
|
42
|
+
MCP::Content.new(text: "#{name}: #{description}\n\n#{content}")
|
54
43
|
when "blob"
|
55
44
|
attachment = MCP::Attachment.new(content, mime_type)
|
56
45
|
MCP::Content.new(text: "#{name}: #{description}", attachments: [attachment])
|
57
46
|
end
|
58
47
|
end
|
59
48
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
49
|
+
private
|
50
|
+
|
51
|
+
def content_type
|
52
|
+
return "text" if @content_response.nil?
|
64
53
|
|
65
|
-
|
54
|
+
if @content_response.key?("blob")
|
55
|
+
"blob"
|
66
56
|
else
|
67
|
-
|
57
|
+
"text"
|
68
58
|
end
|
69
59
|
end
|
70
60
|
|
71
|
-
def template?
|
72
|
-
@template
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
61
|
def read_response(uri: @uri)
|
78
62
|
parsed = URI.parse(uri)
|
79
63
|
case parsed.scheme
|
@@ -88,12 +72,6 @@ module RubyLLM
|
|
88
72
|
response = Faraday.get(uri)
|
89
73
|
{ "result" => { "contents" => [{ "text" => response.body }] } }
|
90
74
|
end
|
91
|
-
|
92
|
-
def apply_template(uri, arguments)
|
93
|
-
uri.gsub(/\{(\w+)\}/) do
|
94
|
-
arguments[::Regexp.last_match(1).to_sym] || "{#{::Regexp.last_match(1)}}"
|
95
|
-
end
|
96
|
-
end
|
97
75
|
end
|
98
76
|
end
|
99
77
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
module MCP
|
5
|
+
class ResourceTemplate
|
6
|
+
attr_reader :uri, :name, :description, :mime_type, :mcp_client, :template
|
7
|
+
|
8
|
+
def initialize(mcp_client, resource)
|
9
|
+
@mcp_client = mcp_client
|
10
|
+
@uri = resource["uriTemplate"]
|
11
|
+
@name = resource["name"]
|
12
|
+
@description = resource["description"]
|
13
|
+
@mime_type = resource["mimeType"]
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch_resource(arguments: {})
|
17
|
+
uri = apply_template(@uri, arguments)
|
18
|
+
response = read_response(uri)
|
19
|
+
content_response = response.dig("result", "contents", 0)
|
20
|
+
|
21
|
+
Resource.new(mcp_client, {
|
22
|
+
"uri" => uri,
|
23
|
+
"name" => "#{@name} (#{uri})",
|
24
|
+
"description" => @description,
|
25
|
+
"mimeType" => @mime_type,
|
26
|
+
"content_response" => content_response
|
27
|
+
})
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_content(arguments: {})
|
31
|
+
fetch_resource(arguments: arguments).to_content
|
32
|
+
end
|
33
|
+
|
34
|
+
def complete(argument, value)
|
35
|
+
if @mcp_client.capabilities.completion?
|
36
|
+
response = @mcp_client.completion_resource(uri: @uri, argument: argument, value: value)
|
37
|
+
response = response.dig("result", "completion")
|
38
|
+
|
39
|
+
Completion.new(values: response["values"], total: response["total"], has_more: response["hasMore"])
|
40
|
+
else
|
41
|
+
raise Errors::CompletionNotAvailable.new(message: "Completion is not available for this MCP server")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def content_type
|
48
|
+
if @content.key?("type")
|
49
|
+
@content["type"]
|
50
|
+
else
|
51
|
+
"text"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def read_response(uri)
|
56
|
+
parsed = URI.parse(uri)
|
57
|
+
case parsed.scheme
|
58
|
+
when "http", "https"
|
59
|
+
fetch_uri_content(uri)
|
60
|
+
else # file:// or git://
|
61
|
+
@mcp_client.resource_read_request(uri: uri)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def fetch_uri_content(uri)
|
66
|
+
response = Faraday.get(uri)
|
67
|
+
{ "result" => { "contents" => [{ "text" => response.body }] } }
|
68
|
+
end
|
69
|
+
|
70
|
+
def apply_template(uri, arguments)
|
71
|
+
uri.gsub(/\{(\w+)\}/) do
|
72
|
+
arguments[::Regexp.last_match(1).to_s] ||
|
73
|
+
arguments[::Regexp.last_match(1).to_sym] ||
|
74
|
+
"{#{::Regexp.last_match(1)}}"
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/ruby_llm/mcp/tool.rb
CHANGED
@@ -10,7 +10,7 @@ module RubyLLM
|
|
10
10
|
@mcp_client = mcp_client
|
11
11
|
|
12
12
|
@name = tool_response["name"]
|
13
|
-
@description = tool_response["description"]
|
13
|
+
@description = tool_response["description"].to_s
|
14
14
|
@parameters = create_parameters(tool_response["inputSchema"])
|
15
15
|
end
|
16
16
|
|
@@ -21,10 +21,11 @@ module RubyLLM
|
|
21
21
|
)
|
22
22
|
|
23
23
|
text_values = response.dig("result", "content").map { |content| content["text"] }.compact.join("\n")
|
24
|
+
|
24
25
|
if text_values.empty?
|
25
26
|
create_content_for_message(response.dig("result", "content", 0))
|
26
27
|
else
|
27
|
-
create_content_for_message({ type
|
28
|
+
create_content_for_message({ "type" => "text", "text" => text_values })
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
@@ -32,20 +33,16 @@ module RubyLLM
|
|
32
33
|
|
33
34
|
def create_parameters(input_schema)
|
34
35
|
params = {}
|
36
|
+
return params if input_schema["properties"].nil?
|
37
|
+
|
35
38
|
input_schema["properties"].each_key do |key|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
if param.type == "array"
|
44
|
-
param.items = input_schema["properties"][key]["items"]
|
45
|
-
elsif param.type == "object"
|
46
|
-
properties = create_parameters(input_schema["properties"][key]["properties"])
|
47
|
-
param.properties = properties
|
48
|
-
end
|
39
|
+
param_data = input_schema.dig("properties", key)
|
40
|
+
|
41
|
+
param = if param_data.key?("oneOf") || param_data.key?("anyOf") || param_data.key?("allOf")
|
42
|
+
process_union_parameter(key, param_data)
|
43
|
+
else
|
44
|
+
process_parameter(key, param_data)
|
45
|
+
end
|
49
46
|
|
50
47
|
params[key] = param
|
51
48
|
end
|
@@ -53,15 +50,63 @@ module RubyLLM
|
|
53
50
|
params
|
54
51
|
end
|
55
52
|
|
53
|
+
def process_union_parameter(key, param_data)
|
54
|
+
union_type = param_data.keys.first
|
55
|
+
param = RubyLLM::MCP::Parameter.new(
|
56
|
+
key,
|
57
|
+
type: :union,
|
58
|
+
union_type: union_type
|
59
|
+
)
|
60
|
+
|
61
|
+
param.properties = param_data[union_type].map do |value|
|
62
|
+
process_parameter(key, value, lifted_type: param_data["type"])
|
63
|
+
end.compact
|
64
|
+
|
65
|
+
param
|
66
|
+
end
|
67
|
+
|
68
|
+
def process_parameter(key, param_data, lifted_type: nil)
|
69
|
+
param = RubyLLM::MCP::Parameter.new(
|
70
|
+
key,
|
71
|
+
type: param_data["type"] || lifted_type,
|
72
|
+
desc: param_data["description"],
|
73
|
+
required: param_data["required"]
|
74
|
+
)
|
75
|
+
|
76
|
+
if param.type == :array
|
77
|
+
items = param_data["items"]
|
78
|
+
param.items = items
|
79
|
+
if items.key?("properties")
|
80
|
+
param.properties = create_parameters(items)
|
81
|
+
end
|
82
|
+
if param_data.key?("enum")
|
83
|
+
param.enum = param_data["enum"]
|
84
|
+
end
|
85
|
+
elsif param.type == :object
|
86
|
+
if param_data.key?("properties")
|
87
|
+
param.properties = create_parameters(param_data)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
param
|
92
|
+
end
|
93
|
+
|
56
94
|
def create_content_for_message(content)
|
57
95
|
case content["type"]
|
58
96
|
when "text"
|
59
97
|
MCP::Content.new(text: content["text"])
|
60
98
|
when "image", "audio"
|
61
|
-
attachment = MCP::Attachment.new(content["
|
99
|
+
attachment = MCP::Attachment.new(content["data"], content["mimeType"])
|
62
100
|
MCP::Content.new(text: nil, attachments: [attachment])
|
63
101
|
when "resource"
|
64
|
-
|
102
|
+
resource_data = {
|
103
|
+
"name" => name,
|
104
|
+
"description" => description,
|
105
|
+
"uri" => content.dig("resource", "uri"),
|
106
|
+
"content" => content["resource"]
|
107
|
+
}
|
108
|
+
|
109
|
+
resource = Resource.new(mcp_client, resource_data)
|
65
110
|
resource.to_content
|
66
111
|
end
|
67
112
|
end
|
@@ -40,8 +40,7 @@ module RubyLLM
|
|
40
40
|
start_sse_listener
|
41
41
|
end
|
42
42
|
|
43
|
-
# rubocop:disable Metrics/MethodLength
|
44
|
-
def request(body, add_id: true, wait_for_response: true)
|
43
|
+
def request(body, add_id: true, wait_for_response: true) # rubocop:disable Metrics/MethodLength
|
45
44
|
# Generate a unique request ID
|
46
45
|
if add_id
|
47
46
|
@id_mutex.synchronize { @id_counter += 1 }
|
@@ -60,7 +59,7 @@ module RubyLLM
|
|
60
59
|
# Send the request using Faraday
|
61
60
|
begin
|
62
61
|
conn = Faraday.new do |f|
|
63
|
-
f.options.timeout =
|
62
|
+
f.options.timeout = @request_timeout / 1000
|
64
63
|
f.options.open_timeout = 5
|
65
64
|
end
|
66
65
|
|
@@ -83,15 +82,20 @@ module RubyLLM
|
|
83
82
|
return unless wait_for_response
|
84
83
|
|
85
84
|
begin
|
86
|
-
Timeout.timeout(
|
85
|
+
Timeout.timeout(@request_timeout / 1000) do
|
87
86
|
response_queue.pop
|
88
87
|
end
|
89
88
|
rescue Timeout::Error
|
90
89
|
@pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
|
91
|
-
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
90
|
+
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
91
|
+
message: "Request timed out after #{@request_timeout / 1000} seconds"
|
92
|
+
)
|
92
93
|
end
|
93
94
|
end
|
94
|
-
|
95
|
+
|
96
|
+
def alive?
|
97
|
+
@running
|
98
|
+
end
|
95
99
|
|
96
100
|
def close
|
97
101
|
@running = false
|
@@ -11,7 +11,8 @@ module RubyLLM
|
|
11
11
|
class Stdio
|
12
12
|
attr_reader :command, :stdin, :stdout, :stderr, :id
|
13
13
|
|
14
|
-
def initialize(command, args: [], env: {})
|
14
|
+
def initialize(command, request_timeout:, args: [], env: {})
|
15
|
+
@request_timeout = request_timeout
|
15
16
|
@command = command
|
16
17
|
@args = args
|
17
18
|
@env = env || {}
|
@@ -23,6 +24,7 @@ module RubyLLM
|
|
23
24
|
@pending_mutex = Mutex.new
|
24
25
|
@running = true
|
25
26
|
@reader_thread = nil
|
27
|
+
@stderr_thread = nil
|
26
28
|
|
27
29
|
start_process
|
28
30
|
end
|
@@ -53,16 +55,22 @@ module RubyLLM
|
|
53
55
|
return unless wait_for_response
|
54
56
|
|
55
57
|
begin
|
56
|
-
Timeout.timeout(
|
58
|
+
Timeout.timeout(@request_timeout / 1000) do
|
57
59
|
response_queue.pop
|
58
60
|
end
|
59
61
|
rescue Timeout::Error
|
60
62
|
@pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
|
61
|
-
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
63
|
+
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
64
|
+
message: "Request timed out after #{@request_timeout / 1000} seconds"
|
65
|
+
)
|
62
66
|
end
|
63
67
|
end
|
64
68
|
|
65
|
-
def
|
69
|
+
def alive?
|
70
|
+
@running
|
71
|
+
end
|
72
|
+
|
73
|
+
def close # rubocop:disable Metrics/MethodLength
|
66
74
|
@running = false
|
67
75
|
|
68
76
|
begin
|
@@ -82,6 +90,7 @@ module RubyLLM
|
|
82
90
|
rescue StandardError
|
83
91
|
nil
|
84
92
|
end
|
93
|
+
|
85
94
|
begin
|
86
95
|
@stderr&.close
|
87
96
|
rescue StandardError
|
@@ -94,11 +103,18 @@ module RubyLLM
|
|
94
103
|
nil
|
95
104
|
end
|
96
105
|
|
106
|
+
begin
|
107
|
+
@stderr_thread&.join(1)
|
108
|
+
rescue StandardError
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
|
97
112
|
@stdin = nil
|
98
113
|
@stdout = nil
|
99
114
|
@stderr = nil
|
100
115
|
@wait_thread = nil
|
101
116
|
@reader_thread = nil
|
117
|
+
@stderr_thread = nil
|
102
118
|
end
|
103
119
|
|
104
120
|
private
|
@@ -109,10 +125,11 @@ module RubyLLM
|
|
109
125
|
@stdin, @stdout, @stderr, @wait_thread = if @env.empty?
|
110
126
|
Open3.popen3(@command, *@args)
|
111
127
|
else
|
112
|
-
Open3.popen3(
|
128
|
+
Open3.popen3(@env, @command, *@args)
|
113
129
|
end
|
114
130
|
|
115
131
|
start_reader_thread
|
132
|
+
start_stderr_thread
|
116
133
|
end
|
117
134
|
|
118
135
|
def restart_process
|
@@ -148,12 +165,34 @@ module RubyLLM
|
|
148
165
|
@reader_thread.abort_on_exception = true
|
149
166
|
end
|
150
167
|
|
151
|
-
def
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
168
|
+
def start_stderr_thread
|
169
|
+
@stderr_thread = Thread.new do
|
170
|
+
while @running
|
171
|
+
begin
|
172
|
+
if @stderr.closed? || @wait_thread.nil? || !@wait_thread.alive?
|
173
|
+
sleep 1
|
174
|
+
next
|
175
|
+
end
|
176
|
+
|
177
|
+
line = @stderr.gets
|
178
|
+
next unless line && !line.strip.empty?
|
179
|
+
|
180
|
+
puts "STDERR: #{line.strip}"
|
181
|
+
rescue IOError, Errno::EPIPE => e
|
182
|
+
puts "Stderr reader error: #{e.message}"
|
183
|
+
sleep 1
|
184
|
+
rescue StandardError => e
|
185
|
+
puts "Error in stderr thread: #{e.message}"
|
186
|
+
sleep 1
|
187
|
+
end
|
188
|
+
end
|
156
189
|
end
|
190
|
+
|
191
|
+
@stderr_thread.abort_on_exception = true
|
192
|
+
end
|
193
|
+
|
194
|
+
def process_response(line)
|
195
|
+
response = JSON.parse(line)
|
157
196
|
request_id = response["id"]&.to_s
|
158
197
|
|
159
198
|
@pending_mutex.synchronize do
|
@@ -162,10 +201,8 @@ module RubyLLM
|
|
162
201
|
response_queue&.push(response)
|
163
202
|
end
|
164
203
|
end
|
165
|
-
|
166
|
-
|
167
|
-
def environment_string
|
168
|
-
@env.map { |key, value| "#{key}=#{value}" }.join(" ")
|
204
|
+
rescue JSON::ParserError => e
|
205
|
+
RubyLLM.logger.error("Error parsing response as JSON: #{e.message}\nRaw response: #{line}")
|
169
206
|
end
|
170
207
|
end
|
171
208
|
end
|
@@ -12,8 +12,9 @@ module RubyLLM
|
|
12
12
|
class Streamable
|
13
13
|
attr_reader :headers, :id, :session_id
|
14
14
|
|
15
|
-
def initialize(url, headers: {})
|
15
|
+
def initialize(url, request_timeout:, headers: {})
|
16
16
|
@url = url
|
17
|
+
@request_timeout = request_timeout
|
17
18
|
@client_id = SecureRandom.uuid
|
18
19
|
@session_id = nil
|
19
20
|
@base_headers = headers.merge({
|
@@ -55,6 +56,10 @@ module RubyLLM
|
|
55
56
|
handle_response(response, request_id, response_queue, wait_for_response)
|
56
57
|
end
|
57
58
|
|
59
|
+
def alive?
|
60
|
+
@running
|
61
|
+
end
|
62
|
+
|
58
63
|
def close
|
59
64
|
@running = false
|
60
65
|
@sse_mutex.synchronize do
|
@@ -83,7 +88,7 @@ module RubyLLM
|
|
83
88
|
|
84
89
|
def create_connection
|
85
90
|
Faraday.new(url: @url) do |f|
|
86
|
-
f.options.timeout =
|
91
|
+
f.options.timeout = @request_timeout / 1000
|
87
92
|
f.options.open_timeout = 10
|
88
93
|
end
|
89
94
|
end
|
@@ -279,12 +284,14 @@ module RubyLLM
|
|
279
284
|
end
|
280
285
|
|
281
286
|
def wait_for_response_with_timeout(request_id, response_queue)
|
282
|
-
Timeout.timeout(
|
287
|
+
Timeout.timeout(@request_timeout / 1000) do
|
283
288
|
response_queue.pop
|
284
289
|
end
|
285
290
|
rescue Timeout::Error
|
286
291
|
@pending_mutex.synchronize { @pending_requests.delete(request_id.to_s) }
|
287
|
-
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
292
|
+
raise RubyLLM::MCP::Errors::TimeoutError.new(
|
293
|
+
message: "Request timed out after #{@request_timeout / 1000} seconds"
|
294
|
+
)
|
288
295
|
end
|
289
296
|
end
|
290
297
|
end
|