datagrout-conduit 0.5.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d3a08c2cb30deefe712f0083b473838a1ec6b578c1b7decbcc53ac14aca4cffb
4
- data.tar.gz: ea88b13e4a5998164c4dfaa1b71d448e5d498b694b8de312449a8c82d4daf555
3
+ metadata.gz: 177af76b0cdd7d7e82fd1384db89da6a5dd4b3152ff5f07e255075dec041a91c
4
+ data.tar.gz: 34b1c4d17471013349db85beb09f167719e4fc0189204b117042c1394fc175f8
5
5
  SHA512:
6
- metadata.gz: 70c40acb181dcc382395c454e769e26fede6798a32dc2dab7797ffaf66b1936dd0e89cf767f2f9f16a5e668ad1ceff9dfd3d2a22d03db34bcb146ab1eef78ba6
7
- data.tar.gz: 3c2cf29896ae08b63876e60e8807ccb7ea85118fdb5d422aaf9b94e4ef3aa76345786b42bb0406f1c8df1b938d3755cf78a84fd3bf9728f5ecc4fd54a6c7b737
6
+ metadata.gz: 6812db873347c88a86bfd0ecce380e6d264b4f66854d3eeb01e3ee4a28b89cf2b6e8f93423442ea9b0c14fcb40c68cd6bcc2d6210a5f343200115dd92343ff35
7
+ data.tar.gz: 23deaeee4cfe9be72cc546b919e5a87cb5ea5fa108831c346a4e02b0faa501578b582632f538a42dd0456095a9b8589511e349318c95f414b086bed7a0d90980
@@ -388,16 +388,24 @@ module DatagroutConduit
388
388
  unwrap_content(raw)
389
389
  end
390
390
 
391
- # Unwrap the MCP content envelope that wraps tool results from both MCP and
392
- # JSONRPC transports: {"content" => [{"type" => "text", "text" => "<json>"}]}
391
+ # Unwrap the MCP content envelope from tool call results.
392
+ #
393
+ # Priority order (MCP 2025):
394
+ # 1. +structuredContent+ — pure JSON Hash, no decoding needed.
395
+ # 2. +content[0]["text"]+ parsed as JSON — legacy text-encoded path.
396
+ # 3. +content[0]+ as-is — plain-text or non-JSON content item.
397
+ # 4. +raw+ unchanged — no content envelope present.
393
398
  def unwrap_content(raw)
394
399
  return raw unless raw.is_a?(Hash)
395
400
 
401
+ # 1. Prefer structuredContent (MCP 2025).
402
+ return raw["structuredContent"] if raw.key?("structuredContent")
403
+
396
404
  content = raw["content"]
397
405
  return raw unless content.is_a?(Array) && !content.empty?
398
406
 
399
407
  first = content.first
400
- return raw unless first.is_a?(Hash) && first["text"].is_a?(String)
408
+ return first unless first.is_a?(Hash) && first["text"].is_a?(String)
401
409
 
402
410
  begin
403
411
  JSON.parse(first["text"])
@@ -32,6 +32,14 @@ module DatagroutConduit
32
32
  class Ws
33
33
  SUBPROTOCOL = "datagrout-jsonrpc.v1"
34
34
 
35
+ # Seconds between client-initiated WS ping frames.
36
+ #
37
+ # Many load balancers and reverse proxies (nginx, AWS ALB) close idle
38
+ # WS connections after 60-120 seconds; pinging every 25 seconds keeps
39
+ # the connection alive well within the tightest common timeout window.
40
+ # Mirrors PING_INTERVAL in the Rust reference implementation.
41
+ PING_INTERVAL_SECONDS = 25
42
+
35
43
  # ── Subscription ─────────────────────────────────────────────────────────
36
44
 
37
45
  # Per-subscription event stream delivered via a thread-safe Queue.
@@ -98,7 +106,7 @@ module DatagroutConduit
98
106
 
99
107
  # ── Construction ─────────────────────────────────────────────────────────
100
108
 
101
- def initialize(url:, auth: {}, identity: nil)
109
+ def initialize(url:, auth: {}, identity: nil, ping_interval: PING_INTERVAL_SECONDS)
102
110
  @url = url
103
111
  @auth = normalize_auth(auth)
104
112
  @identity = identity
@@ -113,9 +121,16 @@ module DatagroutConduit
113
121
  @io = nil
114
122
  @driver = nil
115
123
  @read_thread = nil
124
+ @ping_thread = nil
125
+ @ping_interval = ping_interval.to_f
126
+ @pings_sent = 0
116
127
  @connected = false
117
128
  end
118
129
 
130
+ # Number of ping frames this transport has sent over its lifetime.
131
+ # Exposed for tests; production code can ignore it.
132
+ attr_reader :pings_sent, :ping_interval
133
+
119
134
  # ── Public API ────────────────────────────────────────────────────────────
120
135
 
121
136
  # Establish the WebSocket connection.
@@ -145,6 +160,7 @@ module DatagroutConduit
145
160
  raise ConnectionError, "WebSocket handshake failed: #{err}" if err
146
161
 
147
162
  @connected = true
163
+ start_ping_thread
148
164
  self
149
165
  rescue Timeout::Error
150
166
  cleanup_socket
@@ -423,6 +439,7 @@ module DatagroutConduit
423
439
  end
424
440
 
425
441
  def cleanup_socket
442
+ stop_ping_thread
426
443
  @write_mutex.synchronize do
427
444
  @driver = nil
428
445
  end
@@ -432,6 +449,44 @@ module DatagroutConduit
432
449
  @io = nil
433
450
  end
434
451
 
452
+ # ── Ping keepalive ───────────────────────────────────────────────────────
453
+
454
+ # Start the background ping thread. Sends one WS ping frame every
455
+ # @ping_interval seconds until the connection is torn down. No-op when
456
+ # @ping_interval is non-positive (used by tests to disable pinging).
457
+ def start_ping_thread
458
+ return if @ping_thread
459
+ return if @ping_interval <= 0
460
+
461
+ interval = @ping_interval
462
+ @ping_thread = Thread.new do
463
+ loop do
464
+ sleep interval
465
+ break unless @connected
466
+
467
+ driver = @driver
468
+ break unless driver
469
+
470
+ begin
471
+ # WebSocket::Driver#ping returns false if the connection is
472
+ # already closing; treat that as "stop the loop" too.
473
+ @write_mutex.synchronize { driver.ping } || break
474
+ @pings_sent += 1
475
+ rescue StandardError
476
+ break
477
+ end
478
+ end
479
+ end
480
+ @ping_thread.abort_on_exception = false
481
+ @ping_thread.name = "conduit-ws-ping"
482
+ end
483
+
484
+ def stop_ping_thread
485
+ thread = @ping_thread
486
+ @ping_thread = nil
487
+ thread&.kill
488
+ end
489
+
435
490
  # ── Helpers ───────────────────────────────────────────────────────────────
436
491
 
437
492
  def ensure_connected!
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DatagroutConduit
4
- VERSION = "0.5.0"
4
+ VERSION = "0.7.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: datagrout-conduit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - DataGrout
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-05-10 00:00:00.000000000 Z
11
+ date: 2026-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday