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,627 @@
|
|
1
|
+
# Tools Guide
|
2
|
+
|
3
|
+
Tools are functions that MCP clients can call to perform actions. This guide covers everything you need to know about creating and using tools in Tsikol.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
1. [What are Tools?](#what-are-tools)
|
8
|
+
2. [Creating Tools](#creating-tools)
|
9
|
+
3. [Parameters](#parameters)
|
10
|
+
4. [Parameter Types](#parameter-types)
|
11
|
+
5. [Completions](#completions)
|
12
|
+
6. [Error Handling](#error-handling)
|
13
|
+
7. [Logging](#logging)
|
14
|
+
8. [Advanced Patterns](#advanced-patterns)
|
15
|
+
9. [Testing Tools](#testing-tools)
|
16
|
+
|
17
|
+
## What are Tools?
|
18
|
+
|
19
|
+
Tools are the primary way MCP clients interact with your server. They:
|
20
|
+
- Accept parameters
|
21
|
+
- Perform actions
|
22
|
+
- Return results
|
23
|
+
- Can have side effects
|
24
|
+
|
25
|
+
Think of tools as API endpoints that AI can call.
|
26
|
+
|
27
|
+
## Creating Tools
|
28
|
+
|
29
|
+
### Inline Tools
|
30
|
+
|
31
|
+
Quick tools defined directly in server.rb:
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
Tsikol.server "my-server" do
|
35
|
+
tool "greet" do |name:|
|
36
|
+
"Hello, #{name}!"
|
37
|
+
end
|
38
|
+
|
39
|
+
tool "calculate" do |a:, b:, operation: "add"|
|
40
|
+
case operation
|
41
|
+
when "add" then a + b
|
42
|
+
when "subtract" then a - b
|
43
|
+
when "multiply" then a * b
|
44
|
+
when "divide" then b.zero? ? "Error: Division by zero" : a.to_f / b
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
### Class-Based Tools
|
51
|
+
|
52
|
+
For complex tools, use classes:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# app/tools/file_processor.rb
|
56
|
+
class FileProcessor < Tsikol::Tool
|
57
|
+
description "Process files with various operations"
|
58
|
+
|
59
|
+
parameter :file_path do
|
60
|
+
type :string
|
61
|
+
required
|
62
|
+
description "Path to the file to process"
|
63
|
+
|
64
|
+
complete do |partial|
|
65
|
+
Dir.glob("#{partial}*")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
parameter :operation do
|
70
|
+
type :string
|
71
|
+
required
|
72
|
+
enum ["read", "analyze", "compress", "convert"]
|
73
|
+
description "Operation to perform"
|
74
|
+
end
|
75
|
+
|
76
|
+
parameter :options do
|
77
|
+
type :object
|
78
|
+
optional
|
79
|
+
description "Additional options for the operation"
|
80
|
+
end
|
81
|
+
|
82
|
+
def execute(file_path:, operation:, options: {})
|
83
|
+
unless File.exist?(file_path)
|
84
|
+
raise Tsikol::ValidationError, "File not found: #{file_path}"
|
85
|
+
end
|
86
|
+
|
87
|
+
case operation
|
88
|
+
when "read"
|
89
|
+
read_file(file_path, options)
|
90
|
+
when "analyze"
|
91
|
+
analyze_file(file_path, options)
|
92
|
+
when "compress"
|
93
|
+
compress_file(file_path, options)
|
94
|
+
when "convert"
|
95
|
+
convert_file(file_path, options)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def read_file(path, options)
|
102
|
+
encoding = options["encoding"] || "UTF-8"
|
103
|
+
File.read(path, encoding: encoding)
|
104
|
+
end
|
105
|
+
|
106
|
+
def analyze_file(path, options)
|
107
|
+
content = File.read(path)
|
108
|
+
{
|
109
|
+
size: File.size(path),
|
110
|
+
lines: content.lines.count,
|
111
|
+
words: content.split.count,
|
112
|
+
type: File.extname(path)
|
113
|
+
}.to_json
|
114
|
+
end
|
115
|
+
|
116
|
+
def compress_file(path, options)
|
117
|
+
# Implementation
|
118
|
+
"Compressed #{path}"
|
119
|
+
end
|
120
|
+
|
121
|
+
def convert_file(path, options)
|
122
|
+
# Implementation
|
123
|
+
"Converted #{path}"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
## Parameters
|
129
|
+
|
130
|
+
### Basic Parameters
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
parameter :name do
|
134
|
+
type :string
|
135
|
+
required
|
136
|
+
description "User's name"
|
137
|
+
end
|
138
|
+
|
139
|
+
parameter :age do
|
140
|
+
type :integer
|
141
|
+
optional
|
142
|
+
default 18
|
143
|
+
description "User's age"
|
144
|
+
end
|
145
|
+
```
|
146
|
+
|
147
|
+
### Parameter Options
|
148
|
+
|
149
|
+
All available parameter options:
|
150
|
+
|
151
|
+
```ruby
|
152
|
+
parameter :example do
|
153
|
+
type :string # :string, :number, :integer, :boolean, :array, :object
|
154
|
+
required # or optional
|
155
|
+
default "value" # Default value if optional
|
156
|
+
description "Purpose" # What this parameter does
|
157
|
+
enum ["opt1", "opt2"] # Allowed values
|
158
|
+
|
159
|
+
complete do |partial| # Autocomplete suggestions
|
160
|
+
# Return array of suggestions
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
## Parameter Types
|
166
|
+
|
167
|
+
### String Parameters
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
parameter :message do
|
171
|
+
type :string
|
172
|
+
required
|
173
|
+
description "Message to process"
|
174
|
+
end
|
175
|
+
|
176
|
+
parameter :format do
|
177
|
+
type :string
|
178
|
+
optional
|
179
|
+
default "plain"
|
180
|
+
enum ["plain", "markdown", "html"]
|
181
|
+
end
|
182
|
+
```
|
183
|
+
|
184
|
+
### Number Parameters
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
parameter :amount do
|
188
|
+
type :number
|
189
|
+
required
|
190
|
+
description "Amount in dollars"
|
191
|
+
end
|
192
|
+
|
193
|
+
parameter :percentage do
|
194
|
+
type :number
|
195
|
+
optional
|
196
|
+
default 0.0
|
197
|
+
description "Percentage (0-100)"
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
### Boolean Parameters
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
parameter :verbose do
|
205
|
+
type :boolean
|
206
|
+
optional
|
207
|
+
default false
|
208
|
+
description "Enable verbose output"
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
### Array Parameters
|
213
|
+
|
214
|
+
```ruby
|
215
|
+
parameter :tags do
|
216
|
+
type :array
|
217
|
+
optional
|
218
|
+
default []
|
219
|
+
description "List of tags"
|
220
|
+
end
|
221
|
+
|
222
|
+
def execute(tags: [])
|
223
|
+
# tags is an array
|
224
|
+
tags.join(", ")
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
### Object Parameters
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
parameter :config do
|
232
|
+
type :object
|
233
|
+
optional
|
234
|
+
description "Configuration object"
|
235
|
+
end
|
236
|
+
|
237
|
+
def execute(config: {})
|
238
|
+
# config is a hash
|
239
|
+
timeout = config["timeout"] || 30
|
240
|
+
retries = config["retries"] || 3
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
## Completions
|
245
|
+
|
246
|
+
Add intelligent autocomplete to parameters:
|
247
|
+
|
248
|
+
### Static Completions
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
parameter :language do
|
252
|
+
type :string
|
253
|
+
required
|
254
|
+
|
255
|
+
complete do |partial|
|
256
|
+
languages = ["ruby", "python", "javascript", "go", "rust"]
|
257
|
+
languages.select { |lang| lang.start_with?(partial.downcase) }
|
258
|
+
end
|
259
|
+
end
|
260
|
+
```
|
261
|
+
|
262
|
+
### Dynamic Completions
|
263
|
+
|
264
|
+
```ruby
|
265
|
+
parameter :file do
|
266
|
+
type :string
|
267
|
+
required
|
268
|
+
|
269
|
+
complete do |partial|
|
270
|
+
# File system completion
|
271
|
+
Dir.glob("#{partial}*").map do |path|
|
272
|
+
File.directory?(path) ? "#{path}/" : path
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
```
|
277
|
+
|
278
|
+
### Context-Aware Completions
|
279
|
+
|
280
|
+
```ruby
|
281
|
+
parameter :branch do
|
282
|
+
type :string
|
283
|
+
required
|
284
|
+
|
285
|
+
complete do |partial, context|
|
286
|
+
# Access other parameters via context
|
287
|
+
repo = context[:repository]
|
288
|
+
|
289
|
+
branches = fetch_branches_for_repo(repo)
|
290
|
+
branches.select { |b| b.include?(partial) }
|
291
|
+
end
|
292
|
+
end
|
293
|
+
```
|
294
|
+
|
295
|
+
## Error Handling
|
296
|
+
|
297
|
+
### Validation Errors
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
def execute(file_path:)
|
301
|
+
unless File.exist?(file_path)
|
302
|
+
raise Tsikol::ValidationError, "File not found: #{file_path}"
|
303
|
+
end
|
304
|
+
|
305
|
+
unless File.readable?(file_path)
|
306
|
+
raise Tsikol::ValidationError, "File not readable: #{file_path}"
|
307
|
+
end
|
308
|
+
|
309
|
+
process_file(file_path)
|
310
|
+
end
|
311
|
+
```
|
312
|
+
|
313
|
+
### Graceful Degradation
|
314
|
+
|
315
|
+
```ruby
|
316
|
+
def execute(url:)
|
317
|
+
begin
|
318
|
+
response = fetch_url(url)
|
319
|
+
process_response(response)
|
320
|
+
rescue Net::HTTPError => e
|
321
|
+
log :error, "HTTP error", data: { url: url, error: e.message }
|
322
|
+
"Unable to fetch URL: #{e.message}"
|
323
|
+
rescue => e
|
324
|
+
log :error, "Unexpected error", data: { error: e.class.name }
|
325
|
+
"An error occurred. Please try again."
|
326
|
+
end
|
327
|
+
end
|
328
|
+
```
|
329
|
+
|
330
|
+
## Logging
|
331
|
+
|
332
|
+
### Basic Logging
|
333
|
+
|
334
|
+
```ruby
|
335
|
+
def execute(task:)
|
336
|
+
log :info, "Starting task", data: { task: task }
|
337
|
+
|
338
|
+
result = perform_task(task)
|
339
|
+
|
340
|
+
log :info, "Task completed", data: { task: task, result: result }
|
341
|
+
|
342
|
+
result
|
343
|
+
end
|
344
|
+
```
|
345
|
+
|
346
|
+
### Log Levels
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
log :debug, "Detailed information"
|
350
|
+
log :info, "General information"
|
351
|
+
log :warning, "Warning message"
|
352
|
+
log :error, "Error occurred", data: { error: error.message }
|
353
|
+
```
|
354
|
+
|
355
|
+
### Enabling Logging
|
356
|
+
|
357
|
+
```ruby
|
358
|
+
def set_server(server)
|
359
|
+
@server = server
|
360
|
+
|
361
|
+
define_singleton_method(:log) do |level, message, data: nil|
|
362
|
+
@server.log(level, message, data: data)
|
363
|
+
end
|
364
|
+
end
|
365
|
+
```
|
366
|
+
|
367
|
+
## Advanced Patterns
|
368
|
+
|
369
|
+
### Async Operations
|
370
|
+
|
371
|
+
```ruby
|
372
|
+
class AsyncProcessor < Tsikol::Tool
|
373
|
+
description "Process data asynchronously"
|
374
|
+
|
375
|
+
parameter :data do
|
376
|
+
type :string
|
377
|
+
required
|
378
|
+
end
|
379
|
+
|
380
|
+
parameter :callback_url do
|
381
|
+
type :string
|
382
|
+
optional
|
383
|
+
description "URL to call when complete"
|
384
|
+
end
|
385
|
+
|
386
|
+
def execute(data:, callback_url: nil)
|
387
|
+
job_id = SecureRandom.uuid
|
388
|
+
|
389
|
+
# Start async job
|
390
|
+
Thread.new do
|
391
|
+
result = process_data(data)
|
392
|
+
|
393
|
+
if callback_url
|
394
|
+
notify_completion(callback_url, job_id, result)
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
{
|
399
|
+
job_id: job_id,
|
400
|
+
status: "processing",
|
401
|
+
message: "Job started. Use job_id to check status."
|
402
|
+
}.to_json
|
403
|
+
end
|
404
|
+
end
|
405
|
+
```
|
406
|
+
|
407
|
+
### Batch Operations
|
408
|
+
|
409
|
+
```ruby
|
410
|
+
class BatchProcessor < Tsikol::Tool
|
411
|
+
description "Process multiple items"
|
412
|
+
|
413
|
+
parameter :items do
|
414
|
+
type :array
|
415
|
+
required
|
416
|
+
description "Items to process"
|
417
|
+
end
|
418
|
+
|
419
|
+
parameter :parallel do
|
420
|
+
type :boolean
|
421
|
+
optional
|
422
|
+
default false
|
423
|
+
description "Process in parallel"
|
424
|
+
end
|
425
|
+
|
426
|
+
def execute(items:, parallel: false)
|
427
|
+
if parallel
|
428
|
+
process_parallel(items)
|
429
|
+
else
|
430
|
+
process_sequential(items)
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
private
|
435
|
+
|
436
|
+
def process_parallel(items)
|
437
|
+
threads = items.map do |item|
|
438
|
+
Thread.new { process_item(item) }
|
439
|
+
end
|
440
|
+
|
441
|
+
results = threads.map(&:value)
|
442
|
+
format_results(results)
|
443
|
+
end
|
444
|
+
|
445
|
+
def process_sequential(items)
|
446
|
+
results = items.map { |item| process_item(item) }
|
447
|
+
format_results(results)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
```
|
451
|
+
|
452
|
+
### Tool Composition
|
453
|
+
|
454
|
+
```ruby
|
455
|
+
class CompositeTool < Tsikol::Tool
|
456
|
+
description "Combines multiple operations"
|
457
|
+
|
458
|
+
def execute(input:)
|
459
|
+
# Step 1: Validate
|
460
|
+
validated = ValidationTool.new.execute(data: input)
|
461
|
+
|
462
|
+
# Step 2: Process
|
463
|
+
processed = ProcessingTool.new.execute(data: validated)
|
464
|
+
|
465
|
+
# Step 3: Format
|
466
|
+
FormattingTool.new.execute(data: processed)
|
467
|
+
end
|
468
|
+
end
|
469
|
+
```
|
470
|
+
|
471
|
+
### Caching Results
|
472
|
+
|
473
|
+
```ruby
|
474
|
+
class CachedTool < Tsikol::Tool
|
475
|
+
def initialize
|
476
|
+
super
|
477
|
+
@cache = {}
|
478
|
+
end
|
479
|
+
|
480
|
+
def execute(query:)
|
481
|
+
cache_key = generate_cache_key(query)
|
482
|
+
|
483
|
+
if cached = @cache[cache_key]
|
484
|
+
log :debug, "Cache hit", data: { key: cache_key }
|
485
|
+
return cached[:result]
|
486
|
+
end
|
487
|
+
|
488
|
+
result = expensive_operation(query)
|
489
|
+
|
490
|
+
@cache[cache_key] = {
|
491
|
+
result: result,
|
492
|
+
timestamp: Time.now
|
493
|
+
}
|
494
|
+
|
495
|
+
# Clean old entries
|
496
|
+
clean_cache if @cache.size > 100
|
497
|
+
|
498
|
+
result
|
499
|
+
end
|
500
|
+
|
501
|
+
private
|
502
|
+
|
503
|
+
def generate_cache_key(query)
|
504
|
+
Digest::SHA256.hexdigest(query)
|
505
|
+
end
|
506
|
+
|
507
|
+
def clean_cache
|
508
|
+
cutoff = Time.now - 3600 # 1 hour
|
509
|
+
@cache.delete_if { |_, v| v[:timestamp] < cutoff }
|
510
|
+
end
|
511
|
+
end
|
512
|
+
```
|
513
|
+
|
514
|
+
## Testing Tools
|
515
|
+
|
516
|
+
### Basic Tool Test
|
517
|
+
|
518
|
+
```ruby
|
519
|
+
require 'minitest/autorun'
|
520
|
+
require 'tsikol/test_helpers'
|
521
|
+
|
522
|
+
class FileProcessorTest < Minitest::Test
|
523
|
+
include Tsikol::TestHelpers::Assertions
|
524
|
+
|
525
|
+
def setup
|
526
|
+
@server = Tsikol::Server.new(name: "test")
|
527
|
+
@server.register_tool_instance(FileProcessor.new)
|
528
|
+
@client = Tsikol::TestHelpers::TestClient.new(@server)
|
529
|
+
@client.initialize_connection
|
530
|
+
end
|
531
|
+
|
532
|
+
def test_read_file
|
533
|
+
# Create test file
|
534
|
+
File.write("test.txt", "Hello, World!")
|
535
|
+
|
536
|
+
response = @client.call_tool("file_processor", {
|
537
|
+
"file_path" => "test.txt",
|
538
|
+
"operation" => "read"
|
539
|
+
})
|
540
|
+
|
541
|
+
assert_successful_response(response)
|
542
|
+
result = response.dig(:result, :content, 0, :text)
|
543
|
+
assert_equal "Hello, World!", result
|
544
|
+
ensure
|
545
|
+
File.delete("test.txt") if File.exist?("test.txt")
|
546
|
+
end
|
547
|
+
|
548
|
+
def test_file_not_found
|
549
|
+
response = @client.call_tool("file_processor", {
|
550
|
+
"file_path" => "nonexistent.txt",
|
551
|
+
"operation" => "read"
|
552
|
+
})
|
553
|
+
|
554
|
+
assert_error_response(response, -32603)
|
555
|
+
assert_match /File not found/, response[:error][:message]
|
556
|
+
end
|
557
|
+
end
|
558
|
+
```
|
559
|
+
|
560
|
+
### Testing Completions
|
561
|
+
|
562
|
+
```ruby
|
563
|
+
def test_language_completion
|
564
|
+
response = @client.complete(
|
565
|
+
{ type: "ref/tool", name: "code_analyzer" },
|
566
|
+
{ name: "language", value: "ru" }
|
567
|
+
)
|
568
|
+
|
569
|
+
assert_successful_response(response)
|
570
|
+
values = response.dig(:result, :completion, :values)
|
571
|
+
assert_includes values, "ruby"
|
572
|
+
assert_includes values, "rust"
|
573
|
+
end
|
574
|
+
```
|
575
|
+
|
576
|
+
### Mocking External Dependencies
|
577
|
+
|
578
|
+
```ruby
|
579
|
+
class ExternalApiToolTest < Minitest::Test
|
580
|
+
def setup
|
581
|
+
@server = Tsikol::Server.new(name: "test")
|
582
|
+
@tool = ExternalApiTool.new
|
583
|
+
|
584
|
+
# Mock external API
|
585
|
+
@tool.define_singleton_method(:fetch_from_api) do |endpoint|
|
586
|
+
case endpoint
|
587
|
+
when "/users"
|
588
|
+
[{ id: 1, name: "Test User" }]
|
589
|
+
when "/error"
|
590
|
+
raise "API Error"
|
591
|
+
else
|
592
|
+
[]
|
593
|
+
end
|
594
|
+
end
|
595
|
+
|
596
|
+
@server.register_tool_instance(@tool)
|
597
|
+
@client = Tsikol::TestHelpers::TestClient.new(@server)
|
598
|
+
end
|
599
|
+
|
600
|
+
def test_successful_api_call
|
601
|
+
response = @client.call_tool("external_api", {
|
602
|
+
"endpoint" => "/users"
|
603
|
+
})
|
604
|
+
|
605
|
+
assert_successful_response(response)
|
606
|
+
result = JSON.parse(response.dig(:result, :content, 0, :text))
|
607
|
+
assert_equal 1, result.first["id"]
|
608
|
+
end
|
609
|
+
end
|
610
|
+
```
|
611
|
+
|
612
|
+
## Best Practices
|
613
|
+
|
614
|
+
1. **Clear Descriptions**: Help users understand what your tool does
|
615
|
+
2. **Validate Input**: Check parameters before processing
|
616
|
+
3. **Handle Errors**: Provide helpful error messages
|
617
|
+
4. **Add Completions**: Make tools easier to use
|
618
|
+
5. **Log Important Events**: Aid debugging and monitoring
|
619
|
+
6. **Keep It Focused**: Each tool should do one thing well
|
620
|
+
7. **Test Thoroughly**: Cover success and error cases
|
621
|
+
|
622
|
+
## Next Steps
|
623
|
+
|
624
|
+
- Learn about [Resources](resources.md)
|
625
|
+
- Explore [Prompts](prompts.md)
|
626
|
+
- Add [Completions](completion.md)
|
627
|
+
- Set up [Testing](testing.md)
|
data/examples/README.md
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# Tsikol Examples
|
2
|
+
|
3
|
+
This directory contains example MCP servers built with Tsikol, demonstrating various features and patterns.
|
4
|
+
|
5
|
+
## Examples Overview
|
6
|
+
|
7
|
+
### 1. [basic.rb](basic.rb)
|
8
|
+
A simple inline server showing the original DSL-style API. Great for quick prototypes.
|
9
|
+
|
10
|
+
```bash
|
11
|
+
ruby examples/basic.rb
|
12
|
+
```
|
13
|
+
|
14
|
+
### 2. [basic-migrated/](basic-migrated/)
|
15
|
+
Shows how to migrate from inline DSL to the Rails-like structure with separate files.
|
16
|
+
|
17
|
+
```bash
|
18
|
+
cd examples/basic-migrated
|
19
|
+
./server.rb
|
20
|
+
```
|
21
|
+
|
22
|
+
### 3. [weather-service/](weather-service/)
|
23
|
+
Full-featured example with namespaced modules and proper project structure.
|
24
|
+
|
25
|
+
```bash
|
26
|
+
cd examples/weather-service
|
27
|
+
./server.rb
|
28
|
+
```
|
29
|
+
|
30
|
+
### 4. [middleware_example.rb](middleware_example.rb)
|
31
|
+
Demonstrates the middleware system with authentication, rate limiting, and custom middleware.
|
32
|
+
|
33
|
+
```bash
|
34
|
+
ruby examples/middleware_example.rb
|
35
|
+
```
|
36
|
+
|
37
|
+
### 5. [sampling_example.rb](sampling_example.rb)
|
38
|
+
Shows how to use sampling for AI-assisted features.
|
39
|
+
|
40
|
+
```bash
|
41
|
+
ruby examples/sampling_example.rb
|
42
|
+
```
|
43
|
+
|
44
|
+
### 6. [advanced_features.rb](advanced_features.rb)
|
45
|
+
Demonstrates lifecycle hooks, error handling, and health monitoring.
|
46
|
+
|
47
|
+
```bash
|
48
|
+
ruby examples/advanced_features.rb
|
49
|
+
```
|
50
|
+
|
51
|
+
### 7. [full_featured.rb](full_featured.rb)
|
52
|
+
Complete example showing all Tsikol features working together.
|
53
|
+
|
54
|
+
```bash
|
55
|
+
ruby examples/full_featured.rb
|
56
|
+
```
|
57
|
+
|
58
|
+
## Running Examples
|
59
|
+
|
60
|
+
All examples can be run directly:
|
61
|
+
|
62
|
+
```bash
|
63
|
+
# From the gem root
|
64
|
+
ruby examples/basic.rb
|
65
|
+
|
66
|
+
# Or make them executable
|
67
|
+
chmod +x examples/basic.rb
|
68
|
+
./examples/basic.rb
|
69
|
+
```
|
70
|
+
|
71
|
+
For structured examples with directories:
|
72
|
+
|
73
|
+
```bash
|
74
|
+
cd examples/weather-service
|
75
|
+
bundle install # If needed
|
76
|
+
./server.rb
|
77
|
+
```
|
78
|
+
|
79
|
+
## Testing Examples
|
80
|
+
|
81
|
+
Connect with MCP Inspector or any MCP client to test the servers.
|
82
|
+
|
83
|
+
## Creating Your Own
|
84
|
+
|
85
|
+
Use the Tsikol CLI to create your own project:
|
86
|
+
|
87
|
+
```bash
|
88
|
+
tsikol new my-project
|
89
|
+
cd my-project
|
90
|
+
bundle install
|
91
|
+
./server.rb
|
92
|
+
```
|