tsikol 0.1.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.
Files changed (75) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +22 -0
  3. data/CONTRIBUTING.md +84 -0
  4. data/LICENSE +21 -0
  5. data/README.md +579 -0
  6. data/Rakefile +12 -0
  7. data/docs/README.md +69 -0
  8. data/docs/api/middleware.md +721 -0
  9. data/docs/api/prompt.md +858 -0
  10. data/docs/api/resource.md +651 -0
  11. data/docs/api/server.md +509 -0
  12. data/docs/api/test-helpers.md +591 -0
  13. data/docs/api/tool.md +527 -0
  14. data/docs/cookbook/authentication.md +651 -0
  15. data/docs/cookbook/caching.md +877 -0
  16. data/docs/cookbook/dynamic-tools.md +970 -0
  17. data/docs/cookbook/error-handling.md +887 -0
  18. data/docs/cookbook/logging.md +1044 -0
  19. data/docs/cookbook/rate-limiting.md +717 -0
  20. data/docs/examples/code-assistant.md +922 -0
  21. data/docs/examples/complete-server.md +726 -0
  22. data/docs/examples/database-manager.md +1198 -0
  23. data/docs/examples/devops-tools.md +1382 -0
  24. data/docs/examples/echo-server.md +501 -0
  25. data/docs/examples/weather-service.md +822 -0
  26. data/docs/guides/completion.md +472 -0
  27. data/docs/guides/getting-started.md +462 -0
  28. data/docs/guides/middleware.md +823 -0
  29. data/docs/guides/project-structure.md +434 -0
  30. data/docs/guides/prompts.md +920 -0
  31. data/docs/guides/resources.md +720 -0
  32. data/docs/guides/sampling.md +804 -0
  33. data/docs/guides/testing.md +863 -0
  34. data/docs/guides/tools.md +627 -0
  35. data/examples/README.md +92 -0
  36. data/examples/advanced_features.rb +129 -0
  37. data/examples/basic-migrated/app/prompts/weather_chat.rb +44 -0
  38. data/examples/basic-migrated/app/resources/weather_alerts.rb +18 -0
  39. data/examples/basic-migrated/app/tools/get_current_weather.rb +34 -0
  40. data/examples/basic-migrated/app/tools/get_forecast.rb +30 -0
  41. data/examples/basic-migrated/app/tools/get_weather_by_coords.rb +48 -0
  42. data/examples/basic-migrated/server.rb +25 -0
  43. data/examples/basic.rb +73 -0
  44. data/examples/full_featured.rb +175 -0
  45. data/examples/middleware_example.rb +112 -0
  46. data/examples/sampling_example.rb +104 -0
  47. data/examples/weather-service/app/prompts/weather/chat.rb +90 -0
  48. data/examples/weather-service/app/resources/weather/alerts.rb +59 -0
  49. data/examples/weather-service/app/tools/weather/get_current.rb +82 -0
  50. data/examples/weather-service/app/tools/weather/get_forecast.rb +90 -0
  51. data/examples/weather-service/server.rb +28 -0
  52. data/exe/tsikol +6 -0
  53. data/lib/tsikol/cli/templates/Gemfile.erb +10 -0
  54. data/lib/tsikol/cli/templates/README.md.erb +38 -0
  55. data/lib/tsikol/cli/templates/gitignore.erb +49 -0
  56. data/lib/tsikol/cli/templates/prompt.rb.erb +53 -0
  57. data/lib/tsikol/cli/templates/resource.rb.erb +29 -0
  58. data/lib/tsikol/cli/templates/server.rb.erb +24 -0
  59. data/lib/tsikol/cli/templates/tool.rb.erb +60 -0
  60. data/lib/tsikol/cli.rb +203 -0
  61. data/lib/tsikol/error_handler.rb +141 -0
  62. data/lib/tsikol/health.rb +198 -0
  63. data/lib/tsikol/http_transport.rb +72 -0
  64. data/lib/tsikol/lifecycle.rb +149 -0
  65. data/lib/tsikol/middleware.rb +168 -0
  66. data/lib/tsikol/prompt.rb +101 -0
  67. data/lib/tsikol/resource.rb +53 -0
  68. data/lib/tsikol/router.rb +190 -0
  69. data/lib/tsikol/server.rb +660 -0
  70. data/lib/tsikol/stdio_transport.rb +108 -0
  71. data/lib/tsikol/test_helpers.rb +261 -0
  72. data/lib/tsikol/tool.rb +111 -0
  73. data/lib/tsikol/version.rb +5 -0
  74. data/lib/tsikol.rb +72 -0
  75. metadata +219 -0
@@ -0,0 +1,970 @@
1
+ # Dynamic Tools Recipe
2
+
3
+ This recipe shows how to create and register tools dynamically at runtime, enabling flexible and adaptable MCP servers.
4
+
5
+ ## Basic Dynamic Tool Creation
6
+
7
+ ### Runtime Tool Registration
8
+
9
+ ```ruby
10
+ class DynamicToolManager
11
+ def initialize(server)
12
+ @server = server
13
+ @dynamic_tools = {}
14
+ end
15
+
16
+ def create_tool(name, description, parameters, &block)
17
+ # Create a new tool class dynamically
18
+ tool_class = Class.new(Tsikol::Tool) do
19
+ # Set description
20
+ class_eval { description description }
21
+
22
+ # Define parameters
23
+ parameters.each do |param_name, param_config|
24
+ parameter param_name do
25
+ type param_config[:type]
26
+ param_config[:required] ? required : optional
27
+ default param_config[:default] if param_config[:default]
28
+ enum param_config[:enum] if param_config[:enum]
29
+ description param_config[:description]
30
+
31
+ if param_config[:complete]
32
+ complete(&param_config[:complete])
33
+ end
34
+ end
35
+ end
36
+
37
+ # Define execute method
38
+ define_method(:execute, &block)
39
+ end
40
+
41
+ # Set the tool name
42
+ tool_class.define_singleton_method(:name) { name }
43
+
44
+ # Register with server
45
+ @server.register_tool_class(tool_class)
46
+ @dynamic_tools[name] = tool_class
47
+
48
+ log :info, "Dynamic tool created", name: name
49
+
50
+ tool_class
51
+ end
52
+
53
+ def remove_tool(name)
54
+ if @dynamic_tools[name]
55
+ @server.unregister_tool(name)
56
+ @dynamic_tools.delete(name)
57
+ log :info, "Dynamic tool removed", name: name
58
+ true
59
+ else
60
+ false
61
+ end
62
+ end
63
+
64
+ def list_dynamic_tools
65
+ @dynamic_tools.keys
66
+ end
67
+ end
68
+
69
+ # Usage
70
+ Tsikol.start(name: "dynamic-server") do
71
+ tool_manager = DynamicToolManager.new(self)
72
+
73
+ # Create a dynamic tool
74
+ tool_manager.create_tool(
75
+ "custom_calculator",
76
+ "Performs custom calculations",
77
+ {
78
+ expression: {
79
+ type: :string,
80
+ required: true,
81
+ description: "Mathematical expression"
82
+ },
83
+ variables: {
84
+ type: :object,
85
+ required: false,
86
+ default: {},
87
+ description: "Variable values"
88
+ }
89
+ }
90
+ ) do |expression:, variables: {}|
91
+ # Tool implementation
92
+ evaluator = MathEvaluator.new(variables)
93
+ evaluator.evaluate(expression)
94
+ end
95
+
96
+ # Store tool manager for later use
97
+ @tool_manager = tool_manager
98
+ end
99
+ ```
100
+
101
+ ### Plugin-Based Tools
102
+
103
+ ```ruby
104
+ class PluginSystem
105
+ def initialize(server, plugin_dir = "plugins")
106
+ @server = server
107
+ @plugin_dir = plugin_dir
108
+ @loaded_plugins = {}
109
+ end
110
+
111
+ def load_plugins
112
+ Dir.glob(File.join(@plugin_dir, "*.rb")).each do |plugin_file|
113
+ load_plugin(plugin_file)
114
+ end
115
+ end
116
+
117
+ def load_plugin(plugin_file)
118
+ plugin_name = File.basename(plugin_file, ".rb")
119
+
120
+ begin
121
+ # Load plugin code
122
+ plugin_code = File.read(plugin_file)
123
+ plugin_module = Module.new
124
+ plugin_module.module_eval(plugin_code)
125
+
126
+ # Find tool classes in the plugin
127
+ tool_classes = plugin_module.constants.select do |const|
128
+ klass = plugin_module.const_get(const)
129
+ klass.is_a?(Class) && klass < Tsikol::Tool
130
+ end
131
+
132
+ # Register tools
133
+ tool_classes.each do |tool_const|
134
+ tool_class = plugin_module.const_get(tool_const)
135
+ @server.register_tool_class(tool_class)
136
+
137
+ @loaded_plugins[plugin_name] ||= []
138
+ @loaded_plugins[plugin_name] << tool_class
139
+ end
140
+
141
+ log :info, "Plugin loaded",
142
+ name: plugin_name,
143
+ tools: tool_classes.map(&:to_s)
144
+
145
+ true
146
+
147
+ rescue => e
148
+ log :error, "Failed to load plugin",
149
+ name: plugin_name,
150
+ error: e.message
151
+ false
152
+ end
153
+ end
154
+
155
+ def unload_plugin(plugin_name)
156
+ if tools = @loaded_plugins[plugin_name]
157
+ tools.each do |tool_class|
158
+ @server.unregister_tool(tool_class.name)
159
+ end
160
+
161
+ @loaded_plugins.delete(plugin_name)
162
+ log :info, "Plugin unloaded", name: plugin_name
163
+ true
164
+ else
165
+ false
166
+ end
167
+ end
168
+
169
+ def reload_plugin(plugin_name)
170
+ unload_plugin(plugin_name)
171
+ load_plugin(File.join(@plugin_dir, "#{plugin_name}.rb"))
172
+ end
173
+ end
174
+
175
+ # Example plugin file: plugins/string_tools.rb
176
+ class StringReverse < Tsikol::Tool
177
+ description "Reverses a string"
178
+
179
+ parameter :text do
180
+ type :string
181
+ required
182
+ description "Text to reverse"
183
+ end
184
+
185
+ def execute(text:)
186
+ text.reverse
187
+ end
188
+ end
189
+
190
+ class StringCapitalize < Tsikol::Tool
191
+ description "Capitalizes words in a string"
192
+
193
+ parameter :text do
194
+ type :string
195
+ required
196
+ description "Text to capitalize"
197
+ end
198
+
199
+ parameter :style do
200
+ type :string
201
+ optional
202
+ default "title"
203
+ enum ["title", "sentence", "all"]
204
+ end
205
+
206
+ def execute(text:, style: "title")
207
+ case style
208
+ when "title"
209
+ text.split.map(&:capitalize).join(" ")
210
+ when "sentence"
211
+ text.capitalize
212
+ when "all"
213
+ text.upcase
214
+ end
215
+ end
216
+ end
217
+ ```
218
+
219
+ ## Configuration-Based Tools
220
+
221
+ ### YAML Tool Definition
222
+
223
+ ```ruby
224
+ class ConfigBasedToolLoader
225
+ def initialize(server)
226
+ @server = server
227
+ end
228
+
229
+ def load_from_yaml(yaml_file)
230
+ config = YAML.load_file(yaml_file)
231
+
232
+ config["tools"].each do |tool_config|
233
+ create_tool_from_config(tool_config)
234
+ end
235
+ end
236
+
237
+ def create_tool_from_config(config)
238
+ tool_class = Class.new(Tsikol::Tool) do
239
+ description config["description"]
240
+
241
+ # Define parameters
242
+ config["parameters"].each do |param|
243
+ parameter param["name"].to_sym do
244
+ type param["type"].to_sym
245
+ param["required"] ? required : optional
246
+ default param["default"] if param["default"]
247
+ enum param["enum"] if param["enum"]
248
+ description param["description"]
249
+ end
250
+ end
251
+
252
+ # Define execute method based on type
253
+ case config["type"]
254
+ when "command"
255
+ define_method(:execute) do |**params|
256
+ execute_command(config["command"], params)
257
+ end
258
+ when "http"
259
+ define_method(:execute) do |**params|
260
+ execute_http_request(config["endpoint"], config["method"], params)
261
+ end
262
+ when "script"
263
+ define_method(:execute) do |**params|
264
+ execute_script(config["script"], params)
265
+ end
266
+ end
267
+ end
268
+
269
+ # Set tool name
270
+ tool_class.define_singleton_method(:name) { config["name"] }
271
+
272
+ # Add helper methods
273
+ tool_class.class_eval do
274
+ def execute_command(command_template, params)
275
+ command = substitute_params(command_template, params)
276
+
277
+ log :info, "Executing command", command: command
278
+
279
+ output = `#{command} 2>&1`
280
+ success = $?.success?
281
+
282
+ if success
283
+ output
284
+ else
285
+ raise "Command failed: #{output}"
286
+ end
287
+ end
288
+
289
+ def execute_http_request(endpoint_template, method, params)
290
+ require 'net/http'
291
+ require 'uri'
292
+
293
+ endpoint = substitute_params(endpoint_template, params)
294
+ uri = URI.parse(endpoint)
295
+
296
+ response = case method.upcase
297
+ when "GET"
298
+ Net::HTTP.get_response(uri)
299
+ when "POST"
300
+ Net::HTTP.post_form(uri, params)
301
+ else
302
+ raise "Unsupported HTTP method: #{method}"
303
+ end
304
+
305
+ if response.is_a?(Net::HTTPSuccess)
306
+ response.body
307
+ else
308
+ raise "HTTP request failed: #{response.code} #{response.message}"
309
+ end
310
+ end
311
+
312
+ def execute_script(script_template, params)
313
+ script = substitute_params(script_template, params)
314
+ eval(script)
315
+ end
316
+
317
+ def substitute_params(template, params)
318
+ result = template.dup
319
+ params.each do |key, value|
320
+ result.gsub!("{{#{key}}}", value.to_s)
321
+ end
322
+ result
323
+ end
324
+ end
325
+
326
+ @server.register_tool_class(tool_class)
327
+
328
+ log :info, "Tool created from config", name: config["name"]
329
+ end
330
+ end
331
+
332
+ # Example YAML configuration: tools.yaml
333
+ # tools:
334
+ # - name: disk_usage
335
+ # description: Check disk usage
336
+ # type: command
337
+ # command: "df -h {{path}}"
338
+ # parameters:
339
+ # - name: path
340
+ # type: string
341
+ # required: false
342
+ # default: "/"
343
+ # description: "Path to check"
344
+ #
345
+ # - name: weather
346
+ # description: Get weather information
347
+ # type: http
348
+ # endpoint: "http://api.weather.com/v1/weather?city={{city}}"
349
+ # method: GET
350
+ # parameters:
351
+ # - name: city
352
+ # type: string
353
+ # required: true
354
+ # description: "City name"
355
+ ```
356
+
357
+ ### Database-Driven Tools
358
+
359
+ ```ruby
360
+ class DatabaseToolManager
361
+ def initialize(server, database)
362
+ @server = server
363
+ @database = database
364
+ @loaded_tools = {}
365
+ end
366
+
367
+ def load_tools
368
+ tools = @database[:tools].where(enabled: true).all
369
+
370
+ tools.each do |tool_record|
371
+ create_tool_from_record(tool_record)
372
+ end
373
+ end
374
+
375
+ def create_tool_from_record(record)
376
+ tool_class = Class.new(Tsikol::Tool) do
377
+ description record[:description]
378
+
379
+ # Load parameters
380
+ parameters = JSON.parse(record[:parameters])
381
+ parameters.each do |param_name, param_config|
382
+ parameter param_name.to_sym do
383
+ type param_config["type"].to_sym
384
+ param_config["required"] ? required : optional
385
+ default param_config["default"] if param_config["default"]
386
+ description param_config["description"]
387
+ end
388
+ end
389
+
390
+ # Define execute method
391
+ implementation = record[:implementation]
392
+ implementation_type = record[:implementation_type]
393
+
394
+ case implementation_type
395
+ when "ruby"
396
+ # Direct Ruby code
397
+ class_eval <<~RUBY
398
+ def execute(**params)
399
+ #{implementation}
400
+ end
401
+ RUBY
402
+ when "sql"
403
+ # SQL query
404
+ define_method(:execute) do |**params|
405
+ execute_sql_query(implementation, params)
406
+ end
407
+ when "api"
408
+ # API call configuration
409
+ config = JSON.parse(implementation)
410
+ define_method(:execute) do |**params|
411
+ execute_api_call(config, params)
412
+ end
413
+ end
414
+ end
415
+
416
+ # Set tool name
417
+ tool_class.define_singleton_method(:name) { record[:name] }
418
+ tool_class.define_singleton_method(:tool_id) { record[:id] }
419
+
420
+ # Add database helper methods
421
+ tool_class.class_eval do
422
+ def execute_sql_query(query_template, params)
423
+ query = substitute_params(query_template, params)
424
+
425
+ log :info, "Executing SQL", query: query
426
+
427
+ results = @database.fetch(query).all
428
+ results.to_json
429
+ end
430
+
431
+ def execute_api_call(config, params)
432
+ # Implementation for API calls
433
+ # Similar to HTTP implementation above
434
+ end
435
+ end
436
+
437
+ @server.register_tool_class(tool_class)
438
+ @loaded_tools[record[:id]] = tool_class
439
+
440
+ log :info, "Tool loaded from database",
441
+ name: record[:name],
442
+ id: record[:id]
443
+ end
444
+
445
+ def reload_tool(tool_id)
446
+ # Remove old version
447
+ if old_tool = @loaded_tools[tool_id]
448
+ @server.unregister_tool(old_tool.name)
449
+ @loaded_tools.delete(tool_id)
450
+ end
451
+
452
+ # Load new version
453
+ if record = @database[:tools].where(id: tool_id, enabled: true).first
454
+ create_tool_from_record(record)
455
+ end
456
+ end
457
+
458
+ def sync_tools
459
+ # Get current tool IDs from database
460
+ db_tool_ids = @database[:tools].where(enabled: true).select_map(:id)
461
+
462
+ # Remove tools that no longer exist
463
+ (@loaded_tools.keys - db_tool_ids).each do |tool_id|
464
+ if tool = @loaded_tools[tool_id]
465
+ @server.unregister_tool(tool.name)
466
+ @loaded_tools.delete(tool_id)
467
+ end
468
+ end
469
+
470
+ # Add new tools
471
+ (db_tool_ids - @loaded_tools.keys).each do |tool_id|
472
+ if record = @database[:tools].where(id: tool_id).first
473
+ create_tool_from_record(record)
474
+ end
475
+ end
476
+
477
+ # Reload modified tools
478
+ @loaded_tools.each do |tool_id, tool_class|
479
+ record = @database[:tools].where(id: tool_id).first
480
+ if record && record[:updated_at] > tool_class.loaded_at
481
+ reload_tool(tool_id)
482
+ end
483
+ end
484
+ end
485
+ end
486
+ ```
487
+
488
+ ## Hot Reloading Tools
489
+
490
+ ### File Watcher System
491
+
492
+ ```ruby
493
+ require 'listen'
494
+
495
+ class HotReloadManager
496
+ def initialize(server, watch_dir = "app/tools")
497
+ @server = server
498
+ @watch_dir = watch_dir
499
+ @loaded_files = {}
500
+ end
501
+
502
+ def start
503
+ @listener = Listen.to(@watch_dir) do |modified, added, removed|
504
+ handle_file_changes(modified, added, removed)
505
+ end
506
+
507
+ # Load initial tools
508
+ load_all_tools
509
+
510
+ # Start watching
511
+ @listener.start
512
+
513
+ log :info, "Hot reload started", directory: @watch_dir
514
+ end
515
+
516
+ def stop
517
+ @listener&.stop
518
+ end
519
+
520
+ private
521
+
522
+ def handle_file_changes(modified, added, removed)
523
+ (modified + added).each do |file|
524
+ reload_tool_file(file)
525
+ end
526
+
527
+ removed.each do |file|
528
+ unload_tool_file(file)
529
+ end
530
+ end
531
+
532
+ def load_all_tools
533
+ Dir.glob(File.join(@watch_dir, "**/*.rb")).each do |file|
534
+ load_tool_file(file)
535
+ end
536
+ end
537
+
538
+ def reload_tool_file(file)
539
+ # Unload old version
540
+ unload_tool_file(file)
541
+
542
+ # Load new version
543
+ load_tool_file(file)
544
+ end
545
+
546
+ def load_tool_file(file)
547
+ begin
548
+ # Create isolated namespace
549
+ namespace = Module.new
550
+
551
+ # Load file in namespace
552
+ namespace.module_eval(File.read(file), file)
553
+
554
+ # Find tool classes
555
+ tool_classes = find_tool_classes(namespace)
556
+
557
+ # Register tools
558
+ tool_classes.each do |tool_class|
559
+ @server.register_tool_class(tool_class)
560
+ end
561
+
562
+ @loaded_files[file] = {
563
+ namespace: namespace,
564
+ tools: tool_classes
565
+ }
566
+
567
+ log :info, "Tools loaded",
568
+ file: file,
569
+ count: tool_classes.size
570
+
571
+ rescue => e
572
+ log :error, "Failed to load tool file",
573
+ file: file,
574
+ error: e.message
575
+ end
576
+ end
577
+
578
+ def unload_tool_file(file)
579
+ if loaded = @loaded_files[file]
580
+ loaded[:tools].each do |tool_class|
581
+ @server.unregister_tool(tool_class.name)
582
+ end
583
+
584
+ @loaded_files.delete(file)
585
+
586
+ log :info, "Tools unloaded", file: file
587
+ end
588
+ end
589
+
590
+ def find_tool_classes(namespace)
591
+ namespace.constants.map { |const|
592
+ namespace.const_get(const)
593
+ }.select { |const|
594
+ const.is_a?(Class) && const < Tsikol::Tool
595
+ }
596
+ end
597
+ end
598
+
599
+ # Usage
600
+ Tsikol.start(name: "hot-reload-server") do
601
+ # Enable hot reloading in development
602
+ if ENV['RACK_ENV'] == 'development'
603
+ hot_reload = HotReloadManager.new(self)
604
+ hot_reload.start
605
+
606
+ before_stop do
607
+ hot_reload.stop
608
+ end
609
+ else
610
+ # Load tools normally in production
611
+ Dir.glob('app/tools/**/*.rb').each { |f| require_relative f }
612
+ end
613
+ end
614
+ ```
615
+
616
+ ## AI-Generated Tools
617
+
618
+ ### Tool Generation from Description
619
+
620
+ ```ruby
621
+ class AiToolGenerator
622
+ def initialize(server)
623
+ @server = server
624
+ end
625
+
626
+ def generate_tool(description, examples = [])
627
+ # Use sampling to generate tool implementation
628
+ prompt = build_generation_prompt(description, examples)
629
+
630
+ response = @server.sample_text(
631
+ messages: [
632
+ {
633
+ role: "system",
634
+ content: {
635
+ type: "text",
636
+ text: SYSTEM_PROMPT
637
+ }
638
+ },
639
+ {
640
+ role: "user",
641
+ content: {
642
+ type: "text",
643
+ text: prompt
644
+ }
645
+ }
646
+ ],
647
+ temperature: 0.3,
648
+ max_tokens: 2000
649
+ )
650
+
651
+ if response[:error]
652
+ raise "Failed to generate tool: #{response[:error]}"
653
+ end
654
+
655
+ # Parse generated code
656
+ tool_code = extract_tool_code(response[:text])
657
+
658
+ # Create and register tool
659
+ create_tool_from_code(tool_code)
660
+ end
661
+
662
+ private
663
+
664
+ SYSTEM_PROMPT = <<~PROMPT
665
+ You are an expert Ruby developer creating MCP tools.
666
+ Generate complete, working tool implementations following this pattern:
667
+
668
+ class ToolName < Tsikol::Tool
669
+ description "Clear description"
670
+
671
+ parameter :param_name do
672
+ type :string
673
+ required
674
+ description "Parameter description"
675
+ end
676
+
677
+ def execute(param_name:)
678
+ # Implementation
679
+ end
680
+ end
681
+
682
+ Ensure the code is safe, efficient, and handles errors properly.
683
+ PROMPT
684
+
685
+ def build_generation_prompt(description, examples)
686
+ prompt = "Create a tool with the following requirements:\n\n"
687
+ prompt += "Description: #{description}\n\n"
688
+
689
+ if examples.any?
690
+ prompt += "Examples:\n"
691
+ examples.each_with_index do |example, i|
692
+ prompt += "#{i + 1}. Input: #{example[:input]}\n"
693
+ prompt += " Output: #{example[:output]}\n"
694
+ end
695
+ end
696
+
697
+ prompt
698
+ end
699
+
700
+ def extract_tool_code(response)
701
+ # Extract Ruby code from response
702
+ if match = response.match(/```ruby\n(.*?)```/m)
703
+ match[1]
704
+ else
705
+ response
706
+ end
707
+ end
708
+
709
+ def create_tool_from_code(code)
710
+ # Create isolated namespace
711
+ namespace = Module.new
712
+
713
+ # Evaluate code
714
+ namespace.module_eval(code)
715
+
716
+ # Find tool class
717
+ tool_class = namespace.constants.map { |c|
718
+ namespace.const_get(c)
719
+ }.find { |c|
720
+ c.is_a?(Class) && c < Tsikol::Tool
721
+ }
722
+
723
+ if tool_class
724
+ @server.register_tool_class(tool_class)
725
+ log :info, "AI-generated tool registered", name: tool_class.name
726
+ tool_class
727
+ else
728
+ raise "No valid tool class found in generated code"
729
+ end
730
+ end
731
+ end
732
+
733
+ # Usage
734
+ generator = AiToolGenerator.new(server)
735
+
736
+ tool = generator.generate_tool(
737
+ "Create a tool that converts between different units of measurement",
738
+ [
739
+ { input: { value: 100, from: "celsius", to: "fahrenheit" },
740
+ output: "212°F" },
741
+ { input: { value: 1, from: "mile", to: "kilometer" },
742
+ output: "1.60934 km" }
743
+ ]
744
+ )
745
+ ```
746
+
747
+ ## Tool Composition
748
+
749
+ ### Composite Tools
750
+
751
+ ```ruby
752
+ class CompositeToolBuilder
753
+ def initialize(server)
754
+ @server = server
755
+ end
756
+
757
+ def build_composite(name, description, steps)
758
+ tool_class = Class.new(Tsikol::Tool) do
759
+ description description
760
+
761
+ # Collect all parameters from component tools
762
+ all_params = {}
763
+ steps.each do |step|
764
+ tool = step[:tool]
765
+ step[:params].each do |param_name, source|
766
+ if source.is_a?(Symbol) && !all_params[source]
767
+ # This is an input parameter
768
+ all_params[source] = true
769
+ end
770
+ end
771
+ end
772
+
773
+ # Define parameters
774
+ all_params.keys.each do |param|
775
+ parameter param do
776
+ type :string # Default type
777
+ required
778
+ description "Input for #{param}"
779
+ end
780
+ end
781
+
782
+ # Define execute method
783
+ define_method(:execute) do |**params|
784
+ results = {}
785
+
786
+ steps.each_with_index do |step, index|
787
+ tool_name = step[:tool]
788
+ tool_params = {}
789
+
790
+ # Build parameters for this tool
791
+ step[:params].each do |param_name, source|
792
+ tool_params[param_name] = if source.is_a?(Symbol)
793
+ # Reference to input parameter or previous result
794
+ results[source] || params[source]
795
+ else
796
+ # Literal value
797
+ source
798
+ end
799
+ end
800
+
801
+ # Execute tool
802
+ log :debug, "Executing step #{index + 1}",
803
+ tool: tool_name,
804
+ params: tool_params
805
+
806
+ result = @server.execute_tool(tool_name, tool_params)
807
+
808
+ # Store result
809
+ results[step[:store_as] || :"step_#{index}"] = result
810
+ end
811
+
812
+ # Return final result
813
+ final_step = steps.last
814
+ results[final_step[:store_as] || :"step_#{steps.size - 1}"]
815
+ end
816
+ end
817
+
818
+ # Set tool name
819
+ tool_class.define_singleton_method(:name) { name }
820
+
821
+ @server.register_tool_class(tool_class)
822
+ tool_class
823
+ end
824
+ end
825
+
826
+ # Usage
827
+ builder = CompositeToolBuilder.new(server)
828
+
829
+ # Create a composite tool that:
830
+ # 1. Fetches data from an API
831
+ # 2. Processes the data
832
+ # 3. Generates a report
833
+ builder.build_composite(
834
+ "data_pipeline",
835
+ "Fetches and processes data to generate a report",
836
+ [
837
+ {
838
+ tool: "http_request",
839
+ params: {
840
+ url: :api_url, # From input parameter
841
+ method: "GET" # Literal value
842
+ },
843
+ store_as: :raw_data
844
+ },
845
+ {
846
+ tool: "json_parser",
847
+ params: {
848
+ json: :raw_data # From previous step
849
+ },
850
+ store_as: :parsed_data
851
+ },
852
+ {
853
+ tool: "report_generator",
854
+ params: {
855
+ data: :parsed_data, # From previous step
856
+ format: :report_format # From input parameter
857
+ },
858
+ store_as: :report
859
+ }
860
+ ]
861
+ )
862
+ ```
863
+
864
+ ## Testing Dynamic Tools
865
+
866
+ ```ruby
867
+ require 'minitest/autorun'
868
+
869
+ class DynamicToolTest < Minitest::Test
870
+ def setup
871
+ @server = Tsikol::Server.new(name: "test")
872
+ @manager = DynamicToolManager.new(@server)
873
+ @client = Tsikol::TestHelpers::TestClient.new(@server)
874
+ end
875
+
876
+ def test_creates_dynamic_tool
877
+ @manager.create_tool(
878
+ "test_adder",
879
+ "Adds two numbers",
880
+ {
881
+ a: { type: :number, required: true, description: "First number" },
882
+ b: { type: :number, required: true, description: "Second number" }
883
+ }
884
+ ) do |a:, b:|
885
+ a + b
886
+ end
887
+
888
+ response = @client.call_tool("test_adder", { "a" => 5, "b" => 3 })
889
+
890
+ assert_successful_response(response)
891
+ assert_equal 8, response.dig(:result, :content, 0, :text)
892
+ end
893
+
894
+ def test_removes_dynamic_tool
895
+ @manager.create_tool("temporary", "Temp tool", {}) { "temp" }
896
+
897
+ # Tool exists
898
+ response = @client.call_tool("temporary", {})
899
+ assert_successful_response(response)
900
+
901
+ # Remove tool
902
+ @manager.remove_tool("temporary")
903
+
904
+ # Tool no longer exists
905
+ response = @client.call_tool("temporary", {})
906
+ assert_error_response(response, -32601) # Method not found
907
+ end
908
+
909
+ def test_hot_reload
910
+ # Create a temporary tool file
911
+ tool_file = "test_tool.rb"
912
+ File.write(tool_file, <<~RUBY)
913
+ class TestTool < Tsikol::Tool
914
+ description "Test tool v1"
915
+
916
+ def execute
917
+ "Version 1"
918
+ end
919
+ end
920
+ RUBY
921
+
922
+ # Load tool
923
+ loader = HotReloadManager.new(@server, ".")
924
+ loader.send(:load_tool_file, tool_file)
925
+
926
+ response = @client.call_tool("test_tool", {})
927
+ assert_equal "Version 1", response.dig(:result, :content, 0, :text)
928
+
929
+ # Modify tool
930
+ File.write(tool_file, <<~RUBY)
931
+ class TestTool < Tsikol::Tool
932
+ description "Test tool v2"
933
+
934
+ def execute
935
+ "Version 2"
936
+ end
937
+ end
938
+ RUBY
939
+
940
+ # Reload
941
+ loader.send(:reload_tool_file, tool_file)
942
+
943
+ response = @client.call_tool("test_tool", {})
944
+ assert_equal "Version 2", response.dig(:result, :content, 0, :text)
945
+
946
+ ensure
947
+ File.delete(tool_file) if File.exist?(tool_file)
948
+ end
949
+ end
950
+ ```
951
+
952
+ ## Best Practices
953
+
954
+ 1. **Validate dynamic code** before execution
955
+ 2. **Sandbox untrusted code** for security
956
+ 3. **Version control** for dynamic tools
957
+ 4. **Monitor performance** of generated tools
958
+ 5. **Implement rollback** for tool updates
959
+ 6. **Test thoroughly** especially for AI-generated tools
960
+ 7. **Log all changes** for audit trail
961
+ 8. **Set resource limits** for dynamic tools
962
+ 9. **Use namespacing** to avoid conflicts
963
+ 10. **Cache compiled tools** for performance
964
+
965
+ ## Next Steps
966
+
967
+ - Implement [Security](security.md) for dynamic code execution
968
+ - Add [Monitoring](monitoring.md) for tool usage
969
+ - Set up [Testing](../guides/testing.md) for dynamic tools
970
+ - Review [Error Handling](error-handling.md) for tool failures