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
@@ -14,13 +14,14 @@ HEALTH MONITORING FIX: Updated health checking to be more lenient and trust
14
14
  the underlying transport's health monitoring instead of doing aggressive
15
15
  ping tests that create false negatives.
16
16
  """
17
+
17
18
  from __future__ import annotations
18
19
 
19
20
  import asyncio
20
21
  import time
21
- from enum import Enum
22
- from typing import Any, Dict, List, Optional
23
22
  from dataclasses import dataclass
23
+ from enum import Enum
24
+ from typing import Any
24
25
 
25
26
  from chuk_tool_processor.logging import get_logger
26
27
  from chuk_tool_processor.mcp.stream_manager import StreamManager
@@ -30,8 +31,9 @@ logger = get_logger("chuk_tool_processor.mcp.mcp_tool")
30
31
 
31
32
  class ConnectionState(Enum):
32
33
  """Connection states for the MCP tool."""
34
+
33
35
  HEALTHY = "healthy"
34
- DEGRADED = "degraded"
36
+ DEGRADED = "degraded"
35
37
  DISCONNECTED = "disconnected"
36
38
  FAILED = "failed"
37
39
 
@@ -39,6 +41,7 @@ class ConnectionState(Enum):
39
41
  @dataclass
40
42
  class RecoveryConfig:
41
43
  """Configuration for connection recovery behavior."""
44
+
42
45
  max_retries: int = 3
43
46
  initial_backoff: float = 1.0
44
47
  max_backoff: float = 30.0
@@ -50,21 +53,22 @@ class RecoveryConfig:
50
53
  @dataclass
51
54
  class ConnectionStats:
52
55
  """Statistics for connection monitoring."""
56
+
53
57
  total_calls: int = 0
54
58
  successful_calls: int = 0
55
59
  failed_calls: int = 0
56
60
  connection_errors: int = 0
57
- last_success_time: Optional[float] = None
58
- last_failure_time: Optional[float] = None
61
+ last_success_time: float | None = None
62
+ last_failure_time: float | None = None
59
63
 
60
64
 
61
65
  class MCPTool:
62
66
  """
63
67
  Wrap a remote MCP tool so it can be called like a local tool.
64
-
68
+
65
69
  SIMPLIFIED: This class now focuses only on execution delegation.
66
70
  It does NOT handle configuration files or StreamManager bootstrapping.
67
-
71
+
68
72
  FIXED: Health monitoring is now more lenient and trusts the underlying
69
73
  transport's health reporting instead of doing aggressive health checks.
70
74
  """
@@ -72,84 +76,84 @@ class MCPTool:
72
76
  def __init__(
73
77
  self,
74
78
  tool_name: str = "",
75
- stream_manager: Optional[StreamManager] = None,
79
+ stream_manager: StreamManager | None = None,
76
80
  *,
77
- default_timeout: Optional[float] = None,
81
+ default_timeout: float | None = None,
78
82
  enable_resilience: bool = True,
79
- recovery_config: Optional[RecoveryConfig] = None,
83
+ recovery_config: RecoveryConfig | None = None,
80
84
  ) -> None:
81
85
  if not tool_name:
82
86
  raise ValueError("MCPTool requires a tool_name")
83
-
87
+
84
88
  self.tool_name = tool_name
85
- self._sm: Optional[StreamManager] = stream_manager
89
+ self._sm: StreamManager | None = stream_manager
86
90
  self.default_timeout = default_timeout or 30.0
87
91
 
88
92
  # Resilience features
89
93
  self.enable_resilience = enable_resilience
90
94
  self.recovery_config = recovery_config or RecoveryConfig()
91
-
95
+
92
96
  # State tracking (only if resilience enabled)
93
97
  if self.enable_resilience:
94
98
  self.connection_state = ConnectionState.HEALTHY if stream_manager else ConnectionState.DISCONNECTED
95
99
  self.stats = ConnectionStats()
96
-
100
+
97
101
  # Circuit breaker state
98
102
  self._circuit_open = False
99
- self._circuit_open_time: Optional[float] = None
103
+ self._circuit_open_time: float | None = None
100
104
  self._consecutive_failures = 0
101
105
 
102
106
  # ------------------------------------------------------------------ #
103
107
  # Serialization support for subprocess execution
104
108
  # ------------------------------------------------------------------ #
105
- def __getstate__(self) -> Dict[str, Any]:
109
+ def __getstate__(self) -> dict[str, Any]:
106
110
  """
107
111
  Serialize for subprocess execution.
