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

@@ -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
 
@@ -285,6 +289,7 @@ class StreamManager:
285
289
  connection_timeout: float = 10.0,
286
290
  default_timeout: float = 30.0,
287
291
  initialization_timeout: float = 60.0,
292
+ oauth_refresh_callback: any | None = None, # NEW: OAuth token refresh callback
288
293
  ) -> None:
289
294
  """Initialize with SSE transport with optional headers support."""
290
295
  if self._closed:
@@ -313,6 +318,11 @@ class StreamManager:
313
318
  logger.debug("SSE %s: Using configured headers: %s", name, list(headers.keys()))
314
319
  transport_params["headers"] = headers
315
320
 
321
+ # Add OAuth refresh callback if provided (NEW)
322
+ if oauth_refresh_callback:
323
+ transport_params["oauth_refresh_callback"] = oauth_refresh_callback
324
+ logger.debug("SSE %s: OAuth refresh callback configured", name)
325
+
316
326
  transport = SSETransport(**transport_params)
317
327
 
318
328
  try:
@@ -354,6 +364,7 @@ class StreamManager:
354
364
  connection_timeout: float = 30.0,
355
365
  default_timeout: float = 30.0,
356
366
  initialization_timeout: float = 60.0,
367
+ oauth_refresh_callback: any | None = None, # NEW: OAuth token refresh callback
357
368
  ) -> None:
358
369
  """Initialize with HTTP Streamable transport with graceful headers handling."""
359
370
  if self._closed:
@@ -385,6 +396,11 @@ class StreamManager:
385
396
  transport_params["headers"] = headers
386
397
  logger.debug("HTTP Streamable %s: Custom headers configured: %s", name, list(headers.keys()))
387
398
 
399
+ # Add OAuth refresh callback if provided (NEW)
400
+ if oauth_refresh_callback:
401
+ transport_params["oauth_refresh_callback"] = oauth_refresh_callback
402
+ logger.debug("HTTP Streamable %s: OAuth refresh callback configured", name)
403
+
388
404
  transport = HTTPStreamableTransport(**transport_params)
389
405
 
390
406
  logger.debug(f"Calling transport.initialize() for {name} with timeout={initialization_timeout}s")
@@ -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:
@@ -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
 
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.6.25
3
+ Version: 0.6.27
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
@@ -19,14 +19,14 @@ 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=GKUXqJpRn_O8008qJompwZfdFlb6nRlZZGh8jqr23y0,34184
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
28
+ chuk_tool_processor/mcp/transport/http_streamable_transport.py,sha256=PKU-s2cofagY_x5nwocfi3Q0uJjoAHh8ALP0nePWVRc,26547
29
+ chuk_tool_processor/mcp/transport/sse_transport.py,sha256=WfsfsHzN579vPLl13cjHwi5sn_uKrf4ZnVxasEYHy6I,30629
30
30
  chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=bDh3eqe9BnPvcmG7F4BWqGjGO3e8q0lwcZeswx_jK0U,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
@@ -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.25.dist-info/METADATA,sha256=p0Up04jiimvOqYutjykDUoj7gviM6vSmA6SixQGdWTw,36881
58
- chuk_tool_processor-0.6.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
59
- chuk_tool_processor-0.6.25.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
60
- chuk_tool_processor-0.6.25.dist-info/RECORD,,
57
+ chuk_tool_processor-0.6.27.dist-info/METADATA,sha256=_EJFoLfhbyqOeXNhpHu9N2z4HMLlTfkDMCb9nO97OVo,39362
58
+ chuk_tool_processor-0.6.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
59
+ chuk_tool_processor-0.6.27.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
60
+ chuk_tool_processor-0.6.27.dist-info/RECORD,,