chuk-tool-processor 0.6.7__py3-none-any.whl → 0.6.9__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,14 +1,21 @@
1
1
  # chuk_tool_processor/mcp/transport/sse_transport.py
2
2
  """
3
- Fixed SSE transport that matches your server's actual behavior.
4
- Based on your working debug script with smart URL detection.
3
+ SSE transport for MCP communication.
4
+
5
+ Implements Server-Sent Events transport with two-step async pattern:
6
+ 1. POST messages to /messages endpoint
7
+ 2. Receive responses via SSE stream
8
+
9
+ Note: This transport is deprecated in favor of HTTP Streamable (spec 2025-03-26)
10
+ but remains supported for backward compatibility.
5
11
  """
6
12
  from __future__ import annotations
7
13
 
8
14
  import asyncio
9
15
  import json
16
+ import time
10
17
  import uuid
11
- from typing import Dict, Any, List, Optional, Tuple
18
+ from typing import Dict, Any, List, Optional
12
19
  import logging
13
20
 
14
21
  import httpx
@@ -20,28 +27,43 @@ logger = logging.getLogger(__name__)
20
27
 
21
28
  class SSETransport(MCPBaseTransport):
22
29
  """
23
- SSE transport that works with your server's two-step async pattern:
24
- 1. POST messages to /messages endpoint
25
- 2. Receive responses via SSE stream
30
+ SSE transport implementing the MCP protocol over Server-Sent Events.
31
+
32
+ This transport uses a dual-connection approach:
33
+ - SSE stream for receiving responses
34
+ - HTTP POST for sending requests
26
35
  """
27
36
 
28
37
  def __init__(self, url: str, api_key: Optional[str] = None,
29
38
  headers: Optional[Dict[str, str]] = None,
30
- connection_timeout: float = 30.0, default_timeout: float = 30.0):
31
- """Initialize SSE transport."""
39
+ connection_timeout: float = 30.0,
40
+ default_timeout: float = 30.0,
41
+ enable_metrics: bool = True):
42
+ """
43
+ Initialize SSE transport.
44
+
45
+ Args:
46
+ url: Base URL for the MCP server
47
+ api_key: Optional API key for authentication
48
+ headers: Optional custom headers
49
+ connection_timeout: Timeout for initial connection setup
50
+ default_timeout: Default timeout for operations
51
+ enable_metrics: Whether to track performance metrics
52
+ """
32
53
  self.url = url.rstrip('/')
33
54
  self.api_key = api_key
34
55
  self.configured_headers = headers or {}
35
56
  self.connection_timeout = connection_timeout
36
57
  self.default_timeout = default_timeout
58
+ self.enable_metrics = enable_metrics
37
59
 
38
- # DEBUG: Log what we received
39
- logger.debug("SSE Transport initialized with:")
40
- logger.debug(" URL: %s", self.url)
41
- logger.debug(" API Key: %s", "***" if api_key else None)
42
- logger.debug(" Headers: %s", {k: v[:10] + "..." if len(v) > 10 else v for k, v in self.configured_headers.items()})
60
+ logger.debug("SSE Transport initialized with URL: %s", self.url)
61
+ if self.api_key:
62
+ logger.debug("API key configured for authentication")
63
+ if self.configured_headers:
64
+ logger.debug("Custom headers configured: %s", list(self.configured_headers.keys()))
43
65
 
44
- # State
66
+ # Connection state
45
67
  self.session_id = None
46
68
  self.message_url = None
47
69
  self.pending_requests: Dict[str, asyncio.Future] = {}
@@ -51,10 +73,23 @@ class SSETransport(MCPBaseTransport):
51
73
  self.stream_client = None
52
74
  self.send_client = None
53
75
 
54
- # SSE stream
76
+ # SSE stream management
55
77
  self.sse_task = None
56
78
  self.sse_response = None
57
79
  self.sse_stream_context = None