108
-
112
+
109
113
  SIMPLIFIED: Only preserve essential execution state, not configuration.
110
114
  The StreamManager will be None after deserialization - that's expected.
111
115
  """
112
116
  state = self.__dict__.copy()
113
-
117
+
114
118
  # Remove non-serializable items
115
- state['_sm'] = None # StreamManager will be None in subprocess
116
-
119
+ state["_sm"] = None # StreamManager will be None in subprocess
120
+
117
121
  # Reset connection state for subprocess
118
122
  if self.enable_resilience:
119
- state['connection_state'] = ConnectionState.DISCONNECTED
120
-
123
+ state["connection_state"] = ConnectionState.DISCONNECTED
124
+
121
125
  logger.debug(f"Serializing MCPTool '{self.tool_name}' for subprocess")
122
126
  return state
123
-
124
- def __setstate__(self, state: Dict[str, Any]) -> None:
127
+
128
+ def __setstate__(self, state: dict[str, Any]) -> None:
125
129
  """
126
130
  Deserialize after subprocess execution.
127
-
131
+
128
132
  SIMPLIFIED: Just restore state. StreamManager will be None and that's fine.
129
133
  """
130
134
  self.__dict__.update(state)
131
-
135
+
132
136
  # Ensure critical fields exist
133
- if not hasattr(self, 'tool_name') or not self.tool_name:
137
+ if not hasattr(self, "tool_name") or not self.tool_name:
134
138
  raise ValueError("Invalid MCPTool state: missing tool_name")
135
-
139
+
136
140
  # StreamManager will be None in subprocess - that's expected
137
141
  self._sm = None
138
-
142
+
139
143
  # Initialize resilience state if enabled
140
144
  if self.enable_resilience:
141
- if not hasattr(self, 'connection_state'):
145
+ if not hasattr(self, "connection_state"):
142
146
  self.connection_state = ConnectionState.DISCONNECTED
143
- if not hasattr(self, 'stats'):
147
+ if not hasattr(self, "stats"):
144
148
  self.stats = ConnectionStats()
145
-
149
+
146
150
  logger.debug(f"Deserialized MCPTool '{self.tool_name}' in subprocess")
147
151
 
148
152
  # ------------------------------------------------------------------ #
149
- async def execute(self, timeout: Optional[float] = None, **kwargs: Any) -> Any:
153
+ async def execute(self, timeout: float | None = None, **kwargs: Any) -> Any:
150
154
  """
151
155
  Execute the tool, returning graceful errors if StreamManager unavailable.
152
-
156
+
153
157
  SIMPLIFIED: If no StreamManager, return a structured error response
154
158
  instead of trying to bootstrap one.
