raix 0.7.3 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90b0072e3af33e82ccfb040554e9645920e34b4477457ba79335582c9325a7bd
4
- data.tar.gz: beeb6e0b98b473597d2191b5005fc89cb57fe69633cfe1e8927a6c1c0f61048f
3
+ metadata.gz: 2de11b289a7c245c49e865735e1abc67ff671f02a9f23fb89d262d3ff83e6157
4
+ data.tar.gz: f8cd493f6f018ddd6b981f944786635b6800ca9beac4480e0a4c3c08bb7b8392
5
5
  SHA512:
6
- metadata.gz: cbc28460d07263899c0e45be8ccfd3fa522b3162ad2a53d640c1ff0cccb6c7d96f7ab4bef2afa809d47733a98a4a502602b78a6c53918e676955da9d88220f69
7
- data.tar.gz: d5e4f7dedaeb3ab2de64168f17a9de920a18894d5acfff22c0e00b870bdf96c77478130a61d47f82f7cc01a566b1cbc95f41fb43241cb3ece24c743b80993fed
6
+ metadata.gz: 4d26f6ffad99abb7eaf6c8ef072393d75625e4762206d0d1f94c811e9d44747fa2e9048d5a8951484ded2861ecd5ebac69ea39bf6163432a411e2f9e427b0533
7
+ data.tar.gz: 893d062ab1f23b6c6a47dc715a7c98a3886dbe092645642e74bdb97ad80b2b4f637305b95acc6e6a8984cfe6defcdf020410233f5f818cba16bbbd0c5f49b5ed
data/.rubocop.yml CHANGED
@@ -35,3 +35,9 @@ Metrics/PerceivedComplexity:
35
35
 
36
36
  Metrics/ParameterLists:
37
37
  Enabled: false
38
+
39
+ Style/FrozenStringLiteralComment:
40
+ Enabled: false
41
+
42
+ Style/MultilineBlockChain:
43
+ Enabled: false
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.2.2
1
+ 3.4.2
data/CHANGELOG.md CHANGED
@@ -1,4 +1,23 @@
1
+ ## [0.8.1] - 2025-04-24
2
+ Added ability to filter tool functions (or disable completely) when calling `chat_completion`. Thanks to @parruda for the contribution.
3
+
4
+ ## [0.8.0] - 2025-04-23
5
+ ### Added
6
+ * **MCP integration (Experimental)** — new `Raix::MCP` concern and `mcp` DSL for declaring remote MCP servers.
7
+ * Automatically fetches `tools/list`, registers remote tools as OpenAI‑compatible function schemas, and defines proxy methods that forward `tools/call`.
8
+ * `ChatCompletion#tools` now returns remote MCP tools alongside local `function` declarations.
9
+
10
+ ### Changed
11
+ * `lib/raix.rb` now requires `raix/mcp` so the concern is auto‑loaded.
12
+
13
+ ### Fixed
14
+ * Internal transcript handling spec expectations updated.
15
+
16
+ ### Specs
17
+ * Added `spec/raix/mcp_spec.rb` with comprehensive stubs for tools discovery & call flow.
18
+
1
19
  ## [0.7.3] - 2025-04-23
20
+ - commit function call and result to transcript in one operation for thread safety
2
21
 
3
22
  ## [0.7.2] - 2025-04-19
4
23
  - adds support for `messages` parameter in `chat_completion` to override the transcript
data/Gemfile CHANGED
@@ -5,11 +5,6 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in raix-rails.gemspec
6
6
  gemspec
7
7
 
8
- gem "activesupport", ">= 6.0"
9
- gem "faraday-retry"
10
- gem "open_router", "~> 0.3"
11
- gem "ruby-openai", "~> 7.0"
12
-
13
8
  group :development do
14
9
  gem "dotenv", ">= 2"
15
10
  gem "guard"
data/Gemfile.lock CHANGED
@@ -1,10 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- raix (0.7.3)
4
+ raix (0.8.1)
5
5
  activesupport (>= 6.0)
6
+ faraday-retry (~> 2.0)
6
7
  open_router (~> 0.2)
7
- ruby-openai (~> 7.0)
8
+ ruby-openai (~> 7)
8
9
 
9
10
  GEM
10
11
  remote: https://rubygems.org/
@@ -43,10 +44,10 @@ GEM
43
44
  multipart-post (~> 2)
