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,660 @@
1
+ module Tsikol
2
+ class Server
3
+ include Lifecycle
4
+ include Health
5
+
6
+ attr_reader :name, :version
7
+
8
+ def initialize(name:, version: "1.0.0")
9
+ @name = name
10
+ @version = version
11
+ @tools = {}
12
+ @resources = {}
13
+ @prompts = {}
14
+ @tool_instances = {}
15
+ @resource_instances = {}
16
+ @prompt_instances = {}
17
+ @server_capabilities = {}
18
+ @log_level = :info
19
+ @stdio_transport = nil
20
+ @sampling_handler = nil
21
+ @middleware_stack = nil
22
+ @error_handler = ErrorHandler.new(self)
23
+
24
+ # Initialize health monitoring
25
+ initialize_health_monitoring
26
+ end
27
+
28
+ # Store transport reference for sending notifications
29
+ def set_transport(transport)
30
+ @stdio_transport = transport
31
+ end
32
+
33
+ def capabilities(&block)
34
+ instance_eval(&block) if block_given?
35
+ end
36
+
37
+ def logging(enabled = true)
38
+ @server_capabilities[:logging] = {} if enabled
39
+ end
40
+
41
+ def completion(enabled = true)
42
+ @server_capabilities[:completion] = {} if enabled
43
+ end
44
+
45
+ def sampling(enabled = true, &block)
46
+ @server_capabilities[:sampling] = {} if enabled
47
+ @sampling_handler = block if block_given?
48
+ end
49
+
50
+ # Alternative method name for clarity
51
+ def on_sampling(&block)
52
+ sampling(true, &block)
53
+ end
54
+
55
+ # Middleware support
56
+ def use(middleware_class, *args, **kwargs, &block)
57
+ @middleware_stack ||= MiddlewareStack.new(method(:handle_message_direct))
58
+ @middleware_stack.use(middleware_class, *args, **kwargs, &block)
59
+ end
60
+
61
+ # Register class-based instances
62
+ def register_tool_instance(tool_instance, name: nil)
63
+ tool_name = name || tool_instance.class.tool_name
64
+ @tool_instances[tool_name] = tool_instance
65
+
66
+ # Extract completions if any
67
+ tool_instance.parameters.each do |param_name, config|
68
+ if config[:completion]
69
+ @completions ||= {}
70
+ @completions["tool:#{tool_name}:#{param_name}"] = config[:completion]
71
+ @server_capabilities[:completion] ||= {}
72
+ end
73
+ end
74
+ end
75
+
76
+ def register_resource_instance(resource_instance, uri: nil)
77
+ resource_uri = uri || resource_instance.uri
78
+ @resource_instances[resource_uri] = resource_instance
79
+ end
80
+
81
+ def register_prompt_instance(prompt_instance, name: nil)
82
+ prompt_name = name || prompt_instance.class.prompt_name
83
+ @prompt_instances[prompt_name] = prompt_instance
84
+
85
+ # Extract completions if any
86
+ prompt_instance.arguments.each do |arg_name, config|
87
+ if config[:completion]
88
+ @completions ||= {}
89
+ @completions["prompt:#{prompt_name}:#{arg_name}"] = config[:completion]
90
+ @server_capabilities[:completion] ||= {}
91
+ end
92
+ end
93
+ end
94
+
95
+ def tool(name, description: nil, &block)
96
+ properties, required = extract_parameters_as_schema(block)
97
+
98
+ @tools[name] = {
99
+ description: description || "Tool: #{name}",
100
+ handler: block,
101
+ properties: properties,
102
+ required: required
103
+ }
104
+ end
105
+
106
+ def resource(uri, description: nil, &block)
107
+ @resources[uri] = {
108
+ description: description || "Resource: #{uri}",
109
+ handler: block
110
+ }
111
+ end
112
+
113
+ def prompt(name, description: nil, &block)
114
+ arguments = extract_parameters_as_arguments(block)
115
+
116
+ @prompts[name] = {
117
+ description: description || "Prompt: #{name}",
118
+ handler: block,
119
+ arguments: arguments
120
+ }
121
+ end
122
+
123
+ # Define completions for prompts or resources
124
+ def completion_for(type, name, argument = nil, &block)
125
+ @completions ||= {}
126
+ key = "#{type}:#{name}"
127
+ key += ":#{argument}" if argument
128
+ @completions[key] = block
129
+
130
+ # Auto-enable completion capability
131
+ @server_capabilities[:completion] ||= {}
132
+ end
133
+
134
+ # Logging DSL method
135
+ def log(level, message, data: nil, logger: nil)
136
+ # Auto-enable logging capability when first used
137
+ @server_capabilities[:logging] ||= {}
138
+ # Define log level hierarchy
139
+ levels = [:debug, :info, :notice, :warning, :error, :critical, :alert, :emergency]
140
+ current_level_index = levels.index(@log_level) || 1
141
+ message_level_index = levels.index(level) || 1
142
+
143
+ # Only log if message level is >= current log level
144
+ return if message_level_index < current_level_index
145
+
146
+ # Send notification if transport is available
147
+ if @stdio_transport
148
+ notification = {
149
+ jsonrpc: "2.0",
150
+ method: "notifications/message",
151
+ params: {
152
+ level: level.to_s,
153
+ message: message
154
+ }
155
+ }
156
+
157
+ notification[:params][:data] = data if data
158
+ notification[:params][:logger] = logger if logger
159
+
160
+ @stdio_transport.send_notification(notification)
161
+ end
162
+ end
163
+
164
+ def handle_message(raw_message)
165
+ message = JSON.parse(raw_message)
166
+
167
+ # Use middleware stack if configured
168
+ if @middleware_stack
169
+ @middleware_stack.call(message)
170
+ else
171
+ handle_message_direct(message)
172
+ end
173
+ rescue JSON::ParserError => e
174
+ # For parse errors, we can't send a response without an ID
175
+ log :error, "Parse error: #{e.message}"
176
+ nil
177
+ rescue => e
178
+ # Try to send error response if we have an ID
179
+ if message && message["id"]
180
+ error_response(message["id"], :internal_error, "Internal error: #{e.message}")
181
+ else
182
+ log :error, "Internal error without request ID: #{e.message}"
183
+ nil
184
+ end
185
+ end
186
+
187
+ # Direct message handler (without middleware)
188
+ def handle_message_direct(message)
189
+ start_time = Time.now
190
+ @metrics.increment(:requests_total)
191
+ @metrics.set(:active_requests, @metrics.get(:active_requests) + 1)
192
+
193
+ response = case message["method"]
194
+ when "initialize"
195
+ handle_initialize(message)
196
+ when "tools/list"
197
+ handle_tools_list(message)
198
+ when "tools/call"
199
+ handle_tool_call(message)
200
+ when "resources/list"
201
+ handle_resources_list(message)
202
+ when "resources/read"
203
+ handle_resource_read(message)
204
+ when "prompts/list"
205
+ handle_prompts_list(message)
206
+ when "prompts/get"
207
+ handle_prompt_get(message)
208
+ when "logging/setLevel"
209
+ handle_logging_set_level(message)
210
+ when "completion/complete"
211
+ handle_completion_complete(message)
212
+ when "sampling/createMessage"
213
+ handle_sampling_create_message(message)
214
+ when "ping"
215
+ handle_ping(message)
216
+ else
217
+ error_response(message["id"], :method_not_found, "Method not found: #{message['method']}")
218
+ end
219
+
220
+ # Track metrics
221
+ duration = (Time.now - start_time) * 1000 # Convert to ms
222
+ @metrics.record(:response_time, duration)
223
+ @metrics.set(:active_requests, @metrics.get(:active_requests) - 1)
224
+
225
+ if response && response[:error]
226
+ @metrics.increment(:errors_total)
227
+ end
228
+
229
+ response
230
+ end
231
+
232
+ private
233
+
234
+ # Extract parameters for tool schemas (properties format)
235
+ def extract_parameters_as_schema(block)
236
+ properties = {}
237
+ required = []
238
+
239
+ block.parameters.each do |type, param_name|
240
+ if type == :keyreq # required keyword argument
241
+ properties[param_name.to_s] = {
242
+ type: "string",
243
+ description: "Required parameter: #{param_name}"
244
+ }
245
+ required << param_name.to_s
246
+ elsif type == :key # optional keyword argument
247
+ properties[param_name.to_s] = {
248
+ type: "string",
249
+ description: "Optional parameter: #{param_name}"
250
+ }
251
+ end
252
+ end
253
+
254
+ [properties, required]
255
+ end
256
+
257
+ # Extract parameters for prompt arguments format
258
+ def extract_parameters_as_arguments(block)
259
+ block.parameters.map do |type, param_name|
260
+ if type == :keyreq || type == :key
261
+ {
262
+ name: param_name.to_s,
263
+ description: "#{type == :keyreq ? 'Required' : 'Optional'} parameter: #{param_name}",
264
+ required: type == :keyreq
265
+ }
266
+ end
267
+ end.compact
268
+ end
269
+
270
+ def handle_initialize(message)
271
+ # Build capabilities based on what's available
272
+ capabilities = {}
273
+ capabilities[:tools] = {} if @tools.any?
274
+ capabilities[:resources] = {} if @resources.any?
275
+ capabilities[:prompts] = {} if @prompts.any?
276
+
277
+ # Add declared server capabilities
278
+ capabilities.merge!(@server_capabilities)
279
+
280
+ {
281
+ jsonrpc: "2.0",
282
+ id: message["id"],
283
+ result: {
284
+ protocolVersion: Tsikol::PROTOCOL_VERSION,
285
+ capabilities: capabilities,
286
+ serverInfo: {
287
+ name: @name,
288
+ version: @version
289
+ }
290
+ }
291
+ }
292
+ end
293
+
294
+ def handle_tools_list(message)
295
+ # Combine both DSL tools and class-based tools
296
+ tools = []
297
+
298
+ # DSL-style tools
299
+ @tools.each do |name, tool|
300
+ tools << {
301
+ name: name,
302
+ description: tool[:description],
303
+ inputSchema: {
304
+ type: "object",
305
+ properties: tool[:properties],
306
+ required: tool[:required]
307
+ }
308
+ }
309
+ end
310
+
311
+ # Class-based tools
312
+ @tool_instances.each do |name, instance|
313
+ tools << instance.to_mcp
314
+ end
315
+
316
+ {
317
+ jsonrpc: "2.0",
318
+ id: message["id"],
319
+ result: { tools: tools }
320
+ }
321
+ end
322
+
323
+ def handle_tool_call(message)
324
+ tool_name = message.dig("params", "name")
325
+ arguments = message.dig("params", "arguments") || {}
326
+
327
+ # Run before tool hook
328
+ run_before_tool_hook(tool_name, arguments) if respond_to?(:run_before_tool_hook)
329
+
330
+ # Track tool metrics
331
+ @metrics.increment("tools:#{tool_name}:calls")
332
+ start_time = Time.now
333
+
334
+ # Try class-based tool first
335
+ if (tool_instance = @tool_instances[tool_name])
336
+ begin
337
+ kwargs = arguments.transform_keys(&:to_sym)
338
+ result = @error_handler.wrap_tool_execution(tool_name) do
339
+ tool_instance.execute(**kwargs)
340
+ end
341
+ rescue => e
342
+ @metrics.increment("tools:#{tool_name}:errors")
343
+ return error_response(message["id"], :internal_error, "Tool error: #{e.message}")
344
+ end
345
+ elsif (tool = @tools[tool_name])
346
+ # Fall back to DSL-style tool
347
+ begin
348
+ result = @error_handler.wrap_tool_execution(tool_name) do
349
+ if tool[:handler].parameters.any?
350
+ kwargs = arguments.transform_keys(&:to_sym)
351
+ tool[:handler].call(**kwargs)
352
+ else
353
+ tool[:handler].call
354
+ end
355
+ end
356
+ rescue => e
357
+ @metrics.increment("tools:#{tool_name}:errors")
358
+ return error_response(message["id"], :internal_error, "Tool error: #{e.message}")
359
+ end
360
+ else
361
+ return error_response(message["id"], :invalid_params, "Unknown tool: #{tool_name}")
362
+ end
363
+
364
+ # Track execution time
365
+ duration = (Time.now - start_time) * 1000
366
+ @metrics.record("tools:#{tool_name}:duration", duration)
367
+
368
+ # Run after tool hook
369
+ run_after_tool_hook(tool_name, arguments, result) if respond_to?(:run_after_tool_hook)
370
+
371
+ {
372
+ jsonrpc: "2.0",
373
+ id: message["id"],
374
+ result: {
375
+ content: [
376
+ {
377
+ type: "text",
378
+ text: result.to_s
379
+ }
380
+ ]
381
+ }
382
+ }
383
+ end
384
+
385
+ def handle_resources_list(message)
386
+ resources = []
387
+
388
+ # DSL-style resources
389
+ @resources.each do |uri, resource|
390
+ resources << {
391
+ uri: uri,
392
+ name: uri,
393
+ description: resource[:description]
394
+ }
395
+ end
396
+
397
+ # Class-based resources
398
+ @resource_instances.each do |uri, instance|
399
+ resources << instance.to_mcp
400
+ end
401
+
402
+ {
403
+ jsonrpc: "2.0",
404
+ id: message["id"],
405
+ result: { resources: resources }
406
+ }
407
+ end
408
+
409
+ def handle_resource_read(message)
410
+ uri = message.dig("params", "uri")
411
+
412
+ # Try class-based resource first
413
+ if (resource_instance = @resource_instances[uri])
414
+ content = resource_instance.read
415
+ elsif (resource = @resources[uri])
416
+ # Fall back to DSL-style resource
417
+ content = resource[:handler].call
418
+ else
419
+ return error_response(message["id"], :invalid_params, "Unknown resource: #{uri}")
420
+ end
421
+
422
+ {
423
+ jsonrpc: "2.0",
424
+ id: message["id"],
425
+ result: {
426
+ contents: [
427
+ {
428
+ uri: uri,
429
+ mimeType: "text/plain",
430
+ text: content.to_s
431
+ }
432
+ ]
433
+ }
434
+ }
435
+ end
436
+
437
+ def handle_prompts_list(message)
438
+ prompts = []
439
+
440
+ # DSL-style prompts
441
+ @prompts.each do |name, prompt|
442
+ prompts << {
443
+ name: name,
444
+ description: prompt[:description],
445
+ arguments: prompt[:arguments]
446
+ }
447
+ end
448
+
449
+ # Class-based prompts
450
+ @prompt_instances.each do |name, instance|
451
+ prompts << instance.to_mcp
452
+ end
453
+
454
+ {
455
+ jsonrpc: "2.0",
456
+ id: message["id"],
457
+ result: { prompts: prompts }
458
+ }
459
+ end
460
+
461
+ def handle_prompt_get(message)
462
+ name = message.dig("params", "name")
463
+ arguments = message.dig("params", "arguments") || {}
464
+
465
+ # Try class-based prompt first
466
+ if (prompt_instance = @prompt_instances[name])
467
+ kwargs = arguments.transform_keys(&:to_sym)
468
+ messages = prompt_instance.get_messages(**kwargs)
469
+
470
+ return {
471
+ jsonrpc: "2.0",
472
+ id: message["id"],
473
+ result: {
474
+ messages: messages
475
+ }
476
+ }
477
+ elsif (prompt = @prompts[name])
478
+ # Fall back to DSL-style prompt
479
+ kwargs = arguments.transform_keys(&:to_sym)
480
+ text = prompt[:handler].call(**kwargs)
481
+ else
482
+ return error_response(message["id"], :invalid_params, "Unknown prompt: #{name}")
483
+ end
484
+
485
+ {
486
+ jsonrpc: "2.0",
487
+ id: message["id"],
488
+ result: {
489
+ messages: [
490
+ {
491
+ role: "user",
492
+ content: {
493
+ type: "text",
494
+ text: text
495
+ }
496
+ }
497
+ ]
498
+ }
499
+ }
500
+ end
501
+
502
+ def handle_logging_set_level(message)
503
+ level = message.dig("params", "level")
504
+
505
+ # Validate level
506
+ valid_levels = %w[debug info notice warning error critical alert emergency]
507
+ unless valid_levels.include?(level)
508
+ return error_response(message["id"], :invalid_params, "Invalid log level: #{level}")
509
+ end
510
+
511
+ @log_level = level.to_sym
512
+
513
+ {
514
+ jsonrpc: "2.0",
515
+ id: message["id"],
516
+ result: {}
517
+ }
518
+ end
519
+
520
+ def handle_completion_complete(message)
521
+ params = message["params"] || {}
522
+ ref = params["ref"]
523
+ argument = params["argument"]
524
+
525
+ unless ref
526
+ return error_response(message["id"], :invalid_params, "Missing ref parameter")
527
+ end
528
+
529
+ # Build completion key
530
+ completion_key = case ref["type"]
531
+ when "ref/prompt"
532
+ if argument
533
+ "prompt:#{ref['name']}:#{argument['name']}"
534
+ else
535
+ "prompt:#{ref['name']}"
536
+ end
537
+ when "ref/resource"
538
+ "resource:#{ref['uri']}"
539
+ else
540
+ return error_response(message["id"], :invalid_params, "Unknown ref type: #{ref['type']}")
541
+ end
542
+
543
+ # Get completion handler
544
+ handler = @completions&.[](completion_key)
545
+
546
+ # Get completions
547
+ values = if handler
548
+ begin
549
+ # Call handler with current value
550
+ current_value = argument&.dig("value") || ""
551
+ result = handler.call(current_value)
552
+ Array(result)
553
+ rescue => e
554
+ log :error, "Completion handler error", data: { error: e.message }
555
+ []
556
+ end
557
+ else
558
+ []
559
+ end
560
+
561
+ # Limit to 100 results as per spec
562
+ values = values.first(100)
563
+
564
+ {
565
+ jsonrpc: "2.0",
566
+ id: message["id"],
567
+ result: {
568
+ completion: {
569
+ values: values,
570
+ total: values.size,
571
+ hasMore: false
572
+ }
573
+ }
574
+ }
575
+ end
576
+
577
+ def handle_sampling_create_message(message)
578
+ params = message["params"] || {}
579
+
580
+ # Validate required parameters
581
+ messages = params["messages"]
582
+ unless messages && messages.is_a?(Array)
583
+ return error_response(message["id"], :invalid_params, "Missing or invalid messages parameter")
584
+ end
585
+
586
+ # Extract parameters
587
+ model_preferences = params["modelPreferences"] || {}
588
+ system_prompt = params["systemPrompt"]
589
+ max_tokens = params["maxTokens"]
590
+ include_context = params["includeContext"] || "none"
591
+ temperature = params["temperature"]
592
+ metadata = params["metadata"] || {}
593
+
594
+ # Build sampling request data
595
+ sampling_data = {
596
+ messages: messages,
597
+ model_preferences: model_preferences,
598
+ system_prompt: system_prompt,
599
+ max_tokens: max_tokens,
600
+ temperature: temperature,
601
+ metadata: metadata
602
+ }
603
+
604
+ # Call registered sampling handler if any
605
+ if @sampling_handler
606
+ begin
607
+ result = @sampling_handler.call(sampling_data)
608
+
609
+ # Result should have role and content
610
+ unless result.is_a?(Hash) && result[:role] && result[:content]
611
+ raise "Sampling handler must return hash with :role and :content"
612
+ end
613
+
614
+ {
615
+ jsonrpc: "2.0",
616
+ id: message["id"],
617
+ result: result
618
+ }
619
+ rescue => e
620
+ error_response(message["id"], :internal_error, "Sampling error: #{e.message}")
621
+ end
622
+ else
623
+ # No sampling handler registered
624
+ error_response(message["id"], :internal_error, "No sampling handler registered. Call 'on_sampling' to register a handler.")
625
+ end
626
+ end
627
+
628
+ def handle_ping(message)
629
+ # Simple ping/pong response
630
+ {
631
+ jsonrpc: "2.0",
632
+ id: message["id"],
633
+ result: {}
634
+ }
635
+ end
636
+
637
+ def error_response(id, code_key, message)
638
+ # If no ID, we can't send a proper error response
639
+ return nil if id.nil?
640
+
641
+ code = case code_key
642
+ when :parse_error then Errors::PARSE_ERROR
643
+ when :invalid_request then Errors::INVALID_REQUEST
644
+ when :method_not_found then Errors::METHOD_NOT_FOUND
645
+ when :invalid_params then Errors::INVALID_PARAMS
646
+ when :internal_error then Errors::INTERNAL_ERROR
647
+ else Errors::INTERNAL_ERROR
648
+ end
649
+
650
+ {
651
+ jsonrpc: "2.0",
652
+ id: id,
653
+ error: {
654
+ code: code,
655
+ message: message
656
+ }
657
+ }
658
+ end
659
+ end
660
+ end