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,627 @@
1
+ # Tools Guide
2
+
3
+ Tools are functions that MCP clients can call to perform actions. This guide covers everything you need to know about creating and using tools in Tsikol.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [What are Tools?](#what-are-tools)
8
+ 2. [Creating Tools](#creating-tools)
9
+ 3. [Parameters](#parameters)
10
+ 4. [Parameter Types](#parameter-types)
11
+ 5. [Completions](#completions)
12
+ 6. [Error Handling](#error-handling)
13
+ 7. [Logging](#logging)
14
+ 8. [Advanced Patterns](#advanced-patterns)
15
+ 9. [Testing Tools](#testing-tools)
16
+
17
+ ## What are Tools?
18
+
19
+ Tools are the primary way MCP clients interact with your server. They:
20
+ - Accept parameters
21
+ - Perform actions
22
+ - Return results
23
+ - Can have side effects
24
+
25
+ Think of tools as API endpoints that AI can call.
26
+
27
+ ## Creating Tools
28
+
29
+ ### Inline Tools
30
+
31
+ Quick tools defined directly in server.rb:
32
+
33
+ ```ruby
34
+ Tsikol.server "my-server" do
35
+ tool "greet" do |name:|
36
+ "Hello, #{name}!"
37
+ end
38
+
39
+ tool "calculate" do |a:, b:, operation: "add"|
40
+ case operation
41
+ when "add" then a + b
42
+ when "subtract" then a - b
43
+ when "multiply" then a * b
44
+ when "divide" then b.zero? ? "Error: Division by zero" : a.to_f / b
45
+ end
46
+ end
47
+ end
48
+ ```
49
+
50
+ ### Class-Based Tools
51
+
52
+ For complex tools, use classes:
53
+
54
+ ```ruby
55
+ # app/tools/file_processor.rb
56
+ class FileProcessor < Tsikol::Tool
57
+ description "Process files with various operations"
58
+
59
+ parameter :file_path do
60
+ type :string
61
+ required
62
+ description "Path to the file to process"
63
+
64
+ complete do |partial|
65
+ Dir.glob("#{partial}*")
66
+ end
67
+ end
68
+
69
+ parameter :operation do
70
+ type :string
71
+ required
72
+ enum ["read", "analyze", "compress", "convert"]
73
+ description "Operation to perform"
74
+ end
75
+
76
+ parameter :options do
77
+ type :object
78
+ optional
79
+ description "Additional options for the operation"
80
+ end
81
+
82
+ def execute(file_path:, operation:, options: {})
83
+ unless File.exist?(file_path)
84
+ raise Tsikol::ValidationError, "File not found: #{file_path}"
85
+ end
86
+
87
+ case operation
88
+ when "read"
89
+ read_file(file_path, options)
90
+ when "analyze"
91
+ analyze_file(file_path, options)
92
+ when "compress"
93
+ compress_file(file_path, options)
94
+ when "convert"
95
+ convert_file(file_path, options)
96
+ end
97
+ end
98
+
99
+ private
100
+
101
+ def read_file(path, options)
102
+ encoding = options["encoding"] || "UTF-8"
103
+ File.read(path, encoding: encoding)
104
+ end
105
+
106
+ def analyze_file(path, options)
107
+ content = File.read(path)
108
+ {
109
+ size: File.size(path),
110
+ lines: content.lines.count,
111
+ words: content.split.count,
112
+ type: File.extname(path)
113
+ }.to_json
114
+ end
115
+
116
+ def compress_file(path, options)
117
+ # Implementation
118
+ "Compressed #{path}"
119
+ end
120
+
121
+ def convert_file(path, options)
122
+ # Implementation
123
+ "Converted #{path}"
124
+ end
125
+ end
126
+ ```
127
+
128
+ ## Parameters
129
+
130
+ ### Basic Parameters
131
+
132
+ ```ruby
133
+ parameter :name do
134
+ type :string
135
+ required
136
+ description "User's name"
137
+ end
138
+
139
+ parameter :age do
140
+ type :integer
141
+ optional
142
+ default 18
143
+ description "User's age"
144
+ end
145
+ ```
146
+
147
+ ### Parameter Options
148
+
149
+ All available parameter options:
150
+
151
+ ```ruby
152
+ parameter :example do
153
+ type :string # :string, :number, :integer, :boolean, :array, :object
154
+ required # or optional
155
+ default "value" # Default value if optional
156
+ description "Purpose" # What this parameter does
157
+ enum ["opt1", "opt2"] # Allowed values
158
+
159
+ complete do |partial| # Autocomplete suggestions
160
+ # Return array of suggestions
161
+ end
162
+ end
163
+ ```
164
+
165
+ ## Parameter Types
166
+
167
+ ### String Parameters
168
+
169
+ ```ruby
170
+ parameter :message do
171
+ type :string
172
+ required
173
+ description "Message to process"
174
+ end
175
+
176
+ parameter :format do
177
+ type :string
178
+ optional
179
+ default "plain"
180
+ enum ["plain", "markdown", "html"]
181
+ end
182
+ ```
183
+
184
+ ### Number Parameters
185
+
186
+ ```ruby
187
+ parameter :amount do
188
+ type :number
189
+ required
190
+ description "Amount in dollars"
191
+ end
192
+
193
+ parameter :percentage do
194
+ type :number
195
+ optional
196
+ default 0.0
197
+ description "Percentage (0-100)"
198
+ end
199
+ ```
200
+
201
+ ### Boolean Parameters
202
+
203
+ ```ruby
204
+ parameter :verbose do
205
+ type :boolean
206
+ optional
207
+ default false
208
+ description "Enable verbose output"
209
+ end
210
+ ```
211
+
212
+ ### Array Parameters
213
+
214
+ ```ruby
215
+ parameter :tags do
216
+ type :array
217
+ optional
218
+ default []
219
+ description "List of tags"
220
+ end
221
+
222
+ def execute(tags: [])
223
+ # tags is an array
224
+ tags.join(", ")
225
+ end
226
+ ```
227
+
228
+ ### Object Parameters
229
+
230
+ ```ruby
231
+ parameter :config do
232
+ type :object
233
+ optional
234
+ description "Configuration object"
235
+ end
236
+
237
+ def execute(config: {})
238
+ # config is a hash
239
+ timeout = config["timeout"] || 30
240
+ retries = config["retries"] || 3
241
+ end
242
+ ```
243
+
244
+ ## Completions
245
+
246
+ Add intelligent autocomplete to parameters:
247
+
248
+ ### Static Completions
249
+
250
+ ```ruby
251
+ parameter :language do
252
+ type :string
253
+ required
254
+
255
+ complete do |partial|
256
+ languages = ["ruby", "python", "javascript", "go", "rust"]
257
+ languages.select { |lang| lang.start_with?(partial.downcase) }
258
+ end
259
+ end
260
+ ```
261
+
262
+ ### Dynamic Completions
263
+
264
+ ```ruby
265
+ parameter :file do
266
+ type :string
267
+ required
268
+
269
+ complete do |partial|
270
+ # File system completion
271
+ Dir.glob("#{partial}*").map do |path|
272
+ File.directory?(path) ? "#{path}/" : path
273
+ end
274
+ end
275
+ end
276
+ ```
277
+
278
+ ### Context-Aware Completions
279
+
280
+ ```ruby
281
+ parameter :branch do
282
+ type :string
283
+ required
284
+
285
+ complete do |partial, context|
286
+ # Access other parameters via context
287
+ repo = context[:repository]
288
+
289
+ branches = fetch_branches_for_repo(repo)
290
+ branches.select { |b| b.include?(partial) }
291
+ end
292
+ end
293
+ ```
294
+
295
+ ## Error Handling
296
+
297
+ ### Validation Errors
298
+
299
+ ```ruby
300
+ def execute(file_path:)
301
+ unless File.exist?(file_path)
302
+ raise Tsikol::ValidationError, "File not found: #{file_path}"
303
+ end
304
+
305
+ unless File.readable?(file_path)
306
+ raise Tsikol::ValidationError, "File not readable: #{file_path}"
307
+ end
308
+
309
+ process_file(file_path)
310
+ end
311
+ ```
312
+
313
+ ### Graceful Degradation
314
+
315
+ ```ruby
316
+ def execute(url:)
317
+ begin
318
+ response = fetch_url(url)
319
+ process_response(response)
320
+ rescue Net::HTTPError => e
321
+ log :error, "HTTP error", data: { url: url, error: e.message }
322
+ "Unable to fetch URL: #{e.message}"
323
+ rescue => e
324
+ log :error, "Unexpected error", data: { error: e.class.name }
325
+ "An error occurred. Please try again."
326
+ end
327
+ end
328
+ ```
329
+
330
+ ## Logging
331
+
332
+ ### Basic Logging
333
+
334
+ ```ruby
335
+ def execute(task:)
336
+ log :info, "Starting task", data: { task: task }
337
+
338
+ result = perform_task(task)
339
+
340
+ log :info, "Task completed", data: { task: task, result: result }
341
+
342
+ result
343
+ end
344
+ ```
345
+
346
+ ### Log Levels
347
+
348
+ ```ruby
349
+ log :debug, "Detailed information"
350
+ log :info, "General information"
351
+ log :warning, "Warning message"
352
+ log :error, "Error occurred", data: { error: error.message }
353
+ ```
354
+
355
+ ### Enabling Logging
356
+
357
+ ```ruby
358
+ def set_server(server)
359
+ @server = server
360
+
361
+ define_singleton_method(:log) do |level, message, data: nil|
362
+ @server.log(level, message, data: data)
363
+ end
364
+ end
365
+ ```
366
+
367
+ ## Advanced Patterns
368
+
369
+ ### Async Operations
370
+
371
+ ```ruby
372
+ class AsyncProcessor < Tsikol::Tool
373
+ description "Process data asynchronously"
374
+
375
+ parameter :data do
376
+ type :string
377
+ required
378
+ end
379
+
380
+ parameter :callback_url do
381
+ type :string
382
+ optional
383
+ description "URL to call when complete"
384
+ end
385
+
386
+ def execute(data:, callback_url: nil)
387
+ job_id = SecureRandom.uuid
388
+
389
+ # Start async job
390
+ Thread.new do
391
+ result = process_data(data)
392
+
393
+ if callback_url
394
+ notify_completion(callback_url, job_id, result)
395
+ end
396
+ end
397
+
398
+ {
399
+ job_id: job_id,
400
+ status: "processing",
401
+ message: "Job started. Use job_id to check status."
402
+ }.to_json
403
+ end
404
+ end
405
+ ```
406
+
407
+ ### Batch Operations
408
+
409
+ ```ruby
410
+ class BatchProcessor < Tsikol::Tool
411
+ description "Process multiple items"
412
+
413
+ parameter :items do
414
+ type :array
415
+ required
416
+ description "Items to process"
417
+ end
418
+
419
+ parameter :parallel do
420
+ type :boolean
421
+ optional
422
+ default false
423
+ description "Process in parallel"
424
+ end
425
+
426
+ def execute(items:, parallel: false)
427
+ if parallel
428
+ process_parallel(items)
429
+ else
430
+ process_sequential(items)
431
+ end
432
+ end
433
+
434
+ private
435
+
436
+ def process_parallel(items)
437
+ threads = items.map do |item|
438
+ Thread.new { process_item(item) }
439
+ end
440
+
441
+ results = threads.map(&:value)
442
+ format_results(results)
443
+ end
444
+
445
+ def process_sequential(items)
446
+ results = items.map { |item| process_item(item) }
447
+ format_results(results)
448
+ end
449
+ end
450
+ ```
451
+
452
+ ### Tool Composition
453
+
454
+ ```ruby
455
+ class CompositeTool < Tsikol::Tool
456
+ description "Combines multiple operations"
457
+
458
+ def execute(input:)
459
+ # Step 1: Validate
460
+ validated = ValidationTool.new.execute(data: input)
461
+
462
+ # Step 2: Process
463
+ processed = ProcessingTool.new.execute(data: validated)
464
+
465
+ # Step 3: Format
466
+ FormattingTool.new.execute(data: processed)
467
+ end
468
+ end
469
+ ```
470
+
471
+ ### Caching Results
472
+
473
+ ```ruby
474
+ class CachedTool < Tsikol::Tool
475
+ def initialize
476
+ super
477
+ @cache = {}
478
+ end
479
+
480
+ def execute(query:)
481
+ cache_key = generate_cache_key(query)
482
+
483
+ if cached = @cache[cache_key]
484
+ log :debug, "Cache hit", data: { key: cache_key }
485
+ return cached[:result]
486
+ end
487
+
488
+ result = expensive_operation(query)
489
+
490
+ @cache[cache_key] = {
491
+ result: result,
492
+ timestamp: Time.now
493
+ }
494
+
495
+ # Clean old entries
496
+ clean_cache if @cache.size > 100
497
+
498
+ result
499
+ end
500
+
501
+ private
502
+
503
+ def generate_cache_key(query)
504
+ Digest::SHA256.hexdigest(query)
505
+ end
506
+
507
+ def clean_cache
508
+ cutoff = Time.now - 3600 # 1 hour
509
+ @cache.delete_if { |_, v| v[:timestamp] < cutoff }
510
+ end
511
+ end
512
+ ```
513
+
514
+ ## Testing Tools
515
+
516
+ ### Basic Tool Test
517
+
518
+ ```ruby
519
+ require 'minitest/autorun'
520
+ require 'tsikol/test_helpers'
521
+
522
+ class FileProcessorTest < Minitest::Test
523
+ include Tsikol::TestHelpers::Assertions
524
+
525
+ def setup
526
+ @server = Tsikol::Server.new(name: "test")
527
+ @server.register_tool_instance(FileProcessor.new)
528
+ @client = Tsikol::TestHelpers::TestClient.new(@server)
529
+ @client.initialize_connection
530
+ end
531
+
532
+ def test_read_file
533
+ # Create test file
534
+ File.write("test.txt", "Hello, World!")
535
+
536
+ response = @client.call_tool("file_processor", {
537
+ "file_path" => "test.txt",
538
+ "operation" => "read"
539
+ })
540
+
541
+ assert_successful_response(response)
542
+ result = response.dig(:result, :content, 0, :text)
543
+ assert_equal "Hello, World!", result
544
+ ensure
545
+ File.delete("test.txt") if File.exist?("test.txt")
546
+ end
547
+
548
+ def test_file_not_found
549
+ response = @client.call_tool("file_processor", {
550
+ "file_path" => "nonexistent.txt",
551
+ "operation" => "read"
552
+ })
553
+
554
+ assert_error_response(response, -32603)
555
+ assert_match /File not found/, response[:error][:message]
556
+ end
557
+ end
558
+ ```
559
+
560
+ ### Testing Completions
561
+
562
+ ```ruby
563
+ def test_language_completion
564
+ response = @client.complete(
565
+ { type: "ref/tool", name: "code_analyzer" },
566
+ { name: "language", value: "ru" }
567
+ )
568
+
569
+ assert_successful_response(response)
570
+ values = response.dig(:result, :completion, :values)
571
+ assert_includes values, "ruby"
572
+ assert_includes values, "rust"
573
+ end
574
+ ```
575
+
576
+ ### Mocking External Dependencies
577
+
578
+ ```ruby
579
+ class ExternalApiToolTest < Minitest::Test
580
+ def setup
581
+ @server = Tsikol::Server.new(name: "test")
582
+ @tool = ExternalApiTool.new
583
+
584
+ # Mock external API
585
+ @tool.define_singleton_method(:fetch_from_api) do |endpoint|
586
+ case endpoint
587
+ when "/users"
588
+ [{ id: 1, name: "Test User" }]
589
+ when "/error"
590
+ raise "API Error"
591
+ else
592
+ []
593
+ end
594
+ end
595
+
596
+ @server.register_tool_instance(@tool)
597
+ @client = Tsikol::TestHelpers::TestClient.new(@server)
598
+ end
599
+
600
+ def test_successful_api_call
601
+ response = @client.call_tool("external_api", {
602
+ "endpoint" => "/users"
603
+ })
604
+
605
+ assert_successful_response(response)
606
+ result = JSON.parse(response.dig(:result, :content, 0, :text))
607
+ assert_equal 1, result.first["id"]
608
+ end
609
+ end
610
+ ```
611
+
612
+ ## Best Practices
613
+
614
+ 1. **Clear Descriptions**: Help users understand what your tool does
615
+ 2. **Validate Input**: Check parameters before processing
616
+ 3. **Handle Errors**: Provide helpful error messages
617
+ 4. **Add Completions**: Make tools easier to use
618
+ 5. **Log Important Events**: Aid debugging and monitoring
619
+ 6. **Keep It Focused**: Each tool should do one thing well
620
+ 7. **Test Thoroughly**: Cover success and error cases
621
+
622
+ ## Next Steps
623
+
624
+ - Learn about [Resources](resources.md)
625
+ - Explore [Prompts](prompts.md)
626
+ - Add [Completions](completion.md)
627
+ - Set up [Testing](testing.md)
@@ -0,0 +1,92 @@
1
+ # Tsikol Examples
2
+
3
+ This directory contains example MCP servers built with Tsikol, demonstrating various features and patterns.
4
+
5
+ ## Examples Overview
6
+
7
+ ### 1. [basic.rb](basic.rb)
8
+ A simple inline server showing the original DSL-style API. Great for quick prototypes.
9
+
10
+ ```bash
11
+ ruby examples/basic.rb
12
+ ```
13
+
14
+ ### 2. [basic-migrated/](basic-migrated/)
15
+ Shows how to migrate from inline DSL to the Rails-like structure with separate files.
16
+
17
+ ```bash
18
+ cd examples/basic-migrated
19
+ ./server.rb
20
+ ```
21
+
22
+ ### 3. [weather-service/](weather-service/)
23
+ Full-featured example with namespaced modules and proper project structure.
24
+
25
+ ```bash
26
+ cd examples/weather-service
27
+ ./server.rb
28
+ ```
29
+
30
+ ### 4. [middleware_example.rb](middleware_example.rb)
31
+ Demonstrates the middleware system with authentication, rate limiting, and custom middleware.
32
+
33
+ ```bash
34
+ ruby examples/middleware_example.rb
35
+ ```
36
+
37
+ ### 5. [sampling_example.rb](sampling_example.rb)
38
+ Shows how to use sampling for AI-assisted features.
39
+
40
+ ```bash
41
+ ruby examples/sampling_example.rb
42
+ ```
43
+
44
+ ### 6. [advanced_features.rb](advanced_features.rb)
45
+ Demonstrates lifecycle hooks, error handling, and health monitoring.
46
+
47
+ ```bash
48
+ ruby examples/advanced_features.rb
49
+ ```
50
+
51
+ ### 7. [full_featured.rb](full_featured.rb)
52
+ Complete example showing all Tsikol features working together.
53
+
54
+ ```bash
55
+ ruby examples/full_featured.rb
56
+ ```
57
+
58
+ ## Running Examples
59
+
60
+ All examples can be run directly:
61
+
62
+ ```bash
63
+ # From the gem root
64
+ ruby examples/basic.rb
65
+
66
+ # Or make them executable
67
+ chmod +x examples/basic.rb
68
+ ./examples/basic.rb
69
+ ```
70
+
71
+ For structured examples with directories:
72
+
73
+ ```bash
74
+ cd examples/weather-service
75
+ bundle install # If needed
76
+ ./server.rb
77
+ ```
78
+
79
+ ## Testing Examples
80
+
81
+ Connect with MCP Inspector or any MCP client to test the servers.
82
+
83
+ ## Creating Your Own
84
+
85
+ Use the Tsikol CLI to create your own project:
86
+
87
+ ```bash
88
+ tsikol new my-project
89
+ cd my-project
90
+ bundle install
91
+ ./server.rb
92
+ ```