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

@@ -260,7 +260,7 @@ class ToolProcessor:
260
260
  unknown_tools.append(call.tool)
261
261
 
262
262
  if unknown_tools:
263
- self.logger.warning(f"Unknown tools: {unknown_tools}")
263
+ self.logger.debug(f"Unknown tools: {unknown_tools}")
264
264
 
265
265
  # Execute tools
266
266
  results = await self.executor.execute(calls, timeout=timeout)
@@ -412,7 +412,7 @@ class ToolProcessor:
412
412
  duration=duration,
413
413
  num_calls=0,
414
414
  )
415
- self.logger.error(f"Parser {parser_name} failed: {str(e)}")
415
+ self.logger.debug(f"Parser {parser_name} failed: {str(e)}")
416
416
  return []
417
417
 
418
418
 
@@ -44,6 +44,7 @@ async def setup_mcp_http_streamable(
44
44
  enable_retries: bool = True,
45
45
  max_retries: int = 3,
46
46
  namespace: str = "http",
47
+ oauth_refresh_callback: any | None = None, # NEW: OAuth token refresh callback
47
48
  ) -> tuple[ToolProcessor, StreamManager]:
48
49
  """
49
50
  Initialize HTTP Streamable transport MCP + a :class:`ToolProcessor`.
@@ -69,6 +70,7 @@ async def setup_mcp_http_streamable(
69
70
  enable_retries: Whether to enable automatic retries
70
71
  max_retries: Maximum retry attempts
71
72
  namespace: Namespace for registered tools
73
+ oauth_refresh_callback: Optional async callback to refresh OAuth tokens (NEW)
72
74
 
73
75
  Returns:
74
76
  Tuple of (ToolProcessor, StreamManager)
@@ -93,6 +95,7 @@ async def setup_mcp_http_streamable(
93
95
  connection_timeout=connection_timeout,
94
96
  default_timeout=default_timeout,
95
97
  initialization_timeout=initialization_timeout,
98
+ oauth_refresh_callback=oauth_refresh_callback, # NEW: Pass OAuth callback
96
99
  )
97
100
 
98
101
  # 2️⃣ pull the remote tool list and register each one locally
@@ -40,6 +40,7 @@ async def setup_mcp_sse( # noqa: C901 - long but just a config facade
40
40
  enable_retries: bool = True,
41
41
  max_retries: int = 3,
42
42
  namespace: str = "sse",
43
+ oauth_refresh_callback: any | None = None, # NEW: OAuth token refresh callback
43
44
  ) -> tuple[ToolProcessor, StreamManager]:
44
45
  """
45
46
  Initialise SSE-transport MCP + a :class:`ToolProcessor`.
@@ -61,6 +62,7 @@ async def setup_mcp_sse( # noqa: C901 - long but just a config facade
61
62
  enable_retries: Whether to enable automatic retries
62
63
  max_retries: Maximum retry attempts
63
64
  namespace: Namespace for registered tools
65
+ oauth_refresh_callback: Optional async callback to refresh OAuth tokens (NEW)
64
66
 
65
67
  Returns:
66
68
  Tuple of (ToolProcessor, StreamManager)
@@ -72,6 +74,7 @@ async def setup_mcp_sse( # noqa: C901 - long but just a config facade
72
74
  connection_timeout=connection_timeout, # 🔧 ADD THIS LINE
73
75
  default_timeout=default_timeout, # 🔧 ADD THIS LINE
74
76
  initialization_timeout=initialization_timeout,
77
+ oauth_refresh_callback=oauth_refresh_callback, # NEW: Pass OAuth callback
75
78
  )
76
79
 
77
80
  # 2️⃣ pull the remote tool list and register each one locally
@@ -81,6 +81,7 @@ class StreamManager:
81
81
  connection_timeout: float = 10.0,
82
82
  default_timeout: float = 30.0,
83
83
  initialization_timeout: float = 60.0, # NEW
84
+ oauth_refresh_callback: any | None = None, # NEW: OAuth token refresh callback
84
85
  ) -> StreamManager:
85
86
  """Create StreamManager with SSE transport and timeout protection."""
86
87
  inst = cls()
@@ -90,6 +91,7 @@ class StreamManager:
90
91
  connection_timeout=connection_timeout,
91
92
  default_timeout=default_timeout,
92
93
  initialization_timeout=initialization_timeout,
94
+ oauth_refresh_callback=oauth_refresh_callback, # NEW: Pass OAuth callback
93
95
  )
94
96
  return inst
