chuk-tool-processor 0.6.9__py3-none-any.whl → 0.6.11__py3-none-any.whl

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.

Potentially problematic release.


This version of chuk-tool-processor might be problematic. Click here for more details.

@@ -1,4 +1,4 @@
1
- # chuk_tool_processor/mcp/transport/http_streamable_transport.py - FIXED
1
+ # chuk_tool_processor/mcp/transport/http_streamable_transport.py - ENHANCED
2
2
  from __future__ import annotations
3
3
 
4
4
  import asyncio
@@ -30,21 +30,23 @@ class HTTPStreamableTransport(MCPBaseTransport):
30
30
  """
31
31
  HTTP Streamable transport using chuk-mcp HTTP client.
32
32
 
33
- FIXED: Improved connection management and parameter configuration
34
- to eliminate "server disconnected" errors.
33
+ ENHANCED: Now matches SSE transport robustness with improved connection
34
+ management, health monitoring, and comprehensive error handling.
35
35
  """
36
36
 
37
- def __init__(self, url: str, api_key: Optional[str] = None,
37
+ def __init__(self, url: str, api_key: Optional[str] = None,
38
+ headers: Optional[Dict[str, str]] = None, # NEW: Headers support
38
39
  connection_timeout: float = 30.0,
39
40
  default_timeout: float = 30.0,
40
41
  session_id: Optional[str] = None,
41
42
  enable_metrics: bool = True):
42
43
  """
43
- Initialize HTTP Streamable transport with chuk-mcp.
44
+ Initialize HTTP Streamable transport with enhanced configuration.
44
45
 
45
46
  Args:
46
47
  url: HTTP server URL (should end with /mcp)
47
48
  api_key: Optional API key for authentication
49
+ headers: Optional custom headers (NEW)
48
50
  connection_timeout: Timeout for initial connection
49
51
  default_timeout: Default timeout for operations
50
52
  session_id: Optional session ID for stateful connections
@@ -57,6 +59,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
57
59
  self.url = url
58
60
 
59
61
  self.api_key = api_key
62
+ self.configured_headers = headers or {} # NEW: Store configured headers
60
63
  self.connection_timeout = connection_timeout
61
64
  self.default_timeout = default_timeout
62
65
  self.session_id = session_id
@@ -65,16 +68,23 @@ class HTTPStreamableTransport(MCPBaseTransport):
65
68
  logger.debug("HTTP Streamable transport initialized with URL: %s", self.url)
66
69
  if self.api_key:
67
70
  logger.debug("API key configured for authentication")
71
+ if self.configured_headers:
72
+ logger.debug("Custom headers configured: %s", list(self.configured_headers.keys()))
68
73
  if self.session_id:
69
74
  logger.debug("Session ID configured: %s", self.session_id)
70
75
 
71
- # State tracking
76
+ # State tracking (enhanced like SSE)
72
77
  self._http_context = None
73
78
  self._read_stream = None
74
79
  self._write_stream = None
75
80
  self._initialized = False
76
81
 
77
- # Performance metrics (consistent with other transports)
82
+ # Health monitoring (NEW - like SSE)
83
+ self._last_successful_ping = None
84
+ self._consecutive_failures = 0
85
+ self._max_consecutive_failures = 3
86
+
87
+ # Performance metrics (enhanced like SSE)
78
88
  self._metrics = {
79
89
  "total_calls": 0,
80
90
  "successful_calls": 0,
@@ -84,11 +94,49 @@ class HTTPStreamableTransport(MCPBaseTransport):
84
94
  "last_ping_time": None,
85
95
  "initialization_time": None,
86
96
  "connection_resets": 0,
87
- "stream_errors": 0
97
+ "stream_errors": 0,
98
+ "connection_errors": 0, # NEW
99
+ "recovery_attempts": 0, # NEW
100
+ }
101
+
102
+ def _get_headers(self) -> Dict[str, str]:
103
+ """Get headers with authentication and custom headers (like SSE)."""
104
+ headers = {
105
+ "Content-Type": "application/json",
106
+ "Accept": "application/json, text/event-stream",
107
+ 'User-Agent': 'chuk-tool-processor/1.0.0',
88
108
  }
109
+
110
+ # Add configured headers first
111
+ if self.configured_headers:
112
+ headers.update(self.configured_headers)
113
+
114
+ # Add API key as Bearer token if provided
115
+ if self.api_key:
116
+ headers['Authorization'] = f'Bearer {self.api_key}'
117
+
118
+ # Add session ID if provided
119
+ if self.session_id:
120
+ headers["X-Session-ID"] = self.session_id
121
+
122
+ return headers
123
+
124
+ async def _test_connection_health(self) -> bool:
125
+ """Test basic HTTP connectivity (like SSE's connectivity test)."""
126
+ try:
127
+ import httpx
128
+ async with httpx.AsyncClient(timeout=5.0) as client:
129
+ # Test basic connectivity to base URL
130
+ base_url = self.url.replace('/mcp', '')
131
+ response = await client.get(f"{base_url}/health", headers=self._get_headers())
132
+ logger.debug("Health check response: %s", response.status_code)
133
+ return response.status_code < 500 # Accept any non-server-error
134
+ except Exception as e:
135
+ logger.debug("Connection health test failed: %s", e)
136
+ return True # Don't fail on health check errors
89
137
 
90
138
  async def initialize(self) -> bool:
91
- """Initialize using chuk-mcp http_client with improved configuration."""
139
+ """Initialize with enhanced error handling and health monitoring."""
92
140
  if self._initialized:
93
141
  logger.warning("Transport already initialized")
94
142
  return True
@@ -98,50 +146,41 @@ class HTTPStreamableTransport(MCPBaseTransport):
98
146
  try:
99
147
  logger.debug("Initializing HTTP Streamable transport to %s", self.url)
100
148
 
101
- # FIXED: Proper HTTP headers (match working diagnostic)
102
- headers = {
103
- "Content-Type": "application/json",
104
- "Accept": "application/json, text/event-stream",
105
- }
106
-
107
- # FIXED: Only set Authorization header, not both bearer_token and headers
108
- bearer_token = None
109
- if self.api_key:
110
- headers["Authorization"] = f"Bearer {self.api_key}"
111
- logger.debug("API key configured for authentication")
149
+ # Test basic connectivity first (like SSE)
150
+ if not await self._test_connection_health():
151
+ logger.warning("Connection health test failed, proceeding anyway")
112
152
 
113
- if self.session_id:
114
- headers["X-Session-ID"] = self.session_id
115
- logger.debug("Using session ID: %s", self.session_id)
153
+ # Build headers properly
154
+ headers = self._get_headers()
155
+ logger.debug("Using headers: %s", list(headers.keys()))
116
156
 
117
- # FIXED: Use only valid StreamableHTTPParameters
157
+ # Create StreamableHTTPParameters with proper configuration
118
158
  http_params = StreamableHTTPParameters(
119
159
  url=self.url,
120
- timeout=self.default_timeout, # FIXED: Use default_timeout for operations
160
+ timeout=self.default_timeout,
121
161
  headers=headers,
122
- bearer_token=bearer_token, # FIXED: Don't duplicate auth
162
+ bearer_token=None, # Don't duplicate auth - it's in headers
123
163
  session_id=self.session_id,
124
164
  enable_streaming=True,
125
- max_concurrent_requests=5, # FIXED: Reduce concurrency for stability
126
- max_retries=2, # FIXED: Add retry configuration
127
- retry_delay=1.0, # FIXED: Short retry delay
165
+ max_concurrent_requests=5,
166
+ max_retries=2,
167
+ retry_delay=1.0,
128
168
  user_agent="chuk-tool-processor/1.0.0",
129
169
  )
130
170
 
131
171
  # Create and enter the HTTP context
132
172
  self._http_context = http_client(http_params)
133
173
 
134
- logger.debug("Establishing HTTP connection and MCP handshake...")
174
+ logger.debug("Establishing HTTP connection...")
135
175
  self._read_stream, self._write_stream = await asyncio.wait_for(
136
176
  self._http_context.__aenter__(),
137
177
  timeout=self.connection_timeout
138
178
  )
139
179
 
140
- # FIXED: Simplified MCP initialize sequence (match working diagnostic)
180
+ # Enhanced MCP initialize sequence
141
181
  logger.debug("Sending MCP initialize request...")
142
182
  init_start = time.time()
143
183
 
144
- # Send initialize request with default parameters
145
184
  init_result = await asyncio.wait_for(
146
185
  send_initialize(self._read_stream, self._write_stream),
147
186
  timeout=self.default_timeout
@@ -150,54 +189,83 @@ class HTTPStreamableTransport(MCPBaseTransport):
150
189
  init_time = time.time() - init_start
151
190
  logger.debug("MCP initialize completed in %.3fs", init_time)
152
191
 
153
- # Verify the connection works with a simple ping
192
+ # Verify connection with ping (enhanced like SSE)
154
193
  logger.debug("Verifying connection with ping...")
155
194
  ping_start = time.time()
156
195
  ping_success = await asyncio.wait_for(
157
196
  send_ping(self._read_stream, self._write_stream),
158
- timeout=5.0
197
+ timeout=10.0 # Longer timeout for initial ping
159
198
  )
160
199
  ping_time = time.time() - ping_start
161
200
 
162
201
  if ping_success:
163
202
  self._initialized = True
203
+ self._last_successful_ping = time.time()
204
+ self._consecutive_failures = 0
205
+
164
206
  total_init_time = time.time() - start_time
165
207
  if self.enable_metrics:
166
208
  self._metrics["initialization_time"] = total_init_time
167
209
  self._metrics["last_ping_time"] = ping_time
168
210
 
169
- logger.debug("HTTP Streamable transport initialized successfully in %.3fs (ping: %.3fs)", total_init_time, ping_time)
211
+ logger.debug("HTTP Streamable transport initialized successfully in %.3fs (ping: %.3fs)",
212
+ total_init_time, ping_time)
170
213
  return True
171
214
  else:
172
215
  logger.warning("HTTP connection established but ping failed")
173
216
  # Still consider it initialized since connection was established
174
217
  self._initialized = True
218
+ self._consecutive_failures = 1 # Mark one failure
175
219
  if self.enable_metrics:
176
220
  self._metrics["initialization_time"] = time.time() - start_time
177
221
  return True
178
222
 
179
223
  except asyncio.TimeoutError:
180
224
  logger.error("HTTP Streamable initialization timed out after %ss", self.connection_timeout)
181
- logger.error("This may indicate the server is not responding to MCP initialization")
182
225
  await self._cleanup()
226
+ if self.enable_metrics:
227
+ self._metrics["connection_errors"] += 1
183
228
  return False
184
229
  except Exception as e:
185
230
  logger.error("Error initializing HTTP Streamable transport: %s", e, exc_info=True)
186
231
  await self._cleanup()
232
+ if self.enable_metrics:
233
+ self._metrics["connection_errors"] += 1
234
+ return False
235
+
236
+ async def _attempt_recovery(self) -> bool:
237
+ """Attempt to recover from connection issues (NEW - like SSE resilience)."""
238
+ if self.enable_metrics:
239
+ self._metrics["recovery_attempts"] += 1
240
+
241
+ logger.debug("Attempting HTTP connection recovery...")
242
+
243
+ try:
244
+ # Clean up existing connection
245
+ await self._cleanup()
246
+
247
+ # Re-initialize
248
+ return await self.initialize()
249
+ except Exception as e:
250
+ logger.warning("Recovery attempt failed: %s", e)
187
251
  return False
188
252
 
189
253
  async def close(self) -> None:
190
- """Close the HTTP Streamable transport properly."""
254
+ """Close with enhanced cleanup and metrics reporting."""
191
255
  if not self._initialized:
192
256
  return
193
257
 
194
- # Log final metrics
258
+ # Enhanced metrics logging (like SSE)
195
259
  if self.enable_metrics and self._metrics["total_calls"] > 0:
260
+ success_rate = (self._metrics["successful_calls"] / self._metrics["total_calls"] * 100)
196
261
  logger.debug(
197
- "HTTP Streamable transport closing - Total calls: %d, Success rate: %.1f%%, Avg response time: %.3fs",
262
+ "HTTP Streamable transport closing - Calls: %d, Success: %.1f%%, "
263
+ "Avg time: %.3fs, Recoveries: %d, Errors: %d",
198
264
  self._metrics["total_calls"],
199
- (self._metrics["successful_calls"] / self._metrics["total_calls"] * 100),
200
- self._metrics["avg_response_time"]
265
+ success_rate,
266
+ self._metrics["avg_response_time"],
267
+ self._metrics["recovery_attempts"],
268
+ self._metrics["connection_errors"]
201
269
  )
202
270
 
203
271
  try:
@@ -211,14 +279,14 @@ class HTTPStreamableTransport(MCPBaseTransport):
211
279
  await self._cleanup()
212
280
 
213
281
  async def _cleanup(self) -> None:
214
- """Clean up internal state."""
282
+ """Enhanced cleanup with state reset."""
215
283
  self._http_context = None
216
284
  self._read_stream = None
217
285
  self._write_stream = None
218
286
  self._initialized = False
219
287
 
220
288
  async def send_ping(self) -> bool:
221
- """Send ping with performance tracking."""
289
+ """Enhanced ping with health monitoring (like SSE)."""
222
290
  if not self._initialized or not self._read_stream:
223
291
  logger.error("Cannot send ping: transport not initialized")
224
292
  return False
@@ -230,27 +298,45 @@ class HTTPStreamableTransport(MCPBaseTransport):
230
298
  timeout=self.default_timeout
231
299
  )
232
300
 
301
+ success = bool(result)
302
+
303
+ if success:
304
+ self._last_successful_ping = time.time()
305
+ self._consecutive_failures = 0
306
+ else:
307
+ self._consecutive_failures += 1
308
+
233
309
  if self.enable_metrics:
234
310
  ping_time = time.time() - start_time
235
311
  self._metrics["last_ping_time"] = ping_time
236
- logger.debug("HTTP Streamable ping completed in %.3fs: %s", ping_time, result)
312
+ logger.debug("HTTP Streamable ping completed in %.3fs: %s", ping_time, success)
237
313
 
238
- return bool(result)
314
+ return success
239
315
  except asyncio.TimeoutError:
240
316
  logger.error("HTTP Streamable ping timed out")
317
+ self._consecutive_failures += 1
241
318
  return False
242
319
  except Exception as e:
243
320
  logger.error("HTTP Streamable ping failed: %s", e)
321
+ self._consecutive_failures += 1
244
322
  if self.enable_metrics:
245
323
  self._metrics["stream_errors"] += 1
246
324
  return False
247
325
 
248
326
  def is_connected(self) -> bool:
249
- """Check connection status."""
250
- return self._initialized and self._read_stream is not None and self._write_stream is not None
327
+ """Enhanced connection status check (like SSE)."""
328
+ if not self._initialized or not self._read_stream or not self._write_stream:
329
+ return False
330
+
331
+ # Check if we've had too many consecutive failures (like SSE)
332
+ if self._consecutive_failures >= self._max_consecutive_failures:
333
+ logger.warning("Connection marked unhealthy after %d failures", self._consecutive_failures)
334
+ return False
335
+
336
+ return True
251
337
 
252
338
  async def get_tools(self) -> List[Dict[str, Any]]:
253
- """Get tools list with performance tracking."""
339
+ """Enhanced tools retrieval with error handling."""
254
340
  if not self._initialized:
255
341
  logger.error("Cannot get tools: transport not initialized")
256
342
  return []
@@ -271,6 +357,9 @@ class HTTPStreamableTransport(MCPBaseTransport):
271
357
  logger.warning("Unexpected tools response type: %s", type(tools_response))
272
358
  tools = []
273
359
 
360
+ # Reset failure count on success
361
+ self._consecutive_failures = 0
362
+
274
363
  if self.enable_metrics:
275
364
  response_time = time.time() - start_time
276
365
  logger.debug("Retrieved %d tools in %.3fs", len(tools), response_time)
@@ -279,16 +368,18 @@ class HTTPStreamableTransport(MCPBaseTransport):
279
368
 
280
369
  except asyncio.TimeoutError:
281
370
  logger.error("Get tools timed out")
371
+ self._consecutive_failures += 1
282
372
  return []
283
373
  except Exception as e:
284
374
  logger.error("Error getting tools: %s", e)
375
+ self._consecutive_failures += 1
285
376
  if self.enable_metrics:
286
377
  self._metrics["stream_errors"] += 1
287
378
  return []
288
379
 
289
380
  async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
290
381
  timeout: Optional[float] = None) -> Dict[str, Any]:
291
- """Call tool with enhanced performance tracking and error handling."""
382
+ """Enhanced tool calling with recovery and health monitoring."""
292
383
  if not self._initialized:
293
384
  return {
294
385
  "isError": True,
@@ -304,13 +395,15 @@ class HTTPStreamableTransport(MCPBaseTransport):
304
395
  try:
305
396
  logger.debug("Calling tool '%s' with timeout %ss", tool_name, tool_timeout)
306
397
 
307
- # FIXED: Add connection state check before making call
398
+ # Enhanced connection check with recovery attempt
308
399
  if not self.is_connected():
309
- logger.warning("Connection lost, attempting to reconnect...")
310
- if not await self.initialize():
400
+ logger.warning("Connection unhealthy, attempting recovery...")
401
+ if not await self._attempt_recovery():
402
+ if self.enable_metrics:
403
+ self._update_metrics(time.time() - start_time, False)
311
404
  return {
312
405
  "isError": True,
313
- "error": "Failed to reconnect to server"
406
+ "error": "Failed to recover connection"
314
407
  }
315
408
 
316
409
  raw_response = await asyncio.wait_for(
@@ -326,18 +419,24 @@ class HTTPStreamableTransport(MCPBaseTransport):
326
419
  response_time = time.time() - start_time
327
420
  result = self._normalize_mcp_response(raw_response)
328
421
 
422
+ # Reset failure count on success
423
+ self._consecutive_failures = 0
424
+ self._last_successful_ping = time.time() # Update health timestamp
425
+
329
426
  if self.enable_metrics:
330
427
  self._update_metrics(response_time, not result.get("isError", False))
331
428
 
332
429
  if not result.get("isError", False):
333
430
  logger.debug("Tool '%s' completed successfully in %.3fs", tool_name, response_time)
334
431
  else:
335
- logger.warning("Tool '%s' failed in %.3fs: %s", tool_name, response_time, result.get('error', 'Unknown error'))
432
+ logger.warning("Tool '%s' failed in %.3fs: %s", tool_name, response_time,
433
+ result.get('error', 'Unknown error'))
336
434
 
337
435
  return result
338
436
 
339
437
  except asyncio.TimeoutError:
340
438
  response_time = time.time() - start_time
439
+ self._consecutive_failures += 1
341
440
  if self.enable_metrics:
342
441
  self._update_metrics(response_time, False)
343
442
 
@@ -349,14 +448,19 @@ class HTTPStreamableTransport(MCPBaseTransport):
349
448
  }
350
449
  except Exception as e:
351
450
  response_time = time.time() - start_time
451
+ self._consecutive_failures += 1
352
452
  if self.enable_metrics:
353
453
  self._update_metrics(response_time, False)
354
454
  self._metrics["stream_errors"] += 1
355
455
 
356
- # FIXED: Check if this is a connection error that should trigger reconnect
357
- if "connection" in str(e).lower() or "disconnected" in str(e).lower():
358
- logger.warning("Connection error detected, marking as disconnected: %s", e)
456
+ # Enhanced connection error detection
457
+ error_str = str(e).lower()
458
+ if any(indicator in error_str for indicator in
459
+ ["connection", "disconnected", "broken pipe", "eof"]):
460
+ logger.warning("Connection error detected: %s", e)
359
461
  self._initialized = False
462
+ if self.enable_metrics:
463
+ self._metrics["connection_errors"] += 1
360
464
 
361
465
  error_msg = f"Tool execution failed: {str(e)}"
362
466
  logger.error("Tool '%s' error: %s", tool_name, error_msg)
@@ -366,7 +470,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
366
470
  }
367
471
 
368
472
  def _update_metrics(self, response_time: float, success: bool) -> None:
369
- """Update performance metrics."""
473
+ """Enhanced metrics tracking (like SSE)."""
370
474
  if success:
371
475
  self._metrics["successful_calls"] += 1
372
476
  else:
@@ -379,7 +483,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
379
483
  )