44
45
  faraday-net_http (3.1.0)
45
46
  net-http
46
- faraday-retry (2.2.1)
47
+ faraday-retry (2.3.1)
47
48
  faraday (~> 2.0)
48
- ffi (1.17.0-arm64-darwin)
49
- ffi (1.17.0-x86_64-linux-gnu)
49
+ ffi (1.17.2-arm64-darwin)
50
+ ffi (1.17.2-x86_64-linux-gnu)
50
51
  formatador (1.1.0)
51
52
  guard (2.18.1)
52
53
  formatador (>= 0.2.4)
@@ -84,9 +85,9 @@ GEM
84
85
  net-http (0.4.1)
85
86
  uri
86
87
  netrc (0.11.0)
87
- nokogiri (1.16.6-arm64-darwin)
88
+ nokogiri (1.18.8-arm64-darwin)
88
89
  racc (~> 1.4)
89
- nokogiri (1.16.6-x86_64-linux)
90
+ nokogiri (1.18.8-x86_64-linux-gnu)
90
91
  racc (~> 1.4)
91
92
  notiffany (0.1.3)
92
93
  nenv (~> 0.1)
@@ -216,18 +217,14 @@ PLATFORMS
216
217
  x86_64-linux
217
218
 
218
219
  DEPENDENCIES
219
- activesupport (>= 6.0)
220
220
  dotenv (>= 2)
221
- faraday-retry
222
221
  guard
223
222
  guard-rspec
224
- open_router (~> 0.3)
225
223
  pry (>= 0.14)
226
224
  raix!
227
225
  rake (~> 13.0)
228
226
  rspec (~> 3.0)
229
227
  rubocop (~> 1.21)
230
- ruby-openai (~> 7.0)
231
228
  solargraph-rails (~> 0.2.0.pre)
232
229
  sorbet
233
230
  tapioca
data/README.md CHANGED
@@ -134,6 +134,41 @@ end
134
134
 
135
135
  Note that for security reasons, dispatching functions only works with functions implemented using `Raix::FunctionDispatch#function` or directly on the class.
136
136
 
137
+ #### Tool Filtering
138
+
139
+ You can control which tools are available to the AI on a given chat completion request using the `tools` parameter in the `chat_completion` method:
140
+
141
+ ```ruby
142
+ class WeatherAndTime
143
+ include Raix::ChatCompletion
144
+ include Raix::FunctionDispatch
145
+
146
+ function :check_weather, "Check the weather for a location", location: { type: "string" } do |arguments|
147
+ "The weather in #{arguments[:location]} is sunny"
148
+ end
149
+
150
+ function :get_time, "Get the current time" do |_arguments|
151
+ "The time is 12:00 PM"
152
+ end
153
+ end
154
+
155
+ weather = WeatherAndTime.new
156
+
157
+ # Don't pass any tools to the LLM
158
+ weather.chat_completion(tools: false)
159
+
160
+ # Only pass specific tools to the LLM
161
+ weather.chat_completion(tools: [:check_weather])
162
+
163
+ # Pass all declared tools (default behavior)
164
+ weather.chat_completion
165
+ ```
166
+
167
+ The `tools` parameter accepts three types of values:
168
+ - `false`: No tools are passed to the LLM
169
+ - An array of symbols: Only the specified tools are passed (raises `Raix::UndeclaredToolError` if any tool is not declared)
170
+ - Not provided: All declared tools are passed (default behavior)
171
+
137
172
  #### Multiple Tool Calls
138
173
 
139
174
  Some AI models (like GPT-4) can make multiple tool calls in a single response. When this happens, Raix will automatically handle all the function calls sequentially.
@@ -452,6 +487,50 @@ question.ask("Any question")
452
487
  # => RuntimeError: Please define a yes and/or no block