95
97
 
@@ -101,6 +103,7 @@ class StreamManager:
101
103
  connection_timeout: float = 30.0,
102
104
  default_timeout: float = 30.0,
103
105
  initialization_timeout: float = 60.0, # NEW
106
+ oauth_refresh_callback: any | None = None, # NEW: OAuth token refresh callback
104
107
  ) -> StreamManager:
105
108
  """Create StreamManager with HTTP Streamable transport and timeout protection."""
106
109
  inst = cls()
@@ -110,6 +113,7 @@ class StreamManager:
110
113
  connection_timeout=connection_timeout,
111
114
  default_timeout=default_timeout,
112
115
  initialization_timeout=initialization_timeout,
116
+ oauth_refresh_callback=oauth_refresh_callback, # NEW: Pass OAuth callback
113
117
  )
114
118
  return inst
115
119
 
@@ -178,7 +182,7 @@ class StreamManager:
178
182
  params, connection_timeout=initialization_timeout, default_timeout=default_timeout
179
183
  )
180
184
  elif transport_type == "sse":
181
- logger.warning(
185
+ logger.debug(
182
186
  "Using SSE transport in initialize() - consider using initialize_with_sse() instead"
183
187
  )
184
188
  params = await load_config(config_file, server_name)
@@ -191,7 +195,7 @@ class StreamManager:
191
195
  sse_url = "http://localhost:8000"
192
196
  api_key = None
193
197
  headers = {}
194
- logger.warning("No URL configured for SSE transport, using default: %s", sse_url)
198
+ logger.debug("No URL configured for SSE transport, using default: %s", sse_url)
195
199
 
196
200
  # Build SSE transport with optional headers
197
201
  transport_params = {"url": sse_url, "api_key": api_key, "default_timeout": default_timeout}
@@ -201,7 +205,7 @@ class StreamManager:
201
205
  transport = SSETransport(**transport_params)
202
206
 
203
207
  elif transport_type == "http_streamable":
204
- logger.warning(
208
+ logger.debug(
205
209
  "Using HTTP Streamable transport in initialize() - consider using initialize_with_http_streamable() instead"
206
210
  )
207
211
  params = await load_config(config_file, server_name)
@@ -216,9 +220,7 @@ class StreamManager:
216
220
  api_key = None
217
221
  headers = {}
218
222
  session_id = None
219
- logger.warning(
220
- "No URL configured for HTTP Streamable transport, using default: %s", http_url
221
- )
223
+ logger.debug("No URL configured for HTTP Streamable transport, using default: %s", http_url)
222
224
 
223
225
  # Build HTTP transport (headers not supported yet)
224
226
  transport_params = {
@@ -240,7 +242,7 @@ class StreamManager:
240
242
  # Initialize with timeout protection
241
243
  try:
242
244
  if not await asyncio.wait_for(transport.initialize(), timeout=initialization_timeout):
243
- logger.error("Failed to init %s", server_name)
245
+ logger.warning("Failed to init %s", server_name)
244
246
  continue
245
247
  except TimeoutError:
246
248
  logger.error("Timeout initialising %s (timeout=%ss)", server_name, initialization_timeout)
@@ -285,6 +287,7 @@ class StreamManager:
285
287
  connection_timeout: float = 10.0,
286
288
  default_timeout: float = 30.0,
287
289
  initialization_timeout: float = 60.0,
290
+ oauth_refresh_callback: any | None = None, # NEW: OAuth token refresh callback
288
291
  ) -> None:
289
292
  """Initialize with SSE transport with optional headers support."""
290
293
  if self._closed:
@@ -313,11 +316,16 @@ class StreamManager:
313
316
  logger.debug("SSE %s: Using configured headers: %s", name, list(headers.keys()))
314
317
  transport_params["headers"] = headers
315
318
 
319
+ # Add OAuth refresh callback if provided (NEW)
320
+ if oauth_refresh_callback:
321
+ transport_params["oauth_refresh_callback"] = oauth_refresh_callback
322
+ logger.debug("SSE %s: OAuth refresh callback configured", name)
323
+
316
324
  transport = SSETransport(**transport_params)
317
325
 
318
326
  try:
319
327
  if not await asyncio.wait_for(transport.initialize(), timeout=initialization_timeout):
320
- logger.error("Failed to init SSE %s", name)
328
+ logger.warning("Failed to init SSE %s", name)
321
329
  continue
322
330
  except TimeoutError:
323
331
  logger.error("Timeout initialising SSE %s (timeout=%ss)", name, initialization_timeout)
@@ -354,6 +362,7 @@ class StreamManager:
354
362
  connection_timeout: float = 30.0,
355
363
  default_timeout: float = 30.0,
356
364
  initialization_timeout: float = 60.0,
365
+ oauth_refresh_callback: any | None = None, # NEW: OAuth token refresh callback
357
366
  ) -> None:
358
367
  """Initialize with HTTP Streamable transport with graceful headers handling."""
359
368
  if self._closed:
@@ -385,12 +394,17 @@ class StreamManager:
385
394
  transport_params["headers"] = headers
386
395
  logger.debug("HTTP Streamable %s: Custom headers configured: %s", name, list(headers.keys()))
387
396
 
397
+ # Add OAuth refresh callback if provided (NEW)
398
+ if oauth_refresh_callback:
399
+ transport_params["oauth_refresh_callback"] = oauth_refresh_callback
400
+ logger.debug("HTTP Streamable %s: OAuth refresh callback configured", name)
401
+
388
402
  transport = HTTPStreamableTransport(**transport_params)
389
403
 
390
404
  logger.debug(f"Calling transport.initialize() for {name} with timeout={initialization_timeout}s")
391
405
  try:
392
406
  if not await asyncio.wait_for(transport.initialize(), timeout=initialization_timeout):
393
- logger.error("Failed to init HTTP Streamable %s", name)
407
+ logger.warning("Failed to init HTTP Streamable %s", name)
394
408
  continue
395
409
  except TimeoutError:
396
410
  logger.error(
@@ -45,6 +45,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
45
45
  default_timeout: float = 30.0,
46
46
  session_id: str | None = None,
47
47
  enable_metrics: bool = True,
48
+ oauth_refresh_callback: Any | None = None, # NEW: OAuth token refresh callback
48
49
  ):
49
50
  """
50
51
  Initialize HTTP Streamable transport with enhanced configuration.
@@ -57,6 +58,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
57
58
  default_timeout: Default timeout for operations
58
59
  session_id: Optional session ID for stateful connections
59
60
  enable_metrics: Whether to track performance metrics
61
+ oauth_refresh_callback: Optional async callback to refresh OAuth tokens (NEW)
60
62
  """
61
63
  # Ensure URL points to the /mcp endpoint
62
64
  if not url.endswith("/mcp"):
@@ -70,6 +72,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
70
72
  self.default_timeout = default_timeout
71
73
  self.session_id = session_id
72
74
  self.enable_metrics = enable_metrics
75
+ self.oauth_refresh_callback = oauth_refresh_callback # NEW: OAuth refresh callback
73
76
 
74
77
  logger.debug("HTTP Streamable transport initialized with URL: %s", self.url)
75
78
  if self.api_key:
@@ -226,7 +229,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
226
229
  )
