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,720 @@
|
|
1
|
+
# Resources Guide
|
2
|
+
|
3
|
+
Resources provide read-only data that MCP clients can access. They're perfect for exposing configuration, status information, documentation, and other data.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
1. [What are Resources?](#what-are-resources)
|
8
|
+
2. [Creating Resources](#creating-resources)
|
9
|
+
3. [Resource URIs](#resource-uris)
|
10
|
+
4. [Dynamic Resources](#dynamic-resources)
|
11
|
+
5. [Resource Formats](#resource-formats)
|
12
|
+
6. [Caching](#caching)
|
13
|
+
7. [Advanced Patterns](#advanced-patterns)
|
14
|
+
8. [Testing Resources](#testing-resources)
|
15
|
+
|
16
|
+
## What are Resources?
|
17
|
+
|
18
|
+
Resources are read-only data endpoints that:
|
19
|
+
- Have unique URIs
|
20
|
+
- Return data in various formats
|
21
|
+
- Can be dynamic or static
|
22
|
+
- Don't accept parameters (use URI for context)
|
23
|
+
|
24
|
+
Think of resources as REST API GET endpoints.
|
25
|
+
|
26
|
+
## Creating Resources
|
27
|
+
|
28
|
+
### Inline Resources
|
29
|
+
|
30
|
+
Simple resources defined in server.rb:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
Tsikol.server "my-server" do
|
34
|
+
resource "version" do
|
35
|
+
"1.0.0"
|
36
|
+
end
|
37
|
+
|
38
|
+
resource "config/database" do
|
39
|
+
{
|
40
|
+
host: ENV['DB_HOST'] || 'localhost',
|
41
|
+
port: ENV['DB_PORT'] || 5432,
|
42
|
+
name: ENV['DB_NAME'] || 'development'
|
43
|
+
}.to_json
|
44
|
+
end
|
45
|
+
|
46
|
+
resource "health" do
|
47
|
+
{
|
48
|
+
status: "healthy",
|
49
|
+
timestamp: Time.now.iso8601
|
50
|
+
}.to_json
|
51
|
+
end
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
### Class-Based Resources
|
56
|
+
|
57
|
+
For complex resources:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
# app/resources/system_metrics.rb
|
61
|
+
class SystemMetrics < Tsikol::Resource
|
62
|
+
uri "system/metrics"
|
63
|
+
description "Current system metrics and statistics"
|
64
|
+
|
65
|
+
def read
|
66
|
+
{
|
67
|
+
server: server_info,
|
68
|
+
system: system_info,
|
69
|
+
performance: performance_metrics,
|
70
|
+
timestamp: Time.now.iso8601
|
71
|
+
}.to_json
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def server_info
|
77
|
+
{
|
78
|
+
name: @server.name,
|
79
|
+
version: Tsikol::VERSION,
|
80
|
+
uptime: calculate_uptime
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
def system_info
|
85
|
+
{
|
86
|
+
ruby_version: RUBY_VERSION,
|
87
|
+
platform: RUBY_PLATFORM,
|
88
|
+
pid: Process.pid,
|
89
|
+
memory_usage: memory_usage
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def performance_metrics
|
94
|
+
{
|
95
|
+
total_requests: @server.metrics.get(:requests_total),
|
96
|
+
average_response_time: @server.metrics.average(:response_time),
|
97
|
+
error_rate: calculate_error_rate
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
def calculate_uptime
|
102
|
+
return 0 unless @start_time
|
103
|
+
Time.now - @start_time
|
104
|
+
end
|
105
|
+
|
106
|
+
def memory_usage
|
107
|
+
# Simple calculation
|
108
|
+
"#{(GC.stat[:heap_live_slots] * 40.0 / 1024 / 1024).round(2)}MB"
|
109
|
+
end
|
110
|
+
|
111
|
+
def calculate_error_rate
|
112
|
+
total = @server.metrics.get(:requests_total)
|
113
|
+
errors = @server.metrics.get(:errors_total)
|
114
|
+
return 0.0 if total == 0
|
115
|
+
(errors.to_f / total * 100).round(2)
|
116
|
+
end
|
117
|
+
|
118
|
+
def set_server(server)
|
119
|
+
@server = server
|
120
|
+
@start_time = Time.now
|
121
|
+
end
|
122
|
+
end
|
123
|
+
```
|
124
|
+
|
125
|
+
## Resource URIs
|
126
|
+
|
127
|
+
### URI Conventions
|
128
|
+
|
129
|
+
URIs should be:
|
130
|
+
- Descriptive and hierarchical
|
131
|
+
- Use forward slashes for nesting
|
132
|
+
- Lowercase with hyphens or underscores
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
# Good URIs
|
136
|
+
"config" # Top-level config
|
137
|
+
"config/database" # Database config
|
138
|
+
"system/health" # System health
|
139
|
+
"api/v1/documentation" # Versioned API docs
|
140
|
+
|
141
|
+
# Avoid
|
142
|
+
"getConfig" # Not RESTful
|
143
|
+
"database-config-data" # Too flat
|
144
|
+
"SYSTEM_HEALTH" # Wrong case
|
145
|
+
```
|
146
|
+
|
147
|
+
### Dynamic URI Patterns
|
148
|
+
|
149
|
+
For resources that represent collections:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
# app/resources/user_profile.rb
|
153
|
+
class UserProfile < Tsikol::Resource
|
154
|
+
uri "users/current/profile"
|
155
|
+
description "Current user's profile"
|
156
|
+
|
157
|
+
def read
|
158
|
+
# In real app, get current user from context
|
159
|
+
{
|
160
|
+
id: 1,
|
161
|
+
name: "Current User",
|
162
|
+
preferences: load_preferences
|
163
|
+
}.to_json
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# app/resources/project_readme.rb
|
168
|
+
class ProjectReadme < Tsikol::Resource
|
169
|
+
uri "project/readme"
|
170
|
+
description "Project README file"
|
171
|
+
|
172
|
+
def read
|
173
|
+
if File.exist?("README.md")
|
174
|
+
File.read("README.md")
|
175
|
+
else
|
176
|
+
"No README found"
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
## Dynamic Resources
|
183
|
+
|
184
|
+
### Real-Time Data
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
class LiveMetrics < Tsikol::Resource
|
188
|
+
uri "metrics/live"
|
189
|
+
description "Real-time metrics"
|
190
|
+
|
191
|
+
def read
|
192
|
+
{
|
193
|
+
timestamp: Time.now.iso8601,
|
194
|
+
requests_per_second: calculate_rps,
|
195
|
+
active_connections: active_connections,
|
196
|
+
queue_depth: queue_depth,
|
197
|
+
cpu_usage: cpu_usage,
|
198
|
+
memory_usage: memory_usage
|
199
|
+
}.to_json
|
200
|
+
end
|
201
|
+
|
202
|
+
private
|
203
|
+
|
204
|
+
def calculate_rps
|
205
|
+
# Calculate requests in last second
|
206
|
+
recent = @server.metrics.recent_requests(1)
|
207
|
+
recent.count
|
208
|
+
end
|
209
|
+
|
210
|
+
def active_connections
|
211
|
+
Thread.list.select(&:alive?).count
|
212
|
+
end
|
213
|
+
|
214
|
+
def queue_depth
|
215
|
+
# Implementation specific
|
216
|
+
0
|
217
|
+
end
|
218
|
+
|
219
|
+
def cpu_usage
|
220
|
+
# Platform specific implementation
|
221
|
+
"#{rand(0..100)}%"
|
222
|
+
end
|
223
|
+
end
|
224
|
+
```
|
225
|
+
|
226
|
+
### Database-Backed Resources
|
227
|
+
|
228
|
+
```ruby
|
229
|
+
class DatabaseSchema < Tsikol::Resource
|
230
|
+
uri "database/schema"
|
231
|
+
description "Current database schema"
|
232
|
+
|
233
|
+
def read
|
234
|
+
tables = fetch_tables
|
235
|
+
schema = tables.map do |table|
|
236
|
+
{
|
237
|
+
name: table,
|
238
|
+
columns: fetch_columns(table),
|
239
|
+
indexes: fetch_indexes(table)
|
240
|
+
}
|
241
|
+
end
|
242
|
+
|
243
|
+
{
|
244
|
+
database: database_name,
|
245
|
+
tables: schema,
|
246
|
+
generated_at: Time.now.iso8601
|
247
|
+
}.to_json
|
248
|
+
end
|
249
|
+
|
250
|
+
private
|
251
|
+
|
252
|
+
def fetch_tables
|
253
|
+
# Database specific
|
254
|
+
["users", "posts", "comments"]
|
255
|
+
end
|
256
|
+
|
257
|
+
def fetch_columns(table)
|
258
|
+
# Database specific
|
259
|
+
case table
|
260
|
+
when "users"
|
261
|
+
["id", "email", "name", "created_at"]
|
262
|
+
when "posts"
|
263
|
+
["id", "user_id", "title", "content", "published_at"]
|
264
|
+
else
|
265
|
+
["id", "created_at", "updated_at"]
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def fetch_indexes(table)
|
270
|
+
# Database specific
|
271
|
+
["#{table}_pkey"]
|
272
|
+
end
|
273
|
+
|
274
|
+
def database_name
|
275
|
+
ENV['DATABASE_NAME'] || 'development'
|
276
|
+
end
|
277
|
+
end
|
278
|
+
```
|
279
|
+
|
280
|
+
## Resource Formats
|
281
|
+
|
282
|
+
### JSON Resources
|
283
|
+
|
284
|
+
Most common format:
|
285
|
+
|
286
|
+
```ruby
|
287
|
+
class ApiEndpoints < Tsikol::Resource
|
288
|
+
uri "api/endpoints"
|
289
|
+
description "Available API endpoints"
|
290
|
+
|
291
|
+
def read
|
292
|
+
{
|
293
|
+
version: "1.0",
|
294
|
+
endpoints: [
|
295
|
+
{
|
296
|
+
path: "/users",
|
297
|
+
methods: ["GET", "POST"],
|
298
|
+
description: "User management"
|
299
|
+
},
|
300
|
+
{
|
301
|
+
path: "/users/:id",
|
302
|
+
methods: ["GET", "PUT", "DELETE"],
|
303
|
+
description: "Individual user operations"
|
304
|
+
}
|
305
|
+
]
|
306
|
+
}.to_json
|
307
|
+
end
|
308
|
+
end
|
309
|
+
```
|
310
|
+
|
311
|
+
### Text Resources
|
312
|
+
|
313
|
+
Plain text or markdown:
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
class Changelog < Tsikol::Resource
|
317
|
+
uri "changelog"
|
318
|
+
description "Project changelog"
|
319
|
+
|
320
|
+
def read
|
321
|
+
if File.exist?("CHANGELOG.md")
|
322
|
+
File.read("CHANGELOG.md")
|
323
|
+
else
|
324
|
+
"No changelog available"
|
325
|
+
end
|
326
|
+
end
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
### CSV Resources
|
331
|
+
|
332
|
+
Tabular data:
|
333
|
+
|
334
|
+
```ruby
|
335
|
+
class UserReport < Tsikol::Resource
|
336
|
+
uri "reports/users.csv"
|
337
|
+
description "User report in CSV format"
|
338
|
+
|
339
|
+
def read
|
340
|
+
require 'csv'
|
341
|
+
|
342
|
+
CSV.generate do |csv|
|
343
|
+
csv << ["ID", "Name", "Email", "Created"]
|
344
|
+
|
345
|
+
users.each do |user|
|
346
|
+
csv << [user[:id], user[:name], user[:email], user[:created_at]]
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
private
|
352
|
+
|
353
|
+
def users
|
354
|
+
# Fetch from database
|
355
|
+
[
|
356
|
+
{ id: 1, name: "Alice", email: "alice@example.com", created_at: "2024-01-01" },
|
357
|
+
{ id: 2, name: "Bob", email: "bob@example.com", created_at: "2024-01-02" }
|
358
|
+
]
|
359
|
+
end
|
360
|
+
end
|
361
|
+
```
|
362
|
+
|
363
|
+
### Binary Resources
|
364
|
+
|
365
|
+
Base64 encoded:
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
class Logo < Tsikol::Resource
|
369
|
+
uri "assets/logo"
|
370
|
+
description "Company logo"
|
371
|
+
|
372
|
+
def read
|
373
|
+
if File.exist?("logo.png")
|
374
|
+
content = File.read("logo.png", mode: "rb")
|
375
|
+
{
|
376
|
+
mime_type: "image/png",
|
377
|
+
encoding: "base64",
|
378
|
+
data: Base64.encode64(content)
|
379
|
+
}.to_json
|
380
|
+
else
|
381
|
+
{ error: "Logo not found" }.to_json
|
382
|
+
end
|
383
|
+
end
|
384
|
+
end
|
385
|
+
```
|
386
|
+
|
387
|
+
## Caching
|
388
|
+
|
389
|
+
### Simple Caching
|
390
|
+
|
391
|
+
```ruby
|
392
|
+
class CachedResource < Tsikol::Resource
|
393
|
+
uri "expensive/data"
|
394
|
+
description "Expensive computation (cached)"
|
395
|
+
|
396
|
+
def initialize
|
397
|
+
super
|
398
|
+
@cache = nil
|
399
|
+
@cache_time = nil
|
400
|
+
@cache_ttl = 300 # 5 minutes
|
401
|
+
end
|
402
|
+
|
403
|
+
def read
|
404
|
+
if cache_valid?
|
405
|
+
log :debug, "Cache hit"
|
406
|
+
@cache
|
407
|
+
else
|
408
|
+
log :debug, "Cache miss, computing..."
|
409
|
+
@cache = compute_expensive_data
|
410
|
+
@cache_time = Time.now
|
411
|
+
@cache
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
private
|
416
|
+
|
417
|
+
def cache_valid?
|
418
|
+
return false unless @cache && @cache_time
|
419
|
+
Time.now - @cache_time < @cache_ttl
|
420
|
+
end
|
421
|
+
|
422
|
+
def compute_expensive_data
|
423
|
+
# Simulate expensive operation
|
424
|
+
sleep(0.1)
|
425
|
+
{
|
426
|
+
result: "Expensive data",
|
427
|
+
computed_at: Time.now.iso8601
|
428
|
+
}.to_json
|
429
|
+
end
|
430
|
+
end
|
431
|
+
```
|
432
|
+
|
433
|
+
### Conditional Caching
|
434
|
+
|
435
|
+
```ruby
|
436
|
+
class ConditionalCache < Tsikol::Resource
|
437
|
+
uri "data/conditional"
|
438
|
+
description "Conditionally cached data"
|
439
|
+
|
440
|
+
def read
|
441
|
+
data = fetch_data
|
442
|
+
|
443
|
+
# Cache based on data characteristics
|
444
|
+
if should_cache?(data)
|
445
|
+
@cached_data = data
|
446
|
+
@cache_key = generate_cache_key(data)
|
447
|
+
end
|
448
|
+
|
449
|
+
format_response(data)
|
450
|
+
end
|
451
|
+
|
452
|
+
private
|
453
|
+
|
454
|
+
def should_cache?(data)
|
455
|
+
# Cache if data is large or expensive
|
456
|
+
data.size > 1000 || data[:expensive]
|
457
|
+
end
|
458
|
+
|
459
|
+
def generate_cache_key(data)
|
460
|
+
Digest::SHA256.hexdigest(data.to_s)
|
461
|
+
end
|
462
|
+
end
|
463
|
+
```
|
464
|
+
|
465
|
+
## Advanced Patterns
|
466
|
+
|
467
|
+
### Aggregated Resources
|
468
|
+
|
469
|
+
Combine multiple data sources:
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
class Dashboard < Tsikol::Resource
|
473
|
+
uri "dashboard"
|
474
|
+
description "Aggregated dashboard data"
|
475
|
+
|
476
|
+
def read
|
477
|
+
{
|
478
|
+
summary: fetch_summary,
|
479
|
+
metrics: fetch_metrics,
|
480
|
+
recent_activity: fetch_recent_activity,
|
481
|
+
alerts: fetch_alerts
|
482
|
+
}.to_json
|
483
|
+
end
|
484
|
+
|
485
|
+
private
|
486
|
+
|
487
|
+
def fetch_summary
|
488
|
+
{
|
489
|
+
total_users: count_users,
|
490
|
+
active_sessions: count_sessions,
|
491
|
+
revenue_today: calculate_revenue
|
492
|
+
}
|
493
|
+
end
|
494
|
+
|
495
|
+
def fetch_metrics
|
496
|
+
SystemMetrics.new.read
|
497
|
+
end
|
498
|
+
|
499
|
+
def fetch_recent_activity
|
500
|
+
# Last 10 activities
|
501
|
+
[]
|
502
|
+
end
|
503
|
+
|
504
|
+
def fetch_alerts
|
505
|
+
# Active alerts
|
506
|
+
[]
|
507
|
+
end
|
508
|
+
end
|
509
|
+
```
|
510
|
+
|
511
|
+
### Filtered Resources
|
512
|
+
|
513
|
+
Resources with virtual filters:
|
514
|
+
|
515
|
+
```ruby
|
516
|
+
class FilteredLogs < Tsikol::Resource
|
517
|
+
uri "logs/recent"
|
518
|
+
description "Recent log entries"
|
519
|
+
|
520
|
+
def read
|
521
|
+
# In real app, URI could indicate filter
|
522
|
+
# e.g., "logs/error" for error logs only
|
523
|
+
|
524
|
+
logs = if uri.include?("error")
|
525
|
+
fetch_error_logs
|
526
|
+
elsif uri.include?("warning")
|
527
|
+
fetch_warning_logs
|
528
|
+
else
|
529
|
+
fetch_all_logs
|
530
|
+
end
|
531
|
+
|
532
|
+
format_logs(logs)
|
533
|
+
end
|
534
|
+
|
535
|
+
private
|
536
|
+
|
537
|
+
def fetch_error_logs
|
538
|
+
read_log_file.select { |line| line.include?("[ERROR]") }
|
539
|
+
end
|
540
|
+
|
541
|
+
def fetch_warning_logs
|
542
|
+
read_log_file.select { |line| line.include?("[WARNING]") }
|
543
|
+
end
|
544
|
+
|
545
|
+
def fetch_all_logs
|
546
|
+
read_log_file.last(100)
|
547
|
+
end
|
548
|
+
|
549
|
+
def read_log_file
|
550
|
+
File.readlines("app.log") rescue []
|
551
|
+
end
|
552
|
+
|
553
|
+
def format_logs(logs)
|
554
|
+
{
|
555
|
+
count: logs.size,
|
556
|
+
entries: logs.map { |log| parse_log_entry(log) }
|
557
|
+
}.to_json
|
558
|
+
end
|
559
|
+
end
|
560
|
+
```
|
561
|
+
|
562
|
+
### Streaming Resources
|
563
|
+
|
564
|
+
For large data sets:
|
565
|
+
|
566
|
+
```ruby
|
567
|
+
class LargeDataset < Tsikol::Resource
|
568
|
+
uri "data/large"
|
569
|
+
description "Large dataset (streamed)"
|
570
|
+
|
571
|
+
def read
|
572
|
+
# Return metadata about the dataset
|
573
|
+
{
|
574
|
+
total_records: count_records,
|
575
|
+
chunk_size: 1000,
|
576
|
+
chunks_available: calculate_chunks,
|
577
|
+
access_pattern: "Use data/large/chunk/{n} to access chunks"
|
578
|
+
}.to_json
|
579
|
+
end
|
580
|
+
|
581
|
+
private
|
582
|
+
|
583
|
+
def count_records
|
584
|
+
# Implementation
|
585
|
+
1_000_000
|
586
|
+
end
|
587
|
+
|
588
|
+
def calculate_chunks
|
589
|
+
(count_records / 1000.0).ceil
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
# Separate resource for chunks
|
594
|
+
class DataChunk < Tsikol::Resource
|
595
|
+
uri "data/large/chunk" # Would need custom routing
|
596
|
+
description "Data chunk"
|
597
|
+
|
598
|
+
def read
|
599
|
+
chunk_number = extract_chunk_number
|
600
|
+
offset = chunk_number * 1000
|
601
|
+
|
602
|
+
records = fetch_records(offset, 1000)
|
603
|
+
|
604
|
+
{
|
605
|
+
chunk: chunk_number,
|
606
|
+
records: records,
|
607
|
+
has_more: has_more_records?(offset + 1000)
|
608
|
+
}.to_json
|
609
|
+
end
|
610
|
+
end
|
611
|
+
```
|
612
|
+
|
613
|
+
## Testing Resources
|
614
|
+
|
615
|
+
### Basic Resource Test
|
616
|
+
|
617
|
+
```ruby
|
618
|
+
require 'minitest/autorun'
|
619
|
+
require 'tsikol/test_helpers'
|
620
|
+
|
621
|
+
class SystemMetricsTest < Minitest::Test
|
622
|
+
include Tsikol::TestHelpers::Assertions
|
623
|
+
|
624
|
+
def setup
|
625
|
+
@server = Tsikol::Server.new(name: "test")
|
626
|
+
@server.register_resource_instance(SystemMetrics.new)
|
627
|
+
@client = Tsikol::TestHelpers::TestClient.new(@server)
|
628
|
+
@client.initialize_connection
|
629
|
+
end
|
630
|
+
|
631
|
+
def test_read_metrics
|
632
|
+
response = @client.read_resource("system/metrics")
|
633
|
+
assert_successful_response(response)
|
634
|
+
|
635
|
+
content = response.dig(:result, :contents, 0, :text)
|
636
|
+
data = JSON.parse(content)
|
637
|
+
|
638
|
+
assert data["server"]
|
639
|
+
assert data["system"]
|
640
|
+
assert data["performance"]
|
641
|
+
assert data["timestamp"]
|
642
|
+
end
|
643
|
+
|
644
|
+
def test_metrics_structure
|
645
|
+
response = @client.read_resource("system/metrics")
|
646
|
+
content = response.dig(:result, :contents, 0, :text)
|
647
|
+
data = JSON.parse(content)
|
648
|
+
|
649
|
+
# Check server info
|
650
|
+
assert_equal "test", data["server"]["name"]
|
651
|
+
assert data["server"]["uptime"] >= 0
|
652
|
+
|
653
|
+
# Check system info
|
654
|
+
assert_equal RUBY_VERSION, data["system"]["ruby_version"]
|
655
|
+
assert data["system"]["memory_usage"]
|
656
|
+
end
|
657
|
+
end
|
658
|
+
```
|
659
|
+
|
660
|
+
### Testing Dynamic Resources
|
661
|
+
|
662
|
+
```ruby
|
663
|
+
class LiveMetricsTest < Minitest::Test
|
664
|
+
def test_real_time_data_changes
|
665
|
+
response1 = @client.read_resource("metrics/live")
|
666
|
+
sleep(0.1)
|
667
|
+
response2 = @client.read_resource("metrics/live")
|
668
|
+
|
669
|
+
data1 = JSON.parse(response1.dig(:result, :contents, 0, :text))
|
670
|
+
data2 = JSON.parse(response2.dig(:result, :contents, 0, :text))
|
671
|
+
|
672
|
+
# Timestamps should be different
|
673
|
+
refute_equal data1["timestamp"], data2["timestamp"]
|
674
|
+
end
|
675
|
+
end
|
676
|
+
```
|
677
|
+
|
678
|
+
### Testing Cached Resources
|
679
|
+
|
680
|
+
```ruby
|
681
|
+
class CachedResourceTest < Minitest::Test
|
682
|
+
def test_caching_behavior
|
683
|
+
# First call should compute
|
684
|
+
start = Time.now
|
685
|
+
response1 = @client.read_resource("expensive/data")
|
686
|
+
duration1 = Time.now - start
|
687
|
+
|
688
|
+
# Second call should be cached
|
689
|
+
start = Time.now
|
690
|
+
response2 = @client.read_resource("expensive/data")
|
691
|
+
duration2 = Time.now - start
|
692
|
+
|
693
|
+
# Cached call should be much faster
|
694
|
+
assert duration2 < duration1 / 10
|
695
|
+
|
696
|
+
# Content should be identical
|
697
|
+
assert_equal(
|
698
|
+
response1.dig(:result, :contents, 0, :text),
|
699
|
+
response2.dig(:result, :contents, 0, :text)
|
700
|
+
)
|
701
|
+
end
|
702
|
+
end
|
703
|
+
```
|
704
|
+
|
705
|
+
## Best Practices
|
706
|
+
|
707
|
+
1. **Use Clear URIs**: Make them intuitive and hierarchical
|
708
|
+
2. **Return Consistent Formats**: Usually JSON for structured data
|
709
|
+
3. **Include Metadata**: Timestamps, versions, etc.
|
710
|
+
4. **Handle Errors Gracefully**: Return error info in response
|
711
|
+
5. **Cache When Appropriate**: For expensive operations
|
712
|
+
6. **Keep Resources Read-Only**: Use tools for modifications
|
713
|
+
7. **Document Format**: Describe what the resource returns
|
714
|
+
|
715
|
+
## Next Steps
|
716
|
+
|
717
|
+
- Learn about [Prompts](prompts.md)
|
718
|
+
- Explore [Testing](testing.md)
|
719
|
+
- Add [Caching](../cookbook/caching.md)
|
720
|
+
- Implement [Monitoring](../cookbook/monitoring.md)
|