datagrout-conduit 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.
@@ -0,0 +1,601 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "logger"
5
+
6
+ module DatagroutConduit
7
+ # Main Conduit client. Connects to remote MCP / JSONRPC servers over HTTP,
8
+ # sends requests, and parses responses. This is purely a client library —
9
+ # it does NOT run a server or accept connections.
10
+ class Client
11
+ PROTOCOL_VERSION = "2025-03-26"
12
+ CLIENT_NAME = "datagrout-conduit-ruby"
13
+
14
+ attr_reader :transport, :server_info, :use_intelligent_interface
15
+
16
+ def initialize(url:, auth: {}, transport: :mcp, identity: nil, identity_dir: nil,
17
+ use_intelligent_interface: nil, max_retries: 3, logger: nil, disable_mtls: false)
18
+ @url = url
19
+ @auth = auth
20
+ @transport_mode = transport
21
+ @identity = identity
22
+ @identity_dir = identity_dir
23
+ @disable_mtls = disable_mtls
24
+ @max_retries = max_retries
25
+ @initialized = false
26
+ @server_info = nil
27
+ @logger = logger || default_logger
28
+ @is_dg = DatagroutConduit.dg_url?(url)
29
+ @dg_warned = false
30
+
31
+ @use_intelligent_interface = if use_intelligent_interface.nil?
32
+ @is_dg
33
+ else
34
+ use_intelligent_interface
35
+ end
36
+
37
+ resolve_identity!
38
+ @transport = build_transport
39
+ end
40
+
41
+ # Bootstrap an mTLS identity: discover existing or register a new one.
42
+ #
43
+ # Checks the auto-discovery chain first. If an existing identity is found
44
+ # and not near expiry it is used as-is. Otherwise, a new keypair is
45
+ # generated, registered with DataGrout using the provided bearer token,
46
+ # saved to the identity directory, and loaded as the active identity.
47
+ #
48
+ # After the first successful bootstrap the identity is persisted locally
49
+ # and auto-discovered on subsequent runs — no token or API key is needed.
50
+ def self.bootstrap_identity(url:, auth_token:, name: "conduit-client", identity_dir: nil)
51
+ dir = identity_dir || Registration.default_identity_dir || File.join(Dir.home, ".conduit")
52
+
53
+ identity = Identity.try_discover(override_dir: dir)
54
+ if identity && !identity.needs_rotation?
55
+ return new(url: url, identity: identity)
56
+ end
57
+
58
+ private_pem, public_pem = Registration.generate_keypair
59
+ reg = Registration.register_identity(
60
+ public_pem,
61
+ auth_token: auth_token,
62
+ name: name
63
+ )
64
+ Registration.save_identity(reg.cert_pem, private_pem, dir, ca_pem: reg.ca_cert_pem)
65
+
66
+ ca_path = reg.ca_cert_pem ? File.join(dir, "ca.pem") : nil
67
+ identity = Identity.from_paths(
68
+ File.join(dir, "identity.pem"),
69
+ File.join(dir, "identity_key.pem"),
70
+ ca_path: ca_path
71
+ )
72
+
73
+ new(url: url, identity: identity)
74
+ end
75
+
76
+ # Bootstrap an mTLS identity using OAuth 2.1 +client_credentials+.
77
+ #
78
+ # Same flow as {.bootstrap_identity} but obtains the bearer token via
79
+ # OAuth client_credentials exchange first.
80
+ def self.bootstrap_identity_oauth(url:, client_id:, client_secret:, name: "conduit-client", identity_dir: nil)
81
+ provider = OAuth::TokenProvider.new(
82
+ client_id: client_id,
83
+ client_secret: client_secret,
84
+ token_endpoint: OAuth::TokenProvider.derive_token_endpoint(url)
85
+ )
86
+ token = provider.get_token
87
+ bootstrap_identity(url: url, auth_token: token, name: name, identity_dir: identity_dir)
88
+ end
89
+
90
+ def connect
91
+ @transport.connect
92
+
93
+ params = {
94
+ "protocolVersion" => PROTOCOL_VERSION,
95
+ "clientInfo" => { "name" => CLIENT_NAME, "version" => DatagroutConduit::VERSION },
96
+ "capabilities" => { "tools" => {} }
97
+ }
98
+
99
+ response = @transport.send_request("initialize", params)
100
+
101
+ if response.is_a?(Hash) && response["result"]
102
+ result = response["result"]
103
+ @server_info = result["serverInfo"]
104
+ end
105
+
106
+ @transport.send_request("notifications/initialized", nil, id: nil)
107
+ @initialized = true
108
+ self
109
+ end
110
+
111
+ def disconnect
112
+ @transport.disconnect
113
+ @initialized = false
114
+ self
115
+ end
116
+
117
+ def initialized?
118
+ @initialized
119
+ end
120
+
121
+ # ================================================================
122
+ # Standard MCP Methods
123
+ # ================================================================
124
+
125
+ def list_tools
126
+ ensure_initialized!
127
+
128
+ all_tools = []
129
+ cursor = nil
130
+
131
+ loop do
132
+ params = {}
133
+ params["cursor"] = cursor if cursor
134
+
135
+ response = send_with_retry("tools/list", params)
136
+ result = response.is_a?(Hash) ? (response["result"] || response) : response
137
+
138
+ tools_data = result["tools"] || []
139
+ tools_data.each { |t| all_tools << Tool.from_hash(t) }
140
+
141
+ cursor = result["nextCursor"] || result["next_cursor"]
142
+ break unless cursor
143
+ end
144
+
145
+ if @use_intelligent_interface
146
+ all_tools.reject! { |t| t.name.include?("@") }
147
+ end
148
+
149
+ all_tools
150
+ end
151
+
152
+ def call_tool(name, arguments = {})
153
+ ensure_initialized!
154
+
155
+ params = { "name" => name.to_s, "arguments" => normalize_hash(arguments) }
156
+ response = send_with_retry("tools/call", params)
157
+ result = response.is_a?(Hash) ? (response["result"] || response) : response
158
+ unwrap_content(result)
159
+ end
160
+
161
+ def list_resources
162
+ ensure_initialized!
163
+
164
+ response = send_with_retry("resources/list", {})
165
+ result = response.is_a?(Hash) ? (response["result"] || response) : response
166
+ result["resources"] || []
167
+ end
168
+
169
+ def read_resource(uri)
170
+ ensure_initialized!
171
+
172
+ response = send_with_retry("resources/read", { "uri" => uri.to_s })
173
+ result = response.is_a?(Hash) ? (response["result"] || response) : response
174
+ result["contents"] || []
175
+ end
176
+
177
+ def list_prompts
178
+ ensure_initialized!
179
+
180
+ response = send_with_retry("prompts/list", {})
181
+ result = response.is_a?(Hash) ? (response["result"] || response) : response
182
+ result["prompts"] || []
183
+ end
184
+
185
+ def get_prompt(name, arguments = {})
186
+ ensure_initialized!
187
+
188
+ params = { "name" => name.to_s }
189
+ params["arguments"] = normalize_hash(arguments) unless arguments.nil? || arguments.empty?
190
+
191
+ response = send_with_retry("prompts/get", params)
192
+ result = response.is_a?(Hash) ? (response["result"] || response) : response
193
+ result["messages"] || []
194
+ end
195
+
196
+ # ================================================================
197
+ # DataGrout Extensions
198
+ # ================================================================
199
+
200
+ # Semantic discovery — find tools by natural language goal or query.
201
+ def discover(goal: nil, query: nil, limit: 10, min_score: 0.0,
202
+ integrations: [], servers: [])
203
+ warn_if_not_dg("discover")
204
+ ensure_initialized!
205
+
206
+ params = { "limit" => limit, "min_score" => min_score }
207
+ params["goal"] = goal if goal
208
+ params["query"] = query if query
209
+ params["integrations"] = integrations unless integrations.empty?
210
+ params["servers"] = servers unless servers.empty?
211
+
212
+ result = call_dg_tool("data-grout/discovery.discover", params)
213
+ DiscoverResult.from_hash(result)
214
+ end
215
+
216
+ # Execute a tool call through the DataGrout intelligent interface.
217
+ def perform(tool_name, arguments = {}, demux: false, demux_mode: nil)
218
+ warn_if_not_dg("perform")
219
+ ensure_initialized!
220
+
221
+ params = { "tool" => tool_name.to_s, "args" => normalize_hash(arguments) }
222
+ params["demux"] = demux if demux
223
+ params["demux_mode"] = demux_mode if demux_mode
224
+
225
+ call_dg_tool("data-grout/discovery.perform", params)
226
+ end
227
+
228
+ # Start or continue a guided workflow.
229
+ def guide(goal: nil, session_id: nil, choice: nil)
230
+ warn_if_not_dg("guide")
231
+ ensure_initialized!
232
+
233
+ params = {}
234
+ params["goal"] = goal if goal
235
+ params["session_id"] = session_id if session_id
236
+ params["choice"] = choice if choice
237
+
238
+ result = call_dg_tool("data-grout/discovery.guide", params)
239
+ GuidedSession.new(self, GuideState.from_hash(result))
240
+ end
241
+
242
+ # Execute a multi-step workflow plan.
243
+ def flow_into(plan, validate_ctc: true, save_as_skill: false, input_data: nil)
244
+ warn_if_not_dg("flow_into")
245
+ ensure_initialized!
246
+
247
+ params = {
248
+ "plan" => plan,
249
+ "validate_ctc" => validate_ctc,
250
+ "save_as_skill" => save_as_skill
251
+ }
252
+ params["input_data"] = input_data if input_data
253
+
254
+ call_dg_tool("data-grout/flow.into", params)
255
+ end
256
+
257
+ # Semantic type transformation via Prism.
258
+ def prism_focus(data:, source_type:, target_type:, source_annotations: nil, target_annotations: nil, context: nil)
259
+ params = { "data" => data, "source_type" => source_type, "target_type" => target_type }
260
+ params["source_annotations"] = source_annotations if source_annotations
261
+ params["target_annotations"] = target_annotations if target_annotations
262
+ params["context"] = context if context
263
+ warn_if_not_dg("prism_focus")
264
+ ensure_initialized!
265
+ call_dg_tool("data-grout/prism.focus", params)
266
+ end
267
+
268
+ # Semantic discovery plan — return a ranked list of tools for a goal.
269
+ # At least one of `goal:` or `query:` must be provided.
270
+ def plan(goal: nil, query: nil, **opts)
271
+ raise ArgumentError, "plan() requires at least one of goal: or query:" unless goal || query
272
+
273
+ params = {}
274
+ params["goal"] = goal if goal
275
+ params["query"] = query if query
276
+ params["server"] = opts[:server] if opts[:server]
277
+ params["k"] = opts[:k] if opts[:k]
278
+ params["policy"] = opts[:policy] if opts[:policy]
279
+ params["have"] = opts[:have] if opts[:have]
280
+ params["return_call_handles"] = opts[:return_call_handles] if opts.key?(:return_call_handles)
281
+ params["expose_virtual_skills"] = opts[:expose_virtual_skills] if opts.key?(:expose_virtual_skills)
282
+ params["model_overrides"] = opts[:model_overrides] if opts[:model_overrides]
283
+ warn_if_not_dg("plan")
284
+ ensure_initialized!
285
+ call_dg_tool("data-grout/discovery.plan", params)
286
+ end
287
+
288
+ # Transform / reshape a payload via Prism.
289
+ def refract(goal:, payload:, **opts)
290
+ params = { "goal" => goal, "payload" => payload }
291
+ params["verbose"] = opts[:verbose] if opts.key?(:verbose)
292
+ params["chart"] = opts[:chart] if opts.key?(:chart)
293
+ warn_if_not_dg("refract")
294
+ ensure_initialized!
295
+ call_dg_tool("data-grout/prism.refract", params)
296
+ end
297
+
298
+ # Generate a chart/visual from a payload via Prism.
299
+ def chart(goal:, payload:, **opts)
300
+ params = { "goal" => goal, "payload" => payload }
301
+ params["format"] = opts[:format] if opts[:format]
302
+ params["chart_type"] = opts[:chart_type] if opts[:chart_type]
303
+ params["title"] = opts[:title] if opts[:title]
304
+ params["x_label"] = opts[:x_label] if opts[:x_label]
305
+ params["y_label"] = opts[:y_label] if opts[:y_label]
306
+ params["width"] = opts[:width] if opts[:width]
307
+ params["height"] = opts[:height] if opts[:height]
308
+ warn_if_not_dg("chart")
309
+ ensure_initialized!
310
+ call_dg_tool("data-grout/prism.chart", params)
311
+ end
312
+
313
+ # Generate a document toward a natural-language goal.
314
+ def render(goal:, payload: nil, format: "markdown", sections: nil, **opts)
315
+ params = { "goal" => goal, "format" => format }.merge(normalize_hash(opts))
316
+ params["payload"] = payload if payload
317
+ params["sections"] = sections if sections
318
+ warn_if_not_dg("render")
319
+ ensure_initialized!
320
+ call_dg_tool("data-grout/prism.render", params)
321
+ end
322
+
323
+ # Convert content to another format (no LLM). Supports csv, xlsx, pdf, json, etc.
324
+ def export(content:, format:, style: nil, metadata: nil, **opts)
325
+ params = { "content" => content, "format" => format }.merge(normalize_hash(opts))
326
+ params["style"] = style if style
327
+ params["metadata"] = metadata if metadata
328
+ warn_if_not_dg("export")
329
+ ensure_initialized!
330
+ call_dg_tool("data-grout/prism.export", params)
331
+ end
332
+
333
+ # Pause workflow for human approval.
334
+ def request_approval(action:, details: nil, reason: nil, context: nil, **opts)
335
+ params = { "action" => action }.merge(normalize_hash(opts))
336
+ params["details"] = details if details
337
+ params["reason"] = reason if reason
338
+ params["context"] = context if context
339
+ warn_if_not_dg("request_approval")
340
+ ensure_initialized!
341
+ call_dg_tool("data-grout/flow.request-approval", params)
342
+ end
343
+
344
+ # Request user clarification for missing fields.
345
+ def request_feedback(missing_fields:, reason:, current_data: nil, suggestions: nil, context: nil, **opts)
346
+ params = { "missing_fields" => missing_fields, "reason" => reason }.merge(normalize_hash(opts))
347
+ params["current_data"] = current_data if current_data
348
+ params["suggestions"] = suggestions if suggestions
349
+ params["context"] = context if context
350
+ warn_if_not_dg("request_feedback")
351
+ ensure_initialized!
352
+ call_dg_tool("data-grout/flow.request-feedback", params)
353
+ end
354
+
355
+ # List recent tool executions for the current server.
356
+ def execution_history(limit: 50, offset: 0, status: nil, refractions_only: false, **opts)
357
+ params = { "limit" => limit, "offset" => offset, "refractions_only" => refractions_only }.merge(normalize_hash(opts))
358
+ params["status"] = status if status
359
+ warn_if_not_dg("execution_history")
360
+ ensure_initialized!
361
+ call_dg_tool("data-grout/inspect.execution-history", params)
362
+ end
363
+
364
+ # Get details for a specific execution.
365
+ def execution_details(execution_id:)
366
+ params = { "execution_id" => execution_id }
367
+ warn_if_not_dg("execution_details")
368
+ ensure_initialized!
369
+ call_dg_tool("data-grout/inspect.execution-details", params)
370
+ end
371
+
372
+ # ================================================================
373
+ # Logic Cell Methods
374
+ # ================================================================
375
+
376
+ # Assert a fact or statement into the logic cell.
377
+ def remember(statement: nil, facts: nil, tag: nil)
378
+ raise ArgumentError, "must provide statement or facts" unless statement || facts
379
+
380
+ params = {}
381
+ params["statement"] = statement if statement
382
+ params["facts"] = facts if facts
383
+ params["tag"] = tag if tag
384
+ warn_if_not_dg("remember")
385
+ ensure_initialized!
386
+ call_dg_tool("data-grout/logic.remember", params)
387
+ end
388
+
389
+ # Query the logic cell by question or patterns.
390
+ def query_cell(question: nil, patterns: nil, limit: nil)
391
+ raise ArgumentError, "must provide question or patterns" unless question || patterns
392
+
393
+ params = {}
394
+ params["question"] = question if question
395
+ params["patterns"] = patterns if patterns
396
+ params["limit"] = limit if limit
397
+ warn_if_not_dg("query_cell")
398
+ ensure_initialized!
399
+ call_dg_tool("data-grout/logic.query", params)
400
+ end
401
+
402
+ # Remove facts from the logic cell by handles or pattern.
403
+ def forget(handles: nil, pattern: nil)
404
+ raise ArgumentError, "must provide handles or pattern" unless handles || pattern
405
+
406
+ params = {}
407
+ params["handles"] = handles if handles
408
+ params["pattern"] = pattern if pattern
409
+ warn_if_not_dg("forget")
410
+ ensure_initialized!
411
+ call_dg_tool("data-grout/logic.forget", params)
412
+ end
413
+
414
+ # Assert a constraint rule into the logic cell.
415
+ def constrain(rule:, tag: nil)
416
+ params = { "rule" => rule }
417
+ params["tag"] = tag if tag
418
+ warn_if_not_dg("constrain")
419
+ ensure_initialized!
420
+ call_dg_tool("data-grout/logic.constrain", params)
421
+ end
422
+
423
+ # Reflect on known facts about an entity.
424
+ def reflect(entity: nil, summary_only: false)
425
+ params = { "summary_only" => summary_only }
426
+ params["entity"] = entity if entity
427
+ warn_if_not_dg("reflect")
428
+ ensure_initialized!
429
+ call_dg_tool("data-grout/logic.reflect", params)
430
+ end
431
+
432
+ # Call any DataGrout first-party tool by short name.
433
+ # e.g. client.dg("prism.render", { payload: data, goal: "summary" })
434
+ def dg(tool_short_name, params = {})
435
+ ensure_initialized!
436
+ call_dg_tool("data-grout/#{tool_short_name}", params)
437
+ end
438
+
439
+ # Estimate cost before execution.
440
+ def estimate_cost(tool_name, arguments = {})
441
+ ensure_initialized!
442
+
443
+ args = normalize_hash(arguments).merge("estimate_only" => true)
444
+ call_dg_tool(tool_name.to_s, args)
445
+ end
446
+
447
+ private
448
+
449
+ def ensure_initialized!
450
+ raise NotInitializedError unless @initialized
451
+ end
452
+
453
+ # Route a DataGrout first-party tool call through the standard MCP
454
+ # `tools/call` path. Both the MCP endpoint (/mcp) and the JSONRPC
455
+ # endpoint (/rpc) dispatch on `tools/call`; the tool name goes in
456
+ # `params["name"]` and the tool arguments in `params["arguments"]`.
457
+ # The server resolves both versioned and unversioned tool names.
458
+ def call_dg_tool(tool_name, arguments)
459
+ params = { "name" => tool_name.to_s, "arguments" => normalize_hash(arguments) }
460
+ response = send_with_retry("tools/call", params)
461
+ raw = response.is_a?(Hash) ? (response["result"] || response) : response
462
+ unwrap_content(raw)
463
+ end
464
+
465
+ # Unwrap the MCP content envelope that wraps tool results from both MCP and
466
+ # JSONRPC transports: {"content" => [{"type" => "text", "text" => "<json>"}]}
467
+ def unwrap_content(raw)
468
+ return raw unless raw.is_a?(Hash)
469
+
470
+ content = raw["content"]
471
+ return raw unless content.is_a?(Array) && !content.empty?
472
+
473
+ first = content.first
474
+ return raw unless first.is_a?(Hash) && first["text"].is_a?(String)
475
+
476
+ begin
477
+ JSON.parse(first["text"])
478
+ rescue JSON::ParserError
479
+ { "text" => first["text"] }
480
+ end
481
+ end
482
+
483
+ def send_with_retry(method, params)
484
+ retries = @max_retries
485
+
486
+ loop do
487
+ response = @transport.send_request(method, params)
488
+ return response
489
+ rescue McpError => e
490
+ if not_initialized_error?(e) && retries > 0
491
+ @logger.warn { "Server not initialized, retrying (#{retries} left)..." }
492
+ connect
493
+ retries -= 1
494
+ sleep 0.5
495
+ else
496
+ raise
497
+ end
498
+ end
499
+ end
500
+
501
+ def not_initialized_error?(error)
502
+ return true if error.code == McpCodes::NOT_INITIALIZED
503
+ return true if error.message.include?("not initialized")
504
+
505
+ false
506
+ end
507
+
508
+ def build_transport
509
+ case @transport_mode
510
+ when :mcp, "mcp"
511
+ Transport::Mcp.new(url: @url, auth: @auth, identity: @identity)
512
+ when :jsonrpc, "jsonrpc"
513
+ # When the user passes an MCP URL (ending in /mcp) and selects JSONRPC
514
+ # transport, transparently rewrite the path to the DG JSONRPC endpoint.
515
+ rpc_url = @url.end_with?("/mcp") ? @url.sub(%r{/mcp$}, "/rpc") : @url
516
+ Transport::JsonRpc.new(url: rpc_url, auth: @auth, identity: @identity)
517
+ else
518
+ raise ConfigError, "Unknown transport: #{@transport_mode}. Use :mcp or :jsonrpc."
519
+ end
520
+ end
521
+
522
+ def resolve_identity!
523
+ return if @identity
524
+ return if @disable_mtls
525
+ return unless @is_dg
526
+
527
+ @identity = Identity.try_discover(override_dir: @identity_dir)
528
+ end
529
+
530
+ def warn_if_not_dg(method_name)
531
+ return if @is_dg
532
+ return if @dg_warned
533
+
534
+ @dg_warned = true
535
+ @logger.warn do
536
+ "[conduit] `#{method_name}` is a DataGrout-specific extension. " \
537
+ "The connected server may not support it. " \
538
+ "Standard MCP methods (list_tools, call_tool, ...) work on any server."
539
+ end
540
+ end
541
+
542
+ def normalize_hash(hash)
543
+ return {} if hash.nil?
544
+
545
+ hash.each_with_object({}) do |(k, v), memo|
546
+ memo[k.to_s] = v
547
+ end
548
+ end
549
+
550
+ def default_logger
551
+ logger = Logger.new($stderr)
552
+ logger.level = Logger::WARN
553
+ logger.progname = "conduit"
554
+ logger
555
+ end
556
+ end
557
+
558
+ # Wrapper around an active guided workflow session.
559
+ class GuidedSession
560
+ attr_reader :state
561
+
562
+ def initialize(client, state)
563
+ @client = client
564
+ @state = state
565
+ end
566
+
567
+ def session_id
568
+ @state.session_id
569
+ end
570
+
571
+ def status
572
+ @state.status
573
+ end
574
+
575
+ def options
576
+ @state.options
577
+ end
578
+
579
+ def result
580
+ @state.result
581
+ end
582
+
583
+ def step
584
+ @state.step
585
+ end
586
+
587
+ # Make a choice and advance the workflow.
588
+ def choose(option_id)
589
+ @client.guide(session_id: session_id, choice: option_id.to_s)
590
+ end
591
+
592
+ # Check if the workflow is completed and return the result.
593
+ def complete
594
+ if status == "completed" && result
595
+ return result
596
+ end
597
+
598
+ raise Error, "Workflow not complete (status: #{status}). Call choose() with an option."
599
+ end
600
+ end
601
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DatagroutConduit
4
+ class Error < StandardError; end
5
+
6
+ class AuthError < Error; end
7
+
8
+ class ConnectionError < Error; end
9
+
10
+ class InitializationError < Error; end
11
+
12
+ class TimeoutError < Error; end
13
+
14
+ class ConfigError < Error; end
15
+
16
+ class NotInitializedError < Error
17
+ def initialize(msg = "Session not initialized. Call connect() first.")
18
+ super
19
+ end
20
+ end
21
+
22
+ class ToolNotFoundError < Error; end
23
+
24
+ class ResourceNotFoundError < Error; end
25
+
26
+ class InvalidArgumentsError < Error; end
27
+
28
+ class McpError < Error
29
+ attr_reader :code, :data
30
+
31
+ def initialize(code:, message:, data: nil)
32
+ @code = code
33
+ @data = data
34
+ super("MCP error #{code}: #{message}")
35
+ end
36
+ end
37
+
38
+ class RateLimitedError < Error
39
+ attr_reader :used, :limit
40
+
41
+ def initialize(used:, limit:)
42
+ @used = used
43
+ @limit = limit
44
+ super("Rate limit exceeded (#{used} / #{limit} calls this hour)")
45
+ end
46
+ end
47
+
48
+ module McpCodes
49
+ PARSE_ERROR = -32_700
50
+ INVALID_REQUEST = -32_600
51
+ METHOD_NOT_FOUND = -32_601
52
+ INVALID_PARAMS = -32_602
53
+ INTERNAL_ERROR = -32_603
54
+ NOT_INITIALIZED = -32_002
55
+ end
56
+ end