80
+
81
+ # Performance metrics (consistent with other transports)
82
+ self._metrics = {
83
+ "total_calls": 0,
84
+ "successful_calls": 0,
85
+ "failed_calls": 0,
86
+ "total_time": 0.0,
87
+ "avg_response_time": 0.0,
88
+ "last_ping_time": None,
89
+ "initialization_time": None,
90
+ "session_discoveries": 0,
91
+ "stream_errors": 0
92
+ }
58
93
 
59
94
  def _construct_sse_url(self, base_url: str) -> str:
60
95
  """
@@ -62,53 +97,48 @@ class SSETransport(MCPBaseTransport):
62
97
 
63
98
  Smart detection to avoid double-appending /sse if already present.
64
99
  """
65
- # Remove trailing slashes
66
100
  base_url = base_url.rstrip('/')
67
101
 
68
- # Check if URL already ends with /sse
69
102
  if base_url.endswith('/sse'):
70
- # Already has /sse, use as-is
71
103
  logger.debug("URL already contains /sse endpoint: %s", base_url)
72
104
  return base_url
73
105
 
74
- # Append /sse to the base URL
75
106
  sse_url = f"{base_url}/sse"
76
- logger.debug("Appending /sse to base URL: %s -> %s", base_url, sse_url)
107
+ logger.debug("Constructed SSE URL: %s -> %s", base_url, sse_url)
77
108
  return sse_url
78
109
 
79
110
  def _get_headers(self) -> Dict[str, str]:
80
- """Get headers with auth if available."""
111
+ """Get headers with authentication and custom headers."""
81
112
  headers = {}
82
113
 
83
114
  # Add configured headers first
84
115
  if self.configured_headers:
85
116
  headers.update(self.configured_headers)
86
117
 
87
- # Add API key as Bearer token if provided (this will override any Authorization header)
118
+ # Add API key as Bearer token if provided (overrides Authorization header)
88
119
  if self.api_key:
89
120
  headers['Authorization'] = f'Bearer {self.api_key}'
90
121
 
91
- # DEBUG: Log what headers we're sending
92
- logger.debug("Sending headers: %s", {k: v[:10] + "..." if len(v) > 10 else v for k, v in headers.items()})
93
-
94
122
  return headers
95
123
 
96
124
  async def initialize(self) -> bool:
97
- """Initialize SSE connection and MCP handshake."""
125
+ """Initialize SSE connection and perform MCP handshake."""
98
126
  if self._initialized:
99
127
  logger.warning("Transport already initialized")
100
128
  return True
101
129
 
130
+ start_time = time.time()
131
+
102
132
  try:
103
133
  logger.debug("Initializing SSE transport...")
104
134
 
105
- # Create HTTP clients
135
+ # Create HTTP clients with appropriate timeouts
106
136
  self.stream_client = httpx.AsyncClient(timeout=self.connection_timeout)
107
137
  self.send_client = httpx.AsyncClient(timeout=self.default_timeout)
108
138
 
109
- # Connect to SSE stream with smart URL construction
139
+ # Connect to SSE stream
110
140
  sse_url = self._construct_sse_url(self.url)
111
- logger.debug("Connecting to SSE: %s", sse_url)
141
+ logger.debug("Connecting to SSE endpoint: %s", sse_url)
112
142
 
