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,717 @@
1
+ # Rate Limiting Recipe
2
+
3
+ This recipe shows various strategies for implementing rate limiting in your MCP server to prevent abuse and ensure fair usage.
4
+
5
+ ## Basic Rate Limiting
6
+
7
+ ### Simple Request Counter
8
+
9
+ ```ruby
10
+ class BasicRateLimiter < Tsikol::Middleware
11
+ def initialize(app, options = {})
12
+ @app = app
13
+ @max_requests = options[:max_requests] || 60
14
+ @window_seconds = options[:window_seconds] || 60
15
+ @requests = Hash.new { |h, k| h[k] = [] }
16
+ @mutex = Mutex.new
17
+ end
18
+
19
+ def call(request)
20
+ client_id = identify_client(request)
21
+
22
+ if rate_limited?(client_id)
23
+ return rate_limit_error(request["id"])
24
+ end
25
+
26
+ track_request(client_id)
27
+ @app.call(request)
28
+ end
29
+
30
+ private
31
+
32
+ def identify_client(request)
33
+ # Identify by various methods
34
+ request.dig("params", "_client_id") ||
35
+ request.dig("meta", "client_id") ||
36
+ request.dig("authenticated_user", "id") ||
37
+ "anonymous"
38
+ end
39
+
40
+ def rate_limited?(client_id)
41
+ @mutex.synchronize do
42
+ clean_old_requests(client_id)
43
+ @requests[client_id].size >= @max_requests
44
+ end
45
+ end
46
+
47
+ def track_request(client_id)
48
+ @mutex.synchronize do
49
+ @requests[client_id] << Time.now
50
+ end
51
+ end
52
+
53
+ def clean_old_requests(client_id)
54
+ cutoff = Time.now - @window_seconds
55
+ @requests[client_id].reject! { |time| time < cutoff }
56
+ end
57
+
58
+ def rate_limit_error(id)
59
+ {
60
+ jsonrpc: "2.0",
61
+ id: id,
62
+ error: {
63
+ code: -32005,
64
+ message: "Rate limit exceeded",
65
+ data: {
66
+ retry_after: @window_seconds,
67
+ limit: @max_requests
68
+ }
69
+ }
70
+ }
71
+ end
72
+ end
73
+ ```
74
+
75
+ ## Token Bucket Algorithm
76
+
77
+ ### Advanced Rate Limiting with Bursts
78
+
79
+ ```ruby
80
+ class TokenBucketRateLimiter < Tsikol::Middleware
81
+ def initialize(app, options = {})
82
+ @app = app
83
+ @capacity = options[:capacity] || 10 # Max tokens
84
+ @refill_rate = options[:refill_rate] || 1 # Tokens per second
85
+ @buckets = Hash.new { |h, k| h[k] = create_bucket }
86
+ @mutex = Mutex.new
87
+ end
88
+
89
+ def call(request)
90
+ client_id = identify_client(request)
91
+ cost = request_cost(request)
92
+
93
+ if consume_tokens(client_id, cost)
94
+ @app.call(request)
95
+ else
96
+ rate_limit_error(request["id"], client_id)
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def create_bucket
103
+ {
104
+ tokens: @capacity.to_f,
105
+ last_refill: Time.now
106
+ }
107
+ end
108
+
109
+ def consume_tokens(client_id, cost)
110
+ @mutex.synchronize do
111
+ bucket = @buckets[client_id]
112
+ refill_bucket(bucket)
113
+
114
+ if bucket[:tokens] >= cost
115
+ bucket[:tokens] -= cost
116
+ true
117
+ else
118
+ false
119
+ end
120
+ end
121
+ end
122
+
123
+ def refill_bucket(bucket)
124
+ now = Time.now
125
+ elapsed = now - bucket[:last_refill]
126
+
127
+ # Add tokens based on elapsed time
128
+ tokens_to_add = elapsed * @refill_rate
129
+ bucket[:tokens] = [bucket[:tokens] + tokens_to_add, @capacity].min
130
+ bucket[:last_refill] = now
131
+ end
132
+
133
+ def request_cost(request)
134
+ # Different costs for different operations
135
+ case request["method"]
136
+ when "tools/call"
137
+ case request.dig("params", "name")
138
+ when "expensive_operation" then 5
139
+ when "ai_generation" then 3
140
+ else 1
141
+ end
142
+ when "sampling/sample"
143
+ 10 # AI sampling is expensive
144
+ else
145
+ 1
146
+ end
147
+ end
148
+
149
+ def rate_limit_error(id, client_id)
150
+ bucket = @buckets[client_id]
151
+ wait_time = (1 - bucket[:tokens]) / @refill_rate
152
+
153
+ {
154
+ jsonrpc: "2.0",
155
+ id: id,
156
+ error: {
157
+ code: -32005,
158
+ message: "Rate limit exceeded",
159
+ data: {
160
+ retry_after: wait_time.ceil,
161
+ tokens_remaining: bucket[:tokens].floor,
162
+ refill_rate: @refill_rate
163
+ }
164
+ }
165
+ }
166
+ end
167
+ end
168
+ ```
169
+
170
+ ## Sliding Window Rate Limiter
171
+
172
+ ### Precise Rate Limiting
173
+
174
+ ```ruby
175
+ class SlidingWindowRateLimiter < Tsikol::Middleware
176
+ def initialize(app, options = {})
177
+ @app = app
178
+ @max_requests = options[:max_requests] || 100
179
+ @window_seconds = options[:window_seconds] || 60
180
+ @precision = options[:precision] || 10 # Sub-windows
181
+ @counters = Hash.new { |h, k| h[k] = Array.new(@precision, 0) }
182
+ @mutex = Mutex.new
183
+ end
184
+
185
+ def call(request)
186
+ client_id = identify_client(request)
187
+
188
+ if exceeds_limit?(client_id)
189
+ return rate_limit_error(request["id"])
190
+ end
191
+
192
+ increment_counter(client_id)
193
+ @app.call(request)
194
+ end
195
+
196
+ private
197
+
198
+ def exceeds_limit?(client_id)
199
+ @mutex.synchronize do
200
+ current_count = calculate_current_count(client_id)
201
+ current_count >= @max_requests
202
+ end
203
+ end
204
+
205
+ def calculate_current_count(client_id)
206
+ now = Time.now.to_f
207
+ window_size = @window_seconds.to_f / @precision
208
+ current_window = (now / window_size).floor % @precision
209
+
210
+ # Age out old windows
211
+ @counters[client_id].each_with_index do |count, i|
212
+ window_age = ((current_window - i) % @precision) * window_size
213
+ if window_age >= @window_seconds
214
+ @counters[client_id][i] = 0
215
+ end
216
+ end
217
+
218
+ @counters[client_id].sum
219
+ end
220
+
221
+ def increment_counter(client_id)
222
+ @mutex.synchronize do
223
+ window_size = @window_seconds.to_f / @precision
224
+ current_window = (Time.now.to_f / window_size).floor % @precision
225
+ @counters[client_id][current_window] += 1
226
+ end
227
+ end
228
+ end
229
+ ```
230
+
231
+ ## Distributed Rate Limiting
232
+
233
+ ### Redis-Based Rate Limiter
234
+
235
+ ```ruby
236
+ require 'redis'
237
+
238
+ class RedisRateLimiter < Tsikol::Middleware
239
+ def initialize(app, options = {})
240
+ @app = app
241
+ @redis = options[:redis] || Redis.new
242
+ @max_requests = options[:max_requests] || 100
243
+ @window_seconds = options[:window_seconds] || 60
244
+ @key_prefix = options[:key_prefix] || "mcp:rate_limit"
245
+ end
246
+
247
+ def call(request)
248
+ client_id = identify_client(request)
249
+
250
+ if rate_limited?(client_id)
251
+ return rate_limit_error(request["id"], client_id)
252
+ end
253
+
254
+ @app.call(request)
255
+ end
256
+
257
+ private
258
+
259
+ def rate_limited?(client_id)
260
+ key = "#{@key_prefix}:#{client_id}"
261
+
262
+ # Use Redis pipeline for atomic operations
263
+ result = @redis.pipelined do |pipeline|
264
+ pipeline.incr(key)
265
+ pipeline.expire(key, @window_seconds)
266
+ end
267
+
268
+ current_count = result[0]
269
+ current_count > @max_requests
270
+ end
271
+
272
+ def rate_limit_error(id, client_id)
273
+ key = "#{@key_prefix}:#{client_id}"
274
+ ttl = @redis.ttl(key)
275
+
276
+ {
277
+ jsonrpc: "2.0",
278
+ id: id,
279
+ error: {
280
+ code: -32005,
281
+ message: "Rate limit exceeded",
282
+ data: {
283
+ retry_after: ttl > 0 ? ttl : @window_seconds,
284
+ limit: @max_requests,
285
+ window: @window_seconds
286
+ }
287
+ }
288
+ }
289
+ end
290
+ end
291
+ ```
292
+
293
+ ### Redis Sliding Window
294
+
295
+ ```ruby
296
+ class RedisSlidingWindowLimiter < Tsikol::Middleware
297
+ def initialize(app, options = {})
298
+ @app = app
299
+ @redis = options[:redis] || Redis.new
300
+ @max_requests = options[:max_requests] || 100
301
+ @window_seconds = options[:window_seconds] || 60
302
+ @key_prefix = options[:key_prefix] || "mcp:sliding"
303
+ end
304
+
305
+ def call(request)
306
+ client_id = identify_client(request)
307
+
308
+ if rate_limited?(client_id)
309
+ return rate_limit_error(request["id"])
310
+ end
311
+
312
+ @app.call(request)
313
+ end
314
+
315
+ private
316
+
317
+ def rate_limited?(client_id)
318
+ key = "#{@key_prefix}:#{client_id}"
319
+ now = Time.now.to_f
320
+ window_start = now - @window_seconds
321
+
322
+ # Redis Lua script for atomic sliding window
323
+ lua_script = <<~LUA
324
+ local key = KEYS[1]
325
+ local now = tonumber(ARGV[1])
326
+ local window_start = tonumber(ARGV[2])
327
+ local max_requests = tonumber(ARGV[3])
328
+
329
+ -- Remove old entries
330
+ redis.call('ZREMRANGEBYSCORE', key, 0, window_start)
331
+
332
+ -- Count current entries
333
+ local current = redis.call('ZCARD', key)
334
+
335
+ if current < max_requests then
336
+ -- Add new entry
337
+ redis.call('ZADD', key, now, now)
338
+ redis.call('EXPIRE', key, ARGV[4])
339
+ return 0
340
+ else
341
+ return 1
342
+ end
343
+ LUA
344
+
345
+ result = @redis.eval(lua_script, 1, key, now, window_start, @max_requests, @window_seconds + 1)
346
+ result == 1
347
+ end
348
+ end
349
+ ```
350
+
351
+ ## Tiered Rate Limiting
352
+
353
+ ### Different Limits for Different Users
354
+
355
+ ```ruby
356
+ class TieredRateLimiter < Tsikol::Middleware
357
+ def initialize(app, options = {})
358
+ @app = app
359
+ @tiers = options[:tiers] || default_tiers
360
+ @default_tier = options[:default_tier] || "free"
361
+ @limiters = {}
362
+
363
+ # Create limiter for each tier
364
+ @tiers.each do |tier, config|
365
+ @limiters[tier] = TokenBucketRateLimiter.new(nil, config)
366
+ end
367
+ end
368
+
369
+ def call(request)
370
+ client_tier = determine_tier(request)
371
+ limiter = @limiters[client_tier] || @limiters[@default_tier]
372
+
373
+ # Use tier-specific limiter
374
+ limiter_response = limiter.call(request)
375
+
376
+ if limiter_response[:error]
377
+ # Add tier information to error
378
+ limiter_response[:error][:data][:tier] = client_tier
379
+ limiter_response[:error][:data][:upgrade_available] = can_upgrade?(client_tier)
380
+ return limiter_response
381
+ end
382
+
383
+ @app.call(request)
384
+ end
385
+
386
+ private
387
+
388
+ def default_tiers
389
+ {
390
+ "free" => {
391
+ capacity: 10,
392
+ refill_rate: 0.1 # 0.1 tokens/second = 6/minute
393
+ },
394
+ "basic" => {
395
+ capacity: 50,
396
+ refill_rate: 1 # 1 token/second = 60/minute
397
+ },
398
+ "premium" => {
399
+ capacity: 200,
400
+ refill_rate: 5 # 5 tokens/second = 300/minute
401
+ },
402
+ "enterprise" => {
403
+ capacity: 1000,
404
+ refill_rate: 20 # 20 tokens/second = 1200/minute
405
+ }
406
+ }
407
+ end
408
+
409
+ def determine_tier(request)
410
+ # Check authenticated user's subscription
411
+ user = request["authenticated_user"]
412
+ return @default_tier unless user
413
+
414
+ user["subscription_tier"] || user["tier"] || @default_tier
415
+ end
416
+
417
+ def can_upgrade?(tier)
418
+ tier_order = ["free", "basic", "premium", "enterprise"]
419
+ current_index = tier_order.index(tier) || 0
420
+ current_index < tier_order.length - 1
421
+ end
422
+ end
423
+ ```
424
+
425
+ ## Adaptive Rate Limiting
426
+
427
+ ### Dynamic Limits Based on Load
428
+
429
+ ```ruby
430
+ class AdaptiveRateLimiter < Tsikol::Middleware
431
+ def initialize(app, options = {})
432
+ @app = app
433
+ @base_limit = options[:base_limit] || 100
434
+ @min_limit = options[:min_limit] || 10
435
+ @max_limit = options[:max_limit] || 1000
436
+ @load_threshold = options[:load_threshold] || 0.8
437
+ @limiters = {}
438
+ @mutex = Mutex.new
439
+
440
+ # Start monitoring thread
441
+ start_monitoring
442
+ end
443
+
444
+ def call(request)
445
+ client_id = identify_client(request)
446
+ current_limit = calculate_current_limit
447
+
448
+ if exceeds_limit?(client_id, current_limit)
449
+ return adaptive_rate_limit_error(request["id"], current_limit)
450
+ end
451
+
452
+ track_request(client_id)
453
+ @app.call(request)
454
+ end
455
+
456
+ private
457
+
458
+ def calculate_current_limit
459
+ # Adjust limit based on system load
460
+ load_average = get_system_load
461
+
462
+ if load_average > @load_threshold
463
+ # Reduce limits under high load
464
+ reduction_factor = (load_average - @load_threshold) / (1 - @load_threshold)
465
+ reduced_limit = @base_limit * (1 - reduction_factor * 0.5)
466
+ [@min_limit, reduced_limit].max.to_i
467
+ else
468
+ # Increase limits under low load
469
+ increase_factor = 1 + (1 - load_average) * 0.5
470
+ increased_limit = @base_limit * increase_factor
471
+ [@max_limit, increased_limit].min.to_i
472
+ end
473
+ end
474
+
475
+ def get_system_load
476
+ # Get 1-minute load average normalized by CPU count
477
+ if File.exist?("/proc/loadavg")
478
+ load_avg = File.read("/proc/loadavg").split.first.to_f
479
+ cpu_count = `nproc`.to_i rescue 1
480
+ load_avg / cpu_count
481
+ else
482
+ # Fallback for non-Linux systems
483
+ 0.5
484
+ end
485
+ end
486
+
487
+ def start_monitoring
488
+ Thread.new do
489
+ loop do
490
+ sleep 10
491
+ cleanup_old_requests
492
+ log_metrics if @app.respond_to?(:log)
493
+ end
494
+ end
495
+ end
496
+
497
+ def cleanup_old_requests
498
+ @mutex.synchronize do
499
+ cutoff = Time.now - 300 # 5 minutes
500
+ @limiters.each do |client_id, requests|
501
+ requests.reject! { |time| time < cutoff }
502
+ @limiters.delete(client_id) if requests.empty?
503
+ end
504
+ end
505
+ end
506
+ end
507
+ ```
508
+
509
+ ## Method-Specific Rate Limiting
510
+
511
+ ### Different Limits for Different Operations
512
+
513
+ ```ruby
514
+ class MethodRateLimiter < Tsikol::Middleware
515
+ def initialize(app, options = {})
516
+ @app = app
517
+ @method_limits = options[:method_limits] || default_method_limits
518
+ @default_limit = options[:default_limit] || 60
519
+ @window_seconds = options[:window_seconds] || 60
520
+ @counters = Hash.new { |h, k| h[k] = Hash.new { |h2, k2| h2[k2] = [] } }
521
+ @mutex = Mutex.new
522
+ end
523
+
524
+ def call(request)
525
+ method = request["method"]
526
+ client_id = identify_client(request)
527
+ limit = @method_limits[method] || @default_limit
528
+
529
+ if exceeds_method_limit?(client_id, method, limit)
530
+ return method_rate_limit_error(request["id"], method, limit)
531
+ end
532
+
533
+ track_method_request(client_id, method)
534
+ @app.call(request)
535
+ end
536
+
537
+ private
538
+
539
+ def default_method_limits
540
+ {
541
+ # Expensive operations
542
+ "sampling/sample" => 10,
543
+ "tools/call" => 60,
544
+
545
+ # Moderate operations
546
+ "completion/complete" => 100,
547
+ "resources/read" => 100,
548
+
549
+ # Cheap operations
550
+ "tools/list" => 300,
551
+ "resources/list" => 300,
552
+ "prompts/list" => 300,
553
+
554
+ # Very cheap
555
+ "ping" => 600
556
+ }
557
+ end
558
+
559
+ def exceeds_method_limit?(client_id, method, limit)
560
+ @mutex.synchronize do
561
+ clean_old_requests(client_id, method)
562
+ @counters[client_id][method].size >= limit
563
+ end
564
+ end
565
+
566
+ def track_method_request(client_id, method)
567
+ @mutex.synchronize do
568
+ @counters[client_id][method] << Time.now
569
+ end
570
+ end
571
+
572
+ def clean_old_requests(client_id, method)
573
+ cutoff = Time.now - @window_seconds
574
+ @counters[client_id][method].reject! { |time| time < cutoff }
575
+ end
576
+
577
+ def method_rate_limit_error(id, method, limit)
578
+ {
579
+ jsonrpc: "2.0",
580
+ id: id,
581
+ error: {
582
+ code: -32005,
583
+ message: "Rate limit exceeded for #{method}",
584
+ data: {
585
+ method: method,
586
+ limit: limit,
587
+ window: @window_seconds,
588
+ retry_after: @window_seconds
589
+ }
590
+ }
591
+ }
592
+ end
593
+ end
594
+ ```
595
+
596
+ ## Testing Rate Limiting
597
+
598
+ ```ruby
599
+ require 'minitest/autorun'
600
+
601
+ class RateLimitTest < Minitest::Test
602
+ def setup
603
+ @server = create_rate_limited_server
604
+ @client = Tsikol::TestHelpers::TestClient.new(@server)
605
+ end
606
+
607
+ def test_allows_requests_under_limit
608
+ 3.times do |i|
609
+ response = @client.call_tool("echo", { "message" => "Test #{i}" })
610
+ assert_successful_response(response)
611
+ end
612
+ end
613
+
614
+ def test_blocks_requests_over_limit
615
+ # Make requests up to limit
616
+ 5.times do
617
+ @client.call_tool("echo", { "message" => "Test" })
618
+ end
619
+
620
+ # Next request should be rate limited
621
+ response = @client.call_tool("echo", { "message" => "Over limit" })
622
+ assert_error_response(response, -32005)
623
+ assert_match /rate limit/i, response[:error][:message]
624
+ end
625
+
626
+ def test_resets_after_window
627
+ # Exhaust limit
628
+ 5.times { @client.call_tool("echo", { "message" => "Test" }) }
629
+
630
+ # Wait for window to reset
631
+ sleep(2) # Assuming 1-second window in test
632
+
633
+ # Should allow new requests
634
+ response = @client.call_tool("echo", { "message" => "After reset" })
635
+ assert_successful_response(response)
636
+ end
637
+
638
+ def test_token_bucket_allows_bursts
639
+ server = Tsikol::Server.new(name: "test")
640
+ server.use TokenBucketRateLimiter, capacity: 10, refill_rate: 1
641
+ server.tool "echo" do |message:| message end
642
+
643
+ client = Tsikol::TestHelpers::TestClient.new(server)
644
+
645
+ # Burst of 10 requests should succeed
646
+ 10.times do
647
+ response = client.call_tool("echo", { "message" => "Burst" })
648
+ assert_successful_response(response)
649
+ end
650
+
651
+ # 11th request should fail
652
+ response = client.call_tool("echo", { "message" => "Over capacity" })
653
+ assert_error_response(response, -32005)
654
+ end
655
+
656
+ private
657
+
658
+ def create_rate_limited_server
659
+ server = Tsikol::Server.new(name: "test")
660
+ server.use BasicRateLimiter, max_requests: 5, window_seconds: 1
661
+ server.tool "echo" do |message:| message end
662
+ server
663
+ end
664
+ end
665
+ ```
666
+
667
+ ## Best Practices
668
+
669
+ 1. **Choose the right algorithm** for your use case
670
+ 2. **Consider distributed systems** for multi-server deployments
671
+ 3. **Provide clear error messages** with retry information
672
+ 4. **Monitor rate limiting metrics** to tune limits
673
+ 5. **Different limits for different operations** based on cost
674
+ 6. **Graceful degradation** under high load
675
+ 7. **Consider user tiers** for SaaS applications
676
+ 8. **Test rate limiting** thoroughly including edge cases
677
+
678
+ ## Integration Example
679
+
680
+ ```ruby
681
+ # Complete server with rate limiting
682
+ Tsikol.start(name: "production-server") do
683
+ # Authentication first
684
+ use JWTAuthMiddleware, secret: ENV['JWT_SECRET']
685
+
686
+ # Then rate limiting based on authenticated user
687
+ use TieredRateLimiter, tiers: {
688
+ "free" => { capacity: 10, refill_rate: 0.1 },
689
+ "pro" => { capacity: 100, refill_rate: 2 },
690
+ "enterprise" => { capacity: 1000, refill_rate: 20 }
691
+ }
692
+
693
+ # Method-specific limits
694
+ use MethodRateLimiter, method_limits: {
695
+ "sampling/sample" => 5, # AI operations
696
+ "tools/call" => 60, # General tools
697
+ "resources/read" => 200 # Read operations
698
+ }
699
+
700
+ # Adaptive limiting under load
701
+ use AdaptiveRateLimiter,
702
+ base_limit: 100,
703
+ load_threshold: 0.7
704
+
705
+ # Your application
706
+ tool AiTool
707
+ resource DataResource
708
+ prompt Assistant
709
+ end
710
+ ```
711
+
712
+ ## Next Steps
713
+
714
+ - Implement [Authentication](authentication.md) to identify users
715
+ - Add [Logging](logging.md) for monitoring
716
+ - Set up [Caching](caching.md) to reduce load
717
+ - Review [Error Handling](error-handling.md) for rate limit errors