453
488
  ```
454
489
 
490
+ ## Model Context Protocol (Experimental)
491
+
492
+ The `Raix::MCP` module provides integration with the Model Context Protocol, allowing you to connect your Raix-powered application to remote MCP servers. This feature is currently **experimental**.
493
+
494
+ ### Usage
495
+
496
+ Include the `Raix::MCP` module in your class and declare MCP servers using the `mcp` DSL:
497
+
498
+ ```ruby
499
+ class McpConsumer
500
+ include Raix::ChatCompletion
501
+ include Raix::FunctionDispatch
502
+ include Raix::MCP
503
+
504
+ mcp "https://your-mcp-server.example.com/sse"
505
+ end
506
+ ```
507
+
508
+ ### Features
509
+
510
+ - Automatically fetches available tools from the remote MCP server using `tools/list`
511
+ - Registers remote tools as OpenAI-compatible function schemas
512
+ - Defines proxy methods that forward requests to the remote server via `tools/call`
513
+ - Seamlessly integrates with the existing `FunctionDispatch` workflow
514
+ - Handles transcript recording to maintain consistent conversation history
515
+
516
+ ### Filtering Tools
517
+
518
+ You can filter which remote tools to include:
519
+
520
+ ```ruby
521
+ class FilteredMcpConsumer
522
+ include Raix::ChatCompletion
523
+ include Raix::FunctionDispatch
524
+ include Raix::MCP
525
+
526
+ # Only include specific tools
527
+ mcp "https://server.example.com/sse", only: [:tool_one, :tool_two]
528
+
529
+ # Or exclude specific tools
530
+ mcp "https://server.example.com/sse", except: [:tool_to_exclude]
531
+ end
532
+ ```
533
+
455
534
  ## Response Format (Experimental)
456
535
 
457
536
  The `ResponseFormat` class provides a way to declare a JSON schema for the response format of an AI chat completion. It's particularly useful when you need structured responses from AI models, ensuring the output conforms to your application's requirements.
@@ -9,6 +9,8 @@ require "openai"
9
9
  require_relative "message_adapters/base"
10
10
 
11
11
  module Raix
12
+ class UndeclaredToolError < StandardError; end
13
+
12
14
  # The `ChatCompletion`` module is a Rails concern that provides a way to interact
13
15
  # with the OpenRouter Chat Completion API via its client. The module includes a few
14
16
  # methods that allow you to build a transcript of messages and then send them to
@@ -44,8 +46,9 @@ module Raix
44
46
  # @option params [Boolean] :openai (false) Whether to use OpenAI's API instead of OpenRouter's.
45
47
  # @option params [Boolean] :raw (false) Whether to return the raw response or dig the text content.
46
48
  # @option params [Array] :messages (nil) An array of messages to use instead of the transcript.
49
+ # @option tools [Array|false] :tools (nil) Tools to pass to the LLM. If false, no tools are passed. If an array, only declared tools in the array are passed.
47
50
  # @return [String|Hash] The completed chat response.
48
- def chat_completion(params: {}, loop: false, json: false, raw: false, openai: false, save_response: true, messages: nil)
51
+ def chat_completion(params: {}, loop: false, json: false, raw: false, openai: false, save_response: true, messages: nil, tools: nil)
49
52
  # set params to default values if not provided
50
53
  params[:cache_at] ||= cache_at.presence
51
54
  params[:frequency_penalty] ||= frequency_penalty.presence
@@ -63,7 +66,13 @@ module Raix
63
66
  params[:stop] ||= stop.presence
64
67
  params[:temperature] ||= temperature.presence || Raix.configuration.temperature
65
68
  params[:tool_choice] ||= tool_choice.presence
66
- params[:tools] ||= tools.presence
69
+ params[:tools] = if tools == false
70
+ nil
71
+ elsif tools.is_a?(Array)
72
+ filtered_tools(tools)
73
+ else
74
+ self.tools.presence
75
+ end
67
76
  params[:top_a] ||= top_a.presence
68
77
  params[:top_k] ||= top_k.presence
69
78
  params[:top_logprobs] ||= top_logprobs.presence
@@ -182,6 +191,18 @@ module Raix
182
191
 
183
192
  private
184
193
 
194
+ def filtered_tools(tool_names)
195
+ return nil if tool_names.blank?
196
+
197
+ requested_tools = tool_names.map(&:to_sym)
198
+ available_tool_names = tools.map { |tool| tool.dig(:function, :name).to_sym }
199
+
200
+ undeclared_tools = requested_tools - available_tool_names
201
+ raise UndeclaredToolError, "Undeclared tools: #{undeclared_tools.join(", ")}" if undeclared_tools.any?
202
+
203
+ tools.select { |tool| requested_tools.include?(tool.dig(:function, :name).to_sym) }
204
+ end
205
+
185
206
  def openai_request(params:, model:, messages:)
186
207
  if params[:prediction]
187
208
  params.delete(:max_completion_tokens)