155
159
  """
@@ -159,21 +163,21 @@ class MCPTool:
159
163
  "error": f"Tool '{self.tool_name}' is not available (no stream manager)",
160
164
  "tool_name": self.tool_name,
161
165
  "available": False,
162
- "reason": "disconnected"
166
+ "reason": "disconnected",
163
167
  }
164
168
 
165
169
  # If resilience is disabled, use simple execution
166
170
  if not self.enable_resilience:
167
171
  return await self._simple_execute(timeout, **kwargs)
168
-
172
+
169
173
  # Resilient execution
170
174
  return await self._resilient_execute(timeout, **kwargs)
171
175
 
172
- async def _simple_execute(self, timeout: Optional[float] = None, **kwargs: Any) -> Any:
176
+ async def _simple_execute(self, timeout: float | None = None, **kwargs: Any) -> Any:
173
177
  """Simple execution without resilience features."""
174
178
  effective_timeout = timeout if timeout is not None else self.default_timeout
175
179
 
176
- call_kwargs = {
180
+ call_kwargs: dict[str, Any] = {
177
181
  "tool_name": self.tool_name,
178
182
  "arguments": kwargs,
179
183
  }
@@ -181,8 +185,8 @@ class MCPTool:
181
185
  call_kwargs["timeout"] = effective_timeout
182
186
 
183
187
  try:
184
- result = await self._sm.call_tool(**call_kwargs)
185
- except asyncio.TimeoutError:
188
+ result = await self._sm.call_tool(**call_kwargs) # type: ignore[union-attr]
189
+ except TimeoutError:
186
190
  logger.warning(f"MCP tool '{self.tool_name}' timed out after {effective_timeout}s")
187
191
  raise
188
192
 
@@ -193,7 +197,7 @@ class MCPTool:
193
197
 
194
198
  return result.get("content")
195
199
 
196
- async def _resilient_execute(self, timeout: Optional[float] = None, **kwargs: Any) -> Any:
200
+ async def _resilient_execute(self, timeout: float | None = None, **kwargs: Any) -> Any:
197
201
  """Resilient execution with circuit breaker and health checks."""
198
202
  # Check circuit breaker
199
203
  if self._is_circuit_open():
@@ -201,12 +205,12 @@ class MCPTool:
201
205
  "error": f"Circuit breaker open for tool '{self.tool_name}' - too many recent failures",
202
206
  "tool_name": self.tool_name,
203
207
  "available": False,
204
- "reason": "circuit_breaker"
208
+ "reason": "circuit_breaker",
205
209
  }
206
-
210
+
207
211
  effective_timeout = timeout if timeout is not None else self.default_timeout
208
212
  self.stats.total_calls += 1
209
-
213
+
210
214
  # FIXED: More lenient health check - trust the transport layer
211
215
  if not await self._is_stream_manager_healthy():
212
216
  await self._record_failure(is_connection_error=True)
@@ -214,64 +218,59 @@ class MCPTool:
214
218
  "error": f"Tool '{self.tool_name}' is not available (unhealthy connection)",
215
219
  "tool_name": self.tool_name,
216
220
  "available": False,
217
- "reason": "unhealthy"
221
+ "reason": "unhealthy",
218
222
  }
219
-
223
+
220
224
  # Try execution with retries
221
225
  max_attempts = self.recovery_config.max_retries + 1
222
226
  backoff = self.recovery_config.initial_backoff
223
-
227
+
224
228
  for attempt in range(max_attempts):
225
229
  try:
226
230
  result = await self._execute_with_timeout(effective_timeout, **kwargs)
227
231
  await self._record_success()
228
232
  return result
229
-
230
- except asyncio.TimeoutError:
233
+
234
+ except TimeoutError:
231
235
  error_msg = f"Tool '{self.tool_name}' timed out after {effective_timeout}s"
232
236
  logger.warning(error_msg)
233
237
  await self._record_failure()
234
-
238
+
235
239
  if attempt == max_attempts - 1:
236
- return {
237
- "error": error_msg,
238
- "tool_name": self.tool_name,
239
- "available": False,
240
- "reason": "timeout"
241
- }
242
-
240
+ return {"error": error_msg, "tool_name": self.tool_name, "available": False, "reason": "timeout"}
241
+
243
242
  except Exception as e:
244
243
  error_str = str(e)
245
244
  is_connection_error = self._is_connection_error(e)
246
-
245
+
247
246
  logger.warning(f"Tool '{self.tool_name}' attempt {attempt + 1} failed: {error_str}")
248
247
  await self._record_failure(is_connection_error)
249
-
248
+
250
249
  if attempt == max_attempts - 1:
251
250
  return {
252
251
  "error": f"Tool execution failed after {max_attempts} attempts: {error_str}",
253
252
  "tool_name": self.tool_name,
254
253
  "available": False,
255
- "reason": "execution_failed"
254
+ "reason": "execution_failed",
256
255
  }
257
-
256
+
258
257
  # Exponential backoff
259
258
  if attempt < max_attempts - 1:
260
259
  logger.debug(f"Waiting {backoff:.1f}s before retry {attempt + 2}")
261
260
  await asyncio.sleep(backoff)
262
261
  backoff = min(backoff * self.recovery_config.backoff_multiplier, self.recovery_config.max_backoff)
263
-
262
+
264
263
  # Should never reach here
265
264
  return {
266
265
  "error": f"Tool '{self.tool_name}' failed after all attempts",
267
266
  "tool_name": self.tool_name,
268
267
  "available": False,
269
- "reason": "exhausted_retries"
268
+ "reason": "exhausted_retries",
270
269
  }
271
270
 
272
271
  async def _execute_with_timeout(self, timeout: float, **kwargs: Any) -> Any:
273
272
  """Execute the tool with timeout."""
274
- call_kwargs = {
273
+ call_kwargs: dict[str, Any] = {
275
274
  "tool_name": self.tool_name,
276
275
  "arguments": kwargs,
277
276
  }
@@ -280,17 +279,17 @@ class MCPTool:
280
279
 
281
280
  try:
282
281
  result = await asyncio.wait_for(
283
- self._sm.call_tool(**call_kwargs),
284
- timeout=(timeout + 5.0) if timeout else None
282
+ self._sm.call_tool(**call_kwargs), # type: ignore[union-attr]
283
+ timeout=(timeout + 5.0) if timeout else None,
285
284
  )
286
-
285
+
287
286
  if result.get("isError"):
288
287
  error = result.get("error", "Unknown error")
289
288
  raise RuntimeError(f"Tool execution failed: {error}")
290
-
289
+
291
290
  return result.get("content")
292
-
293
- except asyncio.TimeoutError:
291
+
292
+ except TimeoutError:
294
293
  self.connection_state = ConnectionState.DEGRADED
295
294
  raise
296
295
  except Exception as e:
@@ -303,31 +302,35 @@ class MCPTool:
303
302
  async def _is_stream_manager_healthy(self) -> bool:
304
303
  """
