chuk-tool-processor 0.6.10__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.

@@ -2,15 +2,8 @@
2
2
  """
3
3
  SSE transport for MCP communication.
4
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.
11
-
12
- FIXED: Updated to support both old format (/messages/) and new event-based format
13
- (event: endpoint + data: https://...) for session discovery.
5
+ FIXED: Removed problematic gateway connectivity test that was causing 401 errors.
6
+ The SSE endpoint works perfectly, so we don't need to test the base URL.
14
7
  """
15
8
  from __future__ import annotations
16
9
 
@@ -32,28 +25,16 @@ class SSETransport(MCPBaseTransport):
32
25
  """
33
26
  SSE transport implementing the MCP protocol over Server-Sent Events.
34
27
 
35
- This transport uses a dual-connection approach:
36
- - SSE stream for receiving responses
37
- - HTTP POST for sending requests
38
-
39
- FIXED: Supports both old and new session discovery formats.
28
+ FIXED: Uses SSE endpoint directly without problematic connectivity tests.
40
29
  """
41
30
 
42
31
  def __init__(self, url: str, api_key: Optional[str] = None,
43
32
  headers: Optional[Dict[str, str]] = None,
44
33
  connection_timeout: float = 30.0,
45
- default_timeout: float = 30.0,
34
+ default_timeout: float = 60.0,
46
35
  enable_metrics: bool = True):
47
36
  """
48
37
  Initialize SSE transport.
49
-
50
- Args:
51
- url: Base URL for the MCP server
52
- api_key: Optional API key for authentication
53
- headers: Optional custom headers
54
- connection_timeout: Timeout for initial connection setup
55
- default_timeout: Default timeout for operations
56
- enable_metrics: Whether to track performance metrics
57
38
  """
58
39
  self.url = url.rstrip('/')
59
40
  self.api_key = api_key
@@ -63,10 +44,6 @@ class SSETransport(MCPBaseTransport):
63
44
  self.enable_metrics = enable_metrics
64
45
 
65
46
  logger.debug("SSE Transport initialized with URL: %s", self.url)
66
- if self.api_key:
67
- logger.debug("API key configured for authentication")
68
- if self.configured_headers:
69
- logger.debug("Custom headers configured: %s", list(self.configured_headers.keys()))
70
47
 
71
48
  # Connection state
72
49
  self.session_id = None
@@ -83,7 +60,12 @@ class SSETransport(MCPBaseTransport):
83
60
  self.sse_response = None
84
61
  self.sse_stream_context = None
85
62
 
86
- # Performance metrics (consistent with other transports)
63
+ # Health monitoring
64
+ self._last_successful_ping = None
65
+ self._consecutive_failures = 0
66
+ self._max_consecutive_failures = 3
67
+
68
+ # Performance metrics
87
69
  self._metrics = {
88
70
  "total_calls": 0,
89
71
  "successful_calls": 0,
@@ -97,11 +79,7 @@ class SSETransport(MCPBaseTransport):
97
79
  }
98
80
 
99
81
  def _construct_sse_url(self, base_url: str) -> str:
100
- """
101
- Construct the SSE endpoint URL from the base URL.
102
-
103
- Smart detection to avoid double-appending /sse if already present.
104
- """
82
+ """Construct the SSE endpoint URL from the base URL."""
105
83
  base_url = base_url.rstrip('/')
106
84
 
107
85
  if base_url.endswith('/sse'):
@@ -114,20 +92,34 @@ class SSETransport(MCPBaseTransport):
114
92
 
115
93
  def _get_headers(self) -> Dict[str, str]:
116
94
  """Get headers with authentication and custom headers."""
117
- headers = {}
95
+ headers = {
96
+ 'User-Agent': 'chuk-tool-processor/1.0.0',
97
+ 'Accept': 'text/event-stream',
98
+ 'Cache-Control': 'no-cache',
99
+ }
118
100
 
119
101
  # Add configured headers first
120
102
  if self.configured_headers:
121
103
  headers.update(self.configured_headers)
122
104
 
123
- # Add API key as Bearer token if provided (overrides Authorization header)
105
+ # Add API key as Bearer token if provided
124
106
  if self.api_key:
125
107
  headers['Authorization'] = f'Bearer {self.api_key}'
126
108
 
127
109
  return headers
128
110
 
111
+ async def _test_gateway_connectivity(self) -> bool:
112
+ """
113
+ Skip connectivity test - we know the SSE endpoint works.
114
+
115
+ FIXED: The diagnostic proves SSE endpoint works perfectly.
116
+ No need to test base URL that causes 401 errors.
117
+ """
118
+ logger.debug("Skipping gateway connectivity test - using direct SSE connection")
119
+ return True
120
+
129
121
  async def initialize(self) -> bool:
130
- """Initialize SSE connection and perform MCP handshake."""
122
+ """Initialize SSE connection."""
131
123
  if self._initialized:
132
124
  logger.warning("Transport already initialized")
133
125
  return True
@@ -137,9 +129,22 @@ class SSETransport(MCPBaseTransport):
137
129
  try:
138
130
  logger.debug("Initializing SSE transport...")
139
131
 
140
- # Create HTTP clients with appropriate timeouts
141
- self.stream_client = httpx.AsyncClient(timeout=self.connection_timeout)
142
- self.send_client = httpx.AsyncClient(timeout=self.default_timeout)
132
+ # FIXED: Skip problematic connectivity test
133
+ if not await self._test_gateway_connectivity():
134
+ logger.error("Gateway connectivity test failed")
135
+ return False
136
+
137
+ # Create HTTP clients
138
+ self.stream_client = httpx.AsyncClient(
139
+ timeout=httpx.Timeout(self.connection_timeout),
140
+ follow_redirects=True,
141
+ limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
142
+ )
143
+ self.send_client = httpx.AsyncClient(
144
+ timeout=httpx.Timeout(self.default_timeout),
145
+ follow_redirects=True,
146
+ limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
147
+ )
143
148
 
144
149
  # Connect to SSE stream
145
150
  sse_url = self._construct_sse_url(self.url)
@@ -163,13 +168,21 @@ class SSETransport(MCPBaseTransport):
163
168
  name="sse_stream_processor"
164
169
  )
165
170
 
166
- # Wait for session discovery with timeout
171
+ # Wait for session discovery
167
172
  logger.debug("Waiting for session discovery...")
168
- session_timeout = 5.0 # 5 seconds max for session discovery
173
+ session_timeout = 10.0
169
174
  session_start = time.time()
170
175
 
171
176
  while not self.message_url and (time.time() - session_start) < session_timeout:
172
177
  await asyncio.sleep(0.1)
178
+
179
+ # Check if SSE task died
180
+ if self.sse_task.done():
181
+ exception = self.sse_task.exception()
182
+ if exception:
183
+ logger.error(f"SSE task died during session discovery: {exception}")
184
+ await self._cleanup()
185
+ return False
173
186
 
174
187
  if not self.message_url:
175
188
  logger.error("Failed to discover session endpoint within %.1fs", session_timeout)
@@ -179,7 +192,7 @@ class SSETransport(MCPBaseTransport):
179
192
  if self.enable_metrics:
180
193
  self._metrics["session_discoveries"] += 1
181
194
 
182
- logger.debug("Session endpoint discovered: %s", self.session_id)
195
+ logger.debug("Session endpoint discovered: %s", self.message_url)
183
196
 
184
197
  # Perform MCP initialization handshake
185
198
  try:
@@ -190,7 +203,7 @@ class SSETransport(MCPBaseTransport):
190
203
  "name": "chuk-tool-processor",
191
204
  "version": "1.0.0"
192
205
  }
193
- })
206
+ }, timeout=self.default_timeout)
194
207
 
195
208
  if 'error' in init_response:
196
209
  logger.error("MCP initialize failed: %s", init_response['error'])