227
230
  return True
228
231
  else:
229
- logger.warning("HTTP connection established but ping failed")
232
+ logger.debug("HTTP connection established but ping failed")
230
233
  # Still consider it initialized since connection was established
231
234
  self._initialized = True
232
235
  self._consecutive_failures = 1 # Mark one failure
@@ -302,7 +305,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
302
305
  async def send_ping(self) -> bool:
303
306
  """Enhanced ping with health monitoring (like SSE)."""
304
307
  if not self._initialized or not self._read_stream:
305
- logger.error("Cannot send ping: transport not initialized")
308
+ logger.debug("Cannot send ping: transport not initialized")
306
309
  return False
307
310
 
308
311
  start_time = time.time()
@@ -352,7 +355,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
352
355
  async def get_tools(self) -> list[dict[str, Any]]:
353
356
  """Enhanced tools retrieval with error handling."""
354
357
  if not self._initialized:
355
- logger.error("Cannot get tools: transport not initialized")
358
+ logger.debug("Cannot get tools: transport not initialized")
356
359
  return []
357
360
 
358
361
  start_time = time.time()
@@ -422,9 +425,44 @@ class HTTPStreamableTransport(MCPBaseTransport):
422
425
  response_time = time.time() - start_time
423
426
  result = self._normalize_mcp_response(raw_response)
424
427
 