305
304
  FIXED: Much more lenient health check.
306
-
305
+
307
306
  The diagnostic proves SSE transport is healthy, so we should trust it
308
307
  instead of doing aggressive health checking that creates false negatives.
309
-
308
+
310
309
  This replaces the previous ping_servers() call which was too aggressive
311
310
  and caused "unhealthy connection" false positives.
312
311
  """
313
312
  if self._sm is None:
314
313
  return False
315
-
314
+
316
315
  # FIXED: Simple check - if we have a StreamManager, assume it's available
317
316
  # The underlying SSE transport now has proper health monitoring
318
317
  try:
319
318
  # Just check if the StreamManager has basic functionality
320
- if hasattr(self._sm, 'transports') and self._sm.transports:
321
- logger.debug(f"StreamManager healthy for '{self.tool_name}' - has {len(self._sm.transports)} transports")
319
+ if hasattr(self._sm, "transports") and self._sm.transports:
320
+ logger.debug(
321
+ f"StreamManager healthy for '{self.tool_name}' - has {len(self._sm.transports)} transports"
322
+ )
322
323
  return True
323
-
324
+
324
325
  # Fallback - try very quick operation with short timeout
325
- server_info = await asyncio.wait_for(self._sm.get_server_info(), timeout=1.0)
326
+ server_info: list[dict[str, Any]] = await asyncio.wait_for( # type: ignore[arg-type]
327
+ self._sm.get_server_info(), timeout=1.0
328
+ )
326
329
  healthy = len(server_info) > 0
327
330
  logger.debug(f"StreamManager health for '{self.tool_name}': {healthy} (via server_info)")
328
331
  return healthy
329
-
330
- except asyncio.TimeoutError:
332
+
333
+ except TimeoutError:
331
334
  logger.debug(f"StreamManager health check timed out for '{self.tool_name}' - assuming healthy")
332
335
  # FIXED: Timeout doesn't mean unavailable, just slow
333
336
  return True
@@ -341,10 +344,18 @@ class MCPTool:
341
344
  """Determine if an exception indicates a connection problem."""
342
345
  error_str = str(exception).lower()
343
346
  connection_indicators = [
344
- "connection lost", "connection closed", "connection refused",
345
- "broken pipe", "timeout", "eof", "pipe closed", "process died",
346
- "no route to host", "no server found", "transport not initialized",
347
- "stream manager unavailable"
347
+ "connection lost",
348
+ "connection closed",
349
+ "connection refused",
350
+ "broken pipe",
351
+ "timeout",
352
+ "eof",
353
+ "pipe closed",
354
+ "process died",
355
+ "no route to host",
356
+ "no server found",
357
+ "transport not initialized",
358
+ "stream manager unavailable",
348
359
  ]
349
360
  return any(indicator in error_str for indicator in connection_indicators)
350
361
 
@@ -353,7 +364,7 @@ class MCPTool:
353
364
  self.stats.successful_calls += 1
354
365
  self.stats.last_success_time = time.time()
355
366
  self._consecutive_failures = 0
356
-
367
+
357
368
  # Close circuit breaker if it was open
358
369
  if self._circuit_open:
359
370
  self._circuit_open = False
@@ -365,42 +376,45 @@ class MCPTool:
365
376
  """Record a failed execution."""
366
377
  self.stats.failed_calls += 1
367
378
  self.stats.last_failure_time = time.time()
368
-
379
+
369
380
  if is_connection_error:
370
381
  self.stats.connection_errors += 1
371
382
  self.connection_state = ConnectionState.DISCONNECTED
372
383
  else:
373
384
  self.connection_state = ConnectionState.DEGRADED
374
-
385
+
375
386
  self._consecutive_failures += 1
376
-
387
+
377
388
  # Check if we should open the circuit breaker
378
- if (self._consecutive_failures >= self.recovery_config.circuit_breaker_threshold and
379
- not self._circuit_open):
389
+ if self._consecutive_failures >= self.recovery_config.circuit_breaker_threshold and not self._circuit_open:
380
390
  self._circuit_open = True
381
391
  self._circuit_open_time = time.time()
382
392
  self.connection_state = ConnectionState.FAILED
383
- logger.error(f"Circuit breaker opened for tool '{self.tool_name}' after {self._consecutive_failures} consecutive failures")
393
+ logger.error(
394
+ f"Circuit breaker opened for tool '{self.tool_name}' after {self._consecutive_failures} consecutive failures"
395
+ )
384
396
 
385
397
  def _is_circuit_open(self) -> bool:
386
398
  """Check if the circuit breaker is currently open."""
387
399
  if not self._circuit_open:
388
400
  return False
389
-
401
+
390
402
  # Check if enough time has passed to close the circuit
391
- if (self._circuit_open_time and
392
- time.time() - self._circuit_open_time >= self.recovery_config.circuit_breaker_timeout):
403
+ if (
404
+ self._circuit_open_time
405
+ and time.time() - self._circuit_open_time >= self.recovery_config.circuit_breaker_timeout
406
+ ):
393
407
  self._circuit_open = False
394
408
  self._circuit_open_time = None
395
409
  self.connection_state = ConnectionState.HEALTHY
396
410
  logger.info(f"Circuit breaker reset for tool '{self.tool_name}' after timeout")
397
411
  return False
398
-
412
+
399
413
  return True
400
414
 
401
415
  # ------------------------------------------------------------------ #
402
416
  # Legacy method name support
403
- async def _aexecute(self, timeout: Optional[float] = None, **kwargs: Any) -> Any:
417
+ async def _aexecute(self, timeout: float | None = None, **kwargs: Any) -> Any:
404
418
  """Legacy alias for execute() method."""
405
419
  return await self.execute(timeout=timeout, **kwargs)
406
420
 
@@ -409,23 +423,21 @@ class MCPTool:
409
423
  # ------------------------------------------------------------------ #
410
424
  def is_available(self) -> bool:
411
425
  """Check if this tool is currently available."""
412
- return (self._sm is not None and
413
- not self._is_circuit_open() and
414
- self.connection_state in [ConnectionState.HEALTHY, ConnectionState.DEGRADED])
415
-
416
- def get_stats(self) -> Dict[str, Any]:
426
+ return (
427
+ self._sm is not None
428
+ and not self._is_circuit_open()
429
+ and self.connection_state in [ConnectionState.HEALTHY, ConnectionState.DEGRADED]
430
+ )
431
+
432
+ def get_stats(self) -> dict[str, Any]:
417
433
  """Get connection and execution statistics."""
418
434
  if not self.enable_resilience:
419
- return {
420
- "tool_name": self.tool_name,
421
- "resilience_enabled": False,
422
- "available": self._sm is not None
423
- }
424
-
435
+ return {"tool_name": self.tool_name, "resilience_enabled": False, "available": self._sm is not None}
436
+
425
437
  success_rate = 0.0
426
438
  if self.stats.total_calls > 0:
427
439
  success_rate = (self.stats.successful_calls / self.stats.total_calls) * 100
428
-
440
+
429
441
  return {
430
442
  "tool_name": self.tool_name,
431
443
  "resilience_enabled": True,
@@ -445,7 +457,7 @@ class MCPTool:
445
457
  """Manually reset the circuit breaker."""
446
458
  if not self.enable_resilience:
447
459
  return
448
-
460
+
449
461
  self._circuit_open = False
450
462
  self._circuit_open_time = None
451
463
  self._consecutive_failures = 0
@@ -457,10 +469,10 @@ class MCPTool:
457
469
  self.enable_resilience = False
458
470
  logger.info(f"Resilience features disabled for tool '{self.tool_name}'")
459
471
 
460
- def set_stream_manager(self, stream_manager: Optional[StreamManager]) -> None:
472
+ def set_stream_manager(self, stream_manager: StreamManager | None) -> None:
461
473
  """