380
484
 
381
485
  async def list_resources(self) -> Dict[str, Any]:
382
- """List resources using chuk-mcp."""
486
+ """Enhanced resource listing with error handling."""
383
487
  if not self._initialized:
384
488
  return {}
385
489
 
@@ -391,13 +495,15 @@ class HTTPStreamableTransport(MCPBaseTransport):
391
495
  return response if isinstance(response, dict) else {}
392
496
  except asyncio.TimeoutError:
393
497
  logger.error("List resources timed out")
498
+ self._consecutive_failures += 1
394
499
  return {}
395
500
  except Exception as e:
396
501
  logger.debug("Error listing resources: %s", e)
502
+ self._consecutive_failures += 1
397
503
  return {}
398
504
 
399
505
  async def list_prompts(self) -> Dict[str, Any]:
400
- """List prompts using chuk-mcp."""
506
+ """Enhanced prompt listing with error handling."""
401
507
  if not self._initialized:
402
508
  return {}
403
509
 
@@ -409,51 +515,96 @@ class HTTPStreamableTransport(MCPBaseTransport):
409
515
  return response if isinstance(response, dict) else {}
410
516
  except asyncio.TimeoutError:
411
517
  logger.error("List prompts timed out")
518
+ self._consecutive_failures += 1
412
519
  return {}
