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,720 @@
1
+ # Resources Guide
2
+
3
+ Resources provide read-only data that MCP clients can access. They're perfect for exposing configuration, status information, documentation, and other data.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [What are Resources?](#what-are-resources)
8
+ 2. [Creating Resources](#creating-resources)
9
+ 3. [Resource URIs](#resource-uris)
10
+ 4. [Dynamic Resources](#dynamic-resources)
11
+ 5. [Resource Formats](#resource-formats)
12
+ 6. [Caching](#caching)
13
+ 7. [Advanced Patterns](#advanced-patterns)
14
+ 8. [Testing Resources](#testing-resources)
15
+
16
+ ## What are Resources?
17
+
18
+ Resources are read-only data endpoints that:
19
+ - Have unique URIs
20
+ - Return data in various formats
21
+ - Can be dynamic or static
22
+ - Don't accept parameters (use URI for context)
23
+
24
+ Think of resources as REST API GET endpoints.
25
+
26
+ ## Creating Resources
27
+
28
+ ### Inline Resources
29
+
30
+ Simple resources defined in server.rb:
31
+
32
+ ```ruby
33
+ Tsikol.server "my-server" do
34
+ resource "version" do
35
+ "1.0.0"
36
+ end
37
+
38
+ resource "config/database" do
39
+ {
40
+ host: ENV['DB_HOST'] || 'localhost',
41
+ port: ENV['DB_PORT'] || 5432,
42
+ name: ENV['DB_NAME'] || 'development'
43
+ }.to_json
44
+ end
45
+
46
+ resource "health" do
47
+ {
48
+ status: "healthy",
49
+ timestamp: Time.now.iso8601
50
+ }.to_json
51
+ end
52
+ end
53
+ ```
54
+
55
+ ### Class-Based Resources
56
+
57
+ For complex resources:
58
+
59
+ ```ruby
60
+ # app/resources/system_metrics.rb
61
+ class SystemMetrics < Tsikol::Resource
62
+ uri "system/metrics"
63
+ description "Current system metrics and statistics"
64
+
65
+ def read
66
+ {
67
+ server: server_info,
68
+ system: system_info,
69
+ performance: performance_metrics,
70
+ timestamp: Time.now.iso8601
71
+ }.to_json
72
+ end
73
+
74
+ private
75
+
76
+ def server_info
77
+ {
78
+ name: @server.name,
79
+ version: Tsikol::VERSION,
80
+ uptime: calculate_uptime
81
+ }
82
+ end
83
+
84
+ def system_info
85
+ {
86
+ ruby_version: RUBY_VERSION,
87
+ platform: RUBY_PLATFORM,
88
+ pid: Process.pid,
89
+ memory_usage: memory_usage
90
+ }
91
+ end
92
+
93
+ def performance_metrics
94
+ {
95
+ total_requests: @server.metrics.get(:requests_total),
96
+ average_response_time: @server.metrics.average(:response_time),
97
+ error_rate: calculate_error_rate
98
+ }
99
+ end
100
+
101
+ def calculate_uptime
102
+ return 0 unless @start_time
103
+ Time.now - @start_time
104
+ end
105
+
106
+ def memory_usage
107
+ # Simple calculation
108
+ "#{(GC.stat[:heap_live_slots] * 40.0 / 1024 / 1024).round(2)}MB"
109
+ end
110
+
111
+ def calculate_error_rate
112
+ total = @server.metrics.get(:requests_total)
113
+ errors = @server.metrics.get(:errors_total)
114
+ return 0.0 if total == 0
115
+ (errors.to_f / total * 100).round(2)
116
+ end
117
+
118
+ def set_server(server)
119
+ @server = server
120
+ @start_time = Time.now
121
+ end
122
+ end
123
+ ```
124
+
125
+ ## Resource URIs
126
+
127
+ ### URI Conventions
128
+
129
+ URIs should be:
130
+ - Descriptive and hierarchical
131
+ - Use forward slashes for nesting
132
+ - Lowercase with hyphens or underscores
133
+
134
+ ```ruby
135
+ # Good URIs
136
+ "config" # Top-level config
137
+ "config/database" # Database config
138
+ "system/health" # System health
139
+ "api/v1/documentation" # Versioned API docs
140
+
141
+ # Avoid
142
+ "getConfig" # Not RESTful
143
+ "database-config-data" # Too flat
144
+ "SYSTEM_HEALTH" # Wrong case
145
+ ```
146
+
147
+ ### Dynamic URI Patterns
148
+
149
+ For resources that represent collections:
150
+
151
+ ```ruby
152
+ # app/resources/user_profile.rb
153
+ class UserProfile < Tsikol::Resource
154
+ uri "users/current/profile"
155
+ description "Current user's profile"
156
+
157
+ def read
158
+ # In real app, get current user from context
159
+ {
160
+ id: 1,
161
+ name: "Current User",
162
+ preferences: load_preferences
163
+ }.to_json
164
+ end
165
+ end
166
+
167
+ # app/resources/project_readme.rb
168
+ class ProjectReadme < Tsikol::Resource
169
+ uri "project/readme"
170
+ description "Project README file"
171
+
172
+ def read
173
+ if File.exist?("README.md")
174
+ File.read("README.md")
175
+ else
176
+ "No README found"
177
+ end
178
+ end
179
+ end
180
+ ```
181
+
182
+ ## Dynamic Resources
183
+
184
+ ### Real-Time Data
185
+
186
+ ```ruby
187
+ class LiveMetrics < Tsikol::Resource
188
+ uri "metrics/live"
189
+ description "Real-time metrics"
190
+
191
+ def read
192
+ {
193
+ timestamp: Time.now.iso8601,
194
+ requests_per_second: calculate_rps,
195
+ active_connections: active_connections,
196
+ queue_depth: queue_depth,
197
+ cpu_usage: cpu_usage,
198
+ memory_usage: memory_usage
199
+ }.to_json
200
+ end
201
+
202
+ private
203
+
204
+ def calculate_rps
205
+ # Calculate requests in last second
206
+ recent = @server.metrics.recent_requests(1)
207
+ recent.count
208
+ end
209
+
210
+ def active_connections
211
+ Thread.list.select(&:alive?).count
212
+ end
213
+
214
+ def queue_depth
215
+ # Implementation specific
216
+ 0
217
+ end
218
+
219
+ def cpu_usage
220
+ # Platform specific implementation
221
+ "#{rand(0..100)}%"
222
+ end
223
+ end
224
+ ```
225
+
226
+ ### Database-Backed Resources
227
+
228
+ ```ruby
229
+ class DatabaseSchema < Tsikol::Resource
230
+ uri "database/schema"
231
+ description "Current database schema"
232
+
233
+ def read
234
+ tables = fetch_tables
235
+ schema = tables.map do |table|
236
+ {
237
+ name: table,
238
+ columns: fetch_columns(table),
239
+ indexes: fetch_indexes(table)
240
+ }
241
+ end
242
+
243
+ {
244
+ database: database_name,
245
+ tables: schema,
246
+ generated_at: Time.now.iso8601
247
+ }.to_json
248
+ end
249
+
250
+ private
251
+
252
+ def fetch_tables
253
+ # Database specific
254
+ ["users", "posts", "comments"]
255
+ end
256
+
257
+ def fetch_columns(table)
258
+ # Database specific
259
+ case table
260
+ when "users"
261
+ ["id", "email", "name", "created_at"]
262
+ when "posts"
263
+ ["id", "user_id", "title", "content", "published_at"]
264
+ else
265
+ ["id", "created_at", "updated_at"]
266
+ end
267
+ end
268
+
269
+ def fetch_indexes(table)
270
+ # Database specific
271
+ ["#{table}_pkey"]
272
+ end
273
+
274
+ def database_name
275
+ ENV['DATABASE_NAME'] || 'development'
276
+ end
277
+ end
278
+ ```
279
+
280
+ ## Resource Formats
281
+
282
+ ### JSON Resources
283
+
284
+ Most common format:
285
+
286
+ ```ruby
287
+ class ApiEndpoints < Tsikol::Resource
288
+ uri "api/endpoints"
289
+ description "Available API endpoints"
290
+
291
+ def read
292
+ {
293
+ version: "1.0",
294
+ endpoints: [
295
+ {
296
+ path: "/users",
297
+ methods: ["GET", "POST"],
298
+ description: "User management"
299
+ },
300
+ {
301
+ path: "/users/:id",
302
+ methods: ["GET", "PUT", "DELETE"],
303
+ description: "Individual user operations"
304
+ }
305
+ ]
306
+ }.to_json
307
+ end
308
+ end
309
+ ```
310
+
311
+ ### Text Resources
312
+
313
+ Plain text or markdown:
314
+
315
+ ```ruby
316
+ class Changelog < Tsikol::Resource
317
+ uri "changelog"
318
+ description "Project changelog"
319
+
320
+ def read
321
+ if File.exist?("CHANGELOG.md")
322
+ File.read("CHANGELOG.md")
323
+ else
324
+ "No changelog available"
325
+ end
326
+ end
327
+ end
328
+ ```
329
+
330
+ ### CSV Resources
331
+
332
+ Tabular data:
333
+
334
+ ```ruby
335
+ class UserReport < Tsikol::Resource
336
+ uri "reports/users.csv"
337
+ description "User report in CSV format"
338
+
339
+ def read
340
+ require 'csv'
341
+
342
+ CSV.generate do |csv|
343
+ csv << ["ID", "Name", "Email", "Created"]
344
+
345
+ users.each do |user|
346
+ csv << [user[:id], user[:name], user[:email], user[:created_at]]
347
+ end
348
+ end
349
+ end
350
+
351
+ private
352
+
353
+ def users
354
+ # Fetch from database
355
+ [
356
+ { id: 1, name: "Alice", email: "alice@example.com", created_at: "2024-01-01" },
357
+ { id: 2, name: "Bob", email: "bob@example.com", created_at: "2024-01-02" }
358
+ ]
359
+ end
360
+ end
361
+ ```
362
+
363
+ ### Binary Resources
364
+
365
+ Base64 encoded:
366
+
367
+ ```ruby
368
+ class Logo < Tsikol::Resource
369
+ uri "assets/logo"
370
+ description "Company logo"
371
+
372
+ def read
373
+ if File.exist?("logo.png")
374
+ content = File.read("logo.png", mode: "rb")
375
+ {
376
+ mime_type: "image/png",
377
+ encoding: "base64",
378
+ data: Base64.encode64(content)
379
+ }.to_json
380
+ else
381
+ { error: "Logo not found" }.to_json
382
+ end
383
+ end
384
+ end
385
+ ```
386
+
387
+ ## Caching
388
+
389
+ ### Simple Caching
390
+
391
+ ```ruby
392
+ class CachedResource < Tsikol::Resource
393
+ uri "expensive/data"
394
+ description "Expensive computation (cached)"
395
+
396
+ def initialize
397
+ super
398
+ @cache = nil
399
+ @cache_time = nil
400
+ @cache_ttl = 300 # 5 minutes
401
+ end
402
+
403
+ def read
404
+ if cache_valid?
405
+ log :debug, "Cache hit"
406
+ @cache
407
+ else
408
+ log :debug, "Cache miss, computing..."
409
+ @cache = compute_expensive_data
410
+ @cache_time = Time.now
411
+ @cache
412
+ end
413
+ end
414
+
415
+ private
416
+
417
+ def cache_valid?
418
+ return false unless @cache && @cache_time
419
+ Time.now - @cache_time < @cache_ttl
420
+ end
421
+
422
+ def compute_expensive_data
423
+ # Simulate expensive operation
424
+ sleep(0.1)
425
+ {
426
+ result: "Expensive data",
427
+ computed_at: Time.now.iso8601
428
+ }.to_json
429
+ end
430
+ end
431
+ ```
432
+
433
+ ### Conditional Caching
434
+
435
+ ```ruby
436
+ class ConditionalCache < Tsikol::Resource
437
+ uri "data/conditional"
438
+ description "Conditionally cached data"
439
+
440
+ def read
441
+ data = fetch_data
442
+
443
+ # Cache based on data characteristics
444
+ if should_cache?(data)
445
+ @cached_data = data
446
+ @cache_key = generate_cache_key(data)
447
+ end
448
+
449
+ format_response(data)
450
+ end
451
+
452
+ private
453
+
454
+ def should_cache?(data)
455
+ # Cache if data is large or expensive
456
+ data.size > 1000 || data[:expensive]
457
+ end
458
+
459
+ def generate_cache_key(data)
460
+ Digest::SHA256.hexdigest(data.to_s)
461
+ end
462
+ end
463
+ ```
464
+
465
+ ## Advanced Patterns
466
+
467
+ ### Aggregated Resources
468
+
469
+ Combine multiple data sources:
470
+
471
+ ```ruby
472
+ class Dashboard < Tsikol::Resource
473
+ uri "dashboard"
474
+ description "Aggregated dashboard data"
475
+
476
+ def read
477
+ {
478
+ summary: fetch_summary,
479
+ metrics: fetch_metrics,
480
+ recent_activity: fetch_recent_activity,
481
+ alerts: fetch_alerts
482
+ }.to_json
483
+ end
484
+
485
+ private
486
+
487
+ def fetch_summary
488
+ {
489
+ total_users: count_users,
490
+ active_sessions: count_sessions,
491
+ revenue_today: calculate_revenue
492
+ }
493
+ end
494
+
495
+ def fetch_metrics
496
+ SystemMetrics.new.read
497
+ end
498
+
499
+ def fetch_recent_activity
500
+ # Last 10 activities
501
+ []
502
+ end
503
+
504
+ def fetch_alerts
505
+ # Active alerts
506
+ []
507
+ end
508
+ end
509
+ ```
510
+
511
+ ### Filtered Resources
512
+
513
+ Resources with virtual filters:
514
+
515
+ ```ruby
516
+ class FilteredLogs < Tsikol::Resource
517
+ uri "logs/recent"
518
+ description "Recent log entries"
519
+
520
+ def read
521
+ # In real app, URI could indicate filter
522
+ # e.g., "logs/error" for error logs only
523
+
524
+ logs = if uri.include?("error")
525
+ fetch_error_logs
526
+ elsif uri.include?("warning")
527
+ fetch_warning_logs
528
+ else
529
+ fetch_all_logs
530
+ end
531
+
532
+ format_logs(logs)
533
+ end
534
+
535
+ private
536
+
537
+ def fetch_error_logs
538
+ read_log_file.select { |line| line.include?("[ERROR]") }
539
+ end
540
+
541
+ def fetch_warning_logs
542
+ read_log_file.select { |line| line.include?("[WARNING]") }
543
+ end
544
+
545
+ def fetch_all_logs
546
+ read_log_file.last(100)
547
+ end
548
+
549
+ def read_log_file
550
+ File.readlines("app.log") rescue []
551
+ end
552
+
553
+ def format_logs(logs)
554
+ {
555
+ count: logs.size,
556
+ entries: logs.map { |log| parse_log_entry(log) }
557
+ }.to_json
558
+ end
559
+ end
560
+ ```
561
+
562
+ ### Streaming Resources
563
+
564
+ For large data sets:
565
+
566
+ ```ruby
567
+ class LargeDataset < Tsikol::Resource
568
+ uri "data/large"
569
+ description "Large dataset (streamed)"
570
+
571
+ def read
572
+ # Return metadata about the dataset
573
+ {
574
+ total_records: count_records,
575
+ chunk_size: 1000,
576
+ chunks_available: calculate_chunks,
577
+ access_pattern: "Use data/large/chunk/{n} to access chunks"
578
+ }.to_json
579
+ end
580
+
581
+ private
582
+
583
+ def count_records
584
+ # Implementation
585
+ 1_000_000
586
+ end
587
+
588
+ def calculate_chunks
589
+ (count_records / 1000.0).ceil
590
+ end
591
+ end
592
+
593
+ # Separate resource for chunks
594
+ class DataChunk < Tsikol::Resource
595
+ uri "data/large/chunk" # Would need custom routing
596
+ description "Data chunk"
597
+
598
+ def read
599
+ chunk_number = extract_chunk_number
600
+ offset = chunk_number * 1000
601
+
602
+ records = fetch_records(offset, 1000)
603
+
604
+ {
605
+ chunk: chunk_number,
606
+ records: records,
607
+ has_more: has_more_records?(offset + 1000)
608
+ }.to_json
609
+ end
610
+ end
611
+ ```
612
+
613
+ ## Testing Resources
614
+
615
+ ### Basic Resource Test
616
+
617
+ ```ruby
618
+ require 'minitest/autorun'
619
+ require 'tsikol/test_helpers'
620
+
621
+ class SystemMetricsTest < Minitest::Test
622
+ include Tsikol::TestHelpers::Assertions
623
+
624
+ def setup
625
+ @server = Tsikol::Server.new(name: "test")
626
+ @server.register_resource_instance(SystemMetrics.new)
627
+ @client = Tsikol::TestHelpers::TestClient.new(@server)
628
+ @client.initialize_connection
629
+ end
630
+
631
+ def test_read_metrics
632
+ response = @client.read_resource("system/metrics")
633
+ assert_successful_response(response)
634
+
635
+ content = response.dig(:result, :contents, 0, :text)
636
+ data = JSON.parse(content)
637
+
638
+ assert data["server"]
639
+ assert data["system"]
640
+ assert data["performance"]
641
+ assert data["timestamp"]
642
+ end
643
+
644
+ def test_metrics_structure
645
+ response = @client.read_resource("system/metrics")
646
+ content = response.dig(:result, :contents, 0, :text)
647
+ data = JSON.parse(content)
648
+
649
+ # Check server info
650
+ assert_equal "test", data["server"]["name"]
651
+ assert data["server"]["uptime"] >= 0
652
+
653
+ # Check system info
654
+ assert_equal RUBY_VERSION, data["system"]["ruby_version"]
655
+ assert data["system"]["memory_usage"]
656
+ end
657
+ end
658
+ ```
659
+
660
+ ### Testing Dynamic Resources
661
+
662
+ ```ruby
663
+ class LiveMetricsTest < Minitest::Test
664
+ def test_real_time_data_changes
665
+ response1 = @client.read_resource("metrics/live")
666
+ sleep(0.1)
667
+ response2 = @client.read_resource("metrics/live")
668
+
669
+ data1 = JSON.parse(response1.dig(:result, :contents, 0, :text))
670
+ data2 = JSON.parse(response2.dig(:result, :contents, 0, :text))
671
+
672
+ # Timestamps should be different
673
+ refute_equal data1["timestamp"], data2["timestamp"]
674
+ end
675
+ end
676
+ ```
677
+
678
+ ### Testing Cached Resources
679
+
680
+ ```ruby
681
+ class CachedResourceTest < Minitest::Test
682
+ def test_caching_behavior
683
+ # First call should compute
684
+ start = Time.now
685
+ response1 = @client.read_resource("expensive/data")
686
+ duration1 = Time.now - start
687
+
688
+ # Second call should be cached
689
+ start = Time.now
690
+ response2 = @client.read_resource("expensive/data")
691
+ duration2 = Time.now - start
692
+
693
+ # Cached call should be much faster
694
+ assert duration2 < duration1 / 10
695
+
696
+ # Content should be identical
697
+ assert_equal(
698
+ response1.dig(:result, :contents, 0, :text),
699
+ response2.dig(:result, :contents, 0, :text)
700
+ )
701
+ end
702
+ end
703
+ ```
704
+
705
+ ## Best Practices
706
+
707
+ 1. **Use Clear URIs**: Make them intuitive and hierarchical
708
+ 2. **Return Consistent Formats**: Usually JSON for structured data
709
+ 3. **Include Metadata**: Timestamps, versions, etc.
710
+ 4. **Handle Errors Gracefully**: Return error info in response
711
+ 5. **Cache When Appropriate**: For expensive operations
712
+ 6. **Keep Resources Read-Only**: Use tools for modifications
713
+ 7. **Document Format**: Describe what the resource returns
714
+
715
+ ## Next Steps
716
+
717
+ - Learn about [Prompts](prompts.md)
718
+ - Explore [Testing](testing.md)
719
+ - Add [Caching](../cookbook/caching.md)
720
+ - Implement [Monitoring](../cookbook/monitoring.md)