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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +22 -0
- data/CONTRIBUTING.md +84 -0
- data/LICENSE +21 -0
- data/README.md +579 -0
- data/Rakefile +12 -0
- data/docs/README.md +69 -0
- data/docs/api/middleware.md +721 -0
- data/docs/api/prompt.md +858 -0
- data/docs/api/resource.md +651 -0
- data/docs/api/server.md +509 -0
- data/docs/api/test-helpers.md +591 -0
- data/docs/api/tool.md +527 -0
- data/docs/cookbook/authentication.md +651 -0
- data/docs/cookbook/caching.md +877 -0
- data/docs/cookbook/dynamic-tools.md +970 -0
- data/docs/cookbook/error-handling.md +887 -0
- data/docs/cookbook/logging.md +1044 -0
- data/docs/cookbook/rate-limiting.md +717 -0
- data/docs/examples/code-assistant.md +922 -0
- data/docs/examples/complete-server.md +726 -0
- data/docs/examples/database-manager.md +1198 -0
- data/docs/examples/devops-tools.md +1382 -0
- data/docs/examples/echo-server.md +501 -0
- data/docs/examples/weather-service.md +822 -0
- data/docs/guides/completion.md +472 -0
- data/docs/guides/getting-started.md +462 -0
- data/docs/guides/middleware.md +823 -0
- data/docs/guides/project-structure.md +434 -0
- data/docs/guides/prompts.md +920 -0
- data/docs/guides/resources.md +720 -0
- data/docs/guides/sampling.md +804 -0
- data/docs/guides/testing.md +863 -0
- data/docs/guides/tools.md +627 -0
- data/examples/README.md +92 -0
- data/examples/advanced_features.rb +129 -0
- data/examples/basic-migrated/app/prompts/weather_chat.rb +44 -0
- data/examples/basic-migrated/app/resources/weather_alerts.rb +18 -0
- data/examples/basic-migrated/app/tools/get_current_weather.rb +34 -0
- data/examples/basic-migrated/app/tools/get_forecast.rb +30 -0
- data/examples/basic-migrated/app/tools/get_weather_by_coords.rb +48 -0
- data/examples/basic-migrated/server.rb +25 -0
- data/examples/basic.rb +73 -0
- data/examples/full_featured.rb +175 -0
- data/examples/middleware_example.rb +112 -0
- data/examples/sampling_example.rb +104 -0
- data/examples/weather-service/app/prompts/weather/chat.rb +90 -0
- data/examples/weather-service/app/resources/weather/alerts.rb +59 -0
- data/examples/weather-service/app/tools/weather/get_current.rb +82 -0
- data/examples/weather-service/app/tools/weather/get_forecast.rb +90 -0
- data/examples/weather-service/server.rb +28 -0
- data/exe/tsikol +6 -0
- data/lib/tsikol/cli/templates/Gemfile.erb +10 -0
- data/lib/tsikol/cli/templates/README.md.erb +38 -0
- data/lib/tsikol/cli/templates/gitignore.erb +49 -0
- data/lib/tsikol/cli/templates/prompt.rb.erb +53 -0
- data/lib/tsikol/cli/templates/resource.rb.erb +29 -0
- data/lib/tsikol/cli/templates/server.rb.erb +24 -0
- data/lib/tsikol/cli/templates/tool.rb.erb +60 -0
- data/lib/tsikol/cli.rb +203 -0
- data/lib/tsikol/error_handler.rb +141 -0
- data/lib/tsikol/health.rb +198 -0
- data/lib/tsikol/http_transport.rb +72 -0
- data/lib/tsikol/lifecycle.rb +149 -0
- data/lib/tsikol/middleware.rb +168 -0
- data/lib/tsikol/prompt.rb +101 -0
- data/lib/tsikol/resource.rb +53 -0
- data/lib/tsikol/router.rb +190 -0
- data/lib/tsikol/server.rb +660 -0
- data/lib/tsikol/stdio_transport.rb +108 -0
- data/lib/tsikol/test_helpers.rb +261 -0
- data/lib/tsikol/tool.rb +111 -0
- data/lib/tsikol/version.rb +5 -0
- data/lib/tsikol.rb +72 -0
- metadata +219 -0
@@ -0,0 +1,726 @@
|
|
1
|
+
# Complete MCP Server Example
|
2
|
+
|
3
|
+
This example demonstrates all Tsikol features in a production-ready MCP server.
|
4
|
+
|
5
|
+
## Full Implementation
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
#!/usr/bin/env ruby
|
9
|
+
# frozen_string_literal: true
|
10
|
+
|
11
|
+
require 'bundler/setup'
|
12
|
+
require 'tsikol'
|
13
|
+
|
14
|
+
# Complete MCP server with all features
|
15
|
+
Tsikol.start(name: "complete-mcp-server") do
|
16
|
+
# Load components from files
|
17
|
+
require_relative 'app/tools/code_analyzer'
|
18
|
+
require_relative 'app/tools/file_manager'
|
19
|
+
require_relative 'app/tools/ai_assistant'
|
20
|
+
require_relative 'app/resources/project_info'
|
21
|
+
require_relative 'app/resources/system_status'
|
22
|
+
require_relative 'app/prompts/development_chat'
|
23
|
+
|
24
|
+
# Register components
|
25
|
+
tool CodeAnalyzer
|
26
|
+
tool FileManager
|
27
|
+
tool AiAssistant
|
28
|
+
resource ProjectInfo
|
29
|
+
resource SystemStatus
|
30
|
+
prompt DevelopmentChat
|
31
|
+
|
32
|
+
# Enable all capabilities
|
33
|
+
logging true
|
34
|
+
completion true
|
35
|
+
|
36
|
+
# Configure sampling for AI features
|
37
|
+
on_sampling do |request|
|
38
|
+
# In production, the MCP client handles this
|
39
|
+
# This is for testing/development
|
40
|
+
{
|
41
|
+
role: "assistant",
|
42
|
+
content: {
|
43
|
+
type: "text",
|
44
|
+
text: "AI response for: #{request[:messages].last[:content][:text]}"
|
45
|
+
}
|
46
|
+
}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Add middleware stack
|
50
|
+
use Tsikol::ValidationMiddleware
|
51
|
+
use Tsikol::LoggingMiddleware
|
52
|
+
use Tsikol::RateLimitMiddleware, max_requests: 100, window: 60
|
53
|
+
use AuthenticationMiddleware
|
54
|
+
use MetricsMiddleware
|
55
|
+
|
56
|
+
# Lifecycle hooks
|
57
|
+
before_start do
|
58
|
+
log :info, "Initializing complete MCP server..."
|
59
|
+
@start_time = Time.now
|
60
|
+
load_configuration
|
61
|
+
initialize_connections
|
62
|
+
end
|
63
|
+
|
64
|
+
after_start do
|
65
|
+
log :info, "Server ready!", data: {
|
66
|
+
tools: @tool_instances.keys,
|
67
|
+
resources: @resource_instances.keys,
|
68
|
+
capabilities: @server_capabilities.keys
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
before_stop do
|
73
|
+
log :info, "Shutting down gracefully..."
|
74
|
+
save_state
|
75
|
+
close_connections
|
76
|
+
end
|
77
|
+
|
78
|
+
after_stop do
|
79
|
+
uptime = Time.now - @start_time
|
80
|
+
log :info, "Server stopped", data: { uptime: uptime }
|
81
|
+
end
|
82
|
+
|
83
|
+
# Tool-specific hooks
|
84
|
+
before_tool do |tool_name, params|
|
85
|
+
log :debug, "Executing tool: #{tool_name}", data: { params: params }
|
86
|
+
@metrics.increment("tools.#{tool_name}.calls")
|
87
|
+
end
|
88
|
+
|
89
|
+
after_tool do |tool_name, params, result|
|
90
|
+
log :debug, "Tool completed: #{tool_name}"
|
91
|
+
track_tool_usage(tool_name, result)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Error handling for specific tools
|
95
|
+
before_tool "ai_assistant" do |params|
|
96
|
+
validate_ai_request(params)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Inline tools for simple operations
|
100
|
+
tool "ping" do
|
101
|
+
"pong"
|
102
|
+
end
|
103
|
+
|
104
|
+
tool "echo" do |message:|
|
105
|
+
message
|
106
|
+
end
|
107
|
+
|
108
|
+
# Inline resources
|
109
|
+
resource "health" do
|
110
|
+
{
|
111
|
+
status: "healthy",
|
112
|
+
uptime: Time.now - @start_time,
|
113
|
+
version: Tsikol::VERSION
|
114
|
+
}.to_json
|
115
|
+
end
|
116
|
+
|
117
|
+
# Helper methods
|
118
|
+
private
|
119
|
+
|
120
|
+
def load_configuration
|
121
|
+
# Load config from file or environment
|
122
|
+
@config = {
|
123
|
+
max_file_size: ENV.fetch('MAX_FILE_SIZE', 1024 * 1024).to_i,
|
124
|
+
allowed_extensions: %w[rb py js ts go rs java c cpp md txt json yaml],
|
125
|
+
ai_model: ENV.fetch('AI_MODEL', 'claude-3-sonnet')
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
def initialize_connections
|
130
|
+
# Initialize any external connections
|
131
|
+
@cache = {}
|
132
|
+
@metrics = Tsikol::Metrics.new
|
133
|
+
end
|
134
|
+
|
135
|
+
def save_state
|
136
|
+
# Save any persistent state
|
137
|
+
File.write('server_state.json', {
|
138
|
+
last_shutdown: Time.now,
|
139
|
+
total_requests: @metrics.get(:requests_total)
|
140
|
+
}.to_json)
|
141
|
+
end
|
142
|
+
|
143
|
+
def close_connections
|
144
|
+
# Clean up connections
|
145
|
+
@cache.clear
|
146
|
+
end
|
147
|
+
|
148
|
+
def track_tool_usage(tool_name, result)
|
149
|
+
# Track metrics
|
150
|
+
@metrics.increment("tools.#{tool_name}.success") if result
|
151
|
+
end
|
152
|
+
|
153
|
+
def validate_ai_request(params)
|
154
|
+
# Validate AI requests
|
155
|
+
raise "Message too long" if params[:message]&.length > 10000
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Custom middleware
|
160
|
+
class AuthenticationMiddleware < Tsikol::Middleware
|
161
|
+
def before_request(message)
|
162
|
+
# Skip auth for certain methods
|
163
|
+
return message if %w[initialize ping health tools/list].include?(message["method"])
|
164
|
+
|
165
|
+
# Check authentication
|
166
|
+
token = message.dig("metadata", "auth_token")
|
167
|
+
unless valid_token?(token)
|
168
|
+
raise Tsikol::AuthenticationError, "Invalid authentication token"
|
169
|
+
end
|
170
|
+
|
171
|
+
message
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def valid_token?(token)
|
177
|
+
# In production, validate against real auth system
|
178
|
+
token == ENV['MCP_AUTH_TOKEN']
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class MetricsMiddleware < Tsikol::Middleware
|
183
|
+
def initialize(app)
|
184
|
+
super
|
185
|
+
@metrics = {}
|
186
|
+
end
|
187
|
+
|
188
|
+
def before_request(message)
|
189
|
+
message["_start_time"] = Time.now
|
190
|
+
message
|
191
|
+
end
|
192
|
+
|
193
|
+
def after_response(response, original_message)
|
194
|
+
duration = Time.now - original_message["_start_time"]
|
195
|
+
track_metric(original_message["method"], duration)
|
196
|
+
response
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def track_metric(method, duration)
|
202
|
+
@metrics[method] ||= []
|
203
|
+
@metrics[method] << duration
|
204
|
+
|
205
|
+
# Log slow requests
|
206
|
+
if duration > 1.0
|
207
|
+
log :warning, "Slow request", data: {
|
208
|
+
method: method,
|
209
|
+
duration: duration
|
210
|
+
}
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
## Component Files
|
217
|
+
|
218
|
+
### app/tools/code_analyzer.rb
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
# frozen_string_literal: true
|
222
|
+
|
223
|
+
class CodeAnalyzer < Tsikol::Tool
|
224
|
+
description "Analyze code quality and complexity"
|
225
|
+
|
226
|
+
parameter :file_path do
|
227
|
+
type :string
|
228
|
+
required
|
229
|
+
description "Path to file to analyze"
|
230
|
+
|
231
|
+
complete do |partial|
|
232
|
+
Dir.glob("#{partial}*").select { |f|
|
233
|
+
f.match?(/\.(rb|py|js|ts|go)$/)
|
234
|
+
}
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
parameter :analysis_type do
|
239
|
+
type :string
|
240
|
+
optional
|
241
|
+
default "all"
|
242
|
+
enum ["complexity", "style", "security", "performance", "all"]
|
243
|
+
|
244
|
+
complete do |partial|
|
245
|
+
["complexity", "style", "security", "performance", "all"]
|
246
|
+
.select { |t| t.start_with?(partial) }
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def execute(file_path:, analysis_type: "all")
|
251
|
+
unless File.exist?(file_path)
|
252
|
+
raise Tsikol::ValidationError, "File not found: #{file_path}"
|
253
|
+
end
|
254
|
+
|
255
|
+
log :info, "Analyzing #{file_path}", data: { type: analysis_type }
|
256
|
+
|
257
|
+
code = File.read(file_path)
|
258
|
+
results = {}
|
259
|
+
|
260
|
+
if analysis_type == "all" || analysis_type == "complexity"
|
261
|
+
results[:complexity] = analyze_complexity(code)
|
262
|
+
end
|
263
|
+
|
264
|
+
if analysis_type == "all" || analysis_type == "style"
|
265
|
+
results[:style] = analyze_style(code)
|
266
|
+
end
|
267
|
+
|
268
|
+
if analysis_type == "all" || analysis_type == "security"
|
269
|
+
results[:security] = analyze_security(code)
|
270
|
+
end
|
271
|
+
|
272
|
+
if analysis_type == "all" || analysis_type == "performance"
|
273
|
+
results[:performance] = analyze_performance(code)
|
274
|
+
end
|
275
|
+
|
276
|
+
format_results(results)
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
def analyze_complexity(code)
|
282
|
+
lines = code.lines.count
|
283
|
+
methods = code.scan(/def\s+\w+/).count
|
284
|
+
classes = code.scan(/class\s+\w+/).count
|
285
|
+
|
286
|
+
{
|
287
|
+
lines_of_code: lines,
|
288
|
+
methods: methods,
|
289
|
+
classes: classes,
|
290
|
+
complexity_score: calculate_complexity_score(code)
|
291
|
+
}
|
292
|
+
end
|
293
|
+
|
294
|
+
def analyze_style(code)
|
295
|
+
issues = []
|
296
|
+
|
297
|
+
# Check line length
|
298
|
+
code.lines.each_with_index do |line, index|
|
299
|
+
if line.chomp.length > 100
|
300
|
+
issues << "Line #{index + 1}: Too long (#{line.chomp.length} chars)"
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
# Check indentation
|
305
|
+
if code.match?(/\t/)
|
306
|
+
issues << "Uses tabs instead of spaces"
|
307
|
+
end
|
308
|
+
|
309
|
+
{
|
310
|
+
style_issues: issues,
|
311
|
+
passed: issues.empty?
|
312
|
+
}
|
313
|
+
end
|
314
|
+
|
315
|
+
def analyze_security(code)
|
316
|
+
vulnerabilities = []
|
317
|
+
|
318
|
+
# Check for common security issues
|
319
|
+
if code.match?(/eval\s*\(/)
|
320
|
+
vulnerabilities << "Uses eval() - potential security risk"
|
321
|
+
end
|
322
|
+
|
323
|
+
if code.match?(/system\s*\(|`.*`/)
|
324
|
+
vulnerabilities << "Executes system commands - verify input sanitization"
|
325
|
+
end
|
326
|
+
|
327
|
+
{
|
328
|
+
vulnerabilities: vulnerabilities,
|
329
|
+
secure: vulnerabilities.empty?
|
330
|
+
}
|
331
|
+
end
|
332
|
+
|
333
|
+
def analyze_performance(code)
|
334
|
+
suggestions = []
|
335
|
+
|
336
|
+
# Look for performance anti-patterns
|
337
|
+
if code.match?(/\.select.*\.first/)
|
338
|
+
suggestions << "Use .find instead of .select.first"
|
339
|
+
end
|
340
|
+
|
341
|
+
if code.match?(/\+\s*=.*loop/)
|
342
|
+
suggestions << "String concatenation in loop - consider using array.join"
|
343
|
+
end
|
344
|
+
|
345
|
+
{
|
346
|
+
suggestions: suggestions,
|
347
|
+
optimized: suggestions.empty?
|
348
|
+
}
|
349
|
+
end
|
350
|
+
|
351
|
+
def calculate_complexity_score(code)
|
352
|
+
# Simple cyclomatic complexity estimate
|
353
|
+
score = 1
|
354
|
+
score += code.scan(/\bif\b|\belsif\b|\bcase\b|\bwhen\b/).count
|
355
|
+
score += code.scan(/\bwhile\b|\bfor\b|\buntil\b/).count
|
356
|
+
score += code.scan(/\brescue\b/).count
|
357
|
+
score
|
358
|
+
end
|
359
|
+
|
360
|
+
def format_results(results)
|
361
|
+
output = ["Code Analysis Results", "=" * 50]
|
362
|
+
|
363
|
+
results.each do |type, data|
|
364
|
+
output << "\n#{type.to_s.capitalize} Analysis:"
|
365
|
+
output << JSON.pretty_generate(data)
|
366
|
+
end
|
367
|
+
|
368
|
+
output.join("\n")
|
369
|
+
end
|
370
|
+
|
371
|
+
def set_server(server)
|
372
|
+
@server = server
|
373
|
+
define_singleton_method(:log) do |level, message, data: nil|
|
374
|
+
@server.log(level, message, data: data)
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
```
|
379
|
+
|
380
|
+
### app/tools/file_manager.rb
|
381
|
+
|
382
|
+
```ruby
|
383
|
+
# frozen_string_literal: true
|
384
|
+
|
385
|
+
class FileManager < Tsikol::Tool
|
386
|
+
description "Manage project files"
|
387
|
+
|
388
|
+
parameter :action do
|
389
|
+
type :string
|
390
|
+
required
|
391
|
+
enum ["list", "read", "create", "update", "delete", "search"]
|
392
|
+
|
393
|
+
complete do |partial|
|
394
|
+
["list", "read", "create", "update", "delete", "search"]
|
395
|
+
.select { |a| a.start_with?(partial) }
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
parameter :path do
|
400
|
+
type :string
|
401
|
+
required
|
402
|
+
description "File or directory path"
|
403
|
+
|
404
|
+
complete do |partial|
|
405
|
+
Dir.glob("#{partial}*")
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
parameter :content do
|
410
|
+
type :string
|
411
|
+
optional
|
412
|
+
description "Content for create/update operations"
|
413
|
+
end
|
414
|
+
|
415
|
+
parameter :pattern do
|
416
|
+
type :string
|
417
|
+
optional
|
418
|
+
description "Search pattern for search operation"
|
419
|
+
end
|
420
|
+
|
421
|
+
def execute(action:, path:, content: nil, pattern: nil)
|
422
|
+
case action
|
423
|
+
when "list"
|
424
|
+
list_directory(path)
|
425
|
+
when "read"
|
426
|
+
read_file(path)
|
427
|
+
when "create"
|
428
|
+
create_file(path, content)
|
429
|
+
when "update"
|
430
|
+
update_file(path, content)
|
431
|
+
when "delete"
|
432
|
+
delete_file(path)
|
433
|
+
when "search"
|
434
|
+
search_files(path, pattern)
|
435
|
+
else
|
436
|
+
raise Tsikol::ValidationError, "Unknown action: #{action}"
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
private
|
441
|
+
|
442
|
+
def list_directory(path)
|
443
|
+
unless File.directory?(path)
|
444
|
+
raise Tsikol::ValidationError, "Not a directory: #{path}"
|
445
|
+
end
|
446
|
+
|
447
|
+
entries = Dir.entries(path).reject { |e| e.start_with?('.') }
|
448
|
+
|
449
|
+
entries.map do |entry|
|
450
|
+
full_path = File.join(path, entry)
|
451
|
+
{
|
452
|
+
name: entry,
|
453
|
+
type: File.directory?(full_path) ? "directory" : "file",
|
454
|
+
size: File.size(full_path),
|
455
|
+
modified: File.mtime(full_path).iso8601
|
456
|
+
}
|
457
|
+
end.to_json
|
458
|
+
end
|
459
|
+
|
460
|
+
def read_file(path)
|
461
|
+
unless File.exist?(path)
|
462
|
+
raise Tsikol::ValidationError, "File not found: #{path}"
|
463
|
+
end
|
464
|
+
|
465
|
+
File.read(path)
|
466
|
+
end
|
467
|
+
|
468
|
+
def create_file(path, content)
|
469
|
+
if File.exist?(path)
|
470
|
+
raise Tsikol::ValidationError, "File already exists: #{path}"
|
471
|
+
end
|
472
|
+
|
473
|
+
File.write(path, content || "")
|
474
|
+
"File created: #{path}"
|
475
|
+
end
|
476
|
+
|
477
|
+
def update_file(path, content)
|
478
|
+
unless File.exist?(path)
|
479
|
+
raise Tsikol::ValidationError, "File not found: #{path}"
|
480
|
+
end
|
481
|
+
|
482
|
+
File.write(path, content)
|
483
|
+
"File updated: #{path}"
|
484
|
+
end
|
485
|
+
|
486
|
+
def delete_file(path)
|
487
|
+
unless File.exist?(path)
|
488
|
+
raise Tsikol::ValidationError, "File not found: #{path}"
|
489
|
+
end
|
490
|
+
|
491
|
+
File.delete(path)
|
492
|
+
"File deleted: #{path}"
|
493
|
+
end
|
494
|
+
|
495
|
+
def search_files(path, pattern)
|
496
|
+
unless pattern
|
497
|
+
raise Tsikol::ValidationError, "Pattern required for search"
|
498
|
+
end
|
499
|
+
|
500
|
+
matches = []
|
501
|
+
|
502
|
+
Dir.glob("#{path}/**/*").each do |file|
|
503
|
+
next if File.directory?(file)
|
504
|
+
|
505
|
+
begin
|
506
|
+
content = File.read(file)
|
507
|
+
if content.match?(Regexp.new(pattern, Regexp::IGNORECASE))
|
508
|
+
matches << {
|
509
|
+
file: file,
|
510
|
+
matches: content.lines.select { |line|
|
511
|
+
line.match?(Regexp.new(pattern, Regexp::IGNORECASE))
|
512
|
+
}.first(3)
|
513
|
+
}
|
514
|
+
end
|
515
|
+
rescue => e
|
516
|
+
log :warning, "Error reading #{file}", data: { error: e.message }
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
matches.to_json
|
521
|
+
end
|
522
|
+
|
523
|
+
def set_server(server)
|
524
|
+
@server = server
|
525
|
+
define_singleton_method(:log) do |level, message, data: nil|
|
526
|
+
@server.log(level, message, data: data)
|
527
|
+
end
|
528
|
+
end
|
529
|
+
end
|
530
|
+
```
|
531
|
+
|
532
|
+
### app/resources/system_status.rb
|
533
|
+
|
534
|
+
```ruby
|
535
|
+
# frozen_string_literal: true
|
536
|
+
|
537
|
+
class SystemStatus < Tsikol::Resource
|
538
|
+
uri "system/status"
|
539
|
+
description "Current system status and metrics"
|
540
|
+
|
541
|
+
def read
|
542
|
+
{
|
543
|
+
server: {
|
544
|
+
name: "complete-mcp-server",
|
545
|
+
version: Tsikol::VERSION,
|
546
|
+
protocol_version: Tsikol::PROTOCOL_VERSION,
|
547
|
+
uptime: calculate_uptime
|
548
|
+
},
|
549
|
+
system: {
|
550
|
+
ruby_version: RUBY_VERSION,
|
551
|
+
platform: RUBY_PLATFORM,
|
552
|
+
pid: Process.pid,
|
553
|
+
memory_usage: get_memory_usage
|
554
|
+
},
|
555
|
+
metrics: {
|
556
|
+
total_requests: @server.metrics.get(:requests_total),
|
557
|
+
active_requests: @server.metrics.get(:active_requests),
|
558
|
+
error_rate: calculate_error_rate,
|
559
|
+
average_response_time: @server.metrics.average(:response_time)
|
560
|
+
},
|
561
|
+
capabilities: @server.instance_variable_get(:@server_capabilities).keys,
|
562
|
+
health: determine_health_status
|
563
|
+
}.to_json
|
564
|
+
end
|
565
|
+
|
566
|
+
private
|
567
|
+
|
568
|
+
def calculate_uptime
|
569
|
+
start_time = @server.instance_variable_get(:@start_time)
|
570
|
+
return 0 unless start_time
|
571
|
+
Time.now - start_time
|
572
|
+
end
|
573
|
+
|
574
|
+
def get_memory_usage
|
575
|
+
# Simple memory usage estimate
|
576
|
+
"#{(GC.stat[:heap_live_slots] * 40.0 / 1024 / 1024).round(2)}MB"
|
577
|
+
end
|
578
|
+
|
579
|
+
def calculate_error_rate
|
580
|
+
total = @server.metrics.get(:requests_total)
|
581
|
+
errors = @server.metrics.get(:errors_total)
|
582
|
+
|
583
|
+
return 0.0 if total == 0
|
584
|
+
(errors.to_f / total * 100).round(2)
|
585
|
+
end
|
586
|
+
|
587
|
+
def determine_health_status
|
588
|
+
error_rate = calculate_error_rate
|
589
|
+
|
590
|
+
if error_rate > 50
|
591
|
+
"critical"
|
592
|
+
elsif error_rate > 10
|
593
|
+
"degraded"
|
594
|
+
else
|
595
|
+
"healthy"
|
596
|
+
end
|
597
|
+
end
|
598
|
+
|
599
|
+
def set_server(server)
|
600
|
+
@server = server
|
601
|
+
end
|
602
|
+
end
|
603
|
+
```
|
604
|
+
|
605
|
+
## Testing the Server
|
606
|
+
|
607
|
+
```ruby
|
608
|
+
require 'minitest/autorun'
|
609
|
+
require 'tsikol/test_helpers'
|
610
|
+
|
611
|
+
class CompleteServerTest < Minitest::Test
|
612
|
+
include Tsikol::TestHelpers::Assertions
|
613
|
+
|
614
|
+
def setup
|
615
|
+
# Load the server
|
616
|
+
require_relative '../server'
|
617
|
+
@server = $tsikol_server # Set by server.rb
|
618
|
+
@client = Tsikol::TestHelpers::TestClient.new(@server)
|
619
|
+
|
620
|
+
# Initialize connection
|
621
|
+
@client.initialize_connection
|
622
|
+
end
|
623
|
+
|
624
|
+
def test_ping
|
625
|
+
response = @client.request("ping")
|
626
|
+
assert_successful_response(response)
|
627
|
+
assert_equal({}, response[:result])
|
628
|
+
end
|
629
|
+
|
630
|
+
def test_echo_tool
|
631
|
+
response = @client.call_tool("echo", { "message" => "Hello MCP!" })
|
632
|
+
assert_successful_response(response)
|
633
|
+
assert_equal "Hello MCP!", response.dig(:result, :content, 0, :text)
|
634
|
+
end
|
635
|
+
|
636
|
+
def test_code_analyzer
|
637
|
+
# Create test file
|
638
|
+
File.write("test.rb", "def hello\n puts 'Hi'\nend")
|
639
|
+
|
640
|
+
response = @client.call_tool("code_analyzer", {
|
641
|
+
"file_path" => "test.rb",
|
642
|
+
"analysis_type" => "complexity"
|
643
|
+
})
|
644
|
+
|
645
|
+
assert_successful_response(response)
|
646
|
+
result = response.dig(:result, :content, 0, :text)
|
647
|
+
assert_includes result, "Complexity Analysis"
|
648
|
+
ensure
|
649
|
+
File.delete("test.rb") if File.exist?("test.rb")
|
650
|
+
end
|
651
|
+
|
652
|
+
def test_system_status_resource
|
653
|
+
response = @client.read_resource("system/status")
|
654
|
+
assert_successful_response(response)
|
655
|
+
|
656
|
+
content = response.dig(:result, :contents, 0, :text)
|
657
|
+
data = JSON.parse(content)
|
658
|
+
|
659
|
+
assert_equal "complete-mcp-server", data["server"]["name"]
|
660
|
+
assert_includes ["healthy", "degraded", "critical"], data["health"]
|
661
|
+
end
|
662
|
+
|
663
|
+
def test_health_check
|
664
|
+
response = @client.read_resource("health")
|
665
|
+
assert_successful_response(response)
|
666
|
+
|
667
|
+
content = response.dig(:result, :contents, 0, :text)
|
668
|
+
data = JSON.parse(content)
|
669
|
+
|
670
|
+
assert_equal "healthy", data["status"]
|
671
|
+
end
|
672
|
+
|
673
|
+
def test_completion
|
674
|
+
# Test tool parameter completion
|
675
|
+
response = @client.complete(
|
676
|
+
{ type: "ref/tool", name: "code_analyzer" },
|
677
|
+
{ name: "analysis_type", value: "sec" }
|
678
|
+
)
|
679
|
+
|
680
|
+
assert_successful_response(response)
|
681
|
+
values = response.dig(:result, :completion, :values)
|
682
|
+
assert_includes values, "security"
|
683
|
+
end
|
684
|
+
|
685
|
+
def test_rate_limiting
|
686
|
+
# Make many requests quickly
|
687
|
+
100.times do
|
688
|
+
@client.call_tool("echo", { "message" => "test" })
|
689
|
+
end
|
690
|
+
|
691
|
+
# Next request should fail with rate limit
|
692
|
+
response = @client.call_tool("echo", { "message" => "test" })
|
693
|
+
assert response[:error]
|
694
|
+
assert_includes response[:error][:message], "Rate limit"
|
695
|
+
end
|
696
|
+
end
|
697
|
+
```
|
698
|
+
|
699
|
+
## Running the Server
|
700
|
+
|
701
|
+
1. Save all files in the appropriate directories
|
702
|
+
2. Run the server:
|
703
|
+
```bash
|
704
|
+
./server.rb
|
705
|
+
```
|
706
|
+
3. Connect with MCP Inspector or any MCP client
|
707
|
+
4. The server supports:
|
708
|
+
- All MCP protocol methods including ping
|
709
|
+
- Tools with completion
|
710
|
+
- Resources with real-time data
|
711
|
+
- Prompts for AI interactions
|
712
|
+
- Sampling for AI assistance
|
713
|
+
- Full logging and metrics
|
714
|
+
- Authentication and rate limiting
|
715
|
+
- Health monitoring
|
716
|
+
|
717
|
+
## Key Features Demonstrated
|
718
|
+
|
719
|
+
1. **All Protocol Methods**: Including ping, tools, resources, prompts, completion, sampling
|
720
|
+
2. **Middleware Stack**: Authentication, rate limiting, metrics, logging
|
721
|
+
3. **Lifecycle Hooks**: Server and tool-level hooks
|
722
|
+
4. **Error Handling**: With custom error types and recovery
|
723
|
+
5. **Completions**: Smart autocomplete for parameters
|
724
|
+
6. **Health Monitoring**: Built-in health checks and metrics
|
725
|
+
7. **Testing**: Comprehensive test coverage
|
726
|
+
8. **Production Ready**: Authentication, rate limiting, error handling
|