chuk-tool-processor 0.6.12__py3-none-any.whl → 0.6.13__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.

Files changed (56) hide show
  1. chuk_tool_processor/core/__init__.py +1 -1
  2. chuk_tool_processor/core/exceptions.py +10 -4
  3. chuk_tool_processor/core/processor.py +97 -97
  4. chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
  5. chuk_tool_processor/execution/strategies/subprocess_strategy.py +200 -205
  6. chuk_tool_processor/execution/tool_executor.py +82 -84
  7. chuk_tool_processor/execution/wrappers/caching.py +102 -103
  8. chuk_tool_processor/execution/wrappers/rate_limiting.py +45 -42
  9. chuk_tool_processor/execution/wrappers/retry.py +23 -25
  10. chuk_tool_processor/logging/__init__.py +23 -17
  11. chuk_tool_processor/logging/context.py +40 -45
  12. chuk_tool_processor/logging/formatter.py +22 -21
  13. chuk_tool_processor/logging/helpers.py +24 -38
  14. chuk_tool_processor/logging/metrics.py +11 -13
  15. chuk_tool_processor/mcp/__init__.py +8 -12
  16. chuk_tool_processor/mcp/mcp_tool.py +124 -112
  17. chuk_tool_processor/mcp/register_mcp_tools.py +17 -17
  18. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +11 -13
  19. chuk_tool_processor/mcp/setup_mcp_sse.py +11 -13
  20. chuk_tool_processor/mcp/setup_mcp_stdio.py +7 -9
  21. chuk_tool_processor/mcp/stream_manager.py +168 -204
  22. chuk_tool_processor/mcp/transport/__init__.py +4 -4
  23. chuk_tool_processor/mcp/transport/base_transport.py +43 -58
  24. chuk_tool_processor/mcp/transport/http_streamable_transport.py +145 -163
  25. chuk_tool_processor/mcp/transport/sse_transport.py +217 -255
  26. chuk_tool_processor/mcp/transport/stdio_transport.py +171 -189
  27. chuk_tool_processor/models/__init__.py +1 -1
  28. chuk_tool_processor/models/execution_strategy.py +16 -21
  29. chuk_tool_processor/models/streaming_tool.py +28 -25
  30. chuk_tool_processor/models/tool_call.py +19 -34
  31. chuk_tool_processor/models/tool_export_mixin.py +22 -8
  32. chuk_tool_processor/models/tool_result.py +40 -77
  33. chuk_tool_processor/models/validated_tool.py +14 -16
  34. chuk_tool_processor/plugins/__init__.py +1 -1
  35. chuk_tool_processor/plugins/discovery.py +10 -10
  36. chuk_tool_processor/plugins/parsers/__init__.py +1 -1
  37. chuk_tool_processor/plugins/parsers/base.py +1 -2
  38. chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
  39. chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
  40. chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
  41. chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
  42. chuk_tool_processor/registry/__init__.py +12 -12
  43. chuk_tool_processor/registry/auto_register.py +22 -30
  44. chuk_tool_processor/registry/decorators.py +127 -129
  45. chuk_tool_processor/registry/interface.py +26 -23
  46. chuk_tool_processor/registry/metadata.py +27 -22
  47. chuk_tool_processor/registry/provider.py +17 -18
  48. chuk_tool_processor/registry/providers/__init__.py +16 -19
  49. chuk_tool_processor/registry/providers/memory.py +18 -25
  50. chuk_tool_processor/registry/tool_export.py +42 -51
  51. chuk_tool_processor/utils/validation.py +15 -16
  52. {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/METADATA +1 -1
  53. chuk_tool_processor-0.6.13.dist-info/RECORD +60 -0
  54. chuk_tool_processor-0.6.12.dist-info/RECORD +0 -60
  55. {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/WHEEL +0 -0
  56. {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/top_level.txt +0 -0
@@ -5,14 +5,16 @@ SSE transport for MCP communication.
5
5
  FIXED: Improved health monitoring to avoid false unhealthy states.
6
6
  The SSE endpoint works perfectly, so we need more lenient health checks.
7
7
  """
8
+
8
9
  from __future__ import annotations
9
10
 
10
11
  import asyncio
12
+ import contextlib
11
13
  import json
14
+ import logging
12
15
  import time
13
16
  import uuid
14
- from typing import Dict, Any, List, Optional
15
- import logging
17
+ from typing import Any
16
18
 
17
19
  import httpx
18
20
 
@@ -24,49 +26,53 @@ logger = logging.getLogger(__name__)
24
26
  class SSETransport(MCPBaseTransport):
25
27
  """
26
28
  SSE transport implementing the MCP protocol over Server-Sent Events.
27
-
29
+
28
30
  FIXED: More lenient health monitoring to avoid false unhealthy states.
29
31
  """
30
32
 
31
- def __init__(self, url: str, api_key: Optional[str] = None,
32
- headers: Optional[Dict[str, str]] = None,
33
- connection_timeout: float = 30.0,
34
- default_timeout: float = 60.0,
35
- enable_metrics: bool = True):
33
+ def __init__(
34
+ self,
35
+ url: str,
36
+ api_key: str | None = None,
37
+ headers: dict[str, str] | None = None,
38
+ connection_timeout: float = 30.0,
39
+ default_timeout: float = 60.0,
40
+ enable_metrics: bool = True,
41
+ ):
36
42
  """
37
43
  Initialize SSE transport.
38
44
  """
39
- self.url = url.rstrip('/')
45
+ self.url = url.rstrip("/")
40
46
  self.api_key = api_key
41
47
  self.configured_headers = headers or {}
42
48
  self.connection_timeout = connection_timeout
43
49
  self.default_timeout = default_timeout
44
50
  self.enable_metrics = enable_metrics
45
-
51
+
46
52
  logger.debug("SSE Transport initialized with URL: %s", self.url)
47
-
53
+
48
54
  # Connection state
49
55
  self.session_id = None
50
56
  self.message_url = None
51
- self.pending_requests: Dict[str, asyncio.Future] = {}
57
+ self.pending_requests: dict[str, asyncio.Future] = {}
52
58
  self._initialized = False
53
-
59
+
54
60
  # HTTP clients
55
61
  self.stream_client = None
56
62
  self.send_client = None
57
-
63
+
58
64
  # SSE stream management
59
65
  self.sse_task = None
60
66
  self.sse_response = None
61
67
  self.sse_stream_context = None
62
-
68
+
63
69
  # FIXED: More lenient health monitoring
64
70
  self._last_successful_ping = None
65
71
  self._consecutive_failures = 0
66
72
  self._max_consecutive_failures = 5 # INCREASED: was 3, now 5
67
73
  self._connection_grace_period = 30.0 # NEW: Grace period after initialization
68
74
  self._initialization_time = None # NEW: Track when we initialized
69
-
75
+
70
76
  # Performance metrics
71
77
  self._metrics = {
72
78
  "total_calls": 0,
@@ -77,43 +83,43 @@ class SSETransport(MCPBaseTransport):
77
83
  "last_ping_time": None,
78
84
  "initialization_time": None,
79
85
  "session_discoveries": 0,
80
- "stream_errors": 0
86
+ "stream_errors": 0,
81
87
  }
82
88
 
83
89
  def _construct_sse_url(self, base_url: str) -> str:
84
90
  """Construct the SSE endpoint URL from the base URL."""
85
- base_url = base_url.rstrip('/')
86
-
87
- if base_url.endswith('/sse'):
91
+ base_url = base_url.rstrip("/")
92
+
93
+ if base_url.endswith("/sse"):
88
94
  logger.debug("URL already contains /sse endpoint: %s", base_url)
89
95
  return base_url
90
-
96
+
91
97
  sse_url = f"{base_url}/sse"
92
98
  logger.debug("Constructed SSE URL: %s -> %s", base_url, sse_url)
93
99
  return sse_url
94
100
 
95
- def _get_headers(self) -> Dict[str, str]:
101
+ def _get_headers(self) -> dict[str, str]:
96
102
  """Get headers with authentication and custom headers."""
97
103
  headers = {
98
- 'User-Agent': 'chuk-tool-processor/1.0.0',
99
- 'Accept': 'text/event-stream',
100
- 'Cache-Control': 'no-cache',
104
+ "User-Agent": "chuk-tool-processor/1.0.0",
105
+ "Accept": "text/event-stream",
106
+ "Cache-Control": "no-cache",
101
107
  }
102
-
108
+
103
109
  # Add configured headers first
104
110
  if self.configured_headers:
105
111
  headers.update(self.configured_headers)
106
-
112
+
107
113
  # Add API key as Bearer token if provided
108
114
  if self.api_key:
109
- headers['Authorization'] = f'Bearer {self.api_key}'
110
-
115
+ headers["Authorization"] = f"Bearer {self.api_key}"
116
+
111
117
  return headers
112
118
 
113
119
  async def _test_gateway_connectivity(self) -> bool:
114
120
  """
115
121
  Skip connectivity test - we know the SSE endpoint works.
116
-
122
+
117
123
  FIXED: The diagnostic proves SSE endpoint works perfectly.
118
124
  No need to test base URL that causes 401 errors.
119
125
  """
@@ -125,59 +131,54 @@ class SSETransport(MCPBaseTransport):
125
131
  if self._initialized:
126
132
  logger.warning("Transport already initialized")
127
133
  return True
128
-
134
+
129
135
  start_time = time.time()
130
-
136
+
131
137
  try:
132
138
  logger.debug("Initializing SSE transport...")
133
-
139
+
134
140
  # FIXED: Skip problematic connectivity test
135
141
  if not await self._test_gateway_connectivity():
136
142
  logger.error("Gateway connectivity test failed")
137
143
  return False
138
-
144
+
139
145
  # Create HTTP clients
140
146
  self.stream_client = httpx.AsyncClient(
141
147
  timeout=httpx.Timeout(self.connection_timeout),
142
148
  follow_redirects=True,
143
- limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
149
+ limits=httpx.Limits(max_connections=10, max_keepalive_connections=5),
144
150
  )
145
151
  self.send_client = httpx.AsyncClient(
146
152
  timeout=httpx.Timeout(self.default_timeout),
147
153
  follow_redirects=True,
148
- limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
154
+ limits=httpx.Limits(max_connections=10, max_keepalive_connections=5),
149
155
  )
150
-
156
+
151
157
  # Connect to SSE stream
152
158
  sse_url = self._construct_sse_url(self.url)
153
159
  logger.debug("Connecting to SSE endpoint: %s", sse_url)
154
-
155
- self.sse_stream_context = self.stream_client.stream(
156
- 'GET', sse_url, headers=self._get_headers()
157
- )
160
+
161
+ self.sse_stream_context = self.stream_client.stream("GET", sse_url, headers=self._get_headers())
158
162
  self.sse_response = await self.sse_stream_context.__aenter__()
159
-
163
+
160
164
  if self.sse_response.status_code != 200:
161
165
  logger.error("SSE connection failed with status: %s", self.sse_response.status_code)
162
166
  await self._cleanup()
163
167
  return False
164
-
168
+
165
169
  logger.debug("SSE streaming connection established")
166
-
170
+
167
171
  # Start SSE processing task
168
- self.sse_task = asyncio.create_task(
169
- self._process_sse_stream(),
170
- name="sse_stream_processor"
171
- )
172
-
172
+ self.sse_task = asyncio.create_task(self._process_sse_stream(), name="sse_stream_processor")
173
+
173
174
  # Wait for session discovery
174
175
  logger.debug("Waiting for session discovery...")
175
176
  session_timeout = 10.0
176
177
  session_start = time.time()
177
-
178
+
178
179
  while not self.message_url and (time.time() - session_start) < session_timeout:
179
180
  await asyncio.sleep(0.1)
180
-
181
+
181
182
  # Check if SSE task died
182
183
  if self.sse_task.done():
183
184
  exception = self.sse_task.exception()
@@ -185,54 +186,55 @@ class SSETransport(MCPBaseTransport):
185
186
  logger.error(f"SSE task died during session discovery: {exception}")
186
187
  await self._cleanup()
187
188
  return False
188
-
189
+
189
190
  if not self.message_url:
190
191
  logger.error("Failed to discover session endpoint within %.1fs", session_timeout)
191
192
  await self._cleanup()
192
193
  return False
193
-
194
+
194
195
  if self.enable_metrics:
195
196
  self._metrics["session_discoveries"] += 1
196
-
197
+
197
198
  logger.debug("Session endpoint discovered: %s", self.message_url)
198
-
199
+
199
200
  # Perform MCP initialization handshake
200
201
  try:
201
- init_response = await self._send_request("initialize", {
202
- "protocolVersion": "2024-11-05",
203
- "capabilities": {},
204
- "clientInfo": {
205
- "name": "chuk-tool-processor",
206
- "version": "1.0.0"
207
- }
208
- }, timeout=self.default_timeout)
209
-
210
- if 'error' in init_response:
211
- logger.error("MCP initialize failed: %s", init_response['error'])
202
+ init_response = await self._send_request(
203
+ "initialize",
204
+ {
205
+ "protocolVersion": "2024-11-05",
206
+ "capabilities": {},
207
+ "clientInfo": {"name": "chuk-tool-processor", "version": "1.0.0"},
208
+ },
209
+ timeout=self.default_timeout,
210
+ )
211
+
212
+ if "error" in init_response:
213
+ logger.error("MCP initialize failed: %s", init_response["error"])
212
214
  await self._cleanup()
213
215
  return False
214
-
216
+
215
217
  # Send initialized notification
216
218
  await self._send_notification("notifications/initialized")
217
-
219
+
218
220
  # FIXED: Set health tracking state
219
221
  self._initialized = True
220
222
  self._initialization_time = time.time()
221
223
  self._last_successful_ping = time.time()
222
224
  self._consecutive_failures = 0 # Reset failure count
223
-
225
+
224
226
  if self.enable_metrics:
225
227
  init_time = time.time() - start_time
226
228
  self._metrics["initialization_time"] = init_time
227
-
229
+
228
230
  logger.debug("SSE transport initialized successfully in %.3fs", time.time() - start_time)
229
231
  return True
230
-
232
+
231
233
  except Exception as e:
232
234
  logger.error("MCP handshake failed: %s", e)
233
235
  await self._cleanup()
234
236
  return False
235
-
237
+
236
238
  except Exception as e:
237
239
  logger.error("Error initializing SSE transport: %s", e, exc_info=True)
238
240
  await self._cleanup()
@@ -242,76 +244,76 @@ class SSETransport(MCPBaseTransport):
242
244
  """Process the SSE stream for responses and session discovery."""
243
245
  try:
244
246
  logger.debug("Starting SSE stream processing...")
245
-
247
+
246
248
  current_event = None
247
-
249
+
248
250
  async for line in self.sse_response.aiter_lines():
249
251
  line = line.strip()
250
252
  if not line:
251
253
  continue
252
-
254
+
253
255
  # Handle event type declarations
254
- if line.startswith('event:'):
255
- current_event = line.split(':', 1)[1].strip()
256
+ if line.startswith("event:"):
257
+ current_event = line.split(":", 1)[1].strip()
256
258
  logger.debug("SSE event type: %s", current_event)
257
259
  continue
258
-
260
+
259
261
  # Handle session endpoint discovery
260
- if not self.message_url and line.startswith('data:'):
261
- data_part = line.split(':', 1)[1].strip()
262
-
262
+ if not self.message_url and line.startswith("data:"):
263
+ data_part = line.split(":", 1)[1].strip()
264
+
263
265
  # NEW FORMAT: event: endpoint + data: https://...
264
- if current_event == "endpoint" and data_part.startswith('http'):
266
+ if current_event == "endpoint" and data_part.startswith("http"):
265
267
  self.message_url = data_part
266
-
268
+
267
269
  # Extract session ID from URL if present
268
- if 'session_id=' in data_part:
269
- self.session_id = data_part.split('session_id=')[1].split('&')[0]
270
+ if "session_id=" in data_part:
271
+ self.session_id = data_part.split("session_id=")[1].split("&")[0]
270
272
  else:
271
273
  self.session_id = str(uuid.uuid4())
272
-
274
+
273
275
  logger.debug("Session endpoint discovered via event format: %s", self.message_url)
274
276
  continue
275
-
277
+
276
278
  # OLD FORMAT: data: /messages/... (backwards compatibility)
277
- elif '/messages/' in data_part:
279
+ elif "/messages/" in data_part:
278
280
  endpoint_path = data_part
279
281
  self.message_url = f"{self.url}{endpoint_path}"
280
-
282
+
281
283
  # Extract session ID if present
282
- if 'session_id=' in endpoint_path:
283
- self.session_id = endpoint_path.split('session_id=')[1].split('&')[0]
284
+ if "session_id=" in endpoint_path:
285
+ self.session_id = endpoint_path.split("session_id=")[1].split("&")[0]
284
286
  else:
285
287
  self.session_id = str(uuid.uuid4())
286
-
288
+
287
289
  logger.debug("Session endpoint discovered via old format: %s", self.message_url)
288
290
  continue
289
-
291
+
290
292
  # Handle JSON-RPC responses
291
- if line.startswith('data:'):
292
- data_part = line.split(':', 1)[1].strip()
293
-
293
+ if line.startswith("data:"):
294
+ data_part = line.split(":", 1)[1].strip()
295
+
294
296
  # Skip keepalive pings and empty data
295
- if not data_part or data_part.startswith('ping') or data_part in ('{}', '[]'):
297
+ if not data_part or data_part.startswith("ping") or data_part in ("{}", "[]"):
296
298
  continue
297
-
299
+
298
300
  try:
299
301
  response_data = json.loads(data_part)
300
-
302
+
301
303
  # Handle JSON-RPC responses with request IDs
302
- if 'jsonrpc' in response_data and 'id' in response_data:
303
- request_id = str(response_data['id'])
304
-
304
+ if "jsonrpc" in response_data and "id" in response_data:
305
+ request_id = str(response_data["id"])
306
+
305
307
  # Resolve pending request if found
306
308
  if request_id in self.pending_requests:
307
309
  future = self.pending_requests.pop(request_id)
308
310
  if not future.done():
309
311
  future.set_result(response_data)
310
312
  logger.debug("Resolved request ID: %s", request_id)
311
-
313
+
312
314
  except json.JSONDecodeError as e:
313
315
  logger.debug("Non-JSON data in SSE stream (ignoring): %s", e)
314
-
316
+
315
317
  except Exception as e:
316
318
  if self.enable_metrics:
317
319
  self._metrics["stream_errors"] += 1
@@ -319,43 +321,32 @@ class SSETransport(MCPBaseTransport):
319
321
  # FIXED: Don't increment consecutive failures for stream processing errors
320
322
  # These are often temporary and don't indicate connection health
321
323
 
322
- async def _send_request(self, method: str, params: Dict[str, Any] = None,
323
- timeout: Optional[float] = None) -> Dict[str, Any]:
324
+ async def _send_request(
325
+ self, method: str, params: dict[str, Any] = None, timeout: float | None = None
326
+ ) -> dict[str, Any]:
324
327
  """Send JSON-RPC request and wait for async response via SSE."""
325
328
  if not self.message_url:
326
329
  raise RuntimeError("SSE transport not connected - no message URL")
327
-
330
+
328
331
  request_id = str(uuid.uuid4())
329
- message = {
330
- "jsonrpc": "2.0",
331
- "id": request_id,
332
- "method": method,
333
- "params": params or {}
334
- }
335
-
332
+ message = {"jsonrpc": "2.0", "id": request_id, "method": method, "params": params or {}}
333
+
336
334
  # Create future for async response
337
335
  future = asyncio.Future()
338
336
  self.pending_requests[request_id] = future
339
-
337
+
340
338
  try:
341
339
  # Send HTTP POST request
342
- headers = {
343
- 'Content-Type': 'application/json',
344
- **self._get_headers()
345
- }
346
-
347
- response = await self.send_client.post(
348
- self.message_url,
349
- headers=headers,
350
- json=message
351
- )
352
-
340
+ headers = {"Content-Type": "application/json", **self._get_headers()}
341
+
342
+ response = await self.send_client.post(self.message_url, headers=headers, json=message)
343
+
353
344
  if response.status_code == 202:
354
345
  # Async response - wait for result via SSE
355
346
  request_timeout = timeout or self.default_timeout
356
347
  result = await asyncio.wait_for(future, timeout=request_timeout)
357
348
  # FIXED: Only reset failures on successful tool calls, not all requests
358
- if method.startswith('tools/'):
349
+ if method.startswith("tools/"):
359
350
  self._consecutive_failures = 0
360
351
  self._last_successful_ping = time.time()
361
352
  return result
@@ -363,52 +354,41 @@ class SSETransport(MCPBaseTransport):
363
354
  # Immediate response
364
355
  self.pending_requests.pop(request_id, None)
365
356
  # FIXED: Only reset failures on successful tool calls
366
- if method.startswith('tools/'):
357
+ if method.startswith("tools/"):
367
358
  self._consecutive_failures = 0
368
359
  self._last_successful_ping = time.time()
369
360
  return response.json()
370
361
  else:
371
362
  self.pending_requests.pop(request_id, None)
372
363
  # FIXED: Only increment failures for tool calls, not initialization
373
- if method.startswith('tools/'):
364
+ if method.startswith("tools/"):
374
365
  self._consecutive_failures += 1
375
366
  raise RuntimeError(f"HTTP request failed with status: {response.status_code}")
376
-
377
- except asyncio.TimeoutError:
367
+
368
+ except TimeoutError:
378
369
  self.pending_requests.pop(request_id, None)
379
370
  # FIXED: Only increment failures for tool calls
380
- if method.startswith('tools/'):
371
+ if method.startswith("tools/"):
381
372
  self._consecutive_failures += 1
382
373
  raise
383
374
  except Exception:
384
375
  self.pending_requests.pop(request_id, None)
385
376
  # FIXED: Only increment failures for tool calls
386
- if method.startswith('tools/'):
377
+ if method.startswith("tools/"):
387
378
  self._consecutive_failures += 1
388
379
  raise
389
380
 
390
- async def _send_notification(self, method: str, params: Dict[str, Any] = None):
381
+ async def _send_notification(self, method: str, params: dict[str, Any] = None):
391
382
  """Send JSON-RPC notification (no response expected)."""
392
383
  if not self.message_url:
393
384
  raise RuntimeError("SSE transport not connected - no message URL")
394
-
395
- message = {
396
- "jsonrpc": "2.0",
397
- "method": method,
398
- "params": params or {}
399
- }
400
-
401
- headers = {
402
- 'Content-Type': 'application/json',
403
- **self._get_headers()
404
- }
405
-
406
- response = await self.send_client.post(
407
- self.message_url,
408
- headers=headers,
409
- json=message
410
- )
411
-
385
+
386
+ message = {"jsonrpc": "2.0", "method": method, "params": params or {}}
387
+
388
+ headers = {"Content-Type": "application/json", **self._get_headers()}
389
+
390
+ response = await self.send_client.post(self.message_url, headers=headers, json=message)
391
+
412
392
  if response.status_code not in (200, 202):
413
393
  logger.warning("Notification failed with status: %s", response.status_code)
414
394
 
@@ -416,23 +396,23 @@ class SSETransport(MCPBaseTransport):
416
396
  """Send ping to check connection health with improved logic."""
417
397
  if not self._initialized:
418
398
  return False
419
-
399
+
420
400
  start_time = time.time()
421
401
  try:
422
402
  # Use tools/list as a lightweight ping since not all servers support ping
423
403
  response = await self._send_request("tools/list", {}, timeout=10.0)
424
-
425
- success = 'error' not in response
426
-
404
+
405
+ success = "error" not in response
406
+
427
407
  if success:
428
408
  self._last_successful_ping = time.time()
429
409
  # FIXED: Don't reset consecutive failures here - let tool calls do that
430
-
410
+
431
411
  if self.enable_metrics:
432
412
  ping_time = time.time() - start_time
433
413
  self._metrics["last_ping_time"] = ping_time
434
414
  logger.debug("SSE ping completed in %.3fs: %s", ping_time, success)
435
-
415
+
436
416
  return success
437
417
  except Exception as e:
438
418
  logger.debug("SSE ping failed: %s", e)
@@ -442,74 +422,70 @@ class SSETransport(MCPBaseTransport):
442
422
  def is_connected(self) -> bool:
443
423
  """
444
424
  FIXED: More lenient connection health check.
445
-
425
+
446
426
  The diagnostic shows the connection works fine, so we need to be less aggressive
447
427
  about marking it as unhealthy.
448
428
  """
449
429
  if not self._initialized or not self.session_id:
450
430
  return False
451
-
431
+
452
432
  # FIXED: Grace period after initialization - always return True for a while
453
- if (self._initialization_time and
454
- time.time() - self._initialization_time < self._connection_grace_period):
433
+ if self._initialization_time and time.time() - self._initialization_time < self._connection_grace_period:
455
434
  logger.debug("Within grace period - connection considered healthy")
456
435
  return True
457
-
436
+
458
437
  # FIXED: More lenient failure threshold
459
438
  if self._consecutive_failures >= self._max_consecutive_failures:
460
439
  logger.warning(f"Connection marked unhealthy after {self._consecutive_failures} consecutive failures")
461
440
  return False
462
-
441
+
463
442
  # Check if SSE task is still running
464
443
  if self.sse_task and self.sse_task.done():
465
444
  exception = self.sse_task.exception()
466
445
  if exception:
467
446
  logger.warning(f"SSE task died: {exception}")
468
447
  return False
469
-
448
+
470
449
  # FIXED: If we have a recent successful ping/tool call, we're healthy
471
- if (self._last_successful_ping and
472
- time.time() - self._last_successful_ping < 60.0): # Success within last minute
450
+ if self._last_successful_ping and time.time() - self._last_successful_ping < 60.0: # Success within last minute
473
451
  return True
474
-
452
+
475
453
  # FIXED: Default to healthy if no clear indicators of problems
476
454
  logger.debug("No clear health indicators - defaulting to healthy")
477
455
  return True
478
456
 
479
- async def get_tools(self) -> List[Dict[str, Any]]:
457
+ async def get_tools(self) -> list[dict[str, Any]]:
480
458
  """Get list of available tools from the server."""
481
459
  if not self._initialized:
482
460
  logger.error("Cannot get tools: transport not initialized")
483
461
  return []
484
-
462
+
485
463
  start_time = time.time()
486
464
  try:
487
465
  response = await self._send_request("tools/list", {})
488
-
489
- if 'error' in response:
490
- logger.error("Error getting tools: %s", response['error'])
466
+
467
+ if "error" in response:
468
+ logger.error("Error getting tools: %s", response["error"])
491
469
  return []
492
-
493
- tools = response.get('result', {}).get('tools', [])
494
-
470
+
471
+ tools = response.get("result", {}).get("tools", [])
472
+
495
473
  if self.enable_metrics:
496
474
  response_time = time.time() - start_time
497
475
  logger.debug("Retrieved %d tools in %.3fs", len(tools), response_time)
498
-
476
+
499
477
  return tools
500
-
478
+
501
479
  except Exception as e:
502
480
  logger.error("Error getting tools: %s", e)
503
481
  return []
504
482
 
505
- async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
506
- timeout: Optional[float] = None) -> Dict[str, Any]:
483
+ async def call_tool(
484
+ self, tool_name: str, arguments: dict[str, Any], timeout: float | None = None
485
+ ) -> dict[str, Any]:
507
486
  """Execute a tool with the given arguments."""
508
487
  if not self._initialized:
509
- return {
510
- "isError": True,
511
- "error": "Transport not initialized"
512
- }
488
+ return {"isError": True, "error": "Transport not initialized"}
513
489
 
514
490
  start_time = time.time()
515
491
  if self.enable_metrics:
@@ -517,51 +493,37 @@ class SSETransport(MCPBaseTransport):
517
493
 
518
494
  try:
519
495
  logger.debug("Calling tool '%s' with arguments: %s", tool_name, arguments)
520
-
496
+
521
497
  response = await self._send_request(
522
- "tools/call",
523
- {
524
- "name": tool_name,
525
- "arguments": arguments
526
- },
527
- timeout=timeout
498
+ "tools/call", {"name": tool_name, "arguments": arguments}, timeout=timeout
528
499
  )
529
-
530
- if 'error' in response:
500
+
501
+ if "error" in response:
531
502
  if self.enable_metrics:
532
503
  self._update_metrics(time.time() - start_time, False)
533
-
534
- return {
535
- "isError": True,
536
- "error": response['error'].get('message', 'Unknown error')
537
- }
538
-
504
+
505
+ return {"isError": True, "error": response["error"].get("message", "Unknown error")}
506
+
539
507
  # Extract and normalize result using base class method
540
- result = response.get('result', {})
508
+ result = response.get("result", {})
541
509
  normalized_result = self._normalize_mcp_response({"result": result})
542
-
510
+
543
511
  if self.enable_metrics:
544
512
  self._update_metrics(time.time() - start_time, True)
545
-
513
+
546
514
  return normalized_result
547
-
548
- except asyncio.TimeoutError:
515
+
516
+ except TimeoutError:
549
517
  if self.enable_metrics:
550
518
  self._update_metrics(time.time() - start_time, False)
551
-
552
- return {
553
- "isError": True,
554
- "error": "Tool execution timed out"
555
- }
519
+
520
+ return {"isError": True, "error": "Tool execution timed out"}
556
521
  except Exception as e:
557
522
  if self.enable_metrics:
558
523
  self._update_metrics(time.time() - start_time, False)
559
-
524
+
560
525
  logger.error("Error calling tool '%s': %s", tool_name, e)
561
- return {
562
- "isError": True,
563
- "error": str(e)
564
- }
526
+ return {"isError": True, "error": str(e)}
565
527
 
566
528
  def _update_metrics(self, response_time: float, success: bool) -> None:
567
529
  """Update performance metrics."""
@@ -569,39 +531,37 @@ class SSETransport(MCPBaseTransport):
569
531
  self._metrics["successful_calls"] += 1
570
532
  else:
571
533
  self._metrics["failed_calls"] += 1
572
-
534
+
573
535
  self._metrics["total_time"] += response_time
574
536
  if self._metrics["total_calls"] > 0:
575
- self._metrics["avg_response_time"] = (
576
- self._metrics["total_time"] / self._metrics["total_calls"]
577
- )
537
+ self._metrics["avg_response_time"] = self._metrics["total_time"] / self._metrics["total_calls"]
578
538
 
579
- async def list_resources(self) -> Dict[str, Any]:
539
+ async def list_resources(self) -> dict[str, Any]:
580
540
  """List available resources from the server."""
581
541
  if not self._initialized:
582
542
  return {}
583
-
543
+
584
544
  try:
585
545
  response = await self._send_request("resources/list", {}, timeout=10.0)
586
- if 'error' in response:
587
- logger.debug("Resources not supported: %s", response['error'])
546
+ if "error" in response:
547
+ logger.debug("Resources not supported: %s", response["error"])
588
548
  return {}
589
- return response.get('result', {})
549
+ return response.get("result", {})
590
550
  except Exception as e:
591
551
  logger.debug("Error listing resources: %s", e)
592
552
  return {}
593
553
 
594
- async def list_prompts(self) -> Dict[str, Any]:
554
+ async def list_prompts(self) -> dict[str, Any]:
595
555
  """List available prompts from the server."""
596
556
  if not self._initialized:
597
557
  return {}
598
-
558
+
599
559
  try:
600
560
  response = await self._send_request("prompts/list", {}, timeout=10.0)
601
- if 'error' in response:
602
- logger.debug("Prompts not supported: %s", response['error'])
561
+ if "error" in response:
562
+ logger.debug("Prompts not supported: %s", response["error"])
603
563
  return {}
604
- return response.get('result', {})
564
+ return response.get("result", {})
605
565
  except Exception as e:
606
566
  logger.debug("Error listing prompts: %s", e)
607
567
  return {}
@@ -610,16 +570,16 @@ class SSETransport(MCPBaseTransport):
610
570
  """Close the transport and clean up resources."""
611
571
  if not self._initialized:
612
572
  return
613
-
573
+
614
574
  # Log final metrics
615
575
  if self.enable_metrics and self._metrics["total_calls"] > 0:
616
576
  logger.debug(
617
577
  "SSE transport closing - Total calls: %d, Success rate: %.1f%%, Avg response time: %.3fs",
618
578
  self._metrics["total_calls"],
619
579
  (self._metrics["successful_calls"] / self._metrics["total_calls"] * 100),
620
- self._metrics["avg_response_time"]
580
+ self._metrics["avg_response_time"],
621
581
  )
622
-
582
+
623
583
  await self._cleanup()
624
584
 
625
585
  async def _cleanup(self) -> None:
@@ -627,30 +587,28 @@ class SSETransport(MCPBaseTransport):
627
587
  # Cancel SSE processing task
628
588
  if self.sse_task and not self.sse_task.done():
629
589
  self.sse_task.cancel()
630
- try:
590
+ with contextlib.suppress(asyncio.CancelledError):
631
591
  await self.sse_task
632
- except asyncio.CancelledError:
633
- pass
634
-
592
+
635
593
  # Close SSE stream context
636
594
  if self.sse_stream_context:
637
595
  try:
638
596
  await self.sse_stream_context.__aexit__(None, None, None)
639
597
  except Exception as e:
640
598
  logger.debug("Error closing SSE stream: %s", e)
641
-
599
+
642
600
  # Close HTTP clients
643
601
  if self.stream_client:
644
602
  await self.stream_client.aclose()
645
-
603
+
646
604
  if self.send_client:
647
605
  await self.send_client.aclose()
648
-
606
+
649
607
  # Cancel any pending requests
650
- for request_id, future in self.pending_requests.items():
608
+ for _request_id, future in self.pending_requests.items():
651
609
  if not future.done():
652
610
  future.cancel()
653
-
611
+
654
612
  # Reset state
655
613
  self._initialized = False
656
614
  self.session_id = None
@@ -666,20 +624,24 @@ class SSETransport(MCPBaseTransport):
666
624
  self._last_successful_ping = None
667
625
  self._initialization_time = None
668
626
 
669
- def get_metrics(self) -> Dict[str, Any]:
627
+ def get_metrics(self) -> dict[str, Any]:
670
628
  """Get performance and connection metrics with health info."""
671
629
  metrics = self._metrics.copy()
672
- metrics.update({
673
- "is_connected": self.is_connected(),
674
- "consecutive_failures": self._consecutive_failures,
675
- "max_consecutive_failures": self._max_consecutive_failures,
676
- "last_successful_ping": self._last_successful_ping,
677
- "initialization_time_timestamp": self._initialization_time,
678
- "grace_period_active": (
679
- self._initialization_time and
680
- time.time() - self._initialization_time < self._connection_grace_period
681
- ) if self._initialization_time else False
682
- })
630
+ metrics.update(
631
+ {
632
+ "is_connected": self.is_connected(),
633
+ "consecutive_failures": self._consecutive_failures,
634
+ "max_consecutive_failures": self._max_consecutive_failures,
635
+ "last_successful_ping": self._last_successful_ping,
636
+ "initialization_time_timestamp": self._initialization_time,
637
+ "grace_period_active": (
638
+ self._initialization_time
639
+ and time.time() - self._initialization_time < self._connection_grace_period
640
+ )
641
+ if self._initialization_time
642
+ else False,
643
+ }
644
+ )
683
645
  return metrics
684
646
 
685
647
  def reset_metrics(self) -> None:
@@ -693,10 +655,10 @@ class SSETransport(MCPBaseTransport):
693
655
  "last_ping_time": self._metrics.get("last_ping_time"),
694
656
  "initialization_time": self._metrics.get("initialization_time"),
695
657
  "session_discoveries": self._metrics.get("session_discoveries", 0),
696
- "stream_errors": 0
658
+ "stream_errors": 0,
697
659
  }
698
660
 
699
- def get_streams(self) -> List[tuple]:
661
+ def get_streams(self) -> list[tuple]:
700
662
  """SSE transport doesn't expose raw streams."""
701
663
  return []
702
664
 
@@ -709,4 +671,4 @@ class SSETransport(MCPBaseTransport):
709
671
 
710
672
  async def __aexit__(self, exc_type, exc_val, exc_tb):
711
673
  """Context manager cleanup."""
712
- await self.close()
674
+ await self.close()