428
+ # NEW: Check for OAuth errors and attempt refresh if callback is available
429
+ if result.get("isError", False) and self._is_oauth_error(result.get("error", "")):
430
+ logger.warning("OAuth error detected: %s", result.get("error"))
431
+
432
+ if self.oauth_refresh_callback:
433
+ logger.info("Attempting OAuth token refresh...")
434
+ try:
435
+ # Call the refresh callback
436
+ new_headers = await self.oauth_refresh_callback()
437
+
438
+ if new_headers and "Authorization" in new_headers:
439
+ # Update configured headers with new token
440
+ self.configured_headers.update(new_headers)
441
+ logger.info("OAuth token refreshed, reconnecting...")
442
+
443
+ # Reconnect with new token
444
+ if await self._attempt_recovery():
445
+ logger.info("Retrying tool call after token refresh...")
446
+ # Retry the tool call once with new token
447
+ raw_response = await asyncio.wait_for(
448
+ send_tools_call(self._read_stream, self._write_stream, tool_name, arguments),
449
+ timeout=tool_timeout,
450
+ )
451
+ result = self._normalize_mcp_response(raw_response)
452
+ logger.info("Tool call retry completed")
453
+ else:
454
+ logger.error("Failed to reconnect after token refresh")
455
+ else:
456
+ logger.warning("Token refresh did not return valid Authorization header")
457
+ except Exception as refresh_error:
458
+ logger.error("OAuth token refresh failed: %s", refresh_error)
459
+ else:
460
+ logger.warning("OAuth error detected but no refresh callback configured")
461
+
425
462
  # Reset failure count on success
426
- self._consecutive_failures = 0
427
- self._last_successful_ping = time.time() # Update health timestamp
463
+ if not result.get("isError", False):
464
+ self._consecutive_failures = 0
465
+ self._last_successful_ping = time.time() # Update health timestamp
428
466
 
429
467
  if self.enable_metrics:
430
468
  self._update_metrics(response_time, not result.get("isError", False))
@@ -477,6 +515,24 @@ class HTTPStreamableTransport(MCPBaseTransport):
477
515
  if self._metrics["total_calls"] > 0:
478
516
  self._metrics["avg_response_time"] = self._metrics["total_time"] / self._metrics["total_calls"]
479
517
 
518
+ def _is_oauth_error(self, error_msg: str) -> bool:
519
+ """Detect if error is OAuth-related (NEW)."""
520
+ if not error_msg:
521
+ return False
522
+
523
+ error_lower = error_msg.lower()
524
+ oauth_indicators = [
525
+ "invalid_token",
526
+ "expired token",
527
+ "oauth validation",
528
+ "unauthorized",
529
+ "token expired",
530
+ "authentication failed",
531
+ "invalid access token",
532
+ ]
533
+
534
+ return any(indicator in error_lower for indicator in oauth_indicators)
535
+
480
536
  async def list_resources(self) -> dict[str, Any]:
481
537
  """Enhanced resource listing with error handling."""
482
538
  if not self._initialized:
@@ -38,6 +38,7 @@ class SSETransport(MCPBaseTransport):
38
38
  connection_timeout: float = 30.0,
39
39
  default_timeout: float = 60.0,
40
40
  enable_metrics: bool = True,
41
+ oauth_refresh_callback: Any | None = None, # NEW: OAuth token refresh callback
41
42
  ):