413
520
  except Exception as e:
414
521
  logger.debug("Error listing prompts: %s", e)
522
+ self._consecutive_failures += 1
523
+ return {}
524
+
525
+ async def read_resource(self, uri: str) -> Dict[str, Any]:
526
+ """Read a specific resource."""
527
+ if not self._initialized:
528
+ return {}
529
+
530
+ try:
531
+ response = await asyncio.wait_for(
532
+ send_resources_read(self._read_stream, self._write_stream, uri),
533
+ timeout=self.default_timeout
534
+ )
535
+ return response if isinstance(response, dict) else {}
536
+ except asyncio.TimeoutError:
537
+ logger.error("Read resource timed out")
538
+ self._consecutive_failures += 1
539
+ return {}
540
+ except Exception as e:
541
+ logger.debug("Error reading resource: %s", e)
542
+ self._consecutive_failures += 1
543
+ return {}
544
+
545
+ async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
546
+ """Get a specific prompt."""
547
+ if not self._initialized:
548
+ return {}
549
+
550
+ try:
551
+ response = await asyncio.wait_for(
552
+ send_prompts_get(self._read_stream, self._write_stream, name, arguments or {}),
553
+ timeout=self.default_timeout
554
+ )
555
+ return response if isinstance(response, dict) else {}
556
+ except asyncio.TimeoutError:
557
+ logger.error("Get prompt timed out")
558
+ self._consecutive_failures += 1
559
+ return {}
560
+ except Exception as e:
561
+ logger.debug("Error getting prompt: %s", e)
562
+ self._consecutive_failures += 1
415
563
  return {}
