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,129 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/tsikol'
5
+
6
+ # Example showing advanced features: lifecycle hooks, health monitoring, error handling
7
+
8
+ Tsikol.server "advanced-server" do
9
+ # Configure middleware with error handling
10
+ use Tsikol::ValidationMiddleware
11
+ use Tsikol::LoggingMiddleware, logger: self
12
+
13
+ # Lifecycle hooks
14
+ before_start do
15
+ log :info, "Initializing server resources..."
16
+ # Initialize connections, load configs, etc.
17
+ @startup_time = Time.now
18
+ end
19
+
20
+ after_start do
21
+ log :info, "Server ready to accept requests"
22
+ log :info, "Health endpoint available at: resource health"
23
+ end
24
+
25
+ before_stop do
26
+ log :info, "Gracefully shutting down..."
27
+ # Close connections, save state, etc.
28
+ end
29
+
30
+ after_stop do
31
+ uptime = Time.now - @startup_time
32
+ log :info, "Server stopped after #{uptime.round(2)} seconds"
33
+ end
34
+
35
+ # Tool-specific hooks
36
+ before_tool "critical_operation" do |params|
37
+ log :warning, "Executing critical operation", data: params
38
+ end
39
+
40
+ after_tool "critical_operation" do |params, result|
41
+ log :info, "Critical operation completed", data: { params: params, result: result }
42
+ end
43
+
44
+ # General tool hooks for metrics
45
+ before_tool do |tool_name, params|
46
+ log :debug, "Calling tool: #{tool_name}"
47
+ end
48
+
49
+ after_tool do |tool_name, params, result|
50
+ log :debug, "Tool #{tool_name} completed"
51
+ end
52
+
53
+ # Tools with different error scenarios
54
+ tool "safe_operation" do |input:|
55
+ "Processed: #{input}"
56
+ end
57
+
58
+ tool "risky_operation" do |should_fail: false|
59
+ raise "Simulated failure" if should_fail
60
+ "Operation successful"
61
+ end
62
+
63
+ tool "critical_operation" do |data:, validate: true|
64
+ if validate && data.nil?
65
+ raise Tsikol::ValidationError, "Data cannot be nil"
66
+ end
67
+
68
+ # Simulate some work
69
+ sleep(0.1)
70
+ "Critical operation completed for: #{data}"
71
+ end
72
+
73
+ tool "slow_operation" do |timeout: 5|
74
+ log :info, "Starting slow operation", data: { timeout: timeout }
75
+
76
+ # Simulate work that might timeout
77
+ timeout.times do |i|
78
+ sleep(1)
79
+ log :debug, "Progress: #{i + 1}/#{timeout}"
80
+ end
81
+
82
+ "Completed after #{timeout} seconds"
83
+ end
84
+
85
+ # Tool that demonstrates circuit breaker
86
+ tool "flaky_service" do
87
+ # This would fail randomly to demonstrate circuit breaker
88
+ if rand > 0.7
89
+ raise "Service temporarily unavailable"
90
+ end
91
+ "Service responded successfully"
92
+ end
93
+
94
+ # Resource showing detailed metrics
95
+ resource "admin/metrics" do
96
+ {
97
+ server: {
98
+ name: @name,
99
+ version: @version,
100
+ uptime: Time.now - @startup_time,
101
+ health_status: health_status
102
+ },
103
+ metrics: @metrics.to_h,
104
+ error_handler: {
105
+ circuit_breakers: @error_handler.instance_variable_get(:@circuit_breakers).keys,
106
+ error_counts: @error_handler.instance_variable_get(:@error_counts)
107
+ }
108
+ }.to_json
109
+ end
110
+
111
+ # Resource for testing error scenarios
112
+ resource "test/error" do
113
+ raise "This is a test error"
114
+ end
115
+
116
+ # Prompt that tracks usage
117
+ prompt "analyze" do |data:, depth: "medium"|
118
+ @metrics.increment("prompts:analyze:usage")
119
+ "Analyze the following data with #{depth} depth analysis: #{data}"
120
+ end
121
+
122
+ # Enable all capabilities
123
+ logging true
124
+ completion true
125
+ sampling true
126
+
127
+ # Log server configuration
128
+ log :info, "Advanced server configured with lifecycle hooks and monitoring"
129
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WeatherChat < Tsikol::Prompt
4
+ name "weather_chat"
5
+ description "Weather chat prompt"
6
+
7
+ argument :city do
8
+ type :string
9
+ required
10
+ description "City to ask about weather"
11
+
12
+ complete do |partial|
13
+ # List of cities for autocomplete
14
+ cities = [
15
+ "New York", "London", "Tokyo", "Paris", "Berlin",
16
+ "Sydney", "Toronto", "Mumbai", "Beijing", "Moscow",
17
+ "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia"
18
+ ]
19
+
20
+ # Filter cities that start with the partial input (case-insensitive)
21
+ cities.select { |city| city.downcase.start_with?(partial.downcase) }
22
+ end
23
+ end
24
+
25
+ def get_messages(city:)
26
+ [
27
+ {
28
+ role: "user",
29
+ content: {
30
+ type: "text",
31
+ text: "What's the weather like in #{city}? Please give me current conditions and a forecast."
32
+ }
33
+ }
34
+ ]
35
+ end
36
+
37
+ def set_server(server)
38
+ @server = server
39
+
40
+ define_singleton_method(:log) do |level, message, data: nil, logger: nil|
41
+ @server.log(level, message, data: data, logger: logger)
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WeatherAlerts < Tsikol::Resource
4
+ uri "weather/alerts"
5
+ description "Active weather alerts"
6
+
7
+ def read
8
+ "No active weather alerts"
9
+ end
10
+
11
+ def set_server(server)
12
+ @server = server
13
+
14
+ define_singleton_method(:log) do |level, message, data: nil, logger: nil|
15
+ @server.log(level, message, data: data, logger: logger)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GetCurrentWeather < Tsikol::Tool
4
+ description "Get current weather for a location"
5
+
6
+ parameter :location do
7
+ type :string
8
+ required
9
+ description "Location to get weather for"
10
+ end
11
+
12
+ def execute(location:)
13
+ log :info, "Getting weather for #{location}" if respond_to?(:log)
14
+
15
+ temps = { "New York" => 72, "London" => 61, "Tokyo" => 77 }
16
+ temp = temps[location] || 70
17
+
18
+ if temps[location]
19
+ log :debug, "Found temperature in cache", data: { location: location, temp: temp } if respond_to?(:log)
20
+ else
21
+ log :warning, "Location not found, using default", data: { location: location } if respond_to?(:log)
22
+ end
23
+
24
+ "Currently #{temp}°F in #{location}"
25
+ end
26
+
27
+ def set_server(server)
28
+ @server = server
29
+
30
+ define_singleton_method(:log) do |level, message, data: nil, logger: nil|
31
+ @server.log(level, message, data: data, logger: logger)
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GetForecast < Tsikol::Tool
4
+ description "Get weather forecast for a location"
5
+
6
+ parameter :location do
7
+ type :string
8
+ required
9
+ description "Location to get forecast for"
10
+ end
11
+
12
+ parameter :days do
13
+ type :integer
14
+ optional
15
+ default 3
16
+ description "Number of days to forecast"
17
+ end
18
+
19
+ def execute(location:, days: 3)
20
+ "#{days}-day forecast for #{location}: Mostly sunny"
21
+ end
22
+
23
+ def set_server(server)
24
+ @server = server
25
+
26
+ define_singleton_method(:log) do |level, message, data: nil, logger: nil|
27
+ @server.log(level, message, data: data, logger: logger)
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GetWeatherByCoords < Tsikol::Tool
4
+ description "Get weather by coordinates"
5
+
6
+ parameter :latitude do
7
+ type :number
8
+ required
9
+ description "Latitude coordinate"
10
+ end
11
+
12
+ parameter :longitude do
13
+ type :number
14
+ required
15
+ description "Longitude coordinate"
16
+ end
17
+
18
+ parameter :location_name do
19
+ type :string
20
+ required
21
+ description "Name of the location"
22
+
23
+ complete do |partial|
24
+ # In a real app, this might reverse geocode based on lat/long
25
+ locations = [
26
+ "Central Park, NYC",
27
+ "Times Square, NYC",
28
+ "Brooklyn Bridge, NYC",
29
+ "Statue of Liberty, NYC",
30
+ "Empire State Building, NYC"
31
+ ]
32
+
33
+ locations.select { |loc| loc.downcase.include?(partial.downcase) }
34
+ end
35
+ end
36
+
37
+ def execute(latitude:, longitude:, location_name:)
38
+ "Weather at #{location_name} (#{latitude}, #{longitude}): Sunny, 75°F"
39
+ end
40
+
41
+ def set_server(server)
42
+ @server = server
43
+
44
+ define_singleton_method(:log) do |level, message, data: nil, logger: nil|
45
+ @server.log(level, message, data: data, logger: logger)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../../lib/tsikol'
5
+
6
+ # Option 1: Direct class references (requires explicit require)
7
+ require_relative 'app/tools/get_current_weather'
8
+ require_relative 'app/tools/get_forecast'
9
+ require_relative 'app/tools/get_weather_by_coords'
10
+ require_relative 'app/resources/weather_alerts'
11
+ require_relative 'app/prompts/weather_chat'
12
+
13
+ Tsikol.start(name: "weather-service") do
14
+ # Direct class references
15
+ tool GetCurrentWeather
16
+ tool GetForecast
17
+ tool GetWeatherByCoords
18
+ resource WeatherAlerts
19
+ prompt WeatherChat
20
+
21
+ # Inline definition still works
22
+ resource "server/health" do
23
+ "Weather service is healthy"
24
+ end
25
+ end
data/examples/basic.rb ADDED
@@ -0,0 +1,73 @@
1
+ require_relative '../lib/tsikol'
2
+
3
+ # NOTE: This example uses the original DSL-style API.
4
+ # For the new Rails-like structure with separate files, see examples/basic-migrated/
5
+ # or examples/weather-service/ for a more complete example.
6
+
7
+ Tsikol.server "weather-service" do
8
+ # Declare server capabilities (optional - features auto-enable when used)
9
+ # capabilities do
10
+ # logging # Auto-enabled when using 'log'
11
+ # completion # Auto-enabled when using 'completion_for'
12
+ # sampling # Must be explicitly enabled
13
+ # end
14
+
15
+ tool "get_current_weather" do |location:|
16
+ log :info, "Getting weather for #{location}"
17
+
18
+ temps = { "New York" => 72, "London" => 61, "Tokyo" => 77 }
19
+ temp = temps[location] || 70
20
+
21
+ if temps[location]
22
+ log :debug, "Found temperature in cache", data: { location: location, temp: temp }
23
+ else
24
+ log :warning, "Location not found, using default", data: { location: location }
25
+ end
26
+
27
+ "Currently #{temp}°F in #{location}"
28
+ end
29
+
30
+ tool "get_forecast" do |location:, days: 3|
31
+ "#{days}-day forecast for #{location}: Mostly sunny"
32
+ end
33
+
34
+ resource "weather/alerts" do
35
+ "No active weather alerts"
36
+ end
37
+
38
+ prompt "weather_chat" do |city:|
39
+ "What's the weather like in #{city}? Please give me current conditions and a forecast."
40
+ end
41
+
42
+ # Define completions for the weather_chat prompt's city argument
43
+ completion_for "prompt", "weather_chat", "city" do |partial|
44
+ # List of cities for autocomplete
45
+ cities = [
46
+ "New York", "London", "Tokyo", "Paris", "Berlin",
47
+ "Sydney", "Toronto", "Mumbai", "Beijing", "Moscow",
48
+ "Los Angeles", "Chicago", "Houston", "Phoenix", "Philadelphia"
49
+ ]
50
+
51
+ # Filter cities that start with the partial input (case-insensitive)
52
+ cities.select { |city| city.downcase.start_with?(partial.downcase) }
53
+ end
54
+
55
+ # Tool for demonstrating location autocomplete
56
+ tool "get_weather_by_coords" do |latitude:, longitude:, location_name:|
57
+ "Weather at #{location_name} (#{latitude}, #{longitude}): Sunny, 75°F"
58
+ end
59
+
60
+ # Completion for location names based on coordinates
61
+ completion_for "tool", "get_weather_by_coords", "location_name" do |partial|
62
+ # In a real app, this might reverse geocode based on lat/long
63
+ locations = [
64
+ "Central Park, NYC",
65
+ "Times Square, NYC",
66
+ "Brooklyn Bridge, NYC",
67
+ "Statue of Liberty, NYC",
68
+ "Empire State Building, NYC"
69
+ ]
70
+
71
+ locations.select { |loc| loc.downcase.include?(partial.downcase) }
72
+ end
73
+ end
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/tsikol'
5
+
6
+ # Full-featured example showing all Tsikol capabilities
7
+
8
+ # Custom middleware for API key validation
9
+ class ApiKeyMiddleware < Tsikol::Middleware
10
+ def initialize(app, valid_keys: [])
11
+ super(app)
12
+ @valid_keys = valid_keys
13
+ end
14
+
15
+ def before_request(message)
16
+ # Skip auth for certain methods
17
+ return message if %w[initialize tools/list].include?(message["method"])
18
+
19
+ api_key = message.dig("metadata", "api_key")
20
+ unless @valid_keys.include?(api_key)
21
+ raise "Invalid API key"
22
+ end
23
+
24
+ message
25
+ end
26
+ end
27
+
28
+ Tsikol.start(name: "full-featured-server") do
29
+ # Configure middleware stack
30
+ use Tsikol::ValidationMiddleware
31
+ use Tsikol::LoggingMiddleware
32
+ use ApiKeyMiddleware, valid_keys: ["demo-key-123", "test-key-456"]
33
+ use Tsikol::RateLimitMiddleware, max_requests: 100, window: 60
34
+
35
+ # Enable all capabilities
36
+ logging true
37
+ completion true
38
+
39
+ # Configure sampling with AI assistance
40
+ on_sampling do |request|
41
+ messages = request[:messages]
42
+ system_prompt = request[:system_prompt] || "You are a helpful assistant."
43
+
44
+ # Log sampling request
45
+ log :info, "AI assistance requested", data: {
46
+ messages: messages.size,
47
+ system: system_prompt
48
+ }
49
+
50
+ # Simulate AI response (in production, MCP client handles this)
51
+ {
52
+ role: "assistant",
53
+ content: {
54
+ type: "text",
55
+ text: "AI response would be generated by the MCP client (e.g., Claude Code)"
56
+ }
57
+ }
58
+ end
59
+
60
+ # Advanced tool with parameter validation and completion
61
+ tool "data_processor" do |data:, format: "json", validate: true|
62
+ log :info, "Processing data", data: { format: format, validate: validate }
63
+
64
+ begin
65
+ case format
66
+ when "json"
67
+ parsed = JSON.parse(data)
68
+ "Processed #{parsed.keys.size} fields"
69
+ when "csv"
70
+ lines = data.lines.count
71
+ "Processed #{lines} CSV rows"
72
+ when "xml"
73
+ "XML processing not implemented (demo)"
74
+ else
75
+ raise "Unsupported format: #{format}"
76
+ end
77
+ rescue => e
78
+ log :error, "Processing failed", data: { error: e.message }
79
+ "Error: #{e.message}"
80
+ end
81
+ end
82
+
83
+ # Tool with completions
84
+ tool "query_database" do |table:, query_type: "select"|
85
+ "Executed #{query_type.upcase} on table: #{table}"
86
+ end
87
+
88
+ # Define completions for database tool
89
+ completion_for "tool", "query_database", "table" do |partial|
90
+ tables = ["users", "orders", "products", "categories", "reviews"]
91
+ tables.select { |t| t.start_with?(partial.downcase) }
92
+ end
93
+
94
+ completion_for "tool", "query_database", "query_type" do |partial|
95
+ types = ["select", "insert", "update", "delete", "count"]
96
+ types.select { |t| t.start_with?(partial.downcase) }
97
+ end
98
+
99
+ # Resource with dynamic content
100
+ resource "system/status" do
101
+ {
102
+ server: "full-featured-server",
103
+ version: "1.0.0",
104
+ uptime: Time.now - $server_start_time,
105
+ capabilities: {
106
+ logging: true,
107
+ completion: true,
108
+ sampling: true,
109
+ middleware: true
110
+ },
111
+ stats: {
112
+ requests_handled: rand(1000..5000),
113
+ active_connections: rand(1..10),
114
+ memory_usage: "#{rand(50..200)}MB"
115
+ }
116
+ }.to_json
117
+ end
118
+
119
+ # Advanced prompt with multiple arguments
120
+ prompt "code_review" do |language:, code:, focus: "general"|
121
+ prompt_text = "Please review this #{language} code"
122
+ prompt_text += " with focus on #{focus}" unless focus == "general"
123
+ prompt_text += ":\n\n```#{language}\n#{code}\n```"
124
+ prompt_text
125
+ end
126
+
127
+ # Prompt completions
128
+ completion_for "prompt", "code_review", "language" do |partial|
129
+ languages = ["ruby", "python", "javascript", "typescript", "go", "rust", "java"]
130
+ languages.select { |l| l.start_with?(partial.downcase) }
131
+ end
132
+
133
+ completion_for "prompt", "code_review", "focus" do |partial|
134
+ focuses = ["general", "performance", "security", "style", "bugs", "architecture"]
135
+ focuses.select { |f| f.include?(partial.downcase) }
136
+ end
137
+
138
+ # Tool that uses sampling for enhanced functionality
139
+ tool "generate_documentation" do |code:, style: "markdown"|
140
+ log :info, "Generating documentation for code"
141
+
142
+ # This would trigger sampling in a real implementation
143
+ <<~DOC
144
+ # Generated Documentation
145
+
146
+ This documentation would be AI-generated based on the provided code.
147
+ Style: #{style}
148
+
149
+ ## Overview
150
+ [AI would analyze and document the code here]
151
+
152
+ ## Usage
153
+ [AI would provide usage examples]
154
+
155
+ ## API Reference
156
+ [AI would document the API]
157
+ DOC
158
+ end
159
+
160
+ # Resource showing middleware info
161
+ resource "debug/middleware" do
162
+ "Active middleware: Validation, Logging, ApiKey, RateLimit"
163
+ end
164
+
165
+ # Admin tool protected by middleware
166
+ tool "admin/reset_stats" do
167
+ log :warning, "Stats reset requested"
168
+ "Statistics reset successfully (requires valid API key)"
169
+ end
170
+
171
+ # Initialize server start time for uptime tracking
172
+ $server_start_time = Time.now
173
+
174
+ log :info, "Full-featured server started with all capabilities enabled"
175
+ end
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/tsikol'
5
+
6
+ # Custom middleware example
7
+ class TimingMiddleware < Tsikol::Middleware
8
+ def before_request(message)
9
+ message["_start_time"] = Time.now
10
+ message
11
+ end
12
+
13
+ def after_response(response, original_message)
14
+ if original_message["_start_time"]
15
+ duration = Time.now - original_message["_start_time"]
16
+ puts "Request #{original_message['method']} took #{(duration * 1000).round(2)}ms"
17
+ end
18
+ response
19
+ end
20
+ end
21
+
22
+ # Audit middleware
23
+ class AuditMiddleware < Tsikol::Middleware
24
+ def initialize(app, audit_file: "audit.log")
25
+ super(app)
26
+ @audit_file = audit_file
27
+ end
28
+
29
+ def before_request(message)
30
+ File.open(@audit_file, "a") do |f|
31
+ f.puts "[#{Time.now}] REQUEST: #{message['method']} (id: #{message['id']})"
32
+ f.puts " Params: #{message['params'].inspect}" if message['params']
33
+ end
34
+ message
35
+ end
36
+
37
+ def after_response(response, original_message)
38
+ File.open(@audit_file, "a") do |f|
39
+ if response["error"]
40
+ f.puts "[#{Time.now}] ERROR: #{response['error']['message']}"
41
+ else
42
+ f.puts "[#{Time.now}] SUCCESS: #{original_message['method']}"
43
+ end
44
+ f.puts "-" * 50
45
+ end
46
+ response
47
+ end
48
+ end
49
+
50
+ # Create server with middleware
51
+ Tsikol.server "middleware-demo" do
52
+ # Add middleware
53
+ use Tsikol::LoggingMiddleware
54
+ use TimingMiddleware
55
+ use AuditMiddleware, audit_file: "server_audit.log"
56
+ use Tsikol::RateLimitMiddleware, max_requests: 10, window: 60
57
+
58
+ # Middleware for specific methods only
59
+ use Tsikol::AuthenticationMiddleware do |auth_info, message|
60
+ # Simple auth check - in production this would verify tokens, etc.
61
+ if message["method"] == "admin/shutdown"
62
+ auth_info["admin"] == true
63
+ else
64
+ true # Allow all other methods
65
+ end
66
+ end
67
+
68
+ # Regular tools
69
+ tool "echo" do |message:|
70
+ "Echo: #{message}"
71
+ end
72
+
73
+ tool "slow_operation" do |duration: 1|
74
+ sleep(duration.to_f)
75
+ "Operation completed after #{duration} seconds"
76
+ end
77
+
78
+ tool "error_prone" do |should_fail: false|
79
+ raise "Intentional error" if should_fail
80
+ "Success!"
81
+ end
82
+
83
+ # Admin tool (requires auth)
84
+ tool "admin/shutdown" do
85
+ "Server would shutdown (demo mode - not actually shutting down)"
86
+ end
87
+
88
+ # Resource showing middleware info
89
+ resource "middleware/stack" do
90
+ {
91
+ middlewares: [
92
+ "LoggingMiddleware - Logs all requests and responses",
93
+ "TimingMiddleware - Measures request duration",
94
+ "AuditMiddleware - Writes audit log to file",
95
+ "RateLimitMiddleware - Limits to 10 requests per minute",
96
+ "AuthenticationMiddleware - Protects admin endpoints"
97
+ ],
98
+ benefits: [
99
+ "Cross-cutting concerns handled separately",
100
+ "Easy to add/remove functionality",
101
+ "Consistent request/response processing",
102
+ "Better debugging and monitoring"
103
+ ]
104
+ }.to_json
105
+ end
106
+
107
+ # Enable logging for middleware demo
108
+ logging true
109
+
110
+ # Set log level to debug to see middleware logs
111
+ log :info, "Server started with middleware stack"
112
+ end