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,501 @@
|
|
1
|
+
# Echo Server Example
|
2
|
+
|
3
|
+
A simple MCP server that echoes back input with various transformations.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
This example demonstrates:
|
8
|
+
- Basic tool implementation
|
9
|
+
- Parameter validation
|
10
|
+
- Multiple operations in a single tool
|
11
|
+
- Error handling
|
12
|
+
- Logging
|
13
|
+
|
14
|
+
## Implementation
|
15
|
+
|
16
|
+
### server.rb
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
#!/usr/bin/env ruby
|
20
|
+
|
21
|
+
require 'tsikol'
|
22
|
+
|
23
|
+
# Simple echo tool with transformations
|
24
|
+
class EchoTool < Tsikol::Tool
|
25
|
+
name "echo"
|
26
|
+
description "Echoes text with optional transformations"
|
27
|
+
|
28
|
+
parameter :text do
|
29
|
+
type :string
|
30
|
+
required
|
31
|
+
description "Text to echo"
|
32
|
+
end
|
33
|
+
|
34
|
+
parameter :transform do
|
35
|
+
type :string
|
36
|
+
optional
|
37
|
+
default "none"
|
38
|
+
enum ["none", "uppercase", "lowercase", "reverse", "capitalize"]
|
39
|
+
description "Transformation to apply"
|
40
|
+
end
|
41
|
+
|
42
|
+
parameter :repeat do
|
43
|
+
type :number
|
44
|
+
optional
|
45
|
+
default 1
|
46
|
+
description "Number of times to repeat"
|
47
|
+
end
|
48
|
+
|
49
|
+
def execute(text:, transform: "none", repeat: 1)
|
50
|
+
# Validate repeat count
|
51
|
+
if repeat < 1 || repeat > 10
|
52
|
+
raise ArgumentError, "Repeat must be between 1 and 10"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Apply transformation
|
56
|
+
result = case transform
|
57
|
+
when "uppercase"
|
58
|
+
text.upcase
|
59
|
+
when "lowercase"
|
60
|
+
text.downcase
|
61
|
+
when "reverse"
|
62
|
+
text.reverse
|
63
|
+
when "capitalize"
|
64
|
+
text.split.map(&:capitalize).join(" ")
|
65
|
+
else
|
66
|
+
text
|
67
|
+
end
|
68
|
+
|
69
|
+
# Repeat if requested
|
70
|
+
if repeat > 1
|
71
|
+
result = Array.new(repeat, result).join(" ")
|
72
|
+
end
|
73
|
+
|
74
|
+
log :info, "Echoed text",
|
75
|
+
original_length: text.length,
|
76
|
+
transform: transform,
|
77
|
+
repeat: repeat
|
78
|
+
|
79
|
+
result
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# ASCII art generator tool
|
84
|
+
class AsciiArtTool < Tsikol::Tool
|
85
|
+
name "ascii_art"
|
86
|
+
description "Generate simple ASCII art"
|
87
|
+
|
88
|
+
parameter :type do
|
89
|
+
type :string
|
90
|
+
required
|
91
|
+
enum ["box", "banner", "divider"]
|
92
|
+
description "Type of ASCII art to generate"
|
93
|
+
end
|
94
|
+
|
95
|
+
parameter :text do
|
96
|
+
type :string
|
97
|
+
optional
|
98
|
+
description "Text to include in the art"
|
99
|
+
end
|
100
|
+
|
101
|
+
parameter :width do
|
102
|
+
type :number
|
103
|
+
optional
|
104
|
+
default 40
|
105
|
+
description "Width of the art"
|
106
|
+
end
|
107
|
+
|
108
|
+
def execute(type:, text: nil, width: 40)
|
109
|
+
case type
|
110
|
+
when "box"
|
111
|
+
create_box(text || "Hello", width)
|
112
|
+
when "banner"
|
113
|
+
create_banner(text || "BANNER", width)
|
114
|
+
when "divider"
|
115
|
+
create_divider(width)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def create_box(text, width)
|
122
|
+
# Ensure text fits
|
123
|
+
text = text[0...width-4] if text.length > width - 4
|
124
|
+
|
125
|
+
border = "+" + "-" * (width - 2) + "+"
|
126
|
+
padding = (width - 4 - text.length) / 2
|
127
|
+
content = "| " + " " * padding + text + " " * (width - 4 - padding - text.length) + " |"
|
128
|
+
|
129
|
+
[border, content, border].join("\n")
|
130
|
+
end
|
131
|
+
|
132
|
+
def create_banner(text, width)
|
133
|
+
text = text.upcase[0...width]
|
134
|
+
"=" * width + "\n" + text.center(width) + "\n" + "=" * width
|
135
|
+
end
|
136
|
+
|
137
|
+
def create_divider(width)
|
138
|
+
"-" * width
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Start the server
|
143
|
+
Tsikol.start(
|
144
|
+
name: "echo-server",
|
145
|
+
version: "1.0.0",
|
146
|
+
description: "Simple echo server with text transformations"
|
147
|
+
) do
|
148
|
+
# Enable logging
|
149
|
+
logging true
|
150
|
+
|
151
|
+
# Use logging middleware
|
152
|
+
use Tsikol::LoggingMiddleware, level: :info
|
153
|
+
|
154
|
+
# Register tools
|
155
|
+
tool EchoTool
|
156
|
+
tool AsciiArtTool
|
157
|
+
|
158
|
+
# Add a simple resource
|
159
|
+
resource "status" do
|
160
|
+
description "Server status information"
|
161
|
+
|
162
|
+
def read
|
163
|
+
{
|
164
|
+
status: "operational",
|
165
|
+
uptime: Time.now - @started_at,
|
166
|
+
tools_available: ["echo", "ascii_art"],
|
167
|
+
requests_served: @request_count || 0
|
168
|
+
}.to_json
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Track server start time
|
173
|
+
before_start do
|
174
|
+
@started_at = Time.now
|
175
|
+
@request_count = 0
|
176
|
+
end
|
177
|
+
|
178
|
+
# Count requests
|
179
|
+
after_request do |request, response|
|
180
|
+
@request_count += 1
|
181
|
+
end
|
182
|
+
end
|
183
|
+
```
|
184
|
+
|
185
|
+
### Gemfile
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
source 'https://rubygems.org'
|
189
|
+
|
190
|
+
gem 'tsikol', '~> 1.0'
|
191
|
+
```
|
192
|
+
|
193
|
+
## Usage
|
194
|
+
|
195
|
+
### Starting the Server
|
196
|
+
|
197
|
+
```bash
|
198
|
+
bundle install
|
199
|
+
ruby server.rb
|
200
|
+
```
|
201
|
+
|
202
|
+
### Example Requests
|
203
|
+
|
204
|
+
#### Basic Echo
|
205
|
+
|
206
|
+
```json
|
207
|
+
{
|
208
|
+
"jsonrpc": "2.0",
|
209
|
+
"id": 1,
|
210
|
+
"method": "tools/call",
|
211
|
+
"params": {
|
212
|
+
"name": "echo",
|
213
|
+
"arguments": {
|
214
|
+
"text": "Hello, World!"
|
215
|
+
}
|
216
|
+
}
|
217
|
+
}
|
218
|
+
```
|
219
|
+
|
220
|
+
Response:
|
221
|
+
```json
|
222
|
+
{
|
223
|
+
"jsonrpc": "2.0",
|
224
|
+
"id": 1,
|
225
|
+
"result": {
|
226
|
+
"content": [
|
227
|
+
{
|
228
|
+
"type": "text",
|
229
|
+
"text": "Hello, World!"
|
230
|
+
}
|
231
|
+
]
|
232
|
+
}
|
233
|
+
}
|
234
|
+
```
|
235
|
+
|
236
|
+
#### Echo with Transformation
|
237
|
+
|
238
|
+
```json
|
239
|
+
{
|
240
|
+
"jsonrpc": "2.0",
|
241
|
+
"id": 2,
|
242
|
+
"method": "tools/call",
|
243
|
+
"params": {
|
244
|
+
"name": "echo",
|
245
|
+
"arguments": {
|
246
|
+
"text": "Hello, World!",
|
247
|
+
"transform": "uppercase",
|
248
|
+
"repeat": 3
|
249
|
+
}
|
250
|
+
}
|
251
|
+
}
|
252
|
+
```
|
253
|
+
|
254
|
+
Response:
|
255
|
+
```json
|
256
|
+
{
|
257
|
+
"jsonrpc": "2.0",
|
258
|
+
"id": 2,
|
259
|
+
"result": {
|
260
|
+
"content": [
|
261
|
+
{
|
262
|
+
"type": "text",
|
263
|
+
"text": "HELLO, WORLD! HELLO, WORLD! HELLO, WORLD!"
|
264
|
+
}
|
265
|
+
]
|
266
|
+
}
|
267
|
+
}
|
268
|
+
```
|
269
|
+
|
270
|
+
#### ASCII Art Generation
|
271
|
+
|
272
|
+
```json
|
273
|
+
{
|
274
|
+
"jsonrpc": "2.0",
|
275
|
+
"id": 3,
|
276
|
+
"method": "tools/call",
|
277
|
+
"params": {
|
278
|
+
"name": "ascii_art",
|
279
|
+
"arguments": {
|
280
|
+
"type": "box",
|
281
|
+
"text": "Welcome",
|
282
|
+
"width": 30
|
283
|
+
}
|
284
|
+
}
|
285
|
+
}
|
286
|
+
```
|
287
|
+
|
288
|
+
Response:
|
289
|
+
```json
|
290
|
+
{
|
291
|
+
"jsonrpc": "2.0",
|
292
|
+
"id": 3,
|
293
|
+
"result": {
|
294
|
+
"content": [
|
295
|
+
{
|
296
|
+
"type": "text",
|
297
|
+
"text": "+----------------------------+\n| Welcome |\n+----------------------------+"
|
298
|
+
}
|
299
|
+
]
|
300
|
+
}
|
301
|
+
}
|
302
|
+
```
|
303
|
+
|
304
|
+
#### Server Status
|
305
|
+
|
306
|
+
```json
|
307
|
+
{
|
308
|
+
"jsonrpc": "2.0",
|
309
|
+
"id": 4,
|
310
|
+
"method": "resources/read",
|
311
|
+
"params": {
|
312
|
+
"uri": "status"
|
313
|
+
}
|
314
|
+
}
|
315
|
+
```
|
316
|
+
|
317
|
+
## Testing
|
318
|
+
|
319
|
+
### test_echo_server.rb
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
require 'minitest/autorun'
|
323
|
+
require 'tsikol/test_helpers'
|
324
|
+
require_relative 'server'
|
325
|
+
|
326
|
+
class EchoServerTest < Minitest::Test
|
327
|
+
include Tsikol::TestHelpers
|
328
|
+
|
329
|
+
def setup
|
330
|
+
@server = create_test_server
|
331
|
+
@client = TestClient.new(@server)
|
332
|
+
end
|
333
|
+
|
334
|
+
def test_basic_echo
|
335
|
+
response = @client.call_tool("echo", {
|
336
|
+
"text" => "Hello, Test!"
|
337
|
+
})
|
338
|
+
|
339
|
+
assert_successful_response(response)
|
340
|
+
assert_equal "Hello, Test!", response.dig(:result, :content, 0, :text)
|
341
|
+
end
|
342
|
+
|
343
|
+
def test_echo_with_uppercase
|
344
|
+
response = @client.call_tool("echo", {
|
345
|
+
"text" => "hello",
|
346
|
+
"transform" => "uppercase"
|
347
|
+
})
|
348
|
+
|
349
|
+
assert_successful_response(response)
|
350
|
+
assert_equal "HELLO", response.dig(:result, :content, 0, :text)
|
351
|
+
end
|
352
|
+
|
353
|
+
def test_echo_with_repeat
|
354
|
+
response = @client.call_tool("echo", {
|
355
|
+
"text" => "Hi",
|
356
|
+
"repeat" => 3
|
357
|
+
})
|
358
|
+
|
359
|
+
assert_successful_response(response)
|
360
|
+
assert_equal "Hi Hi Hi", response.dig(:result, :content, 0, :text)
|
361
|
+
end
|
362
|
+
|
363
|
+
def test_invalid_repeat_count
|
364
|
+
response = @client.call_tool("echo", {
|
365
|
+
"text" => "Test",
|
366
|
+
"repeat" => 20
|
367
|
+
})
|
368
|
+
|
369
|
+
assert_error_response(response)
|
370
|
+
assert_match /between 1 and 10/, response[:error][:message]
|
371
|
+
end
|
372
|
+
|
373
|
+
def test_ascii_art_box
|
374
|
+
response = @client.call_tool("ascii_art", {
|
375
|
+
"type" => "box",
|
376
|
+
"text" => "Test",
|
377
|
+
"width" => 20
|
378
|
+
})
|
379
|
+
|
380
|
+
assert_successful_response(response)
|
381
|
+
result = response.dig(:result, :content, 0, :text)
|
382
|
+
assert_match /\+\-+\+/, result
|
383
|
+
assert_match /Test/, result
|
384
|
+
end
|
385
|
+
|
386
|
+
def test_server_status
|
387
|
+
response = @client.read_resource("status")
|
388
|
+
|
389
|
+
assert_successful_response(response)
|
390
|
+
|
391
|
+
status = JSON.parse(response.dig(:result, :contents, 0, :text))
|
392
|
+
assert_equal "operational", status["status"]
|
393
|
+
assert_includes status["tools_available"], "echo"
|
394
|
+
assert_includes status["tools_available"], "ascii_art"
|
395
|
+
end
|
396
|
+
|
397
|
+
private
|
398
|
+
|
399
|
+
def create_test_server
|
400
|
+
Tsikol::Server.new(name: "test-echo-server") do
|
401
|
+
tool EchoTool
|
402
|
+
tool AsciiArtTool
|
403
|
+
|
404
|
+
resource "status" do
|
405
|
+
def read
|
406
|
+
{ status: "operational" }.to_json
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
end
|
411
|
+
end
|
412
|
+
```
|
413
|
+
|
414
|
+
Run tests:
|
415
|
+
```bash
|
416
|
+
ruby test_echo_server.rb
|
417
|
+
```
|
418
|
+
|
419
|
+
## Extensions
|
420
|
+
|
421
|
+
### Adding More Transformations
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
parameter :transform do
|
425
|
+
type :string
|
426
|
+
optional
|
427
|
+
default "none"
|
428
|
+
enum ["none", "uppercase", "lowercase", "reverse", "capitalize",
|
429
|
+
"rot13", "leetspeak", "titlecase", "snakecase", "camelcase"]
|
430
|
+
description "Transformation to apply"
|
431
|
+
end
|
432
|
+
|
433
|
+
def execute(text:, transform: "none", repeat: 1)
|
434
|
+
result = case transform
|
435
|
+
when "rot13"
|
436
|
+
text.tr('A-Za-z', 'N-ZA-Mn-za-m')
|
437
|
+
when "leetspeak"
|
438
|
+
text.gsub(/[aeioAEIO]/, 'a' => '4', 'e' => '3', 'i' => '1', 'o' => '0',
|
439
|
+
'A' => '4', 'E' => '3', 'I' => '1', 'O' => '0')
|
440
|
+
when "titlecase"
|
441
|
+
text.split.map { |word| word[0].upcase + word[1..-1].downcase }.join(" ")
|
442
|
+
when "snakecase"
|
443
|
+
text.downcase.gsub(/\s+/, '_')
|
444
|
+
when "camelcase"
|
445
|
+
text.split.map.with_index { |word, i|
|
446
|
+
i == 0 ? word.downcase : word.capitalize
|
447
|
+
}.join
|
448
|
+
# ... existing cases
|
449
|
+
end
|
450
|
+
|
451
|
+
# ... rest of method
|
452
|
+
end
|
453
|
+
```
|
454
|
+
|
455
|
+
### Adding Word Statistics
|
456
|
+
|
457
|
+
```ruby
|
458
|
+
class WordStatsTool < Tsikol::Tool
|
459
|
+
name "word_stats"
|
460
|
+
description "Analyze text and provide statistics"
|
461
|
+
|
462
|
+
parameter :text do
|
463
|
+
type :string
|
464
|
+
required
|
465
|
+
description "Text to analyze"
|
466
|
+
end
|
467
|
+
|
468
|
+
def execute(text:)
|
469
|
+
words = text.split(/\s+/)
|
470
|
+
|
471
|
+
{
|
472
|
+
character_count: text.length,
|
473
|
+
word_count: words.length,
|
474
|
+
line_count: text.lines.count,
|
475
|
+
average_word_length: words.map(&:length).sum.to_f / words.length,
|
476
|
+
longest_word: words.max_by(&:length),
|
477
|
+
most_common_word: words.group_by(&:itself)
|
478
|
+
.max_by { |_, v| v.length }
|
479
|
+
&.first
|
480
|
+
}.to_json
|
481
|
+
end
|
482
|
+
end
|
483
|
+
```
|
484
|
+
|
485
|
+
## Best Practices Demonstrated
|
486
|
+
|
487
|
+
1. **Parameter Validation** - Check bounds and provide clear errors
|
488
|
+
2. **Comprehensive Logging** - Log operations for debugging
|
489
|
+
3. **Error Handling** - Graceful handling of invalid inputs
|
490
|
+
4. **Testing** - Full test coverage including edge cases
|
491
|
+
5. **Documentation** - Clear examples and usage instructions
|
492
|
+
6. **Extensibility** - Easy to add new transformations
|
493
|
+
7. **Resource Monitoring** - Status endpoint for health checks
|
494
|
+
|
495
|
+
## Next Steps
|
496
|
+
|
497
|
+
- Add more text transformation options
|
498
|
+
- Implement caching for repeated requests
|
499
|
+
- Add rate limiting for production use
|
500
|
+
- Create a web UI for interactive testing
|
501
|
+
- Add persistent storage for echo history
|