416
564
 
417
- # ------------------------------------------------------------------ #
418
- # Metrics and monitoring (consistent with other transports) #
419
- # ------------------------------------------------------------------ #
420
565
  def get_metrics(self) -> Dict[str, Any]:
421
- """Get performance metrics."""
422
- return self._metrics.copy()
566
+ """Enhanced metrics with health information."""
567
+ metrics = self._metrics.copy()
568
+ metrics.update({
569
+ "is_connected": self.is_connected(),
570
+ "consecutive_failures": self._consecutive_failures,
571
+ "last_successful_ping": self._last_successful_ping,
572
+ "max_consecutive_failures": self._max_consecutive_failures,
573
+ })
574
+ return metrics
423
575
 
424
576
  def reset_metrics(self) -> None:
425
- """Reset performance metrics."""
577
+ """Enhanced metrics reset preserving health state."""
578
+ preserved_init_time = self._metrics.get("initialization_time")
579
+ preserved_last_ping = self._metrics.get("last_ping_time")
580
+
426
581
  self._metrics = {
427
582
  "total_calls": 0,
428
583
  "successful_calls": 0,
429
584
  "failed_calls": 0,
430
585
  "total_time": 0.0,
431
586
  "avg_response_time": 0.0,
432
- "last_ping_time": self._metrics.get("last_ping_time"),
433
- "initialization_time": self._metrics.get("initialization_time"),
587
+ "last_ping_time": preserved_last_ping,
588
+ "initialization_time": preserved_init_time,
434
589
  "connection_resets": self._metrics.get("connection_resets", 0),
435
- "stream_errors": 0
590
+ "stream_errors": 0,
591
+ "connection_errors": 0,
592
+ "recovery_attempts": 0,
436
593
  }
437
594
 
438
- # ------------------------------------------------------------------ #
439
- # Backward compatibility #
440
- # ------------------------------------------------------------------ #
441
595
  def get_streams(self) -> List[tuple]:
442
- """Provide streams for backward compatibility."""
596
+ """Enhanced streams access with connection check."""
443
597
  if self._initialized and self._read_stream and self._write_stream:
444
598
  return [(self._read_stream, self._write_stream)]
445
599
  return []
446
600
 
447
- # ------------------------------------------------------------------ #
448
- # Context manager support #
449
- # ------------------------------------------------------------------ #
450
601
  async def __aenter__(self):
451
- """Context manager support."""
602
+ """Enhanced context manager entry."""
452
603
  success = await self.initialize()
453
604
  if not success:
454
605
  raise RuntimeError("Failed to initialize HTTPStreamableTransport")
455
606
  return self
456
607
 
457
608
  async def __aexit__(self, exc_type, exc_val, exc_tb):
458
- """Context manager cleanup."""
609
+ """Enhanced context manager cleanup."""
459
610
  await self.close()