462
474
  Set or update the StreamManager for this tool.
463
-
475
+
464
476
  This can be used by external systems to reconnect tools after
465
477
  StreamManager recovery at a higher level.
466
478
  """
@@ -473,5 +485,5 @@ class MCPTool:
473
485
  logger.info(f"Circuit breaker closed for tool '{self.tool_name}' due to new stream manager")
474
486
  else:
475
487
  self.connection_state = ConnectionState.DISCONNECTED
476
-
477
- logger.debug(f"StreamManager {'set' if stream_manager else 'cleared'} for tool '{self.tool_name}'")
488
+
489
+ logger.debug(f"StreamManager {'set' if stream_manager else 'cleared'} for tool '{self.tool_name}'")
@@ -8,7 +8,7 @@ CLEAN & SIMPLE: Just the essentials - create MCPTool wrappers for remote tools.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from typing import Any, Dict, List, Optional
11
+ from typing import Any
12
12
 
13
13
  from chuk_tool_processor.logging import get_logger
14
14
  from chuk_tool_processor.mcp.mcp_tool import MCPTool, RecoveryConfig
@@ -25,8 +25,8 @@ async def register_mcp_tools(
25
25
  # Optional resilience configuration
26
26
  default_timeout: float = 30.0,
27
27
  enable_resilience: bool = True,
28
- recovery_config: Optional[RecoveryConfig] = None,
29
- ) -> List[str]:
28
+ recovery_config: RecoveryConfig | None = None,
29
+ ) -> list[str]:
30
30
  """