@@ -201,6 +214,7 @@ class SSETransport(MCPBaseTransport):
201
214
  await self._send_notification("notifications/initialized")
202
215
 
203
216
  self._initialized = True
217
+ self._last_successful_ping = time.time()
204
218
 
205
219
  if self.enable_metrics:
206
220
  init_time = time.time() - start_time
@@ -220,16 +234,11 @@ class SSETransport(MCPBaseTransport):
220
234
  return False
221
235
 
222
236
  async def _process_sse_stream(self):
223
- """
224
- Process the persistent SSE stream for responses and session discovery.
225
-
226
- FIXED: Supports both old format (/messages/) and new event-based format
227
- (event: endpoint + data: https://...) for session discovery.
228
- """
237
+ """Process the SSE stream for responses and session discovery."""
229
238
  try:
230
239
  logger.debug("Starting SSE stream processing...")
231
240
 
232
- current_event = None # Track current event type
241
+ current_event = None
233
242
 
234
243
  async for line in self.sse_response.aiter_lines():
235
244
  line = line.strip()
@@ -242,7 +251,7 @@ class SSETransport(MCPBaseTransport):
242
251
  logger.debug("SSE event type: %s", current_event)
243
252
  continue
244
253
 
245
- # Handle session endpoint discovery (BOTH FORMATS)
254
+ # Handle session endpoint discovery
246
255
  if not self.message_url and line.startswith('data:'):
247
256
  data_part = line.split(':', 1)[1].strip()
248
257
 
@@ -253,8 +262,10 @@ class SSETransport(MCPBaseTransport):
253
262
  # Extract session ID from URL if present
254
263
  if 'session_id=' in data_part:
255
264
  self.session_id = data_part.split('session_id=')[1].split('&')[0]
265
+ else:
266
+ self.session_id = str(uuid.uuid4())
256
267
 
257
- logger.debug("Session endpoint discovered via event format: %s", self.session_id)
268
+ logger.debug("Session endpoint discovered via event format: %s", self.message_url)
258
269
  continue
259
270
 
260
271
  # OLD FORMAT: data: /messages/... (backwards compatibility)
@@ -265,8 +276,10 @@ class SSETransport(MCPBaseTransport):
265
276
  # Extract session ID if present
266
277
  if 'session_id=' in endpoint_path:
267
278
  self.session_id = endpoint_path.split('session_id=')[1].split('&')[0]
279
+ else:
280
+ self.session_id = str(uuid.uuid4())
268
281
 
269
- logger.debug("Session endpoint discovered via old format: %s", self.session_id)
282
+ logger.debug("Session endpoint discovered via old format: %s", self.message_url)
270
283
  continue
271
284
 
272
285
  # Handle JSON-RPC responses
@@ -293,15 +306,12 @@ class SSETransport(MCPBaseTransport):
293
306
 
294
307
  except json.JSONDecodeError as e:
295
308
  logger.debug("Non-JSON data in SSE stream (ignoring): %s", e)
296
-
297
- # Reset event type after processing data (only if we processed JSON-RPC)
298
- if line.startswith('data:') and current_event not in ("endpoint",):
299
- current_event = None
300
309
 
301
310
  except Exception as e:
302
311
  if self.enable_metrics:
303
312
  self._metrics["stream_errors"] += 1
304
313
  logger.error("SSE stream processing error: %s", e)
314
+ self._consecutive_failures += 1
305
315
 
306
316
  async def _send_request(self, method: str, params: Dict[str, Any] = None,
307
317
  timeout: Optional[float] = None) -> Dict[str, Any]:
@@ -338,20 +348,25 @@ class SSETransport(MCPBaseTransport):
338
348
  # Async response - wait for result via SSE
339
349
  request_timeout = timeout or self.default_timeout
340
350
  result = await asyncio.wait_for(future, timeout=request_timeout)
351
+ self._consecutive_failures = 0 # Reset on success
341
352
  return result
342
353
  elif response.status_code == 200:
343
354
  # Immediate response
344
355
  self.pending_requests.pop(request_id, None)
356
+ self._consecutive_failures = 0 # Reset on success
345
357
  return response.json()
346
358
  else:
347
359
  self.pending_requests.pop(request_id, None)
360
+ self._consecutive_failures += 1
348
361
  raise RuntimeError(f"HTTP request failed with status: {response.status_code}")
349
362
 
350
363
  except asyncio.TimeoutError:
351
364
  self.pending_requests.pop(request_id, None)
365
+ self._consecutive_failures += 1
352
366
  raise
353
367
  except Exception:
354
368
  self.pending_requests.pop(request_id, None)
369
+ self._consecutive_failures += 1
355
370
  raise
356
371
 
357
372
  async def _send_notification(self, method: str, params: Dict[str, Any] = None):
@@ -387,21 +402,43 @@ class SSETransport(MCPBaseTransport):
387
402
  start_time = time.time()
388
403
  try:
389
404
  # Use tools/list as a lightweight ping since not all servers support ping
390
- response = await self._send_request("tools/list", {}, timeout=5.0)
405
+ response = await self._send_request("tools/list", {}, timeout=10.0)
406
+
407
+ success = 'error' not in response
408
+
409
+ if success:
410
+ self._last_successful_ping = time.time()
411
+ self._consecutive_failures = 0
391
412
 
392
413
  if self.enable_metrics:
393
414
  ping_time = time.time() - start_time
394
415
  self._metrics["last_ping_time"] = ping_time
395
- logger.debug("SSE ping completed in %.3fs", ping_time)
416
+ logger.debug("SSE ping completed in %.3fs: %s", ping_time, success)
396
417
 
397
- return 'error' not in response
418
+ return success
398
419
  except Exception as e:
399
420
  logger.debug("SSE ping failed: %s", e)
421
+ self._consecutive_failures += 1
400
422
  return False
401
423
 
402
424
  def is_connected(self) -> bool:
403
425
  """Check if the transport is connected and ready."""
404
- return self._initialized and self.session_id is not None
426
+ if not self._initialized or not self.session_id:
427
+ return False
428
+
429
+ # Check if we've had too many consecutive failures
430
+ if self._consecutive_failures >= self._max_consecutive_failures:
431
+ logger.warning(f"Connection marked unhealthy after {self._consecutive_failures} failures")
432
+ return False
433
+
434
+ # Check if SSE task is still running
435
+ if self.sse_task and self.sse_task.done():
436
+ exception = self.sse_task.exception()
437
+ if exception:
438
+ logger.warning(f"SSE task died: {exception}")
439
+ return False
440
+
441
+ return True
405
442
 
406
443
  async def get_tools(self) -> List[Dict[str, Any]]:
407
444
  """Get list of available tools from the server."""
@@ -589,9 +626,6 @@ class SSETransport(MCPBaseTransport):
589
626
  self.stream_client = None
590
627
  self.send_client = None
591
628
 
592
- # ------------------------------------------------------------------ #
593
- # Metrics and monitoring (consistent with other transports) #
594
- # ------------------------------------------------------------------ #
595
629
  def get_metrics(self) -> Dict[str, Any]:
596
630
  """Get performance and connection metrics."""
597
631
  return self._metrics.copy()
@@ -610,16 +644,10 @@ class SSETransport(MCPBaseTransport):
610
644
  "stream_errors": 0
611
645
  }
612
646
 
613
- # ------------------------------------------------------------------ #
614
- # Backward compatibility #
615
- # ------------------------------------------------------------------ #
616
647
  def get_streams(self) -> List[tuple]:
617
648
  """SSE transport doesn't expose raw streams."""
618
649
  return []
619
650
 
620
- # ------------------------------------------------------------------ #
621
- # Context manager support #
622
- # ------------------------------------------------------------------ #
623
651
  async def __aenter__(self):
624
652
  """Context manager entry."""
625
653
  success = await self.initialize()