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,823 @@
1
+ # Middleware Guide
2
+
3
+ Middleware in Tsikol provides a powerful way to add cross-cutting concerns to your MCP server. From logging and authentication to rate limiting and error handling, middleware helps keep your tools and resources focused on their core functionality.
4
+
5
+ ## Table of Contents
6
+
7
+ 1. [What is Middleware?](#what-is-middleware)
8
+ 2. [Built-in Middleware](#built-in-middleware)
9
+ 3. [Creating Custom Middleware](#creating-custom-middleware)
10
+ 4. [Middleware Chain](#middleware-chain)
11
+ 5. [Request/Response Manipulation](#requestresponse-manipulation)
12
+ 6. [Advanced Patterns](#advanced-patterns)
13
+ 7. [Testing Middleware](#testing-middleware)
14
+
15
+ ## What is Middleware?
16
+
17
+ Middleware components:
18
+ - Intercept requests before they reach tools/resources
19
+ - Process responses before returning to the client
20
+ - Handle cross-cutting concerns (logging, auth, etc.)
21
+ - Can short-circuit the request chain
22
+ - Execute in a defined order
23
+
24
+ ## Built-in Middleware
25
+
26
+ ### Logging Middleware
27
+
28
+ Log all requests and responses:
29
+
30
+ ```ruby
31
+ Tsikol.server "my-server" do
32
+ use Tsikol::LoggingMiddleware
33
+
34
+ # Or with options
35
+ use Tsikol::LoggingMiddleware,
36
+ level: :debug,
37
+ include_params: true,
38
+ include_response: true
39
+ end
40
+ ```
41
+
42
+ ### Rate Limiting Middleware
43
+
44
+ Prevent abuse with rate limiting:
45
+
46
+ ```ruby
47
+ Tsikol.server "my-server" do
48
+ use Tsikol::RateLimitMiddleware,
49
+ max_requests: 100, # Per window
50
+ window_seconds: 60, # Time window
51
+ by: :client_id # Group by client
52
+ end
53
+ ```
54
+
55
+ ### Error Handling Middleware
56
+
57
+ Consistent error handling:
58
+
59
+ ```ruby
60
+ Tsikol.server "my-server" do
61
+ use Tsikol::ErrorHandlingMiddleware,
62
+ log_errors: true,
63
+ include_backtrace: false,
64
+ error_handler: ->(error) { notify_error_service(error) }
65
+ end
66
+ ```
67
+
68
+ ### Metrics Middleware
69
+
70
+ Collect performance metrics:
71
+
72
+ ```ruby
73
+ Tsikol.server "my-server" do
74
+ use Tsikol::MetricsMiddleware,
75
+ include_histogram: true,
76
+ track_errors: true,
77
+ custom_tags: { environment: "production" }
78
+ end
79
+ ```
80
+
81
+ ## Creating Custom Middleware
82
+
83
+ ### Basic Middleware Structure
84
+
85
+ ```ruby
86
+ class MyMiddleware < Tsikol::Middleware
87
+ def initialize(app, options = {})
88
+ @app = app
89
+ @options = options
90
+ end
91
+
92
+ def call(request)
93
+ # Before request processing
94
+ log(:debug, "Processing request", method: request["method"])
95
+
96
+ # Call the next middleware or handler
97
+ response = @app.call(request)
98
+
99
+ # After response processing
100
+ log(:debug, "Request completed", status: response[:result] ? "success" : "error")
101
+
102
+ response
103
+ end
104
+ end
105
+ ```
106
+
107
+ ### Authentication Middleware
108
+
109
+ ```ruby
110
+ class AuthenticationMiddleware < Tsikol::Middleware
111
+ def initialize(app, options = {})
112
+ super
113
+ @secret_key = options[:secret_key] || ENV['MCP_SECRET_KEY']
114
+ @required_methods = options[:required_methods] || []
115
+ end
116
+
117
+ def call(request)
118
+ method = request["method"]
119
+
120
+ # Skip auth for certain methods
121
+ unless requires_auth?(method)
122
+ return @app.call(request)
123
+ end
124
+
125
+ # Extract and verify token
126
+ token = extract_token(request)
127
+
128
+ unless valid_token?(token)
129
+ return error_response(request["id"], -32001, "Unauthorized")
130
+ end
131
+
132
+ # Add user context
133
+ request["authenticated_user"] = decode_token(token)
134
+
135
+ @app.call(request)
136
+ end
137
+
138
+ private
139
+
140
+ def requires_auth?(method)
141
+ return true if @required_methods.empty?
142
+ @required_methods.include?(method)
143
+ end
144
+
145
+ def extract_token(request)
146
+ # Look for token in various places
147
+ request.dig("params", "_token") ||
148
+ request.dig("params", "authorization") ||
149
+ request.dig("meta", "authorization")
150
+ end
151
+
152
+ def valid_token?(token)
153
+ return false unless token
154
+
155
+ begin
156
+ payload = JWT.decode(token, @secret_key, true, algorithm: 'HS256')
157
+ !expired?(payload[0])
158
+ rescue JWT::DecodeError
159
+ false
160
+ end
161
+ end
162
+
163
+ def expired?(payload)
164
+ return false unless payload["exp"]
165
+ Time.now.to_i > payload["exp"]
166
+ end
167
+
168
+ def decode_token(token)
169
+ JWT.decode(token, @secret_key, true, algorithm: 'HS256')[0]
170
+ rescue
171
+ nil
172
+ end
173
+
174
+ def error_response(id, code, message)
175
+ {
176
+ jsonrpc: "2.0",
177
+ id: id,
178
+ error: {
179
+ code: code,
180
+ message: message
181
+ }
182
+ }
183
+ end
184
+ end
185
+ ```
186
+
187
+ ### Request Validation Middleware
188
+
189
+ ```ruby
190
+ class ValidationMiddleware < Tsikol::Middleware
191
+ def initialize(app, options = {})
192
+ super
193
+ @max_request_size = options[:max_request_size] || 1_048_576 # 1MB
194
+ @allowed_methods = options[:allowed_methods] || nil
195
+ end
196
+
197
+ def call(request)
198
+ # Validate request structure
199
+ unless valid_jsonrpc?(request)
200
+ return error_response(request["id"], -32600, "Invalid Request")
201
+ end
202
+
203
+ # Check request size
204
+ if request.to_json.bytesize > @max_request_size
205
+ return error_response(request["id"], -32600, "Request too large")
206
+ end
207
+
208
+ # Check allowed methods
209
+ if @allowed_methods && !@allowed_methods.include?(request["method"])
210
+ return error_response(request["id"], -32601, "Method not found")
211
+ end
212
+
213
+ @app.call(request)
214
+ end
215
+
216
+ private
217
+
218
+ def valid_jsonrpc?(request)
219
+ request.is_a?(Hash) &&
220
+ request["jsonrpc"] == "2.0" &&
221
+ request["id"] &&
222
+ request["method"].is_a?(String)
223
+ end
224
+ end
225
+ ```
226
+
227
+ ### Caching Middleware
228
+
229
+ ```ruby
230
+ class CachingMiddleware < Tsikol::Middleware
231
+ def initialize(app, options = {})
232
+ super
233
+ @cache = {}
234
+ @ttl = options[:ttl] || 300 # 5 minutes
235
+ @cacheable_methods = options[:cacheable_methods] || ["resources/read"]
236
+ end
237
+
238
+ def call(request)
239
+ method = request["method"]
240
+
241
+ # Only cache certain methods
242
+ unless cacheable?(method)
243
+ return @app.call(request)
244
+ end
245
+
246
+ # Generate cache key
247
+ cache_key = generate_cache_key(request)
248
+
249
+ # Check cache
250
+ if cached = get_cached(cache_key)
251
+ log(:debug, "Cache hit", key: cache_key)
252
+ return cached
253
+ end
254
+
255
+ # Process request
256
+ response = @app.call(request)
257
+
258
+ # Cache successful responses
259
+ if response[:result]
260
+ set_cached(cache_key, response)
261
+ end
262
+
263
+ response
264
+ end
265
+
266
+ private
267
+
268
+ def cacheable?(method)
269
+ @cacheable_methods.include?(method)
270
+ end
271
+
272
+ def generate_cache_key(request)
273
+ params = request["params"] || {}
274
+ "#{request['method']}:#{params.sort.to_h.hash}"
275
+ end
276
+
277
+ def get_cached(key)
278
+ entry = @cache[key]
279
+ return nil unless entry
280
+
281
+ if Time.now - entry[:timestamp] > @ttl
282
+ @cache.delete(key)
283
+ return nil
284
+ end
285
+
286
+ entry[:response]
287
+ end
288
+
289
+ def set_cached(key, response)
290
+ @cache[key] = {
291
+ response: response,
292
+ timestamp: Time.now
293
+ }
294
+
295
+ # Limit cache size
296
+ cleanup_cache if @cache.size > 1000
297
+ end
298
+
299
+ def cleanup_cache
300
+ # Remove oldest entries
301
+ sorted = @cache.sort_by { |_, v| v[:timestamp] }
302
+ sorted.first(@cache.size - 800).each { |k, _| @cache.delete(k) }
303
+ end
304
+ end
305
+ ```
306
+
307
+ ## Middleware Chain
308
+
309
+ ### Ordering Middleware
310
+
311
+ Middleware executes in the order it's defined:
312
+
313
+ ```ruby
314
+ Tsikol.server "my-server" do
315
+ # Executes first -> last for requests
316
+ # Executes last -> first for responses
317
+
318
+ use Tsikol::LoggingMiddleware # 1. Logs request
319
+ use AuthenticationMiddleware # 2. Checks auth
320
+ use ValidationMiddleware # 3. Validates
321
+ use Tsikol::RateLimitMiddleware # 4. Rate limits
322
+ use CachingMiddleware # 5. Checks cache
323
+ use Tsikol::ErrorHandlingMiddleware # 6. Handles errors
324
+
325
+ # Your tools and resources handle the request here
326
+
327
+ # Response travels back through middleware in reverse
328
+ end
329
+ ```
330
+
331
+ ### Conditional Middleware
332
+
333
+ Apply middleware conditionally:
334
+
335
+ ```ruby
336
+ class ConditionalMiddleware < Tsikol::Middleware
337
+ def initialize(app, options = {})
338
+ super
339
+ @condition = options[:condition] || ->(_) { true }
340
+ @wrapped_middleware = options[:middleware].new(app, options[:middleware_options] || {})
341
+ end
342
+
343
+ def call(request)
344
+ if @condition.call(request)
345
+ @wrapped_middleware.call(request)
346
+ else
347
+ @app.call(request)
348
+ end
349
+ end
350
+ end
351
+
352
+ # Usage
353
+ Tsikol.server "my-server" do
354
+ use ConditionalMiddleware,
355
+ condition: ->(req) { req["method"].start_with?("tools/") },
356
+ middleware: AuthenticationMiddleware,
357
+ middleware_options: { secret_key: "secret" }
358
+ end
359
+ ```
360
+
361
+ ## Request/Response Manipulation
362
+
363
+ ### Modifying Requests
364
+
365
+ ```ruby
366
+ class RequestEnrichmentMiddleware < Tsikol::Middleware
367
+ def call(request)
368
+ # Add metadata to request
369
+ request["meta"] ||= {}
370
+ request["meta"]["timestamp"] = Time.now.iso8601
371
+ request["meta"]["request_id"] = SecureRandom.uuid
372
+
373
+ # Add default parameters
374
+ if request["method"] == "tools/call"
375
+ request["params"] ||= {}
376
+ request["params"]["_metadata"] = {
377
+ server_version: Tsikol::VERSION,
378
+ ruby_version: RUBY_VERSION
379
+ }
380
+ end
381
+
382
+ @app.call(request)
383
+ end
384
+ end
385
+ ```
386
+
387
+ ### Modifying Responses
388
+
389
+ ```ruby
390
+ class ResponseFormattingMiddleware < Tsikol::Middleware
391
+ def call(request)
392
+ response = @app.call(request)
393
+
394
+ # Add metadata to all responses
395
+ if response[:result]
396
+ response[:result] = {
397
+ data: response[:result],
398
+ meta: {
399
+ timestamp: Time.now.iso8601,
400
+ duration_ms: calculate_duration(request),
401
+ server: @server.name
402
+ }
403
+ }
404
+ end
405
+
406
+ # Format error responses
407
+ if response[:error]
408
+ response[:error][:data] ||= {}
409
+ response[:error][:data][:request_id] = request.dig("meta", "request_id")
410
+ end
411
+
412
+ response
413
+ end
414
+
415
+ private
416
+
417
+ def calculate_duration(request)
418
+ start_time = request.dig("meta", "_start_time")
419
+ return nil unless start_time
420
+ ((Time.now - start_time) * 1000).round(2)
421
+ end
422
+ end
423
+ ```
424
+
425
+ ### Short-Circuit Responses
426
+
427
+ ```ruby
428
+ class CircuitBreakerMiddleware < Tsikol::Middleware
429
+ def initialize(app, options = {})
430
+ super
431
+ @failure_threshold = options[:failure_threshold] || 5
432
+ @timeout = options[:timeout] || 60
433
+ @failure_counts = Hash.new(0)
434
+ @circuit_opened_at = {}
435
+ end
436
+
437
+ def call(request)
438
+ key = circuit_key(request)
439
+
440
+ # Check if circuit is open
441
+ if circuit_open?(key)
442
+ if should_attempt_reset?(key)
443
+ # Try one request
444
+ @circuit_opened_at.delete(key)
445
+ else
446
+ # Short-circuit
447
+ return error_response(
448
+ request["id"],
449
+ -32003,
450
+ "Service temporarily unavailable"
451
+ )
452
+ end
453
+ end
454
+
455
+ # Process request
456
+ response = @app.call(request)
457
+
458
+ # Track failures
459
+ if response[:error]
460
+ @failure_counts[key] += 1
461
+
462
+ if @failure_counts[key] >= @failure_threshold
463
+ open_circuit(key)
464
+ end
465
+ else
466
+ # Reset on success
467
+ @failure_counts[key] = 0
468
+ end
469
+
470
+ response
471
+ end
472
+
473
+ private
474
+
475
+ def circuit_key(request)
476
+ request["method"]
477
+ end
478
+
479
+ def circuit_open?(key)
480
+ @circuit_opened_at.key?(key)
481
+ end
482
+
483
+ def should_attempt_reset?(key)
484
+ Time.now - @circuit_opened_at[key] > @timeout
485
+ end
486
+
487
+ def open_circuit(key)
488
+ @circuit_opened_at[key] = Time.now
489
+ log(:warning, "Circuit breaker opened", method: key)
490
+ end
491
+ end
492
+ ```
493
+
494
+ ## Advanced Patterns
495
+
496
+ ### Async Middleware
497
+
498
+ Handle async operations:
499
+
500
+ ```ruby
501
+ class AsyncMiddleware < Tsikol::Middleware
502
+ def initialize(app, options = {})
503
+ super
504
+ @thread_pool = options[:thread_pool] || default_thread_pool
505
+ end
506
+
507
+ def call(request)
508
+ if async_method?(request["method"])
509
+ handle_async(request)
510
+ else
511
+ @app.call(request)
512
+ end
513
+ end
514
+
515
+ private
516
+
517
+ def async_method?(method)
518
+ method.end_with?("_async")
519
+ end
520
+
521
+ def handle_async(request)
522
+ job_id = SecureRandom.uuid
523
+
524
+ # Start async processing
525
+ @thread_pool.post do
526
+ begin
527
+ result = @app.call(request)
528
+ store_result(job_id, result)
529
+ rescue => e
530
+ store_error(job_id, e)
531
+ end
532
+ end
533
+
534
+ # Return immediately with job ID
535
+ {
536
+ jsonrpc: "2.0",
537
+ id: request["id"],
538
+ result: {
539
+ job_id: job_id,
540
+ status: "processing",
541
+ check_url: "/jobs/#{job_id}"
542
+ }
543
+ }
544
+ end
545
+
546
+ def default_thread_pool
547
+ Concurrent::ThreadPoolExecutor.new(
548
+ min_threads: 2,
549
+ max_threads: 10,
550
+ max_queue: 100
551
+ )
552
+ end
553
+ end
554
+ ```
555
+
556
+ ### Middleware Composition
557
+
558
+ Combine multiple middleware behaviors:
559
+
560
+ ```ruby
561
+ class CompositeMiddleware < Tsikol::Middleware
562
+ def initialize(app, *middlewares)
563
+ @app = app
564
+ @middleware_chain = build_chain(middlewares)
565
+ end
566
+
567
+ def call(request)
568
+ @middleware_chain.call(request)
569
+ end
570
+
571
+ private
572
+
573
+ def build_chain(middlewares)
574
+ middlewares.reverse.reduce(@app) do |app, (middleware_class, options)|
575
+ middleware_class.new(app, options || {})
576
+ end
577
+ end
578
+ end
579
+
580
+ # Usage
581
+ Tsikol.server "my-server" do
582
+ use CompositeMiddleware,
583
+ [Tsikol::LoggingMiddleware, { level: :debug }],
584
+ [AuthenticationMiddleware, { secret_key: "secret" }],
585
+ [Tsikol::RateLimitMiddleware, { max_requests: 100 }]
586
+ end
587
+ ```
588
+
589
+ ### Context Propagation
590
+
591
+ Pass context through the middleware chain:
592
+
593
+ ```ruby
594
+ class ContextMiddleware < Tsikol::Middleware
595
+ def call(request)
596
+ # Initialize context
597
+ context = Thread.current[:mcp_context] = {
598
+ request_id: SecureRandom.uuid,
599
+ start_time: Time.now,
600
+ user: nil,
601
+ metadata: {}
602
+ }
603
+
604
+ begin
605
+ response = @app.call(request)
606
+
607
+ # Add context to response
608
+ if response[:result] && context[:metadata].any?
609
+ response[:result][:_context] = context[:metadata]
610
+ end
611
+
612
+ response
613
+ ensure
614
+ # Clean up context
615
+ Thread.current[:mcp_context] = nil
616
+ end
617
+ end
618
+ end
619
+
620
+ # Other middleware can access context
621
+ class LoggingMiddleware < Tsikol::Middleware
622
+ def call(request)
623
+ context = Thread.current[:mcp_context]
624
+
625
+ log(:info, "Request received",
626
+ request_id: context[:request_id],
627
+ method: request["method"])
628
+
629
+ @app.call(request)
630
+ end
631
+ end
632
+ ```
633
+
634
+ ## Testing Middleware
635
+
636
+ ### Basic Middleware Test
637
+
638
+ ```ruby
639
+ require 'minitest/autorun'
640
+
641
+ class MiddlewareTest < Minitest::Test
642
+ def setup
643
+ @app = ->(_request) { { jsonrpc: "2.0", id: 1, result: "success" } }
644
+ @middleware = MyMiddleware.new(@app)
645
+ end
646
+
647
+ def test_passes_request_through
648
+ request = { "jsonrpc" => "2.0", "id" => 1, "method" => "test" }
649
+ response = @middleware.call(request)
650
+
651
+ assert_equal "success", response[:result]
652
+ end
653
+
654
+ def test_modifies_request
655
+ # Test that middleware modifies request as expected
656
+ end
657
+
658
+ def test_modifies_response
659
+ # Test that middleware modifies response as expected
660
+ end
661
+ end
662
+ ```
663
+
664
+ ### Testing Authentication
665
+
666
+ ```ruby
667
+ class AuthMiddlewareTest < Minitest::Test
668
+ def setup
669
+ @app = ->(_request) { { jsonrpc: "2.0", id: 1, result: "success" } }
670
+ @middleware = AuthenticationMiddleware.new(@app,
671
+ secret_key: "test_secret",
672
+ required_methods: ["tools/call"])
673
+ end
674
+
675
+ def test_allows_unauthenticated_methods
676
+ request = {
677
+ "jsonrpc" => "2.0",
678
+ "id" => 1,
679
+ "method" => "initialize"
680
+ }
681
+
682
+ response = @middleware.call(request)
683
+ assert_equal "success", response[:result]
684
+ end
685
+
686
+ def test_requires_auth_for_tools
687
+ request = {
688
+ "jsonrpc" => "2.0",
689
+ "id" => 1,
690
+ "method" => "tools/call"
691
+ }
692
+
693
+ response = @middleware.call(request)
694
+ assert_equal -32001, response[:error][:code]
695
+ assert_equal "Unauthorized", response[:error][:message]
696
+ end
697
+
698
+ def test_accepts_valid_token
699
+ token = generate_valid_token
700
+ request = {
701
+ "jsonrpc" => "2.0",
702
+ "id" => 1,
703
+ "method" => "tools/call",
704
+ "params" => { "_token" => token }
705
+ }
706
+
707
+ response = @middleware.call(request)
708
+ assert_equal "success", response[:result]
709
+ end
710
+
711
+ private
712
+
713
+ def generate_valid_token
714
+ payload = {
715
+ user_id: 123,
716
+ exp: Time.now.to_i + 3600
717
+ }
718
+ JWT.encode(payload, "test_secret", 'HS256')
719
+ end
720
+ end
721
+ ```
722
+
723
+ ### Testing Rate Limiting
724
+
725
+ ```ruby
726
+ class RateLimitTest < Minitest::Test
727
+ def setup
728
+ @app = ->(_request) { { jsonrpc: "2.0", id: 1, result: "success" } }
729
+ @middleware = Tsikol::RateLimitMiddleware.new(@app,
730
+ max_requests: 3,
731
+ window_seconds: 1)
732
+ end
733
+
734
+ def test_allows_requests_under_limit
735
+ 3.times do |i|
736
+ request = { "jsonrpc" => "2.0", "id" => i, "method" => "test" }
737
+ response = @middleware.call(request)
738
+ assert_equal "success", response[:result]
739
+ end
740
+ end
741
+
742
+ def test_blocks_requests_over_limit
743
+ # Make max requests
744
+ 3.times do |i|
745
+ request = { "jsonrpc" => "2.0", "id" => i, "method" => "test" }
746
+ @middleware.call(request)
747
+ end
748
+
749
+ # Next request should be rate limited
750
+ request = { "jsonrpc" => "2.0", "id" => 4, "method" => "test" }
751
+ response = @middleware.call(request)
752
+
753
+ assert response[:error]
754
+ assert_match /rate limit/i, response[:error][:message]
755
+ end
756
+
757
+ def test_resets_after_window
758
+ # Make max requests
759
+ 3.times { |i| @middleware.call(base_request(i)) }
760
+
761
+ # Wait for window to reset
762
+ sleep(1.1)
763
+
764
+ # Should allow new requests
765
+ response = @middleware.call(base_request(4))
766
+ assert_equal "success", response[:result]
767
+ end
768
+
769
+ private
770
+
771
+ def base_request(id)
772
+ { "jsonrpc" => "2.0", "id" => id, "method" => "test" }
773
+ end
774
+ end
775
+ ```
776
+
777
+ ## Best Practices
778
+
779
+ 1. **Keep Middleware Focused**: Each middleware should have one responsibility
780
+ 2. **Order Matters**: Place auth before rate limiting, logging first/last
781
+ 3. **Handle Errors**: Don't let middleware errors crash the server
782
+ 4. **Performance**: Middleware runs on every request, keep it fast
783
+ 5. **Thread Safety**: Ensure middleware is thread-safe
784
+ 6. **Configuration**: Make middleware configurable via options
785
+ 7. **Testing**: Test middleware in isolation and integration
786
+
787
+ ## Common Middleware Stack
788
+
789
+ A typical production middleware stack:
790
+
791
+ ```ruby
792
+ Tsikol.server "production-server" do
793
+ # Observability
794
+ use Tsikol::LoggingMiddleware, level: :info
795
+ use Tsikol::MetricsMiddleware
796
+
797
+ # Security
798
+ use ValidationMiddleware, max_request_size: 5_242_880 # 5MB
799
+ use AuthenticationMiddleware, secret_key: ENV['SECRET_KEY']
800
+
801
+ # Protection
802
+ use Tsikol::RateLimitMiddleware, max_requests: 1000, window_seconds: 60
803
+ use CircuitBreakerMiddleware, failure_threshold: 10
804
+
805
+ # Performance
806
+ use CachingMiddleware, ttl: 300
807
+
808
+ # Error handling (last)
809
+ use Tsikol::ErrorHandlingMiddleware, log_errors: true
810
+
811
+ # Your application
812
+ tool FileManager
813
+ resource SystemStatus
814
+ prompt Assistant
815
+ end
816
+ ```
817
+
818
+ ## Next Steps
819
+
820
+ - Learn about [Testing](testing.md)
821
+ - Implement [Authentication](../cookbook/authentication.md)
822
+ - Add [Rate Limiting](../cookbook/rate-limiting.md)
823
+ - Set up [Logging](../cookbook/logging.md)