42
43
  """
43
44
  Initialize SSE transport.
@@ -48,6 +49,7 @@ class SSETransport(MCPBaseTransport):
48
49
  self.connection_timeout = connection_timeout
49
50
  self.default_timeout = default_timeout
50
51
  self.enable_metrics = enable_metrics
52
+ self.oauth_refresh_callback = oauth_refresh_callback # NEW: OAuth refresh callback
51
53
 
52
54
  logger.debug("SSE Transport initialized with URL: %s", self.url)
53
55
 
@@ -184,12 +186,12 @@ class SSETransport(MCPBaseTransport):
184
186
  if self.sse_task.done():
185
187
  exception = self.sse_task.exception()
186
188
  if exception:
187
- logger.error(f"SSE task died during session discovery: {exception}")
189
+ logger.debug(f"SSE task died during session discovery: {exception}")
188
190
  await self._cleanup()
189
191
  return False
190
192
 
191
193
  if not self.message_url:
192
- logger.error("Failed to discover session endpoint within %.1fs", session_timeout)
194
+ logger.warning("Failed to discover session endpoint within %.1fs", session_timeout)
193
195
  await self._cleanup()
194
196
  return False
195
197
 
@@ -211,7 +213,7 @@ class SSETransport(MCPBaseTransport):
211
213
  )
212
214
 
213
215
  if "error" in init_response:
214
- logger.error("MCP initialize failed: %s", init_response["error"])
216
+ logger.warning("MCP initialize failed: %s", init_response["error"])
215
217
  await self._cleanup()
216
218
  return False
217
219
 
@@ -476,7 +478,7 @@ class SSETransport(MCPBaseTransport):
476
478
  async def get_tools(self) -> list[dict[str, Any]]:
477
479
  """Get list of available tools from the server."""
478
480
  if not self._initialized:
479
- logger.error("Cannot get tools: transport not initialized")
481
+ logger.debug("Cannot get tools: transport not initialized")
480
482
  return []
481
483
 
482
484
  start_time = time.time()
@@ -484,7 +486,7 @@ class SSETransport(MCPBaseTransport):
484
486
  response = await self._send_request("tools/list", {})
485
487
 
486
488
  if "error" in response:
487
- logger.error("Error getting tools: %s", response["error"])
489
+ logger.warning("Error getting tools: %s", response["error"])
488
490
  return []
489
491
 
490
492
  tools = response.get("result", {}).get("tools", [])
@@ -517,11 +519,55 @@ class SSETransport(MCPBaseTransport):
517
519
  "tools/call", {"name": tool_name, "arguments": arguments}, timeout=timeout
518
520
  )
519
521
 
522
+ # Check for errors
520
523
  if "error" in response:
524
+ error_msg = response["error"].get("message", "Unknown error")
525
+
526
+ # NEW: Check for OAuth errors and attempt refresh if callback is available
527
+ if self._is_oauth_error(error_msg):
528
+ logger.warning("OAuth error detected: %s", error_msg)
529
+
530
+ if self.oauth_refresh_callback:
531
+ logger.info("Attempting OAuth token refresh...")
532
+ try:
533
+ # Call the refresh callback
534
+ new_headers = await self.oauth_refresh_callback()
535
+
536
+ if new_headers and "Authorization" in new_headers:
537
+ # Update configured headers with new token
538
+ self.configured_headers.update(new_headers)
539
+ logger.info("OAuth token refreshed, retrying tool call...")
540
+
541
+ # Retry the tool call once with new token
542
+ response = await self._send_request(
543
+ "tools/call", {"name": tool_name, "arguments": arguments}, timeout=timeout
544
+ )
545
+
546
+ # Check if retry succeeded
547
+ if "error" not in response:
548
+ logger.info("Tool call succeeded after token refresh")
549
+ result = response.get("result", {})
550
+ normalized_result = self._normalize_mcp_response({"result": result})
551
+
552
+ if self.enable_metrics:
553
+ self._update_metrics(time.time() - start_time, True)
554
+
555
+ return normalized_result
556
+ else:
557
+ error_msg = response["error"].get("message", "Unknown error")
558
+ logger.error("Tool call failed after token refresh: %s", error_msg)
559
+ else:
560
+ logger.warning("Token refresh did not return valid Authorization header")
561
+ except Exception as refresh_error:
562
+ logger.error("OAuth token refresh failed: %s", refresh_error)
563
+ else:
564
+ logger.warning("OAuth error detected but no refresh callback configured")
565
+
566
+ # Return error (original or from failed retry)
521
567
  if self.enable_metrics:
522
568
  self._update_metrics(time.time() - start_time, False)
523
569
 
524
- return {"isError": True, "error": response["error"].get("message", "Unknown error")}
570
+ return {"isError": True, "error": error_msg}
525
571
 
526
572
  # Extract and normalize result using base class method
527
573
  result = response.get("result", {})
@@ -555,6 +601,24 @@ class SSETransport(MCPBaseTransport):
555
601
  if self._metrics["total_calls"] > 0:
556
602
  self._metrics["avg_response_time"] = self._metrics["total_time"] / self._metrics["total_calls"]
557
603
 
604
+ def _is_oauth_error(self, error_msg: str) -> bool:
605
+ """Detect if error is OAuth-related (NEW)."""
606
+ if not error_msg:
607
+ return False
608
+
609
+ error_lower = error_msg.lower()
610
+ oauth_indicators = [
611
+ "invalid_token",
612
+ "expired token",
613
+ "oauth validation",
614
+ "unauthorized",
615
+ "token expired",
616
+ "authentication failed",
617
+ "invalid access token",
618
+ ]
619
+
620
+ return any(indicator in error_lower for indicator in oauth_indicators)
621
+
558
622
  async def list_resources(self) -> dict[str, Any]:
559
623
  """List available resources from the server."""
560
624
  if not self._initialized:
@@ -221,7 +221,7 @@ class StdioTransport(MCPBaseTransport):
221
221
  )
222
222
  return True
223
223
  else:
224
- logger.warning("STDIO connection established but ping failed")
224
+ logger.debug("STDIO connection established but ping failed")
225
225
  # Still consider it initialized
226
226
  self._initialized = True
227
227
  self._consecutive_failures = 1
@@ -229,7 +229,7 @@ class StdioTransport(MCPBaseTransport):
229
229
  self._metrics["initialization_time"] = time.time() - start_time
230
230
  return True
231
231
  else:
232
- logger.error("STDIO initialization failed")
232
+ logger.warning("STDIO initialization failed")
233
233
  await self._cleanup()
234
234
  return False
235
235
 
@@ -382,7 +382,7 @@ class StdioTransport(MCPBaseTransport):
382
382
  async def get_tools(self) -> list[dict[str, Any]]:
383
383
  """Enhanced tools retrieval with recovery."""
384
384
  if not self._initialized:
385
- logger.error("Cannot get tools: transport not initialized")
385
+ logger.debug("Cannot get tools: transport not initialized")
386
386
  return []
387
387
 
388
388
  start_time = time.time()
@@ -121,7 +121,7 @@ class PluginDiscovery:
121
121
  # ------------------- Parser plugins -------------------------
122
122
  if issubclass(cls, ParserPlugin) and cls is not ParserPlugin:
123
123
  if not inspect.iscoroutinefunction(getattr(cls, "try_parse", None)):
124
- logger.warning("Skipping parser plugin %s: try_parse is not async", cls.__qualname__)
124
+ logger.debug("Skipping parser plugin %s: try_parse is not async", cls.__qualname__)
125
125
  else:
126
126
  try:
127
127
  self._registry.register_plugin("parser", cls.__name__, cls())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.6.26
3
+ Version: 0.6.28
4
4
  Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
5
5
  Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
6
6
  Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
@@ -770,6 +770,78 @@ results = await processor.process(
770
770
 
771
771
  See `examples/notion_oauth.py`, `examples/stdio_sqlite.py`, and `examples/stdio_echo.py` for complete working implementations.
772
772
 
773
+ #### OAuth Token Refresh
774
+
775
+ For MCP servers that use OAuth authentication, CHUK Tool Processor supports automatic token refresh when access tokens expire. This prevents your tools from failing due to expired tokens during long-running sessions.
776
+
777
+ **How it works:**
778
+ 1. When a tool call receives an OAuth-related error (e.g., "invalid_token", "expired token", "unauthorized")
779
+ 2. The processor automatically calls your refresh callback
780
+ 3. Updates the authentication headers with the new token
781
+ 4. Retries the tool call with fresh credentials
782
+
783
+ **Setup with HTTP Streamable:**
784
+
785
+ ```python
786
+ from chuk_tool_processor.mcp import setup_mcp_http_streamable
787
+
788
+ async def refresh_oauth_token():
789
+ """Called automatically when tokens expire."""
790
+ # Your token refresh logic here
791
+ # Return dict with new Authorization header
792
+ new_token = await your_refresh_logic()
793
+ return {"Authorization": f"Bearer {new_token}"}
794
+
795
+ processor, manager = await setup_mcp_http_streamable(
796
+ servers=[{
797
+ "name": "notion",
798
+ "url": "https://mcp.notion.com/mcp",
799
+ "headers": {"Authorization": f"Bearer {initial_access_token}"}
800
+ }],
801
+ namespace="notion",
802
+ oauth_refresh_callback=refresh_oauth_token # Enable auto-refresh
803
+ )
804
+ ```
805
+
806
+ **Setup with SSE:**
807
+
808
+ ```python
809
+ from chuk_tool_processor.mcp import setup_mcp_sse
810
+
811
+ async def refresh_oauth_token():
812
+ """Refresh expired OAuth token."""
813
+ # Exchange refresh token for new access token
814
+ new_access_token = await exchange_refresh_token(refresh_token)
815
+ return {"Authorization": f"Bearer {new_access_token}"}
816
+
817
+ processor, manager = await setup_mcp_sse(
818
+ servers=[{
819
+ "name": "atlassian",
820
+ "url": "https://mcp.atlassian.com/v1/sse",
821
+ "headers": {"Authorization": f"Bearer {initial_token}"}
822
+ }],
823
+ namespace="atlassian",
824
+ oauth_refresh_callback=refresh_oauth_token
825
+ )
826
+ ```
827
+
828
+ **OAuth errors detected automatically:**
829
+ - `invalid_token`
830
+ - `expired token`
831
+ - `OAuth validation failed`
832
+ - `unauthorized`
833
+ - `token expired`
834
+ - `authentication failed`
835
+ - `invalid access token`
836
+
837
+ **Important notes:**
838
+ - The refresh callback must return a dict with an `Authorization` key
839
+ - If refresh fails or returns invalid headers, the original error is returned
840
+ - Token refresh is attempted only once per tool call (no infinite retry loops)
841
+ - After successful refresh, the updated headers are used for all subsequent calls
842
+
843
+ See `examples/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
844
+
773
845
  ### Observability
774
846
 
775
847
  #### Structured Logging
@@ -1,7 +1,7 @@
1
1
  chuk_tool_processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  chuk_tool_processor/core/__init__.py,sha256=O4uwbEQN6G6sGrKKQNHRW-99ROlPEVct7wOSzVoazXQ,39
3
3
  chuk_tool_processor/core/exceptions.py,sha256=s35RVMIt8PQGP10ZS7L7sS0Pddpj0kc3Ut3wISDYn_U,1559
4
- chuk_tool_processor/core/processor.py,sha256=1pv76USunPXGIfeDs-wj2amdKvBBrBgkjgI2_JOK6vU,17825
4
+ chuk_tool_processor/core/processor.py,sha256=jIMdVjG5bqlUp2nAS6qr9dss2TOSpxodagOpb_0HT_g,17823
5
5
  chuk_tool_processor/execution/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  chuk_tool_processor/execution/tool_executor.py,sha256=zVQbNS9qUNn-1J1BPaFMGptKVFt0tXxh2bKiK-o1P2E,13705
7
7
  chuk_tool_processor/execution/strategies/__init__.py,sha256=fkHvK2Ca6c4npcpmS5aYHrdqYrgFm7DYdor-t-yCwuc,286
@@ -19,15 +19,15 @@ chuk_tool_processor/logging/metrics.py,sha256=8LRHjgkfdcQnh4H7AP2FJDfcRO3q1UpsBS
19
19
  chuk_tool_processor/mcp/__init__.py,sha256=xqtoSGX1_5jZDJ6AKJpBByaytS22baDOrhzFiecvVSs,1031
20
20
  chuk_tool_processor/mcp/mcp_tool.py,sha256=zOx3YeuKyuFK-PbUt3gqdq_q8VRrHFD6sgma6qKbfPY,19170
21
21
  chuk_tool_processor/mcp/register_mcp_tools.py,sha256=OyHczwVnqhvBZO9g4I0T56EPMvFYBOl0Y2ivNPdKjCE,4822
22
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py,sha256=dL-sWS7WivzmAc3SxrPS1jSmhX6AFg3NBegsEy7W9gk,4682
23
- chuk_tool_processor/mcp/setup_mcp_sse.py,sha256=7suVUR9Is4dFlgrG3_6ebgek8Yy9KpZA3hTWaCoo7vs,3921
22
+ chuk_tool_processor/mcp/setup_mcp_http_streamable.py,sha256=8NCjeEZjV0KrapCqAvGh6jr5G8B24xOxxAaKUyoiykw,4935
23
+ chuk_tool_processor/mcp/setup_mcp_sse.py,sha256=gvixVml8HN2BdM9Ug_JYp0yHqA1owieyX8Yxx0HNOqg,4174
24
24
  chuk_tool_processor/mcp/setup_mcp_stdio.py,sha256=KxCC0BL0C6z5ZHxBzPhWZC9CKrGUACXqx1tkjru-UYI,2922
25
- chuk_tool_processor/mcp/stream_manager.py,sha256=P3XsT__e-Giv3SCgv5w7X7sUX85HUL4CSpgoWUU4StE,33056
25
+ chuk_tool_processor/mcp/stream_manager.py,sha256=BA1vhk6azGg-Yt4N2iyqIT3ryvwqoq2ncRNuLMmStkk,34120
26
26
  chuk_tool_processor/mcp/transport/__init__.py,sha256=Gpw9QZctxfO-tWZ8URpyFU8rePc5Xe7VZiAvXaiF8cw,657
27
27
  chuk_tool_processor/mcp/transport/base_transport.py,sha256=rG61TlaignbVZbsqdBS38TnFzTVO666ehKEI0IUAJCM,8675
28
- chuk_tool_processor/mcp/transport/http_streamable_transport.py,sha256=00-kGM3Ie-fdi8VdIE-_YFsKP9_O8lEz9rGtPrR-Vy4,23619
29
- chuk_tool_processor/mcp/transport/sse_transport.py,sha256=gjtDe7KHnJMECITqjxfUhg3nVBuUFZhZv-k77gRiUsE,27399
30
- chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=bDh3eqe9BnPvcmG7F4BWqGjGO3e8q0lwcZeswx_jK0U,29558
28
+ chuk_tool_processor/mcp/transport/http_streamable_transport.py,sha256=aE-ITNoT-iipJ5tF97eGzv3hxABnbfFpjPokbCsy1rg,26545
29
+ chuk_tool_processor/mcp/transport/sse_transport.py,sha256=BCd2e8QlIQN0qaPBAC9w0Q9ybOXnRdg8GUkKZ6Oo4k0,30635
30
+ chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=kS90NpQA-KdxRdH-hYN63P22vc0fF4MvQ4N2spo8U6k,29558
31
31
  chuk_tool_processor/models/__init__.py,sha256=A3ysSvRxaxso_AN57QZt5ZYahJH5zlL-IENqNaius84,41
32
32
  chuk_tool_processor/models/execution_strategy.py,sha256=O0h8d8JSgm-tv26Cc5jAkZqup8vIgx0zfb5n0b2vpSk,1967
33
33
  chuk_tool_processor/models/streaming_tool.py,sha256=3IXe9VV6sgPHzMeHpuNzZThFhu5BuB3MQdoYSogz348,3359
@@ -36,7 +36,7 @@ chuk_tool_processor/models/tool_export_mixin.py,sha256=aP65iHmpDquB16L2PwXvZ2glt
36
36
  chuk_tool_processor/models/tool_result.py,sha256=aq-RVRaYSIl7fwCb8nr9yjazyT0qfL408PrqY05bSVM,4615
37
37
  chuk_tool_processor/models/validated_tool.py,sha256=5O8eiEsRPuPb7SHEtCoRwGKUBUPlMx4qXe9GtsQZSGo,5681
38
38
  chuk_tool_processor/plugins/__init__.py,sha256=LsW8KTaW2LogUvpoA6538NJyMLFmN9TISdLNog2NlH8,49
39
- chuk_tool_processor/plugins/discovery.py,sha256=bDaXMIMstPZOIzhdBtQpg1HlahbX5Hf1T5LEH86fjtE,7067
39
+ chuk_tool_processor/plugins/discovery.py,sha256=Trq1P-AxWEKAtOUCB9UMwbp96mtGAuM4cBytjE-ahBU,7065
40
40
  chuk_tool_processor/plugins/parsers/__init__.py,sha256=D0NUmlObNeOevHGv_eaurFiUmoptmd1mEda2sXLcTMs,50
41
41
  chuk_tool_processor/plugins/parsers/base.py,sha256=nbBb8Nfn8Q0pXxCkhj7GSyuqnO2aoEBag-7NoobI8GA,726
42
42
  chuk_tool_processor/plugins/parsers/function_call_tool.py,sha256=1fFuP4sqT5LvQHuosqmg8LSFPreIaOnlLroRsJzwPfA,3338
@@ -54,7 +54,7 @@ chuk_tool_processor/registry/providers/__init__.py,sha256=iGc_2JzlYJSBRQ6tFbX781
54
54
  chuk_tool_processor/registry/providers/memory.py,sha256=udfboAHH0gRxtnf3GsI3wMshhobJxYnCkMwKjQ_uqkw,5017
55
55
  chuk_tool_processor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
56
56
  chuk_tool_processor/utils/validation.py,sha256=jHPO65sB61ynm9P6V3th4pN7j4u0SQhYR-bstj5QjnI,4175
57
- chuk_tool_processor-0.6.26.dist-info/METADATA,sha256=B8mZl0NXBSyt2jc9D9FGVB-JhEQK3d1VRFh2A9Ecrxg,36881
58
- chuk_tool_processor-0.6.26.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
59
- chuk_tool_processor-0.6.26.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
60
- chuk_tool_processor-0.6.26.dist-info/RECORD,,
57
+ chuk_tool_processor-0.6.28.dist-info/METADATA,sha256=6GwYbhRhrVlOCYCd0fX9fMkHuBniPUEIDhp4byjdrdg,39362
58
+ chuk_tool_processor-0.6.28.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
59
+ chuk_tool_processor-0.6.28.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
60
+ chuk_tool_processor-0.6.28.dist-info/RECORD,,