@@ -94,10 +94,7 @@ module Raix
94
94
  end
95
95
 
96
96
  def chat_completion(**chat_completion_args)
97
- raise "No functions defined" if self.class.functions.blank?
98
-
99
97
  self.chat_completion_args = chat_completion_args
100
-
101
98
  super
102
99
  end
103
100
 
@@ -109,6 +106,8 @@ module Raix
109
106
  end
110
107
 
111
108
  def tools
109
+ return [] unless self.class.functions
110
+
112
111
  self.class.functions.map { |function| { type: "function", function: } }
113
112
  end
114
113
  end
data/lib/raix/mcp.rb ADDED
@@ -0,0 +1,337 @@
1
+ # Simple integration layer that lets Raix classes declare an MCP server
2
+ # with a single DSL call:
3
+ #
4
+ # mcp "https://my-server.example.com/sse"
5
+ #
6
+ # The concern fetches the remote server's tool list (via JSON‑RPC 2.0
7
+ # `tools/list`) and exposes each remote tool as if it were an inline
8
+ # `function` declared with Raix::FunctionDispatch. When the tool is
9
+ # invoked by the model, the generated instance method forwards the
10
+ # request to the remote server using `tools/call`, captures the result,
11
+ # and appends the appropriate messages to the transcript so that the
12
+ # conversation history stays consistent.
13
+
14
+ require "active_support/concern"
15
+ require "active_support/inflector"
16
+ require "securerandom"
17
+ require "faraday"
18
+ require "uri"
19
+ require "json"
20
+
21
+ module Raix
22
+ # Model Context Protocol integration for Raix
23
+ #
24
+ # Allows declaring MCP servers with a simple DSL that automatically:
25
+ # - Queries tools from the remote server
26
+ # - Exposes each tool as a function callable by LLMs
27
+ # - Handles transcript recording and response processing
28
+ module MCP
29
+ extend ActiveSupport::Concern
30
+
31
+ JSONRPC_VERSION = "2.0".freeze
32
+ PROTOCOL_VERSION = "2024-11-05".freeze # Current supported protocol version
33
+ CONNECTION_TIMEOUT = 10
34
+ OPEN_TIMEOUT = 30
35
+
36
+ class_methods do
37
+ # Declare an MCP server by URL.
38
+ #
39
+ # mcp "https://server.example.com/sse"
40
+ #
41
+ # This will automatically:
42
+ # • query `tools/list` on the server
43
+ # • register each remote tool with FunctionDispatch so that the
44
+ # OpenAI / OpenRouter request body includes its JSON‑Schema
45
+ # • define an instance method for each tool that forwards the
46
+ # call to the server and appends the proper messages to the
47
+ # transcript.
48
+ # NOTE TO SELF: NEVER MOCK SERVER RESPONSES! THIS MUST WORK WITH REAL SERVERS!
49
+ def mcp(url, only: nil, except: nil)
50
+ @mcp_servers ||= {}
51
+
52
+ return if @mcp_servers.key?(url) # avoid duplicate definitions
53
+
54
+ # Connect and initialize the SSE endpoint
55
+
56
+ result = Thread::Queue.new
57
+ Thread.new do
58
+ establish_sse_connection(url, result:)
59
+ end
60
+ tools = result.pop
61
+
62
+ if tools.empty?
63
+ puts "[MCP DEBUG] No tools found from MCP server at #{url}"
64
+ return nil
65
+ end
66
+
67
+ # 3. Register each tool so ChatCompletion#tools picks them up
68
+ # Apply filters
69
+ filtered_tools = if only.present?
70
+ only_symbols = Array(only).map(&:to_sym)
71
+ tools.select { |tool| only_symbols.include?(tool["name"].to_sym) }
72
+ elsif except.present?
73
+ except_symbols = Array(except).map(&:to_sym)
74
+ tools.reject { |tool| except_symbols.include?(tool["name"].to_sym) }
75
+ else
76
+ tools
77
+ end
78
+
79
+ # Ensure FunctionDispatch is included in the class
80
+ # Explicit include in the class context
81
+ include FunctionDispatch unless included_modules.include?(FunctionDispatch)
82
+ puts "[MCP DEBUG] FunctionDispatch included in #{name}"
83
+
84
+ filtered_tools.each do |tool|
85
+ remote_name = tool[:name]
86
+ # TODO: Revisit later whether this much context is needed in the function name
87
+ local_name = "#{url.parameterize.underscore}_#{remote_name}".gsub("https_", "").to_sym
88
+
89
+ description = tool["description"]
90
+ input_schema = tool["inputSchema"] || {}
91
+
92
+ # --- register with FunctionDispatch (adds to .functions)
93
+ function(local_name, description, **{}) # placeholder parameters replaced next
94
+ latest_definition = functions.last
95
+ latest_definition[:parameters] = input_schema.deep_symbolize_keys if input_schema.present?
96
+
97
+ # --- define an instance method that proxies to the server
98
+ define_method(local_name) do |**arguments|
99
+ arguments ||= {}
100
+
101
+ call_id = SecureRandom.uuid
102
+ result = Thread::Queue.new
103
+ Thread.new do
104
+ self.class.establish_sse_connection(url, name: remote_name, arguments:, result:)
105
+ end
106
+
107
+ content_item = result.pop
108
+
109
+ # Decide what to add to the transcript
110
+ content_text = if content_item.is_a?(Hash) && content_item["type"] == "text"
111
+ content_item["text"]
112
+ else
113
+ content_item.to_json
114
+ end
115
+
116
+ # Mirror FunctionDispatch transcript behaviour
117
+ transcript << [
118
+ {
119
+ role: "assistant",
120
+ content: nil,
121
+ tool_calls: [
122
+ {
123
+ id: call_id,
124
+ type: "function",
125
+ function: {
126
+ name: remote_name,
127
+ arguments: arguments.to_json
128
+ }
129
+ }
130
+ ]
131
+ },
132
+ {
133
+ role: "tool",
134
+ tool_call_id: call_id,
135
+ name: remote_name,
136
+ content: content_text
137
+ }
138
+ ]
139
+
140
+ # Continue the chat loop if requested (same semantics as FunctionDispatch)
141
+ chat_completion(**chat_completion_args) if loop
142
+
143
+ content_text
144
+ end
145
+ end
146
+
147
+ # Store the URL and tools for future use
148
+ @mcp_servers[url] = { tools: }
149
+ end
150
+
151
+ # Establishes an SSE connection to +url+ and returns the JSON‑RPC POST endpoint
152
+ # advertised by the server. The MCP specification allows two different event
153
+ # formats during initialization:
154
+ #
155
+ # 1. A generic JSON‑RPC *initialize* event (the behaviour previously
156
+ # implemented):
157
+ #
158
+ # event: message (implicit when no explicit event type is given)
159
+ # data: {"jsonrpc":"2.0","method":"initialize","params":{"endpoint_url":"https://…/rpc"}}
160
+ #
161
+ # 2. A dedicated *endpoint* event, as implemented by the reference
162
+ # TypeScript SDK and the public GitMCP server used in our test-suite:
163
+ #
164
+ # event: endpoint\n
165
+ # data: /rpc\n
166
+ #
167
+ # This method now supports **both** formats.
168
+ #
169
+ # It uses Net::HTTP directly rather than Faraday streaming because the latter
170
+ # does not consistently surface partial body reads across adapters. The
171
+ # implementation reads the response body incrementally, splitting on the
172
+ # SSE record delimiter (double newline) and processing each event until an
173
+ # endpoint is discovered (or a timeout / connection error occurs).
174
+ def establish_sse_connection(url, name: nil, arguments: {}, result: nil)
175
+ puts "[MCP DEBUG] Establishing MCP connection with URL: #{url}"
176
+
177
+ headers = {
178
+ "Accept" => "text/event-stream",
179
+ "Cache-Control" => "no-cache",
180
+ "Connection" => "keep-alive",
181
+ "MCP-Version" => PROTOCOL_VERSION
182
+ }
183
+
184
+ endpoint_url = nil
185
+ buffer = ""
186
+
187
+ connection = Faraday.new(url:) do |faraday|
188
+ faraday.options.timeout = CONNECTION_TIMEOUT
189
+ faraday.options.open_timeout = OPEN_TIMEOUT
190
+ end
191
+
192
+ connection.get do |req|
193
+ req.headers = headers
194
+ req.options.on_data = proc do |chunk, _size|
195
+ buffer << chunk
196
+
197
+ # Process complete SSE events (separated by a blank line)
198
+ while (idx = buffer.index("\n\n"))
199
+ event_text = buffer.slice!(0..idx + 1) # include delimiter
200
+ event_type, event_data = parse_sse_fields(event_text)
201
+
202
+ case event_type
203
+ when "endpoint"
204
+ # event data is expected to be a plain string with the endpoint
205
+ puts "[MCP DEBUG] Found endpoint event: #{event_data}"
206
+ endpoint_url = build_absolute_url(url, event_data)
207
+ initialize_mcp_connection(connection, endpoint_url)
208
+ when "message"
209
+ puts "[MCP DEBUG] Received message: #{event_data}"
210
+ dispatch_event(event_data, connection, endpoint_url, name, arguments, result)
211
+ else
212
+ puts "[MCP DEBUG] Unexpected event type: #{event_type} with data: #{event_data}"
213
+ end
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ # Parses an SSE *event block* (text up to the blank line delimiter) and
220
+ # returns `[event_type, data]` where *event_type* defaults to "message" when
221
+ # no explicit `event:` field is present. The *data* combines all `data:`
222
+ # lines separated by newlines, as per the SSE specification.
223
+ def parse_sse_fields(event_text)
224
+ event_type = "message"
225
+ data_lines = []
226
+
227
+ event_text.each_line do |line|
228
+ case line
229
+ when /^event:\s*(.+)$/
230
+ event_type = Regexp.last_match(1).strip
231
+ when /^data:\s*(.*)$/
232
+ data_lines << Regexp.last_match(1)
233
+ end
234
+ end
235
+
236
+ [event_type, data_lines.join("\n").strip]
237
+ end
238
+
239
+ # Builds an absolute URL for +candidate+ relative to +base+.
240
+ # If +candidate+ is already absolute, it is returned unchanged.
241
+ def build_absolute_url(base, candidate)
242
+ uri = URI.parse(candidate)
243
+ return candidate if uri.absolute?
244
+
245
+ URI.join(base, candidate).to_s
246
+ rescue URI::InvalidURIError
247
+ candidate # fall back to original string
248
+ end
249
+
250
+ def initialize_mcp_connection(connection, endpoint_url)
251
+ puts "[MCP DEBUG] Initializing MCP connection with URL: #{endpoint_url}"
252
+ connection.post(endpoint_url) do |req|
253
+ req.headers["Content-Type"] = "application/json"
254
+ req.body = {
255
+ jsonrpc: JSONRPC_VERSION,
256
+ id: SecureRandom.uuid,
257
+ method: "initialize",
258
+ params: {
259
+ protocolVersion: PROTOCOL_VERSION,
260
+ capabilities: {
261
+ roots: {
262
+ listChanged: true
263
+ },
264
+ sampling: {}
265
+ },
266
+ clientInfo: {
267
+ name: "Raix",
268
+ version: Raix::VERSION
269
+ }
270
+ }
271
+ }.to_json
272
+ end
273
+ end
274
+
275
+ def dispatch_event(event_data, connection, endpoint_url, name, arguments, result)
276
+ event_data = JSON.parse(event_data, symbolize_names: true)
277
+ case event_data
278
+ in { result: { capabilities: { tools: { listChanged: true } } } }
279
+ puts "[MCP DEBUG] Received listChanged event"
280
+ acknowledge_event(connection, endpoint_url)
281
+ fetch_mcp_tools(connection, endpoint_url)
282
+ in { result: { tools: } }
283
+ puts "[MCP DEBUG] Received tools event: #{tools}"
284
+ if name.present?
285
+ puts "[MCP DEBUG] Calling function: #{name} with params: #{arguments.inspect}"
286
+ remote_dispatch(connection, endpoint_url, name, arguments)
287
+ else
288
+ result << tools # will unblock the pop on the main thread
289
+ connection.close
290
+ end
291
+ in { result: { content: } }
292
+ puts "[MCP DEBUG] Received content event: #{content}"
293
+ result << content # will unblock the pop on the main thread
294
+ connection.close
295
+ else
296
+ puts "[MCP DEBUG] Received unexpected event: #{event_data}"
297
+ end
298
+ end
299
+
300
+ def remote_dispatch(connection, endpoint_url, name, arguments)
301
+ connection.post(endpoint_url) do |req|
302
+ req.headers["Content-Type"] = "application/json"
303
+ req.body = {
304
+ jsonrpc: JSONRPC_VERSION,
305
+ id: SecureRandom.uuid,
306
+ method: "tools/call",
307
+ params: { name:, arguments: }
308
+ }.to_json
309
+ end
310
+ end
311
+
312
+ def acknowledge_event(connection, endpoint_url)
313
+ puts "[MCP DEBUG] Acknowledging event"
314
+ connection.post(endpoint_url) do |req|
315
+ req.headers["Content-Type"] = "application/json"
316
+ req.body = {
317
+ jsonrpc: JSONRPC_VERSION,
318
+ method: "notifications/initialized"
319
+ }.to_json
320
+ end
321
+ end
322
+
323
+ def fetch_mcp_tools(connection, endpoint_url)
324
+ puts "[MCP DEBUG] Fetching tools"
325
+ connection.post(endpoint_url) do |req|
326
+ req.headers["Content-Type"] = "application/json"
327
+ req.body = {
328
+ jsonrpc: JSONRPC_VERSION,
329
+ id: SecureRandom.uuid,
330
+ method: "tools/list",
331
+ params: {}
332
+ }.to_json
333
+ end
334
+ end
335
+ end
336
+ end
337
+ end
data/lib/raix/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Raix
4
- VERSION = "0.7.3"
4
+ VERSION = "0.8.1"
5
5
  end
