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,721 @@
1
+ # Middleware API Reference
2
+
3
+ The `Tsikol::Middleware` class provides the foundation for creating middleware components that intercept and process requests/responses.
4
+
5
+ ## Class: Tsikol::Middleware
6
+
7
+ ### Constructor
8
+
9
+ ```ruby
10
+ class MyMiddleware < Tsikol::Middleware
11
+ def initialize(app, options = {})
12
+ @app = app
13
+ @options = options
14
+ # Additional initialization
15
+ end
16
+ end
17
+ ```
18
+
19
+ #### Parameters
20
+
21
+ - `app`: The next middleware or handler in the chain
22
+ - `options` (Hash): Configuration options for the middleware
23
+
24
+ ### Core Method
25
+
26
+ #### `#call(request)`
27
+
28
+ Process a request and optionally modify the response.
29
+
30
+ ```ruby
31
+ def call(request)
32
+ # Before request processing
33
+ modified_request = process_request(request)
34
+
35
+ # Call next middleware/handler
36
+ response = @app.call(modified_request)
37
+
38
+ # After response processing
39
+ process_response(response)
40
+ end
41
+ ```
42
+
43
+ ## Built-in Middleware
44
+
45
+ ### Tsikol::LoggingMiddleware
46
+
47
+ Logs all requests and responses.
48
+
49
+ ```ruby
50
+ server.use Tsikol::LoggingMiddleware,
51
+ level: :info, # Log level (:debug, :info, :warning, :error)
52
+ include_params: true, # Log request parameters
53
+ include_response: true, # Log response data
54
+ max_response_length: 1000 # Truncate long responses
55
+ ```
56
+
57
+ Example implementation:
58
+
59
+ ```ruby
60
+ class LoggingMiddleware < Tsikol::Middleware
61
+ def initialize(app, options = {})
62
+ super
63
+ @level = options[:level] || :info
64
+ @include_params = options.fetch(:include_params, true)
65
+ @include_response = options.fetch(:include_response, true)
66
+ @max_response_length = options[:max_response_length] || 1000
67
+ end
68
+
69
+ def call(request)
70
+ start_time = Time.now
71
+ request_id = request["id"]
72
+ method = request["method"]
73
+
74
+ # Log request
75
+ log_data = { method: method, id: request_id }
76
+ log_data[:params] = request["params"] if @include_params && request["params"]
77
+
78
+ log @level, "Request received", log_data
79
+
80
+ # Process request
81
+ response = @app.call(request)
82
+
83
+ # Log response
84
+ duration = ((Time.now - start_time) * 1000).round(2)
85
+
86
+ response_data = {
87
+ method: method,
88
+ id: request_id,
89
+ duration_ms: duration,
90
+ status: response[:error] ? "error" : "success"
91
+ }
92
+
93
+ if @include_response
94
+ if response[:error]
95
+ response_data[:error] = response[:error]
96
+ elsif response[:result]
97
+ result_str = response[:result].to_s
98
+ if result_str.length > @max_response_length
99
+ response_data[:result] = result_str[0...@max_response_length] + "..."
100
+ response_data[:truncated] = true
101
+ else
102
+ response_data[:result] = response[:result]
103
+ end
104
+ end
105
+ end
106
+
107
+ log @level, "Request completed", response_data
108
+
109
+ response
110
+ rescue => e
111
+ log :error, "Request failed",
112
+ method: method,
113
+ id: request_id,
114
+ error: e.class.name,
115
+ message: e.message
116
+ raise
117
+ end
118
+
119
+ private
120
+
121
+ def log(level, message, data)
122
+ return unless @app.respond_to?(:log)
123
+ @app.log(level, message, data: data)
124
+ end
125
+ end
126
+ ```
127
+
128
+ ### Tsikol::ErrorHandlingMiddleware
129
+
130
+ Provides consistent error handling and formatting.
131
+
132
+ ```ruby
133
+ server.use Tsikol::ErrorHandlingMiddleware,
134
+ log_errors: true, # Log errors
135
+ include_backtrace: false, # Include stack traces in development
136
+ error_handler: ->(e) { } # Custom error handler callback
137
+ ```
138
+
139
+ Example implementation:
140
+
141
+ ```ruby
142
+ class ErrorHandlingMiddleware < Tsikol::Middleware
143
+ def initialize(app, options = {})
144
+ super
145
+ @log_errors = options.fetch(:log_errors, true)
146
+ @include_backtrace = options.fetch(:include_backtrace, false)
147
+ @error_handler = options[:error_handler]
148
+ end
149
+
150
+ def call(request)
151
+ @app.call(request)
152
+ rescue Tsikol::ValidationError => e
153
+ handle_validation_error(request, e)
154
+ rescue Tsikol::NotFoundError => e
155
+ handle_not_found_error(request, e)
156
+ rescue StandardError => e
157
+ handle_internal_error(request, e)
158
+ end
159
+
160
+ private
161
+
162
+ def handle_validation_error(request, error)
163
+ log_error(request, error) if @log_errors
164
+
165
+ {
166
+ jsonrpc: "2.0",
167
+ id: request["id"],
168
+ error: {
169
+ code: -32602,
170
+ message: "Invalid params",
171
+ data: { details: error.message }
172
+ }
173
+ }
174
+ end
175
+
176
+ def handle_not_found_error(request, error)
177
+ log_error(request, error) if @log_errors
178
+
179
+ {
180
+ jsonrpc: "2.0",
181
+ id: request["id"],
182
+ error: {
183
+ code: -32601,
184
+ message: "Method not found",
185
+ data: { details: error.message }
186
+ }
187
+ }
188
+ end
189
+
190
+ def handle_internal_error(request, error)
191
+ log_error(request, error) if @log_errors
192
+ @error_handler&.call(error)
193
+
194
+ error_data = { message: error.message }
195
+ error_data[:backtrace] = error.backtrace.first(10) if @include_backtrace
196
+
197
+ {
198
+ jsonrpc: "2.0",
199
+ id: request["id"],
200
+ error: {
201
+ code: -32603,
202
+ message: "Internal error",
203
+ data: error_data
204
+ }
205
+ }
206
+ end
207
+
208
+ def log_error(request, error)
209
+ return unless @app.respond_to?(:log)
210
+
211
+ @app.log :error, "Request error",
212
+ data: {
213
+ method: request["method"],
214
+ error_class: error.class.name,
215
+ error_message: error.message,
216
+ backtrace: error.backtrace&.first(5)
217
+ }
218
+ end
219
+ end
220
+ ```
221
+
222
+ ### Tsikol::RateLimitMiddleware
223
+
224
+ Implements rate limiting to prevent abuse.
225
+
226
+ ```ruby
227
+ server.use Tsikol::RateLimitMiddleware,
228
+ max_requests: 100, # Maximum requests per window
229
+ window_seconds: 60, # Time window in seconds
230
+ by: :client_id, # Group requests by this key
231
+ skip_methods: [] # Methods to exclude from rate limiting
232
+ ```
233
+
234
+ Example implementation:
235
+
236
+ ```ruby
237
+ class RateLimitMiddleware < Tsikol::Middleware
238
+ def initialize(app, options = {})
239
+ super
240
+ @max_requests = options[:max_requests] || 60
241
+ @window_seconds = options[:window_seconds] || 60
242
+ @by = options[:by] || :client_id
243
+ @skip_methods = options[:skip_methods] || []
244
+ @buckets = {}
245
+ @mutex = Mutex.new
246
+ end
247
+
248
+ def call(request)
249
+ method = request["method"]
250
+
251
+ # Skip rate limiting for certain methods
252
+ return @app.call(request) if @skip_methods.include?(method)
253
+
254
+ # Get client identifier
255
+ client_id = get_client_id(request)
256
+
257
+ # Check rate limit
258
+ if rate_limited?(client_id)
259
+ return rate_limit_response(request)
260
+ end
261
+
262
+ # Track request
263
+ track_request(client_id)
264
+
265
+ # Process request
266
+ @app.call(request)
267
+ end
268
+
269
+ private
270
+
271
+ def get_client_id(request)
272
+ case @by
273
+ when :client_id
274
+ request.dig("params", "_client_id") || "anonymous"
275
+ when :ip
276
+ request.dig("meta", "remote_ip") || "unknown"
277
+ when Proc
278
+ @by.call(request)
279
+ else
280
+ "global"
281
+ end
282
+ end
283
+
284
+ def rate_limited?(client_id)
285
+ @mutex.synchronize do
286
+ bucket = get_bucket(client_id)
287
+ clean_bucket(bucket)
288
+ bucket[:requests].size >= @max_requests
289
+ end
290
+ end
291
+
292
+ def track_request(client_id)
293
+ @mutex.synchronize do
294
+ bucket = get_bucket(client_id)
295
+ bucket[:requests] << Time.now
296
+ end
297
+ end
298
+
299
+ def get_bucket(client_id)
300
+ @buckets[client_id] ||= { requests: [] }
301
+ end
302
+
303
+ def clean_bucket(bucket)
304
+ cutoff = Time.now - @window_seconds
305
+ bucket[:requests].reject! { |time| time < cutoff }
306
+ end
307
+
308
+ def rate_limit_response(request)
309
+ {
310
+ jsonrpc: "2.0",
311
+ id: request["id"],
312
+ error: {
313
+ code: -32000,
314
+ message: "Rate limit exceeded",
315
+ data: {
316
+ max_requests: @max_requests,
317
+ window_seconds: @window_seconds,
318
+ retry_after: @window_seconds
319
+ }
320
+ }
321
+ }
322
+ end
323
+ end
324
+ ```
325
+
326
+ ### Tsikol::MetricsMiddleware
327
+
328
+ Collects performance metrics.
329
+
330
+ ```ruby
331
+ server.use Tsikol::MetricsMiddleware,
332
+ include_histogram: true, # Track response time distribution
333
+ track_errors: true, # Count errors by type
334
+ custom_tags: {} # Additional tags for metrics
335
+ ```
336
+
337
+ ## Creating Custom Middleware
338
+
339
+ ### Basic Template
340
+
341
+ ```ruby
342
+ class CustomMiddleware < Tsikol::Middleware
343
+ def initialize(app, options = {})
344
+ @app = app
345
+ # Initialize with options
346
+ end
347
+
348
+ def call(request)
349
+ # Pre-processing
350
+
351
+ # Call next middleware
352
+ response = @app.call(request)
353
+
354
+ # Post-processing
355
+
356
+ response
357
+ end
358
+ end
359
+ ```
360
+
361
+ ### Request Modification
362
+
363
+ ```ruby
364
+ class RequestEnrichmentMiddleware < Tsikol::Middleware
365
+ def call(request)
366
+ # Add metadata
367
+ request["meta"] ||= {}
368
+ request["meta"]["timestamp"] = Time.now.iso8601
369
+ request["meta"]["request_id"] = SecureRandom.uuid
370
+
371
+ # Add default parameters
372
+ if request["params"]
373
+ request["params"]["_server_version"] = Tsikol::VERSION
374
+ end
375
+
376
+ @app.call(request)
377
+ end
378
+ end
379
+ ```
380
+
381
+ ### Response Modification
382
+
383
+ ```ruby
384
+ class ResponseFormattingMiddleware < Tsikol::Middleware
385
+ def call(request)
386
+ start_time = Time.now
387
+
388
+ response = @app.call(request)
389
+
390
+ # Add timing information
391
+ duration = Time.now - start_time
392
+
393
+ if response[:result]
394
+ response[:result] = {
395
+ data: response[:result],
396
+ meta: {
397
+ duration_seconds: duration,
398
+ timestamp: Time.now.iso8601
399
+ }
400
+ }
401
+ end
402
+
403
+ response
404
+ end
405
+ end
406
+ ```
407
+
408
+ ### Conditional Processing
409
+
410
+ ```ruby
411
+ class ConditionalMiddleware < Tsikol::Middleware
412
+ def initialize(app, options = {})
413
+ super
414
+ @condition = options[:if] || ->(_) { true }
415
+ @unless = options[:unless] || ->(_) { false }
416
+ end
417
+
418
+ def call(request)
419
+ if should_process?(request)
420
+ # Apply middleware logic
421
+ process_with_middleware(request)
422
+ else
423
+ # Skip middleware
424
+ @app.call(request)
425
+ end
426
+ end
427
+
428
+ private
429
+
430
+ def should_process?(request)
431
+ @condition.call(request) && !@unless.call(request)
432
+ end
433
+
434
+ def process_with_middleware(request)
435
+ # Middleware logic here
436
+ @app.call(request)
437
+ end
438
+ end
439
+ ```
440
+
441
+ ### Short-Circuit Responses
442
+
443
+ ```ruby
444
+ class CacheMiddleware < Tsikol::Middleware
445
+ def initialize(app, options = {})
446
+ super
447
+ @cache = {}
448
+ @ttl = options[:ttl] || 300
449
+ end
450
+
451
+ def call(request)
452
+ # Only cache certain methods
453
+ return @app.call(request) unless cacheable?(request)
454
+
455
+ cache_key = generate_key(request)
456
+
457
+ # Check cache
458
+ if cached = get_cached(cache_key)
459
+ # Return cached response without calling app
460
+ return cached
461
+ end
462
+
463
+ # Process and cache
464
+ response = @app.call(request)
465
+
466
+ if response[:result]
467
+ set_cached(cache_key, response)
468
+ end
469
+
470
+ response
471
+ end
472
+
473
+ private
474
+
475
+ def cacheable?(request)
476
+ ["resources/read", "prompts/get"].include?(request["method"])
477
+ end
478
+
479
+ def generate_key(request)
480
+ "#{request['method']}:#{request['params'].to_json}"
481
+ end
482
+
483
+ def get_cached(key)
484
+ entry = @cache[key]
485
+ return nil unless entry
486
+ return nil if Time.now - entry[:time] > @ttl
487
+ entry[:response]
488
+ end
489
+
490
+ def set_cached(key, response)
491
+ @cache[key] = {
492
+ response: response,
493
+ time: Time.now
494
+ }
495
+ end
496
+ end
497
+ ```
498
+
499
+ ## Middleware Composition
500
+
501
+ ### Combining Middleware
502
+
503
+ ```ruby
504
+ class CompositeMiddleware < Tsikol::Middleware
505
+ def initialize(app, *middleware_classes)
506
+ @app = build_chain(app, middleware_classes)
507
+ end
508
+
509
+ def call(request)
510
+ @app.call(request)
511
+ end
512
+
513
+ private
514
+
515
+ def build_chain(app, middleware_classes)
516
+ middleware_classes.reverse.reduce(app) do |next_app, (klass, options)|
517
+ klass.new(next_app, options || {})
518
+ end
519
+ end
520
+ end
521
+
522
+ # Usage
523
+ server.use CompositeMiddleware,
524
+ [LoggingMiddleware, { level: :debug }],
525
+ [RateLimitMiddleware, { max_requests: 100 }],
526
+ [CacheMiddleware, { ttl: 600 }]
527
+ ```
528
+
529
+ ### Middleware Stack
530
+
531
+ ```ruby
532
+ class MiddlewareStack
533
+ def initialize
534
+ @middlewares = []
535
+ end
536
+
537
+ def use(middleware_class, options = {})
538
+ @middlewares << [middleware_class, options]
539
+ end
540
+
541
+ def build(app)
542
+ @middlewares.reverse.reduce(app) do |next_app, (klass, opts)|
543
+ klass.new(next_app, opts)
544
+ end
545
+ end
546
+ end
547
+ ```
548
+
549
+ ## Testing Middleware
550
+
551
+ ### Basic Middleware Test
552
+
553
+ ```ruby
554
+ require 'minitest/autorun'
555
+
556
+ class MiddlewareTest < Minitest::Test
557
+ def setup
558
+ @app = MockApp.new
559
+ @middleware = MyMiddleware.new(@app, option: "value")
560
+ end
561
+
562
+ def test_passes_request_through
563
+ request = build_request("test.method")
564
+ response = @middleware.call(request)
565
+
566
+ assert_equal "success", response[:result]
567
+ assert @app.called_with?(request)
568
+ end
569
+
570
+ def test_modifies_request
571
+ request = build_request("test.method")
572
+ @middleware.call(request)
573
+
574
+ # Check that middleware modified the request
575
+ assert request["meta"]["added_by_middleware"]
576
+ end
577
+
578
+ def test_handles_app_errors
579
+ @app.raise_error = true
580
+ request = build_request("test.method")
581
+
582
+ assert_raises(StandardError) do
583
+ @middleware.call(request)
584
+ end
585
+ end
586
+
587
+ private
588
+
589
+ def build_request(method, params = {})
590
+ {
591
+ "jsonrpc" => "2.0",
592
+ "id" => 1,
593
+ "method" => method,
594
+ "params" => params
595
+ }
596
+ end
597
+ end
598
+
599
+ class MockApp
600
+ attr_accessor :raise_error
601
+ attr_reader :last_request
602
+
603
+ def call(request)
604
+ @last_request = request
605
+ raise StandardError, "Mock error" if raise_error
606
+ { jsonrpc: "2.0", id: request["id"], result: "success" }
607
+ end
608
+
609
+ def called_with?(request)
610
+ @last_request == request
611
+ end
612
+ end
613
+ ```
614
+
615
+ ### Integration Testing
616
+
617
+ ```ruby
618
+ class MiddlewareIntegrationTest < Minitest::Test
619
+ def setup
620
+ @server = Tsikol::Server.new(name: "test")
621
+
622
+ # Add middleware stack
623
+ @server.use TimingMiddleware
624
+ @server.use LoggingMiddleware, level: :debug
625
+ @server.use ErrorHandlingMiddleware
626
+
627
+ # Add a test tool
628
+ @server.register_tool("echo") do |message:|
629
+ message
630
+ end
631
+ end
632
+
633
+ def test_middleware_chain_execution_order
634
+ # Create a tracking middleware
635
+ call_order = []
636
+
637
+ tracking_middleware = Class.new(Tsikol::Middleware) do
638
+ define_method :initialize do |app, name:|
639
+ @app = app
640
+ @name = name
641
+ end
642
+
643
+ define_method :call do |request|
644
+ call_order << "#{@name}:before"
645
+ response = @app.call(request)
646
+ call_order << "#{@name}:after"
647
+ response
648
+ end
649
+ end
650
+
651
+ server = Tsikol::Server.new(name: "test")
652
+ server.use tracking_middleware, name: "first"
653
+ server.use tracking_middleware, name: "second"
654
+ server.use tracking_middleware, name: "third"
655
+
656
+ server.handle_request({
657
+ "jsonrpc" => "2.0",
658
+ "id" => 1,
659
+ "method" => "initialize"
660
+ })
661
+
662
+ # Verify execution order
663
+ expected = [
664
+ "first:before",
665
+ "second:before",
666
+ "third:before",
667
+ "third:after",
668
+ "second:after",
669
+ "first:after"
670
+ ]
671
+
672
+ assert_equal expected, call_order
673
+ end
674
+ end
675
+ ```
676
+
677
+ ## Thread Safety
678
+
679
+ Middleware should be thread-safe:
680
+
681
+ ```ruby
682
+ class ThreadSafeMiddleware < Tsikol::Middleware
683
+ def initialize(app, options = {})
684
+ super
685
+ @mutex = Mutex.new
686
+ @counter = 0
687
+ end
688
+
689
+ def call(request)
690
+ # Thread-safe increment
691
+ @mutex.synchronize { @counter += 1 }
692
+
693
+ # Process request
694
+ response = @app.call(request)
695
+
696
+ # Thread-safe operation
697
+ @mutex.synchronize do
698
+ # Critical section
699
+ end
700
+
701
+ response
702
+ end
703
+ end
704
+ ```
705
+
706
+ ## Best Practices
707
+
708
+ 1. **Single Responsibility**: Each middleware should have one clear purpose
709
+ 2. **Minimize Overhead**: Middleware runs on every request, keep it efficient
710
+ 3. **Error Handling**: Always handle errors gracefully, don't swallow exceptions
711
+ 4. **Thread Safety**: Ensure middleware is thread-safe for concurrent requests
712
+ 5. **Configuration**: Make middleware configurable through options
713
+ 6. **Logging**: Use appropriate log levels and don't log sensitive data
714
+ 7. **Testing**: Test middleware in isolation and as part of the stack
715
+
716
+ ## See Also
717
+
718
+ - [Middleware Guide](../guides/middleware.md)
719
+ - [Server API](server.md)
720
+ - [Error Handling](../cookbook/error-handling.md)
721
+ - [Testing Guide](../guides/testing.md)