raix 0.9.0 → 0.9.1
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/CHANGELOG.md +11 -0
- data/Gemfile.lock +1 -1
- data/lib/mcp/sse_client.rb +12 -2
- data/lib/mcp/stdio_client.rb +25 -5
- data/lib/raix/mcp.rb +96 -1
- data/lib/raix/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0868c4163ea58511e3c0dfc66b2b22a5cb2dbb16a7575746b36cd70af4777137'
|
4
|
+
data.tar.gz: 4e396262603a787dc58817e9fbb536264f25cdb9462bcbe035d675e59cb179ad
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8ae8a80e0e45dfba22290aefe27c39fd914f9046228dac33af1bc631e55a9eff73e38500c12c9eec6ba7bff67a5a16d51438ccf80cbcf423b070125dcd5710c4
|
7
|
+
data.tar.gz: ffb45352168291e28041b787fa8c2bf61c032d5bd5c59fb5a5653a602d6b1bb5ef282475ce7d828722113df797369127a1a4cdeec5da1c53ad887f1665b0385a
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
## [0.9.1] - 2025-05-30
|
2
|
+
### Added
|
3
|
+
- **MCP Type Coercion** - Automatic type conversion for MCP tool arguments based on JSON schema
|
4
|
+
- Supports integer, number, boolean, array, and object types
|
5
|
+
- Handles nested objects and arrays of objects with proper coercion
|
6
|
+
- Gracefully handles invalid JSON and type mismatches
|
7
|
+
- **MCP Image Support** - MCP tools can now return image content as structured JSON
|
8
|
+
|
9
|
+
### Fixed
|
10
|
+
- Fixed handling of nil values in MCP argument coercion
|
11
|
+
|
1
12
|
## [0.9.0] - 2025-05-30
|
2
13
|
### Added
|
3
14
|
- **MCP (Model Context Protocol) Support**
|
data/Gemfile.lock
CHANGED
data/lib/mcp/sse_client.rb
CHANGED
@@ -42,7 +42,8 @@ module Raix
|
|
42
42
|
end
|
43
43
|
end
|
44
44
|
|
45
|
-
# Executes a tool with given arguments
|
45
|
+
# Executes a tool with given arguments.
|
46
|
+
# Returns text content directly, or JSON-encoded data for other content types.
|
46
47
|
def call_tool(name, **arguments)
|
47
48
|
request_id = SecureRandom.uuid
|
48
49
|
send_json_rpc(request_id, "tools/call", name:, arguments:)
|
@@ -56,9 +57,18 @@ module Raix
|
|
56
57
|
first_item = content.first
|
57
58
|
case first_item
|
58
59
|
when Hash
|
59
|
-
|
60
|
+
case first_item[:type]
|
61
|
+
when "text"
|
60
62
|
first_item[:text]
|
63
|
+
when "image"
|
64
|
+
# Return a structured response for images
|
65
|
+
{
|
66
|
+
type: "image",
|
67
|
+
data: first_item[:data],
|
68
|
+
mime_type: first_item[:mimeType] || "image/png"
|
69
|
+
}.to_json
|
61
70
|
else
|
71
|
+
# For any other type, return the item as JSON
|
62
72
|
first_item.to_json
|
63
73
|
end
|
64
74
|
else
|
data/lib/mcp/stdio_client.rb
CHANGED
@@ -21,14 +21,34 @@ module Raix
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
# Executes a tool with given arguments
|
24
|
+
# Executes a tool with given arguments.
|
25
|
+
# Returns text content directly, or JSON-encoded data for other content types.
|
25
26
|
def call_tool(name, **arguments)
|
26
27
|
result = call("tools/call", name:, arguments:)
|
27
|
-
|
28
|
-
|
29
|
-
end
|
28
|
+
content = result["content"]
|
29
|
+
return "" if content.nil? || content.empty?
|
30
30
|
|
31
|
-
|
31
|
+
# Handle different content formats
|
32
|
+
first_item = content.first
|
33
|
+
case first_item
|
34
|
+
when Hash
|
35
|
+
case first_item["type"]
|
36
|
+
when "text"
|
37
|
+
first_item["text"]
|
38
|
+
when "image"
|
39
|
+
# Return a structured response for images
|
40
|
+
{
|
41
|
+
type: "image",
|
42
|
+
data: first_item["data"],
|
43
|
+
mime_type: first_item["mimeType"] || "image/png"
|
44
|
+
}.to_json
|
45
|
+
else
|
46
|
+
# For any other type, return the item as JSON
|
47
|
+
first_item.to_json
|
48
|
+
end
|
49
|
+
else
|
50
|
+
first_item.to_s
|
51
|
+
end
|
32
52
|
end
|
33
53
|
|
34
54
|
# Closes the connection to the server.
|
data/lib/raix/mcp.rb
CHANGED
@@ -115,11 +115,19 @@ module Raix
|
|
115
115
|
# Required by OpenAI
|
116
116
|
latest_definition[:parameters][:properties] ||= {}
|
117
117
|
|
118
|
+
# Store the schema for type coercion
|
119
|
+
tool_schemas = @tool_schemas ||= {}
|
120
|
+
tool_schemas[local_name] = input_schema
|
121
|
+
|
118
122
|
# --- define an instance method that proxies to the server
|
119
123
|
define_method(local_name) do |arguments, _cache|
|
120
124
|
arguments ||= {}
|
121
125
|
|
122
|
-
|
126
|
+
# Coerce argument types based on the input schema
|
127
|
+
stored_schema = self.class.instance_variable_get(:@tool_schemas)&.dig(local_name)
|
128
|
+
coerced_arguments = coerce_arguments(arguments, stored_schema)
|
129
|
+
|
130
|
+
content_text = client.call_tool(remote_name, **coerced_arguments)
|
123
131
|
call_id = SecureRandom.uuid
|
124
132
|
|
125
133
|
# Mirror FunctionDispatch transcript behaviour
|
@@ -157,5 +165,92 @@ module Raix
|
|
157
165
|
@mcp_servers[client.unique_key] = { tools: filtered_tools, client: }
|
158
166
|
end
|
159
167
|
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
# Coerce argument types based on the JSON schema
|
172
|
+
def coerce_arguments(arguments, schema)
|
173
|
+
return arguments unless schema.is_a?(Hash) && schema["properties"].is_a?(Hash)
|
174
|
+
|
175
|
+
coerced = {}
|
176
|
+
schema["properties"].each do |key, prop_schema|
|
177
|
+
value = if arguments.key?(key)
|
178
|
+
arguments[key]
|
179
|
+
elsif arguments.key?(key.to_sym)
|
180
|
+
arguments[key.to_sym]
|
181
|
+
end
|
182
|
+
next if value.nil?
|
183
|
+
|
184
|
+
coerced[key] = coerce_value(value, prop_schema)
|
185
|
+
end
|
186
|
+
|
187
|
+
# Include any additional arguments not in the schema
|
188
|
+
arguments.each do |key, value|
|
189
|
+
key_str = key.to_s
|
190
|
+
coerced[key_str] = value unless coerced.key?(key_str)
|
191
|
+
end
|
192
|
+
|
193
|
+
coerced.with_indifferent_access
|
194
|
+
end
|
195
|
+
|
196
|
+
# Coerce a single value based on its schema
|
197
|
+
def coerce_value(value, schema)
|
198
|
+
return value unless schema.is_a?(Hash)
|
199
|
+
|
200
|
+
case schema["type"]
|
201
|
+
when "number", "integer"
|
202
|
+
if value.is_a?(String) && value.match?(/\A-?\d+(\.\d+)?\z/)
|
203
|
+
schema["type"] == "integer" ? value.to_i : value.to_f
|
204
|
+
else
|
205
|
+
value
|
206
|
+
end
|
207
|
+
when "boolean"
|
208
|
+
case value
|
209
|
+
when "true", true then true
|
210
|
+
when "false", false then false
|
211
|
+
else value
|
212
|
+
end
|
213
|
+
when "array"
|
214
|
+
array_value = begin
|
215
|
+
value.is_a?(String) ? JSON.parse(value) : value
|
216
|
+
rescue JSON::ParserError
|
217
|
+
value
|
218
|
+
end
|
219
|
+
|
220
|
+
# If there's an items schema, coerce each element
|
221
|
+
if array_value.is_a?(Array) && schema["items"]
|
222
|
+
array_value.map { |item| coerce_value(item, schema["items"]) }
|
223
|
+
else
|
224
|
+
array_value
|
225
|
+
end
|
226
|
+
when "object"
|
227
|
+
object_value = begin
|
228
|
+
value.is_a?(String) ? JSON.parse(value) : value
|
229
|
+
rescue JSON::ParserError
|
230
|
+
value
|
231
|
+
end
|
232
|
+
|
233
|
+
# If there are properties defined, coerce them recursively
|
234
|
+
if object_value.is_a?(Hash) && schema["properties"]
|
235
|
+
coerced_object = {}
|
236
|
+
schema["properties"].each do |prop_key, prop_schema|
|
237
|
+
prop_value = object_value[prop_key] || object_value[prop_key.to_sym]
|
238
|
+
coerced_object[prop_key] = coerce_value(prop_value, prop_schema) unless prop_value.nil?
|
239
|
+
end
|
240
|
+
|
241
|
+
# Include any additional properties not in the schema
|
242
|
+
object_value.each do |obj_key, obj_value|
|
243
|
+
obj_key_str = obj_key.to_s
|
244
|
+
coerced_object[obj_key_str] = obj_value unless coerced_object.key?(obj_key_str)
|
245
|
+
end
|
246
|
+
|
247
|
+
coerced_object
|
248
|
+
else
|
249
|
+
object_value
|
250
|
+
end
|
251
|
+
else
|
252
|
+
value
|
253
|
+
end
|
254
|
+
end
|
160
255
|
end
|
161
256
|
end
|
data/lib/raix/version.rb
CHANGED