tool_forge 0.0.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67f747b53bc12b00107b180fd1fe950d7146f2937a3c5a5cf9b31638ee8d4a31
4
- data.tar.gz: cca823e5fa54ffdc144497b63e37e6f977f2d5cc6ccb70bf38982d62d1db135d
3
+ metadata.gz: 7b224a5666b3a1f3129f0f6c433d0da618be11d7e96401cdf632b268bb4667b3
4
+ data.tar.gz: e9e1b5d02fcbb000c8ef15b6e7c1707ac214b5b35d9346c711ab150507ee8209
5
5
  SHA512:
6
- metadata.gz: 76b3684d98cf72f531ce523f00fc56c3d885fe0b9f2d1f64ee2a7f3597fb8b4f13204846fbbdbf40a66e3083f095bd2f964aa7a9d3b1c22a3d23b4285d2a9d6e
7
- data.tar.gz: 6d2b16c9539435fdfa11060d60606da6b5b3213f8dac86df08be35f2e42bb186cec5f80133f784696df497fc0e245adb1e396f6cc060031b8c43a95216912127
6
+ metadata.gz: d50f61a50e87a6a3c5754b4d0f71910363651386cc47bb3634f5bc888f35214737449c10c411085fb95888f41ba9e0c96af7168be31a7f165ecb104a904627bc
7
+ data.tar.gz: 4d02123db7d4b56c83bdf53913f238c5e408f250edfdcbd2e963abcfd0049e2736762b2e4798ff7d9b6440b2242e1be39fcb5e9c3b640d29b66153e7bc3b0591
data/README.md CHANGED
@@ -1,28 +1,336 @@
1
1
  # ToolForge
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ ToolForge is a Ruby gem that provides a unified DSL for defining tools that can be converted to both [RubyLLM](https://github.com/ruby-llm/ruby-llm) and [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) formats. Write your tool once, use it anywhere.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/tool_forge`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ ## Features
6
6
 
7
- ## Installation
7
+ - 🎯 **Unified DSL**: Define tools once, convert to multiple formats
8
+ - 🔧 **Helper Methods**: Support for both instance and class helper methods
9
+ - 📊 **Type Safety**: Parameter validation and type conversion
10
+ - 🚀 **Framework Agnostic**: Works with RubyLLM and MCP frameworks
11
+ - 📝 **Clean API**: Intuitive, Ruby-like syntax
8
12
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
13
+ ## Installation
10
14
 
11
15
  Install the gem and add to the application's Gemfile by executing:
12
16
 
13
17
  ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
18
+ bundle add tool_forge
15
19
  ```
16
20
 
17
21
  If bundler is not being used to manage dependencies, install the gem by executing:
18
22
 
19
23
  ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
24
+ gem install tool_forge
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```ruby
30
+ require 'tool_forge'
31
+
32
+ # Define a tool
33
+ tool = ToolForge.define(:greet_user) do
34
+ description 'Greets a user with a personalized message'
35
+
36
+ param :name, type: :string, description: 'User name'
37
+ param :greeting, type: :string, description: 'Greeting style',
38
+ required: false, default: 'Hello'
39
+
40
+ execute do |name:, greeting:|
41
+ "#{greeting}, #{name}! Welcome to ToolForge!"
42
+ end
43
+ end
44
+
45
+ # Convert to RubyLLM format
46
+ ruby_llm_tool = tool.to_ruby_llm_tool
47
+ instance = ruby_llm_tool.new
48
+ result = instance.execute(name: 'Alice')
49
+ #=> "Hello, Alice! Welcome to ToolForge!"
50
+
51
+ # Convert to MCP format
52
+ mcp_tool = tool.to_mcp_tool
53
+ result = mcp_tool.call(server_context: nil, name: 'Bob')
54
+ #=> Returns MCP::Tool::Response object
55
+ ```
56
+
57
+ ## Detailed Usage
58
+
59
+ ### Basic Tool Definition
60
+
61
+ ```ruby
62
+ tool = ToolForge.define(:file_reader) do
63
+ description 'Reads and processes files'
64
+
65
+ # Define parameters with types and validation
66
+ param :filename, type: :string, description: 'Path to file'
67
+ param :encoding, type: :string, required: false, default: 'utf-8'
68
+ param :max_lines, type: :integer, required: false
69
+
70
+ # Define execution logic
71
+ execute do |filename:, encoding:, max_lines:|
72
+ content = File.read(filename, encoding: encoding)
73
+ lines = content.lines
74
+
75
+ if max_lines
76
+ lines.first(max_lines).join
77
+ else
78
+ content
79
+ end
80
+ end
81
+ end
21
82
  ```
22
83
 
23
- ## Usage
84
+ ### Helper Methods
85
+
86
+ ToolForge supports two types of helper methods:
87
+
88
+ #### Instance Helper Methods
89
+
90
+ Use `helper` for methods that operate on instance data:
91
+
92
+ ```ruby
93
+ tool = ToolForge.define(:text_processor) do
94
+ description 'Processes text with formatting'
95
+
96
+ param :text, type: :string
97
+ param :format, type: :string, default: 'uppercase'
98
+
99
+ # Instance helper method
100
+ helper(:format_text) do |text, format|
101
+ case format
102
+ when 'uppercase' then text.upcase
103
+ when 'lowercase' then text.downcase
104
+ when 'title' then text.split.map(&:capitalize).join(' ')
105
+ else text
106
+ end
107
+ end
108
+
109
+ helper(:add_prefix) do |text|
110
+ "PROCESSED: #{text}"
111
+ end
112
+
113
+ execute do |text:, format:|
114
+ formatted = format_text(text, format)
115
+ add_prefix(formatted)
116
+ end
117
+ end
118
+ ```
119
+
120
+ #### Class Helper Methods
121
+
122
+ Use `class_helper` for utility methods that don't depend on instance state:
123
+
124
+ ```ruby
125
+ tool = ToolForge.define(:docker_copy) do
126
+ description 'Copies files to Docker containers'
127
+
128
+ param :container_id, type: :string
129
+ param :source_path, type: :string
130
+ param :dest_path, type: :string
131
+
132
+ # Class helper method - useful for utilities
133
+ class_helper(:add_to_tar) do |file_path, tar_path|
134
+ # Implementation for tar operations
135
+ "Added #{file_path} to tar archive as #{tar_path}"
136
+ end
137
+
138
+ class_helper(:validate_container) do |container_id|
139
+ # Validation logic
140
+ container_id.match?(/^[a-f0-9]{12}$/)
141
+ end
142
+
143
+ execute do |container_id:, source_path:, dest_path:|
144
+ return "Invalid container ID" unless self.class.validate_container(container_id)
145
+
146
+ tar_result = self.class.add_to_tar(source_path, dest_path)
147
+ "Copied #{source_path} to #{container_id}:#{dest_path} - #{tar_result}"
148
+ end
149
+ end
150
+ ```
151
+
152
+ ### Parameter Types
153
+
154
+ ToolForge supports various parameter types:
155
+
156
+ ```ruby
157
+ tool = ToolForge.define(:complex_tool) do
158
+ param :name, type: :string # String parameter
159
+ param :count, type: :integer # Integer parameter
160
+ param :active, type: :boolean # Boolean parameter
161
+ param :rate, type: :number # Numeric parameter
162
+ param :tags, type: :array # Array parameter
163
+ param :metadata, type: :object # Object/Hash parameter
164
+ param :config, type: :string, required: false, default: 'default.json'
165
+
166
+ execute do |**params|
167
+ # Access all parameters
168
+ params.inspect
169
+ end
170
+ end
171
+ ```
172
+
173
+ ### Framework Integration
174
+
175
+ #### RubyLLM Integration
176
+
177
+ ```ruby
178
+ require 'ruby_llm'
179
+ require 'tool_forge'
180
+
181
+ tool = ToolForge.define(:my_tool) do
182
+ # ... tool definition
183
+ end
184
+
185
+ # Convert to RubyLLM format
186
+ ruby_llm_class = tool.to_ruby_llm_tool
187
+
188
+ # Use with RubyLLM
189
+ llm = RubyLLM::Client.new
190
+ llm.add_tool(ruby_llm_class)
191
+ ```
192
+
193
+ #### MCP Integration
194
+
195
+ ```ruby
196
+ require 'mcp'
197
+ require 'tool_forge'
198
+
199
+ tool = ToolForge.define(:my_tool) do
200
+ # ... tool definition
201
+ end
202
+
203
+ # Convert to MCP format
204
+ mcp_class = tool.to_mcp_tool
205
+
206
+ # Use with MCP server
207
+ server = MCP::Server.new
208
+ server.add_tool(mcp_class)
209
+ ```
210
+
211
+ ## Advanced Features
212
+
213
+ ### Complex Data Processing
214
+
215
+ ```ruby
216
+ tool = ToolForge.define(:data_analyzer) do
217
+ description 'Analyzes data files and generates reports'
218
+
219
+ param :files, type: :array, description: 'List of file paths'
220
+ param :output_format, type: :string, default: 'json'
221
+
222
+ helper(:read_file_data) do |file_path|
223
+ return { error: "File not found: #{file_path}" } unless File.exist?(file_path)
224
+
225
+ {
226
+ path: file_path,
227
+ size: File.size(file_path),
228
+ lines: File.readlines(file_path).count,
229
+ modified: File.mtime(file_path)
230
+ }
231
+ end
232
+
233
+ helper(:format_output) do |data, format|
234
+ case format
235
+ when 'json' then JSON.pretty_generate(data)
236
+ when 'yaml' then data.to_yaml
237
+ when 'csv' then data.map { |row| row.values.join(',') }.join("\n")
238
+ else data.inspect
239
+ end
240
+ end
241
+
242
+ execute do |files:, output_format:|
243
+ results = files.map { |file| read_file_data(file) }
244
+
245
+ summary = {
246
+ total_files: results.count,
247
+ total_size: results.sum { |r| r[:size] || 0 },
248
+ files: results
249
+ }
250
+
251
+ format_output(summary, output_format)
252
+ end
253
+ end
254
+ ```
255
+
256
+ ### Error Handling
257
+
258
+ ```ruby
259
+ tool = ToolForge.define(:safe_processor) do
260
+ description 'Processes data with comprehensive error handling'
261
+
262
+ param :input, type: :string
263
+ param :operation, type: :string
264
+
265
+ helper(:validate_input) do |input|
266
+ raise ArgumentError, "Input cannot be empty" if input.nil? || input.empty?
267
+ raise ArgumentError, "Input too long" if input.length > 1000
268
+ true
269
+ end
270
+
271
+ execute do |input:, operation:|
272
+ begin
273
+ validate_input(input)
274
+
275
+ case operation
276
+ when 'reverse' then input.reverse
277
+ when 'upcase' then input.upcase
278
+ when 'analyze' then { length: input.length, words: input.split.count }
279
+ else
280
+ { error: "Unknown operation: #{operation}" }
281
+ end
282
+ rescue => e
283
+ { error: e.message }
284
+ end
285
+ end
286
+ end
287
+ ```
288
+
289
+ ## API Reference
290
+
291
+ ### ToolForge.define(name, &block)
292
+
293
+ Creates a new tool definition.
294
+
295
+ - `name` (Symbol): The tool name
296
+ - `block`: Configuration block using the DSL
297
+
298
+ ### DSL Methods
299
+
300
+ #### `description(text)`
301
+ Sets the tool description.
302
+
303
+ #### `param(name, options = {})`
304
+ Defines a parameter with options:
305
+ - `type`: Parameter type (`:string`, `:integer`, `:boolean`, `:number`, `:array`, `:object`)
306
+ - `description`: Parameter description
307
+ - `required`: Whether required (default: `true`)
308
+ - `default`: Default value for optional parameters
309
+
310
+ #### `helper(name, &block)`
311
+ Defines an instance helper method.
312
+
313
+ #### `class_helper(name, &block)`
314
+ Defines a class helper method.
315
+
316
+ #### `execute(&block)`
317
+ Defines the tool execution logic.
318
+
319
+ ### Conversion Methods
320
+
321
+ #### `#to_ruby_llm_tool`
322
+ Converts to a RubyLLM::Tool class.
323
+
324
+ #### `#to_mcp_tool`
325
+ Converts to an MCP::Tool class.
326
+
327
+ ## Examples
24
328
 
25
- TODO: Write usage instructions here
329
+ See the [examples directory](examples/) for more comprehensive examples including:
330
+ - File processing tools
331
+ - API integration tools
332
+ - Data transformation tools
333
+ - Docker management tools
26
334
 
27
335
  ## Development
28
336
 
@@ -32,7 +340,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
32
340
 
33
341
  ## Contributing
34
342
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/tool_forge.
343
+ Bug reports and pull requests are welcome on GitHub at https://github.com/afstanton/tool_forge.
36
344
 
37
345
  ## License
38
346
 
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
8
+ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
@@ -0,0 +1,95 @@
1
+ # ToolForge Examples
2
+
3
+ This directory contains practical examples of ToolForge usage, demonstrating various features and patterns.
4
+
5
+ ## Examples
6
+
7
+ ### 1. Docker Copy Tool (`docker_copy_tool.rb`)
8
+ Demonstrates:
9
+ - **Class helper methods** for utility functions (`add_to_tar`, `validate_container_id`)
10
+ - **Instance helper methods** for result formatting
11
+ - **Parameter validation** and error handling
12
+ - **Complex return values** with structured data
13
+
14
+ **Key Features:**
15
+ - Container ID validation
16
+ - Tar archive operations simulation
17
+ - File existence checking
18
+ - Comprehensive error handling
19
+
20
+ ### 2. File Processor Tool (`file_processor_tool.rb`)
21
+ Demonstrates:
22
+ - **Multiple instance helper methods** for different concerns
23
+ - **Array parameters** for multiple operations
24
+ - **Complex text processing** with various transformations
25
+ - **Multiple output formats** (JSON, YAML, text)
26
+ - **Preserve original data** option
27
+
28
+ **Key Features:**
29
+ - Text analysis (word count, line count, etc.)
30
+ - Text transformations (case changes, reversals, etc.)
31
+ - Flexible output formatting
32
+ - Operation chaining
33
+
34
+ ## Running Examples
35
+
36
+ To run these examples:
37
+
38
+ ```bash
39
+ # From the project root
40
+ ruby examples/docker_copy_tool.rb
41
+ ruby examples/file_processor_tool.rb
42
+ ```
43
+
44
+ ## Creating Your Own Tools
45
+
46
+ Use these examples as templates for your own tools:
47
+
48
+ 1. **Start with a clear purpose** - what does your tool do?
49
+ 2. **Define parameters** - what inputs does it need?
50
+ 3. **Break down complex logic** into helper methods
51
+ 4. **Choose the right helper type**:
52
+ - Use `helper` for instance methods that work with tool data
53
+ - Use `class_helper` for utility functions and static operations
54
+ 5. **Handle errors gracefully** - return structured error information
55
+ 6. **Consider output format** - how will users consume the results?
56
+
57
+ ## Integration Examples
58
+
59
+ ### With RubyLLM
60
+
61
+ ```ruby
62
+ require 'ruby_llm'
63
+ require_relative 'docker_copy_tool'
64
+
65
+ # Convert to RubyLLM format
66
+ ruby_llm_tool = docker_copy_tool.to_ruby_llm_tool
67
+
68
+ # Use with RubyLLM framework
69
+ llm = RubyLLM::Client.new
70
+ llm.add_tool(ruby_llm_tool)
71
+ ```
72
+
73
+ ### With MCP
74
+
75
+ ```ruby
76
+ require 'mcp'
77
+ require_relative 'file_processor_tool'
78
+
79
+ # Convert to MCP format
80
+ mcp_tool = file_processor_tool.to_mcp_tool
81
+
82
+ # Use with MCP server
83
+ server = MCP::Server.new
84
+ server.add_tool(mcp_tool)
85
+ ```
86
+
87
+ ## Best Practices Demonstrated
88
+
89
+ 1. **Clear parameter documentation** - each parameter has a description
90
+ 2. **Sensible defaults** - optional parameters have reasonable defaults
91
+ 3. **Input validation** - check inputs before processing
92
+ 4. **Error handling** - graceful failure with informative messages
93
+ 5. **Structured outputs** - consistent, parseable return values
94
+ 6. **Helper method organization** - logical separation of concerns
95
+ 7. **Type safety** - appropriate parameter types for inputs
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example: Docker Copy Tool
4
+ # Demonstrates class helper methods for utility functions
5
+
6
+ require 'tool_forge'
7
+
8
+ docker_copy_tool = ToolForge.define(:docker_copy) do
9
+ description 'Copies files to Docker containers with tar archive support'
10
+
11
+ param :container_id, type: :string, description: 'Docker container ID'
12
+ param :source_path, type: :string, description: 'Local file path to copy'
13
+ param :dest_path, type: :string, description: 'Destination path in container'
14
+ param :create_archive, type: :boolean, required: false, default: true
15
+
16
+ # Class helper method for tar operations
17
+ class_helper(:add_to_tar) do |file_path, tar_path|
18
+ # In a real implementation, this would create a tar archive
19
+ {
20
+ operation: 'tar_add',
21
+ source: file_path,
22
+ target: tar_path,
23
+ timestamp: Time.now.iso8601,
24
+ success: true
25
+ }
26
+ end
27
+
28
+ # Class helper method for container validation
29
+ class_helper(:validate_container_id) do |container_id|
30
+ # Simple validation - real implementation would check with Docker
31
+ container_id.match?(/^[a-f0-9]{12,64}$/)
32
+ end
33
+
34
+ # Instance helper method for formatting results
35
+ helper(:format_result) do |operation_result, container_id, dest_path|
36
+ if operation_result[:success]
37
+ "✅ Successfully copied to #{container_id}:#{dest_path} at #{operation_result[:timestamp]}"
38
+ else
39
+ "❌ Failed to copy: #{operation_result[:error]}"
40
+ end
41
+ end
42
+
43
+ execute do |container_id:, source_path:, dest_path:, create_archive:|
44
+ # Validate container ID using class helper
45
+ unless self.class.validate_container_id(container_id)
46
+ return { error: "Invalid container ID format: #{container_id}" }
47
+ end
48
+
49
+ # Check if source file exists
50
+ return { error: "Source file not found: #{source_path}" } unless File.exist?(source_path)
51
+
52
+ begin
53
+ if create_archive
54
+ # Use class helper for tar operations
55
+ tar_result = self.class.add_to_tar(source_path, dest_path)
56
+
57
+ # Use instance helper for formatting
58
+ format_result(tar_result, container_id, dest_path)
59
+ else
60
+ # Direct copy simulation
61
+ {
62
+ message: "Direct copy to #{container_id}:#{dest_path}",
63
+ source: source_path,
64
+ destination: dest_path,
65
+ method: 'direct_copy',
66
+ timestamp: Time.now.iso8601
67
+ }
68
+ end
69
+ rescue StandardError => e
70
+ { error: "Copy operation failed: #{e.message}" }
71
+ end
72
+ end
73
+ end
74
+
75
+ # Example usage:
76
+ if __FILE__ == $PROGRAM_NAME
77
+ # This would normally require the actual RubyLLM or MCP frameworks
78
+ puts 'Docker Copy Tool Definition Created'
79
+ puts "Description: #{docker_copy_tool.description}"
80
+ puts "Parameters: #{docker_copy_tool.params.map { |p| p[:name] }.join(', ')}"
81
+ puts "Helper methods: #{docker_copy_tool.helper_methods.map { |type, methods| "#{type}: #{methods.keys}" }}"
82
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example: File Processor Tool
4
+ # Demonstrates instance helper methods and complex data processing
5
+
6
+ require 'tool_forge'
7
+
8
+ file_processor_tool = ToolForge.define(:file_processor) do
9
+ description 'Processes text files with various transformations and analysis'
10
+
11
+ param :file_path, type: :string, description: 'Path to the file to process'
12
+ param :operations, type: :array, description: 'List of operations to perform'
13
+ param :output_format, type: :string, required: false, default: 'json'
14
+ param :preserve_original, type: :boolean, required: false, default: true
15
+
16
+ # Instance helper methods for text processing
17
+ helper(:read_file_content) do |path|
18
+ return { error: "File not found: #{path}" } unless File.exist?(path)
19
+
20
+ {
21
+ content: File.read(path),
22
+ size: File.size(path),
23
+ lines: File.readlines(path).count,
24
+ encoding: File.read(path).encoding.name
25
+ }
26
+ end
27
+
28
+ helper(:analyze_text) do |content|
29
+ lines = content.lines
30
+ words = content.split(/\s+/)
31
+
32
+ {
33
+ character_count: content.length,
34
+ word_count: words.length,
35
+ line_count: lines.length,
36
+ average_line_length: lines.empty? ? 0 : (content.length.to_f / lines.length).round(2),
37
+ longest_word: words.max_by(&:length) || '',
38
+ unique_words: words.map(&:downcase).uniq.length
39
+ }
40
+ end
41
+
42
+ helper(:transform_text) do |content, operation|
43
+ case operation.downcase
44
+ when 'uppercase'
45
+ content.upcase
46
+ when 'lowercase'
47
+ content.downcase
48
+ when 'title_case'
49
+ content.split.map(&:capitalize).join(' ')
50
+ when 'reverse_lines'
51
+ content.lines.reverse.join
52
+ when 'reverse_words'
53
+ content.split.reverse.join(' ')
54
+ when 'remove_blank_lines'
55
+ content.lines.reject { |line| line.strip.empty? }.join
56
+ when 'number_lines'
57
+ content.lines.map.with_index(1) { |line, i| "#{i}. #{line}" }.join
58
+ else
59
+ content
60
+ end
61
+ end
62
+
63
+ helper(:format_output) do |data, format|
64
+ case format.downcase
65
+ when 'json'
66
+ JSON.pretty_generate(data)
67
+ when 'yaml'
68
+ begin
69
+ require 'yaml'
70
+ data.to_yaml
71
+ rescue LoadError
72
+ "YAML not available, falling back to JSON:\n#{JSON.pretty_generate(data)}"
73
+ end
74
+ when 'text'
75
+ if data.is_a?(Hash)
76
+ data.map { |k, v| "#{k}: #{v}" }.join("\n")
77
+ else
78
+ data.to_s
79
+ end
80
+ else
81
+ data.inspect
82
+ end
83
+ end
84
+
85
+ execute do |file_path:, operations:, output_format:, preserve_original:|
86
+ # Read the file
87
+ file_data = read_file_content(file_path)
88
+ return file_data if file_data[:error]
89
+
90
+ content = file_data[:content]
91
+ original_content = preserve_original ? content.dup : nil
92
+
93
+ # Process each operation
94
+ processed_content = content
95
+ operation_results = []
96
+
97
+ operations.each do |operation|
98
+ case operation.downcase
99
+ when 'analyze'
100
+ analysis = analyze_text(processed_content)
101
+ operation_results << { operation: operation, result: analysis }
102
+ else
103
+ # Text transformation
104
+ old_content = processed_content
105
+ processed_content = transform_text(processed_content, operation)
106
+ operation_results << {
107
+ operation: operation,
108
+ applied: old_content != processed_content,
109
+ preview: processed_content[0..100] + (processed_content.length > 100 ? '...' : '')
110
+ }
111
+ end
112
+ end
113
+
114
+ # Prepare final result
115
+ result = {
116
+ file_info: file_data.except(:content),
117
+ operations_applied: operation_results,
118
+ final_content: processed_content,
119
+ processing_summary: {
120
+ operations_count: operations.length,
121
+ content_changed: preserve_original ? (original_content != processed_content) : nil,
122
+ final_size: processed_content.length
123
+ }
124
+ }
125
+
126
+ result[:original_content] = original_content if preserve_original
127
+
128
+ # Format output according to preference
129
+ format_output(result, output_format)
130
+ rescue StandardError => e
131
+ format_output({ error: e.message, backtrace: e.backtrace.first(3) }, output_format)
132
+ end
133
+ end
134
+
135
+ # Example usage:
136
+ if __FILE__ == $PROGRAM_NAME
137
+ puts 'File Processor Tool Definition Created'
138
+ puts "Description: #{file_processor_tool.description}"
139
+ puts "Parameters: #{file_processor_tool.params.map { |p| "#{p[:name]} (#{p[:type]})" }.join(', ')}"
140
+
141
+ # Example of what the tool can do:
142
+ puts "\nSupported operations:"
143
+ puts '- analyze: Provides text statistics'
144
+ puts '- uppercase, lowercase, title_case: Text case transformations'
145
+ puts '- reverse_lines, reverse_words: Content reversal'
146
+ puts '- remove_blank_lines: Cleanup operations'
147
+ puts '- number_lines: Add line numbers'
148
+ end
@@ -0,0 +1,274 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ToolForge
4
+ # ToolDefinition is the core class for defining tools that can be converted
5
+ # to both RubyLLM and MCP tool formats. It provides a clean DSL for defining
6
+ # tool metadata, parameters, helper methods, and execution logic.
7
+ #
8
+ # @example Basic tool definition
9
+ # tool = ToolForge::ToolDefinition.new(:greet_user) do
10
+ # description 'Greets a user by name'
11
+ # param :name, type: :string, description: 'User name'
12
+ # execute { |name:| "Hello, #{name}!" }
13
+ # end
14
+ #
15
+ # @example Tool with helper methods
16
+ # tool = ToolForge::ToolDefinition.new(:file_processor) do
17
+ # description 'Processes files with helper methods'
18
+ # param :file_path, type: :string
19
+ #
20
+ # # Instance helper method
21
+ # helper(:format_data) { |data| "FORMATTED: #{data}" }
22
+ #
23
+ # # Class helper method (useful for utilities like tar operations)
24
+ # class_helper(:add_to_tar) { |file, path| "Added #{file} to #{path}" }
25
+ #
26
+ # execute do |file_path:|
27
+ # data = File.read(file_path)
28
+ # formatted = format_data(data)
29
+ # tar_result = self.class.add_to_tar(file_path, '/archive')
30
+ # "#{formatted} - #{tar_result}"
31
+ # end
32
+ # end
33
+ class ToolDefinition
34
+ # @return [Symbol] the name of the tool
35
+ # @return [Array<Hash>] the parameters defined for the tool
36
+ # @return [Proc] the execution block for the tool
37
+ # @return [Hash] the helper methods organized by type (:instance and :class)
38
+ attr_reader :name, :params, :execute_block, :helper_methods
39
+
40
+ # Creates a new tool definition with the given name.
41
+ #
42
+ # @param name [Symbol] the name of the tool
43
+ # @yield [] optional block for configuring the tool using the DSL
44
+ #
45
+ # @example
46
+ # tool = ToolDefinition.new(:my_tool) do
47
+ # description 'A sample tool'
48
+ # param :input, type: :string
49
+ # execute { |input:| "Processed: #{input}" }
50
+ # end
51
+ def initialize(name, &)
52
+ @name = name
53
+ @description = nil
54
+ @params = []
55
+ @execute_block = nil
56
+ @helper_methods = { instance: {}, class: {} }
57
+
58
+ instance_eval(&) if block_given?
59
+ end
60
+
61
+ # Sets or returns the tool description.
62
+ #
63
+ # @param text [String, nil] the description text to set, or nil to return current description
64
+ # @return [String, nil] the current description when called without arguments
65
+ #
66
+ # @example Setting description
67
+ # description 'This tool processes files'
68
+ #
69
+ # @example Getting description
70
+ # tool.description #=> 'This tool processes files'
71
+ def description(text = nil)
72
+ if text
73
+ @description = text
74
+ else
75
+ @description
76
+ end
77
+ end
78
+
79
+ # Defines a parameter for the tool.
80
+ #
81
+ # @param name [Symbol] the parameter name
82
+ # @param type [Symbol] the parameter type (:string, :integer, :boolean, etc.)
83
+ # @param description [String, nil] optional description of the parameter
84
+ # @param required [Boolean] whether the parameter is required (default: true)
85
+ # @param default [Object, nil] default value for optional parameters
86
+ #
87
+ # @example Required string parameter
88
+ # param :filename, type: :string, description: 'File to process'
89
+ #
90
+ # @example Optional parameter with default
91
+ # param :format, type: :string, description: 'Output format', required: false, default: 'json'
92
+ def param(name, type: :string, description: nil, required: true, default: nil)
93
+ @params << {
94
+ name: name,
95
+ type: type,
96
+ description: description,
97
+ required: required,
98
+ default: default
99
+ }
100
+ end
101
+
102
+ # Defines the execution logic for the tool.
103
+ #
104
+ # @yield [**args] block that receives tool parameters as keyword arguments
105
+ # @return [void]
106
+ #
107
+ # @example
108
+ # execute do |filename:, format:|
109
+ # data = File.read(filename)
110
+ # format == 'json' ? JSON.parse(data) : data
111
+ # end
112
+ def execute(&block)
113
+ @execute_block = block
114
+ end
115
+
116
+ # Defines an instance helper method that can be called within the execute block.
117
+ # Instance helper methods are available as regular method calls in the execution context.
118
+ #
119
+ # @param method_name [Symbol] the name of the helper method
120
+ # @yield [*args] block that defines the helper method logic
121
+ # @return [void]
122
+ #
123
+ # @example
124
+ # helper(:format_output) do |data|
125
+ # "FORMATTED: #{data.upcase}"
126
+ # end
127
+ #
128
+ # execute do |input:|
129
+ # format_output(input) # Called as instance method
130
+ # end
131
+ def helper(method_name, &block)
132
+ @helper_methods[:instance][method_name] = block
133
+ end
134
+
135
+ # Defines a class helper method that can be called within the execute block.
136
+ # Class helper methods are useful for utility functions that don't depend on instance state.
137
+ # They are accessed via self.class.method_name in the execution context.
138
+ #
139
+ # @param method_name [Symbol] the name of the class helper method
140
+ # @yield [*args] block that defines the helper method logic
141
+ # @return [void]
142
+ #
143
+ # @example
144
+ # class_helper(:add_to_tar) do |file_path, tar_path|
145
+ # # Implementation for adding files to tar archive
146
+ # "Added #{file_path} to tar as #{tar_path}"
147
+ # end
148
+ #
149
+ # execute do |file:|
150
+ # self.class.add_to_tar(file, '/archive/file.tar') # Called as class method
151
+ # end
152
+ def class_helper(method_name, &block)
153
+ @helper_methods[:class][method_name] = block
154
+ end
155
+
156
+ # Converts this tool definition to a RubyLLM::Tool class.
157
+ # The resulting class can be instantiated and used with the RubyLLM framework.
158
+ #
159
+ # Instance helper methods become instance methods on the generated class.
160
+ # Class helper methods become singleton methods on the generated class.
161
+ #
162
+ # @return [Class] a class that inherits from RubyLLM::Tool
163
+ # @raise [LoadError] if RubyLLM is not loaded
164
+ #
165
+ # @example
166
+ # tool_class = tool_definition.to_ruby_llm_tool
167
+ # instance = tool_class.new
168
+ # result = instance.execute(filename: 'data.txt')
169
+ def to_ruby_llm_tool
170
+ raise LoadError, 'RubyLLM is not loaded. Please require "ruby_llm" first.' unless defined?(RubyLLM::Tool)
171
+ raise LoadError, 'RubyLLM is not loaded. Please require "ruby_llm" first.' if RubyLLM::Tool.nil?
172
+
173
+ definition = self
174
+
175
+ Class.new(RubyLLM::Tool) do
176
+ description definition.description
177
+
178
+ definition.params.each do |param_def|
179
+ param param_def[:name], type: param_def[:type], desc: param_def[:description]
180
+ end
181
+
182
+ # Add instance helper methods
183
+ definition.helper_methods[:instance].each do |method_name, method_block|
184
+ define_method(method_name, &method_block)
185
+ end
186
+
187
+ # Add class helper methods
188
+ definition.helper_methods[:class].each do |method_name, method_block|
189
+ define_singleton_method(method_name, &method_block)
190
+ end
191
+
192
+ define_method(:execute) do |**args|
193
+ # Execute the block in the context of this instance so helper methods are available
194
+ instance_exec(**args, &definition.execute_block)
195
+ end
196
+ end
197
+ end
198
+
199
+ # Converts this tool definition to an MCP::Tool class.
200
+ # The resulting class can be used with the Model Context Protocol framework.
201
+ #
202
+ # Both instance and class helper methods are available in the execution context.
203
+ # Class helper methods are accessed via self.class.method_name.
204
+ #
205
+ # @return [Class] a class that inherits from MCP::Tool
206
+ # @raise [LoadError] if MCP SDK is not loaded
207
+ #
208
+ # @example
209
+ # tool_class = tool_definition.to_mcp_tool
210
+ # result = tool_class.call(server_context: nil, filename: 'data.txt')
211
+ def to_mcp_tool
212
+ raise LoadError, 'MCP SDK is not loaded. Please require "mcp" first.' unless defined?(MCP::Tool)
213
+ raise LoadError, 'MCP SDK is not loaded. Please require "mcp" first.' if MCP::Tool.nil?
214
+
215
+ definition = self
216
+
217
+ Class.new(MCP::Tool) do
218
+ description definition.description
219
+
220
+ # Build properties hash for input schema
221
+ properties = {}
222
+ required_params = []
223
+
224
+ definition.params.each do |param_def|
225
+ prop = {
226
+ type: param_def[:type].to_s
227
+ }
228
+ prop[:description] = param_def[:description] if param_def[:description]
229
+
230
+ properties[param_def[:name].to_s] = prop
231
+ required_params << param_def[:name].to_s if param_def[:required]
232
+ end
233
+
234
+ input_schema(
235
+ properties: properties,
236
+ required: required_params
237
+ )
238
+
239
+ # Create a helper object that contains all the helper methods
240
+ helper_class = Class.new do
241
+ definition.helper_methods[:instance].each do |method_name, method_block|
242
+ define_method(method_name, &method_block)
243
+ end
244
+
245
+ # Class methods are defined as singleton methods on the class itself
246
+ definition.helper_methods[:class].each do |method_name, method_block|
247
+ define_singleton_method(method_name, &method_block)
248
+ end
249
+ end
250
+
251
+ define_singleton_method(:call) do |server_context:, **args|
252
+ # Create an instance of the helper class to provide context for helper methods
253
+ helper_instance = helper_class.new
254
+
255
+ # Execute the block in the context of the helper instance so helper methods are available
256
+ # For class methods, they'll be available on the helper_class itself
257
+ result = helper_instance.instance_exec(**args, &definition.execute_block)
258
+
259
+ # Smart formatting for different return types
260
+ result_text = case result
261
+ when String
262
+ result
263
+ when Hash, Array
264
+ JSON.pretty_generate(result)
265
+ else
266
+ result.to_s
267
+ end
268
+
269
+ MCP::Tool::Response.new([{ type: 'text', text: result_text }])
270
+ end
271
+ end
272
+ end
273
+ end
274
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ToolForge
4
- VERSION = "0.0.1"
4
+ # The current version of the ToolForge gem
5
+ VERSION = '0.2.0'
5
6
  end
data/lib/tool_forge.rb CHANGED
@@ -1,8 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "tool_forge/version"
3
+ require 'json'
4
+ require 'zeitwerk'
4
5
 
6
+ loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
7
+ loader.setup # ready!
8
+
9
+ require_relative 'tool_forge/version'
10
+
11
+ # ToolForge provides a unified DSL for defining tools that can be converted
12
+ # to both RubyLLM and Model Context Protocol (MCP) formats.
13
+ #
14
+ # @example Basic usage
15
+ # tool = ToolForge.define(:greet_user) do
16
+ # description 'Greets a user by name'
17
+ # param :name, type: :string, description: 'User name'
18
+ # execute { |name:| "Hello, #{name}!" }
19
+ # end
20
+ #
21
+ # # Convert to RubyLLM format
22
+ # ruby_llm_tool = tool.to_ruby_llm_tool
23
+ #
24
+ # # Convert to MCP format
25
+ # mcp_tool = tool.to_mcp_tool
5
26
  module ToolForge
27
+ # Base error class for ToolForge-specific errors
6
28
  class Error < StandardError; end
7
- # Your code goes here...
29
+
30
+ # Creates a new tool definition with the given name and configuration block.
31
+ #
32
+ # @param name [Symbol] the name of the tool
33
+ # @yield [] block for configuring the tool using the DSL
34
+ # @return [ToolDefinition] a new tool definition instance
35
+ #
36
+ # @example Define a simple tool
37
+ # tool = ToolForge.define(:calculator) do
38
+ # description 'Performs basic arithmetic'
39
+ # param :operation, type: :string, description: 'Operation to perform'
40
+ # param :a, type: :number, description: 'First number'
41
+ # param :b, type: :number, description: 'Second number'
42
+ #
43
+ # execute do |operation:, a:, b:|
44
+ # case operation
45
+ # when 'add' then a + b
46
+ # when 'subtract' then a - b
47
+ # when 'multiply' then a * b
48
+ # when 'divide' then b != 0 ? a / b : 'Cannot divide by zero'
49
+ # else 'Unknown operation'
50
+ # end
51
+ # end
52
+ # end
53
+ def self.define(name, &)
54
+ ToolDefinition.new(name, &)
55
+ end
8
56
  end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tool_forge
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron F Stanton
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: zeitwerk
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
12
26
  description: A Ruby gem for building AI tools for large language models using a simple
13
27
  domain-specific language.
14
28
  email:
@@ -20,7 +34,11 @@ files:
20
34
  - LICENSE.txt
21
35
  - README.md
22
36
  - Rakefile
37
+ - examples/README.md
38
+ - examples/docker_copy_tool.rb
39
+ - examples/file_processor_tool.rb
23
40
  - lib/tool_forge.rb
41
+ - lib/tool_forge/tool_definition.rb
24
42
  - lib/tool_forge/version.rb
25
43
  - sig/tool_forge.rbs
26
44
  homepage: https://github.com/afstanton/tool_forge
@@ -30,6 +48,7 @@ metadata:
30
48
  allowed_push_host: https://rubygems.org
31
49
  homepage_uri: https://github.com/afstanton/tool_forge
32
50
  source_code_uri: https://github.com/afstanton/tool_forge
51
+ rubygems_mfa_required: 'true'
33
52
  rdoc_options: []
34
53
  require_paths:
35
54
  - lib