113
143
  self.sse_stream_context = self.stream_client.stream(
114
144
  'GET', sse_url, headers=self._get_headers()
@@ -116,28 +146,37 @@ class SSETransport(MCPBaseTransport):
116
146
  self.sse_response = await self.sse_stream_context.__aenter__()
117
147
 
118
148
  if self.sse_response.status_code != 200:
119
- logger.error("SSE connection failed: %s", self.sse_response.status_code)
149
+ logger.error("SSE connection failed with status: %s", self.sse_response.status_code)
150
+ await self._cleanup()
120
151
  return False
121
152
 
122
153
  logger.debug("SSE streaming connection established")
123
154
 
124
155
  # Start SSE processing task
125
- self.sse_task = asyncio.create_task(self._process_sse_stream())
156
+ self.sse_task = asyncio.create_task(
157
+ self._process_sse_stream(),
158
+ name="sse_stream_processor"
159
+ )
126
160
 
127
- # Wait for session discovery
161
+ # Wait for session discovery with timeout
128
162
  logger.debug("Waiting for session discovery...")
129
- for i in range(50): # 5 seconds max
130
- if self.message_url:
131
- break
163
+ session_timeout = 5.0 # 5 seconds max for session discovery
164
+ session_start = time.time()
165
+
166
+ while not self.message_url and (time.time() - session_start) < session_timeout:
132
167
  await asyncio.sleep(0.1)
133
168
 
134
169
  if not self.message_url:
135
- logger.error("Failed to get session info from SSE")
170
+ logger.error("Failed to discover session endpoint within %.1fs", session_timeout)
171
+ await self._cleanup()
136
172
  return False
137
173
 
138
- logger.debug("Session ready: %s", self.session_id)
174
+ if self.enable_metrics:
175
+ self._metrics["session_discoveries"] += 1
176
+
177
+ logger.debug("Session endpoint discovered: %s", self.session_id)
139
178
 
140
- # Now do MCP initialization
179
+ # Perform MCP initialization handshake
141
180
  try:
142
181
  init_response = await self._send_request("initialize", {
143
182
  "protocolVersion": "2024-11-05",
@@ -149,18 +188,25 @@ class SSETransport(MCPBaseTransport):
149
188
  })
150
189
 
151
190
  if 'error' in init_response:
152
- logger.error("Initialize failed: %s", init_response['error'])
191
+ logger.error("MCP initialize failed: %s", init_response['error'])
192
+ await self._cleanup()
153
193
  return False
154
194
 
155
195
  # Send initialized notification
156
196
  await self._send_notification("notifications/initialized")
157
197
 
158
198
  self._initialized = True
159
- logger.debug("SSE transport initialized successfully")
199
+
200
+ if self.enable_metrics:
201
+ init_time = time.time() - start_time
202
+ self._metrics["initialization_time"] = init_time
203
+
204
+ logger.debug("SSE transport initialized successfully in %.3fs", time.time() - start_time)
160
205
  return True
161
206
 
162
207
  except Exception as e:
163
- logger.error("MCP initialization failed: %s", e)
208
+ logger.error("MCP handshake failed: %s", e)
209
+ await self._cleanup()
164
210
  return False
165
211
 
166
212
  except Exception as e:
@@ -169,7 +215,7 @@ class SSETransport(MCPBaseTransport):
169
215
  return False
170
216
 
171
217
  async def _process_sse_stream(self):
172
- """Process the persistent SSE stream."""
218
+ """Process the persistent SSE stream for responses and session discovery."""
173
219
  try:
174
220
  logger.debug("Starting SSE stream processing...")
175
221
 
@@ -183,44 +229,48 @@ class SSETransport(MCPBaseTransport):
183
229
  endpoint_path = line.split(':', 1)[1].strip()
184
230
  self.message_url = f"{self.url}{endpoint_path}"
185
231
 
232
+ # Extract session ID if present
186
233
  if 'session_id=' in endpoint_path:
187
234
  self.session_id = endpoint_path.split('session_id=')[1].split('&')[0]
188
235
 
189
- logger.debug("Got session info: %s", self.session_id)
236
+ logger.debug("Session endpoint discovered: %s", self.session_id)
190
237
  continue
191
238
 
192
239
  # Handle JSON-RPC responses
193
240
  if line.startswith('data:'):
194
241
  data_part = line.split(':', 1)[1].strip()
195
242
 
196
- # Skip pings and empty data
243
+ # Skip keepalive pings and empty data
197
244
  if not data_part or data_part.startswith('ping'):
198
245
  continue
199
246
 
200
247
  try:
201
248
  response_data = json.loads(data_part)
202
249
 
250
+ # Handle JSON-RPC responses with request IDs
203
251
  if 'jsonrpc' in response_data and 'id' in response_data:
204
252
  request_id = str(response_data['id'])
205
253
 
206
- # Resolve pending request
254
+ # Resolve pending request if found
207
255
  if request_id in self.pending_requests:
208
256
  future = self.pending_requests.pop(request_id)
209
257
  if not future.done():
210
258
  future.set_result(response_data)
211
- logger.debug("Resolved request: %s", request_id)
259
+ logger.debug("Resolved request ID: %s", request_id)
212
260
 
213
- except json.JSONDecodeError:
214
- pass # Not JSON, ignore
261
+ except json.JSONDecodeError as e:
262
+ logger.debug("Non-JSON data in SSE stream (ignoring): %s", e)
215
263
 
216
264
  except Exception as e:
217
- logger.error("SSE stream error: %s", e)
265
+ if self.enable_metrics:
266
+ self._metrics["stream_errors"] += 1
267
+ logger.error("SSE stream processing error: %s", e)
218
268
 
219
269
  async def _send_request(self, method: str, params: Dict[str, Any] = None,
220
270
  timeout: Optional[float] = None) -> Dict[str, Any]:
221
- """Send request and wait for async response."""
271
+ """Send JSON-RPC request and wait for async response via SSE."""
222
272
  if not self.message_url:
223
- raise RuntimeError("Not connected")
273
+ raise RuntimeError("SSE transport not connected - no message URL")
224
274
 
225
275
  request_id = str(uuid.uuid4())
226
276
  message = {
@@ -230,12 +280,12 @@ class SSETransport(MCPBaseTransport):
230
280
  "params": params or {}
231
281
  }
232
282
 
233
- # Create future for response
283
+ # Create future for async response
234
284
  future = asyncio.Future()
235
285
  self.pending_requests[request_id] = future
236
286
 
237
287
  try:
238
- # Send message
288
+ # Send HTTP POST request
239
289
  headers = {
240
290
  'Content-Type': 'application/json',
241
291
  **self._get_headers()
@@ -248,9 +298,9 @@ class SSETransport(MCPBaseTransport):
248
298
  )
249
299
 
250
300
  if response.status_code == 202:
251
- # Wait for async response
252
- timeout = timeout or self.default_timeout
253
- result = await asyncio.wait_for(future, timeout=timeout)
301
+ # Async response - wait for result via SSE
302
+ request_timeout = timeout or self.default_timeout
303
+ result = await asyncio.wait_for(future, timeout=request_timeout)
254
304
  return result
255
305
  elif response.status_code == 200:
256
306
  # Immediate response
@@ -258,7 +308,7 @@ class SSETransport(MCPBaseTransport):
258
308
  return response.json()
259
309
  else:
260
310
  self.pending_requests.pop(request_id, None)
261
- raise RuntimeError(f"Request failed: {response.status_code}")
311
+ raise RuntimeError(f"HTTP request failed with status: {response.status_code}")
262
312
 
263
313
  except asyncio.TimeoutError:
264
314
  self.pending_requests.pop(request_id, None)
@@ -268,9 +318,9 @@ class SSETransport(MCPBaseTransport):
268
318
  raise
269
319
 
270
320
  async def _send_notification(self, method: str, params: Dict[str, Any] = None):
271
- """Send notification (no response expected)."""
321
+ """Send JSON-RPC notification (no response expected)."""
272
322
  if not self.message_url:
273
- raise RuntimeError("Not connected")
323
+ raise RuntimeError("SSE transport not connected - no message URL")
274
324
 
275
325
  message = {
276
326
  "jsonrpc": "2.0",
@@ -283,30 +333,46 @@ class SSETransport(MCPBaseTransport):
283
333
  **self._get_headers()
284
334
  }
285
335
 
286
- await self.send_client.post(
336
+ response = await self.send_client.post(
287
337
  self.message_url,
288
338
  headers=headers,
289
339
  json=message
290
340
  )
341
+
342
+ if response.status_code not in (200, 202):
343
+ logger.warning("Notification failed with status: %s", response.status_code)
291
344
 
292
345
  async def send_ping(self) -> bool:
293
- """Send ping to check connection."""
346
+ """Send ping to check connection health."""
294
347
  if not self._initialized:
295
348
  return False
296
349
 
350
+ start_time = time.time()
297
351
  try:
298
- # Your server might not support ping, so we'll just check if we can list tools
352
+ # Use tools/list as a lightweight ping since not all servers support ping
299
353
  response = await self._send_request("tools/list", {}, timeout=5.0)
354
+
355
+ if self.enable_metrics:
356
+ ping_time = time.time() - start_time
357
+ self._metrics["last_ping_time"] = ping_time
358
+ logger.debug("SSE ping completed in %.3fs", ping_time)
359
+
300
360
  return 'error' not in response
301
- except Exception:
361
+ except Exception as e:
362
+ logger.debug("SSE ping failed: %s", e)
302
363
  return False
303
364
 
365
+ def is_connected(self) -> bool:
366
+ """Check if the transport is connected and ready."""
367
+ return self._initialized and self.session_id is not None
368
+
304
369
  async def get_tools(self) -> List[Dict[str, Any]]:
305
- """Get tools list."""
370
+ """Get list of available tools from the server."""
306
371
  if not self._initialized:
307
372
  logger.error("Cannot get tools: transport not initialized")
308
373
  return []
309
374
 
375
+ start_time = time.time()
310
376
  try:
311
377
  response = await self._send_request("tools/list", {})
312
378
 
@@ -315,7 +381,11 @@ class SSETransport(MCPBaseTransport):
315
381
  return []
316
382
 
317
383
  tools = response.get('result', {}).get('tools', [])
318
- logger.debug("Retrieved %d tools", len(tools))
384
+
385
+ if self.enable_metrics:
386
+ response_time = time.time() - start_time
387
+ logger.debug("Retrieved %d tools in %.3fs", len(tools), response_time)
388
+
319
389
  return tools
320
390
 
321
391
  except Exception as e:
@@ -324,15 +394,19 @@ class SSETransport(MCPBaseTransport):
324
394
 
325
395
  async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
326
396
  timeout: Optional[float] = None) -> Dict[str, Any]:
327
- """Call a tool."""
397
+ """Execute a tool with the given arguments."""
328
398
  if not self._initialized:
329
399
  return {
330
400
  "isError": True,
331
401
  "error": "Transport not initialized"
332
402
  }
333
403
 
404
+ start_time = time.time()
405
+ if self.enable_metrics:
406
+ self._metrics["total_calls"] += 1 # FIXED: INCREMENT FIRST
407
+
334
408
  try:
335
- logger.debug("Calling tool %s with args: %s", tool_name, arguments)
409
+ logger.debug("Calling tool '%s' with arguments: %s", tool_name, arguments)
336
410
 
337
411
  response = await self._send_request(
338
412
  "tools/call",
@@ -344,58 +418,57 @@ class SSETransport(MCPBaseTransport):
344
418
  )
345
419
 
346
420
  if 'error' in response:
421
+ if self.enable_metrics:
422
+ self._update_metrics(time.time() - start_time, False)
423
+
347
424
  return {
348
425
  "isError": True,
349
426
  "error": response['error'].get('message', 'Unknown error')
350
427
  }
351
428
 
352
- # Extract result
429
+ # Extract and normalize result using base class method
353
430
  result = response.get('result', {})
431
+ normalized_result = self._normalize_mcp_response({"result": result})
354
432
 
355
- # Handle content format
356
- if 'content' in result:
357
- content = result['content']
358
- if isinstance(content, list) and len(content) == 1:
359
- content_item = content[0]
360
- if isinstance(content_item, dict) and content_item.get('type') == 'text':
361
- text_content = content_item.get('text', '')
362
- try:
363
- # Try to parse as JSON
364
- parsed_content = json.loads(text_content)
365
- return {
366
- "isError": False,
367
- "content": parsed_content
368
- }
369
- except json.JSONDecodeError:
370
- return {
371
- "isError": False,
372
- "content": text_content
373
- }
374
-
375
- return {
376
- "isError": False,
377
- "content": content
378
- }
433
+ if self.enable_metrics:
434
+ self._update_metrics(time.time() - start_time, True)
379
435
 
380
- return {
381
- "isError": False,
382
- "content": result
383
- }
436
+ return normalized_result
384
437
 
385
438
  except asyncio.TimeoutError:
439
+ if self.enable_metrics:
440
+ self._update_metrics(time.time() - start_time, False)
441
+
386
442
  return {
387
443
  "isError": True,
388
444
  "error": "Tool execution timed out"
389
445
  }
390
446
  except Exception as e:
391
- logger.error("Error calling tool %s: %s", tool_name, e)
447
+ if self.enable_metrics:
448
+ self._update_metrics(time.time() - start_time, False)
449
+
450
+ logger.error("Error calling tool '%s': %s", tool_name, e)
392
451
  return {
393
452
  "isError": True,
394
453
  "error": str(e)
395
454
  }
396
455
 
456
+ def _update_metrics(self, response_time: float, success: bool) -> None:
457
+ """Update performance metrics."""
458
+ if success:
459
+ self._metrics["successful_calls"] += 1
460
+ else:
461
+ self._metrics["failed_calls"] += 1
462
+
463
+ self._metrics["total_time"] += response_time
464
+ # FIXED: Only calculate average if we have total calls
465
+ if self._metrics["total_calls"] > 0:
466
+ self._metrics["avg_response_time"] = (
467
+ self._metrics["total_time"] / self._metrics["total_calls"]
468
+ )
469
+
397
470
  async def list_resources(self) -> Dict[str, Any]:
398
- """List resources."""
471
+ """List available resources from the server."""
399
472
  if not self._initialized:
400
473
  return {}
401
474
 
@@ -405,11 +478,12 @@ class SSETransport(MCPBaseTransport):
405
478
  logger.debug("Resources not supported: %s", response['error'])
406
479
  return {}
407
480
  return response.get('result', {})
408
- except Exception:
481
+ except Exception as e:
482
+ logger.debug("Error listing resources: %s", e)
409
483
  return {}
410
484
 
411
485
  async def list_prompts(self) -> Dict[str, Any]:
412
- """List prompts."""
486
+ """List available prompts from the server."""
413
487
  if not self._initialized:
414
488
  return {}
415
489
 
@@ -419,59 +493,104 @@ class SSETransport(MCPBaseTransport):
419
493
  logger.debug("Prompts not supported: %s", response['error'])
420
494
  return {}
421
495
  return response.get('result', {})
422
- except Exception:
496
+ except Exception as e:
497
+ logger.debug("Error listing prompts: %s", e)
423
498
  return {}
424
499
 
425
500
  async def close(self) -> None:
426
- """Close the transport."""
501
+ """Close the transport and clean up resources."""
502
+ if not self._initialized:
503
+ return
504
+
505
+ # Log final metrics
506
+ if self.enable_metrics and self._metrics["total_calls"] > 0:
507
+ logger.debug(
508
+ "SSE transport closing - Total calls: %d, Success rate: %.1f%%, Avg response time: %.3fs",
509
+ self._metrics["total_calls"],
510
+ (self._metrics["successful_calls"] / self._metrics["total_calls"] * 100),
511
+ self._metrics["avg_response_time"]
512
+ )
513
+
427
514
  await self._cleanup()
428
515
 
429
516
  async def _cleanup(self) -> None:
430
- """Clean up resources."""
431
- if self.sse_task:
517
+ """Clean up all resources and reset state."""
518
+ # Cancel SSE processing task
519
+ if self.sse_task and not self.sse_task.done():
432
520
  self.sse_task.cancel()
433
521
  try:
434
522
  await self.sse_task
435
523
  except asyncio.CancelledError:
436
524
  pass
437
525
 
526
+ # Close SSE stream context
438
527
  if self.sse_stream_context:
439
528
  try:
440
529
  await self.sse_stream_context.__aexit__(None, None, None)
441
- except Exception:
442
- pass
530
+ except Exception as e:
531
+ logger.debug("Error closing SSE stream: %s", e)
443
532
 
533
+ # Close HTTP clients
444
534
  if self.stream_client:
445
535
  await self.stream_client.aclose()
446
536
 
447
537
  if self.send_client:
448
538
  await self.send_client.aclose()
449
539
 
540
+ # Cancel any pending requests
541
+ for request_id, future in self.pending_requests.items():
542
+ if not future.done():
543
+ future.cancel()
544
+
545
+ # Reset state
450
546
  self._initialized = False
451
547
  self.session_id = None
452
548
  self.message_url = None
453
549
  self.pending_requests.clear()
550
+ self.sse_task = None
551
+ self.sse_response = None
552
+ self.sse_stream_context = None
553
+ self.stream_client = None
554
+ self.send_client = None
454
555
 
556
+ # ------------------------------------------------------------------ #
557
+ # Metrics and monitoring (consistent with other transports) #
558
+ # ------------------------------------------------------------------ #
559
+ def get_metrics(self) -> Dict[str, Any]:
560
+ """Get performance and connection metrics."""
561
+ return self._metrics.copy()
562
+
563
+ def reset_metrics(self) -> None:
564
+ """Reset performance metrics."""
565
+ self._metrics = {
566
+ "total_calls": 0,
567
+ "successful_calls": 0,
568
+ "failed_calls": 0,
569
+ "total_time": 0.0,
570
+ "avg_response_time": 0.0,
571
+ "last_ping_time": self._metrics.get("last_ping_time"),
572
+ "initialization_time": self._metrics.get("initialization_time"),
573
+ "session_discoveries": self._metrics.get("session_discoveries", 0),
574
+ "stream_errors": 0
575
+ }
576
+
577
+ # ------------------------------------------------------------------ #
578
+ # Backward compatibility #
579
+ # ------------------------------------------------------------------ #
455
580
  def get_streams(self) -> List[tuple]:
456
- """Not applicable for this transport."""
581
+ """SSE transport doesn't expose raw streams."""
457
582
  return []
458
583
 
459
- def is_connected(self) -> bool:
460
- """Check if connected."""
461
- return self._initialized and self.session_id is not None
462
-
584
+ # ------------------------------------------------------------------ #
585
+ # Context manager support (now uses base class with fixed error) #
586
+ # ------------------------------------------------------------------ #
463
587
  async def __aenter__(self):
464
- """Context manager support."""
588
+ """Context manager entry."""
465
589
  success = await self.initialize()
466
590
  if not success:
467
- raise RuntimeError("Failed to initialize SSE transport")
591
+ raise RuntimeError("Failed to initialize SSETransport") # FIXED: message
468
592
  return self
469
593
 
470
594
  async def __aexit__(self, exc_type, exc_val, exc_tb):
471
595
  """Context manager cleanup."""
472
- await self.close()
473
-
474
- def __repr__(self) -> str:
475
- """String representation."""
476
- status = "initialized" if self._initialized else "not initialized"
477
- return f"SSETransport(status={status}, url={self.url}, session={self.session_id})"
596
+ await self.close()