31
31
  Pull the remote tool catalogue and create local MCPTool wrappers.
32
32
 
@@ -49,10 +49,10 @@ async def register_mcp_tools(
49
49
  The tool names that were registered.
50
50
  """
51
51
  registry = await ToolRegistryProvider.get_registry()
52
- registered: List[str] = []
52
+ registered: list[str] = []
53
53
 
54
54
  # Get the remote tool catalogue
55
- mcp_tools: List[Dict[str, Any]] = stream_manager.get_all_tools()
55
+ mcp_tools: list[dict[str, Any]] = stream_manager.get_all_tools()
56
56
 
57
57
  for tool_def in mcp_tools:
58
58
  tool_name = tool_def.get("name")
@@ -61,7 +61,7 @@ async def register_mcp_tools(
61
61
  continue
62
62
 
63
63
  description = tool_def.get("description") or f"MCP tool • {tool_name}"
64
- meta: Dict[str, Any] = {
64
+ meta: dict[str, Any] = {
65
65
  "description": description,
66
66
  "is_async": True,
67
67
  "tags": {"mcp", "remote"},
@@ -101,20 +101,20 @@ async def register_mcp_tools(
101
101
 
102
102
  async def update_mcp_tools_stream_manager(
103
103
  namespace: str,
104
- new_stream_manager: Optional[StreamManager],
104
+ new_stream_manager: StreamManager | None,
105
105
  ) -> int:
106
106
  """
107
107
  Update the StreamManager reference for all MCP tools in a namespace.
108
-
108
+
109
109
  Useful for reconnecting tools after StreamManager recovery at the service level.
110
-
110
+
111
111
  Parameters
112
112
  ----------
113
113
  namespace
114
114
  The namespace containing MCP tools to update
115
115
  new_stream_manager
116
116
  The new StreamManager to use, or None to disconnect
117
-
117
+
118
118
  Returns
119
119
  -------
120
120
  int
@@ -122,26 +122,26 @@ async def update_mcp_tools_stream_manager(
122
122
  """
123
123
  registry = await ToolRegistryProvider.get_registry()
124
124
  updated_count = 0
125
-
125
+
126
126
  try:
127
127
  # List all tools in the namespace
128
128
  all_tools = await registry.list_tools()
129
129
  namespace_tools = [name for ns, name in all_tools if ns == namespace]
130
-
130
+
131
131
  for tool_name in namespace_tools:
132
132
  try:
133
133
  tool = await registry.get_tool(tool_name, namespace)
134
- if tool and hasattr(tool, 'set_stream_manager'):
134
+ if tool and hasattr(tool, "set_stream_manager"):
135
135
  tool.set_stream_manager(new_stream_manager)
136
136
  updated_count += 1
137
137
  logger.debug("Updated StreamManager for tool '%s:%s'", namespace, tool_name)
138
138
  except Exception as e:
139
139
  logger.warning("Failed to update StreamManager for tool '%s:%s': %s", namespace, tool_name, e)
140
-
140
+
141
141
  action = "connected" if new_stream_manager else "disconnected"
142
142
  logger.debug("StreamManager %s for %d tools in namespace '%s'", action, updated_count, namespace)
143
-
143
+
144
144
  except Exception as e:
145
145
  logger.error("Failed to update tools in namespace '%s': %s", namespace, e)
146
-
147
- return updated_count
146
+
147
+ return updated_count