chuk-tool-processor 0.6.26__tar.gz → 0.6.28__tar.gz
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.
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/PKG-INFO +73 -1
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/README.md +72 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/pyproject.toml +1 -1
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/core/processor.py +2 -2
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +3 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +3 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/stream_manager.py +23 -9
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +61 -5
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/transport/sse_transport.py +70 -6
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +3 -3
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/plugins/discovery.py +1 -1
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor.egg-info/PKG-INFO +73 -1
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/setup.cfg +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/core/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/core/exceptions.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/execution/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/logging/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/logging/context.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/logging/formatter.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/logging/helpers.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/logging/metrics.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/models/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/models/tool_call.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/models/tool_result.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/models/validated_tool.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/plugins/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/registry/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/registry/auto_register.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/registry/decorators.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/registry/interface.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/registry/metadata.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/registry/provider.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/registry/tool_export.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/utils/__init__.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/utils/validation.py +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
- {chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-tool-processor
|
|
3
|
-
Version: 0.6.
|
|
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
|
|
@@ -742,6 +742,78 @@ results = await processor.process(
|
|
|
742
742
|
|
|
743
743
|
See `examples/notion_oauth.py`, `examples/stdio_sqlite.py`, and `examples/stdio_echo.py` for complete working implementations.
|
|
744
744
|
|
|
745
|
+
#### OAuth Token Refresh
|
|
746
|
+
|
|
747
|
+
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.
|
|
748
|
+
|
|
749
|
+
**How it works:**
|
|
750
|
+
1. When a tool call receives an OAuth-related error (e.g., "invalid_token", "expired token", "unauthorized")
|
|
751
|
+
2. The processor automatically calls your refresh callback
|
|
752
|
+
3. Updates the authentication headers with the new token
|
|
753
|
+
4. Retries the tool call with fresh credentials
|
|
754
|
+
|
|
755
|
+
**Setup with HTTP Streamable:**
|
|
756
|
+
|
|
757
|
+
```python
|
|
758
|
+
from chuk_tool_processor.mcp import setup_mcp_http_streamable
|
|
759
|
+
|
|
760
|
+
async def refresh_oauth_token():
|
|
761
|
+
"""Called automatically when tokens expire."""
|
|
762
|
+
# Your token refresh logic here
|
|
763
|
+
# Return dict with new Authorization header
|
|
764
|
+
new_token = await your_refresh_logic()
|
|
765
|
+
return {"Authorization": f"Bearer {new_token}"}
|
|
766
|
+
|
|
767
|
+
processor, manager = await setup_mcp_http_streamable(
|
|
768
|
+
servers=[{
|
|
769
|
+
"name": "notion",
|
|
770
|
+
"url": "https://mcp.notion.com/mcp",
|
|
771
|
+
"headers": {"Authorization": f"Bearer {initial_access_token}"}
|
|
772
|
+
}],
|
|
773
|
+
namespace="notion",
|
|
774
|
+
oauth_refresh_callback=refresh_oauth_token # Enable auto-refresh
|
|
775
|
+
)
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
**Setup with SSE:**
|
|
779
|
+
|
|
780
|
+
```python
|
|
781
|
+
from chuk_tool_processor.mcp import setup_mcp_sse
|
|
782
|
+
|
|
783
|
+
async def refresh_oauth_token():
|
|
784
|
+
"""Refresh expired OAuth token."""
|
|
785
|
+
# Exchange refresh token for new access token
|
|
786
|
+
new_access_token = await exchange_refresh_token(refresh_token)
|
|
787
|
+
return {"Authorization": f"Bearer {new_access_token}"}
|
|
788
|
+
|
|
789
|
+
processor, manager = await setup_mcp_sse(
|
|
790
|
+
servers=[{
|
|
791
|
+
"name": "atlassian",
|
|
792
|
+
"url": "https://mcp.atlassian.com/v1/sse",
|
|
793
|
+
"headers": {"Authorization": f"Bearer {initial_token}"}
|
|
794
|
+
}],
|
|
795
|
+
namespace="atlassian",
|
|
796
|
+
oauth_refresh_callback=refresh_oauth_token
|
|
797
|
+
)
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
**OAuth errors detected automatically:**
|
|
801
|
+
- `invalid_token`
|
|
802
|
+
- `expired token`
|
|
803
|
+
- `OAuth validation failed`
|
|
804
|
+
- `unauthorized`
|
|
805
|
+
- `token expired`
|
|
806
|
+
- `authentication failed`
|
|
807
|
+
- `invalid access token`
|
|
808
|
+
|
|
809
|
+
**Important notes:**
|
|
810
|
+
- The refresh callback must return a dict with an `Authorization` key
|
|
811
|
+
- If refresh fails or returns invalid headers, the original error is returned
|
|
812
|
+
- Token refresh is attempted only once per tool call (no infinite retry loops)
|
|
813
|
+
- After successful refresh, the updated headers are used for all subsequent calls
|
|
814
|
+
|
|
815
|
+
See `examples/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
|
|
816
|
+
|
|
745
817
|
### Observability
|
|
746
818
|
|
|
747
819
|
#### Structured Logging
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "chuk-tool-processor"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.28"
|
|
8
8
|
description = "Async-native framework for registering, discovering, and executing tools referenced in LLM responses"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/core/processor.py
RENAMED
|
@@ -260,7 +260,7 @@ class ToolProcessor:
|
|
|
260
260
|
unknown_tools.append(call.tool)
|
|
261
261
|
|
|
262
262
|
if unknown_tools:
|
|
263
|
-
self.logger.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
427
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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":
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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())
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-tool-processor
|
|
3
|
-
Version: 0.6.
|
|
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
|
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/__init__.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/core/__init__.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/core/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/logging/context.py
RENAMED
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/logging/helpers.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/logging/metrics.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/__init__.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/mcp/mcp_tool.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.6.26 → chuk_tool_processor-0.6.28}/src/chuk_tool_processor/utils/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|