data/lib/raix.rb CHANGED
@@ -6,6 +6,7 @@ require_relative "raix/function_dispatch"
6
6
  require_relative "raix/prompt_declarations"
7
7
  require_relative "raix/predicate"
8
8
  require_relative "raix/response_format"
9
+ require_relative "raix/mcp"
9
10
 
10
11
  # The Raix module provides configuration options for the Raix gem.
11
12
  module Raix
data/raix.gemspec CHANGED
@@ -29,6 +29,7 @@ Gem::Specification.new do |spec|
29
29
  spec.require_paths = ["lib"]
30
30
 
31
31
  spec.add_dependency "activesupport", ">= 6.0"
32
+ spec.add_dependency "faraday-retry", "~> 2.0"
32
33
  spec.add_dependency "open_router", "~> 0.2"
33
- spec.add_dependency "ruby-openai", "~> 7.0"
34
+ spec.add_dependency "ruby-openai", "~> 7"
34
35
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: raix
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.3
4
+ version: 0.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Obie Fernandez
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2025-04-23 00:00:00.000000000 Z
10
+ date: 2025-04-25 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: activesupport
@@ -24,6 +23,20 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: faraday-retry
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '2.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '2.0'
27
40
  - !ruby/object:Gem::Dependency
28
41
  name: open_router
29
42
  requirement: !ruby/object:Gem::Requirement
@@ -44,15 +57,14 @@ dependencies:
44
57
  requirements:
45
58
  - - "~>"
46
59
  - !ruby/object:Gem::Version
47
- version: '7.0'
60
+ version: '7'
48
61
  type: :runtime
49
62
  prerelease: false
50
63
  version_requirements: !ruby/object:Gem::Requirement
51
64
  requirements:
52
65
  - - "~>"
53
66
  - !ruby/object:Gem::Version
54
- version: '7.0'
55
- description:
67
+ version: '7'
56
68
  email:
57
69
  - obiefernandez@gmail.com
58
70
  executables: []
@@ -74,6 +86,7 @@ files:
74
86
  - lib/raix.rb
75
87
  - lib/raix/chat_completion.rb
76
88
  - lib/raix/function_dispatch.rb
89
+ - lib/raix/mcp.rb
77
90
  - lib/raix/message_adapters/base.rb
78
91
  - lib/raix/predicate.rb
79
92
  - lib/raix/prompt_declarations.rb
@@ -88,7 +101,6 @@ metadata:
88
101
  homepage_uri: https://github.com/OlympiaAI/raix
89
102
  source_code_uri: https://github.com/OlympiaAI/raix
90
103
  changelog_uri: https://github.com/OlympiaAI/raix/blob/main/CHANGELOG.md
91
- post_install_message:
92
104
  rdoc_options: []
93
105
  require_paths:
94
106
  - lib
@@ -103,8 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
103
115
  - !ruby/object:Gem::Version
104
116
  version: '0'
105
117
  requirements: []
106
- rubygems_version: 3.5.21
107
- signing_key:
118
+ rubygems_version: 3.6.2
108
119
  specification_version: 4
109
120
  summary: Ruby AI eXtensions
110
121
  test_files: []