chuk-tool-processor 0.6.10__tar.gz → 0.6.11__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.
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/PKG-INFO +2 -1
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/pyproject.toml +3 -2
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +229 -78
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/transport/sse_transport.py +98 -70
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +293 -58
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor.egg-info/PKG-INFO +2 -1
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor.egg-info/requires.txt +1 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/README.md +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/setup.cfg +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/core/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/core/exceptions.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/core/processor.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/execution/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/logging/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/logging/context.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/logging/formatter.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/logging/helpers.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/logging/metrics.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/models/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/models/tool_call.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/models/tool_result.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/models/validated_tool.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/plugins/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/plugins/discovery.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/registry/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/registry/auto_register.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/registry/decorators.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/registry/interface.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/registry/metadata.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/registry/provider.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/registry/tool_export.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/utils/__init__.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor/utils/validation.py +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
- {chuk_tool_processor-0.6.10 → chuk_tool_processor-0.6.11}/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.11
|
|
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>
|
|
@@ -22,6 +22,7 @@ Requires-Python: >=3.11
|
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
23
|
Requires-Dist: chuk-mcp>=0.5.2
|
|
24
24
|
Requires-Dist: dotenv>=0.9.9
|
|
25
|
+
Requires-Dist: psutil>=7.0.0
|
|
25
26
|
Requires-Dist: pydantic>=2.11.3
|
|
26
27
|
Requires-Dist: uuid>=1.30
|
|
27
28
|
|
|
@@ -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.11"
|
|
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"
|
|
@@ -43,6 +43,7 @@ classifiers = [
|
|
|
43
43
|
dependencies = [
|
|
44
44
|
"chuk-mcp>=0.5.2",
|
|
45
45
|
"dotenv>=0.9.9",
|
|
46
|
+
"psutil>=7.0.0",
|
|
46
47
|
"pydantic>=2.11.3",
|
|
47
48
|
"uuid>=1.30",
|
|
48
49
|
]
|
|
@@ -68,4 +69,4 @@ dev = [
|
|
|
68
69
|
"uvicorn>=0.34.2",
|
|
69
70
|
"fastapi>=0.115.12",
|
|
70
71
|
"langchain>=0.3.25",
|
|
71
|
-
]
|
|
72
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# chuk_tool_processor/mcp/transport/http_streamable_transport.py -
|
|
1
|
+
# chuk_tool_processor/mcp/transport/http_streamable_transport.py - ENHANCED
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
@@ -30,21 +30,23 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
30
30
|
"""
|
|
31
31
|
HTTP Streamable transport using chuk-mcp HTTP client.
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
ENHANCED: Now matches SSE transport robustness with improved connection
|
|
34
|
+
management, health monitoring, and comprehensive error handling.
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
def __init__(self, url: str, api_key: Optional[str] = None,
|
|
37
|
+
def __init__(self, url: str, api_key: Optional[str] = None,
|
|
38
|
+
headers: Optional[Dict[str, str]] = None, # NEW: Headers support
|
|
38
39
|
connection_timeout: float = 30.0,
|
|
39
40
|
default_timeout: float = 30.0,
|
|
40
41
|
session_id: Optional[str] = None,
|
|
41
42
|
enable_metrics: bool = True):
|
|
42
43
|
"""
|
|
43
|
-
Initialize HTTP Streamable transport with
|
|
44
|
+
Initialize HTTP Streamable transport with enhanced configuration.
|
|
44
45
|
|
|
45
46
|
Args:
|
|
46
47
|
url: HTTP server URL (should end with /mcp)
|
|
47
48
|
api_key: Optional API key for authentication
|
|
49
|
+
headers: Optional custom headers (NEW)
|
|
48
50
|
connection_timeout: Timeout for initial connection
|
|
49
51
|
default_timeout: Default timeout for operations
|
|
50
52
|
session_id: Optional session ID for stateful connections
|
|
@@ -57,6 +59,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
57
59
|
self.url = url
|
|
58
60
|
|
|
59
61
|
self.api_key = api_key
|
|
62
|
+
self.configured_headers = headers or {} # NEW: Store configured headers
|
|
60
63
|
self.connection_timeout = connection_timeout
|
|
61
64
|
self.default_timeout = default_timeout
|
|
62
65
|
self.session_id = session_id
|
|
@@ -65,16 +68,23 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
65
68
|
logger.debug("HTTP Streamable transport initialized with URL: %s", self.url)
|
|
66
69
|
if self.api_key:
|
|
67
70
|
logger.debug("API key configured for authentication")
|
|
71
|
+
if self.configured_headers:
|
|
72
|
+
logger.debug("Custom headers configured: %s", list(self.configured_headers.keys()))
|
|
68
73
|
if self.session_id:
|
|
69
74
|
logger.debug("Session ID configured: %s", self.session_id)
|
|
70
75
|
|
|
71
|
-
# State tracking
|
|
76
|
+
# State tracking (enhanced like SSE)
|
|
72
77
|
self._http_context = None
|
|
73
78
|
self._read_stream = None
|
|
74
79
|
self._write_stream = None
|
|
75
80
|
self._initialized = False
|
|
76
81
|
|
|
77
|
-
#
|
|
82
|
+
# Health monitoring (NEW - like SSE)
|
|
83
|
+
self._last_successful_ping = None
|
|
84
|
+
self._consecutive_failures = 0
|
|
85
|
+
self._max_consecutive_failures = 3
|
|
86
|
+
|
|
87
|
+
# Performance metrics (enhanced like SSE)
|
|
78
88
|
self._metrics = {
|
|
79
89
|
"total_calls": 0,
|
|
80
90
|
"successful_calls": 0,
|
|
@@ -84,11 +94,49 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
84
94
|
"last_ping_time": None,
|
|
85
95
|
"initialization_time": None,
|
|
86
96
|
"connection_resets": 0,
|
|
87
|
-
"stream_errors": 0
|
|
97
|
+
"stream_errors": 0,
|
|
98
|
+
"connection_errors": 0, # NEW
|
|
99
|
+
"recovery_attempts": 0, # NEW
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
103
|
+
"""Get headers with authentication and custom headers (like SSE)."""
|
|
104
|
+
headers = {
|
|
105
|
+
"Content-Type": "application/json",
|
|
106
|
+
"Accept": "application/json, text/event-stream",
|
|
107
|
+
'User-Agent': 'chuk-tool-processor/1.0.0',
|
|
88
108
|
}
|
|
109
|
+
|
|
110
|
+
# Add configured headers first
|
|
111
|
+
if self.configured_headers:
|
|
112
|
+
headers.update(self.configured_headers)
|
|
113
|
+
|
|
114
|
+
# Add API key as Bearer token if provided
|
|
115
|
+
if self.api_key:
|
|
116
|
+
headers['Authorization'] = f'Bearer {self.api_key}'
|
|
117
|
+
|
|
118
|
+
# Add session ID if provided
|
|
119
|
+
if self.session_id:
|
|
120
|
+
headers["X-Session-ID"] = self.session_id
|
|
121
|
+
|
|
122
|
+
return headers
|
|
123
|
+
|
|
124
|
+
async def _test_connection_health(self) -> bool:
|
|
125
|
+
"""Test basic HTTP connectivity (like SSE's connectivity test)."""
|
|
126
|
+
try:
|
|
127
|
+
import httpx
|
|
128
|
+
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
129
|
+
# Test basic connectivity to base URL
|
|
130
|
+
base_url = self.url.replace('/mcp', '')
|
|
131
|
+
response = await client.get(f"{base_url}/health", headers=self._get_headers())
|
|
132
|
+
logger.debug("Health check response: %s", response.status_code)
|
|
133
|
+
return response.status_code < 500 # Accept any non-server-error
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.debug("Connection health test failed: %s", e)
|
|
136
|
+
return True # Don't fail on health check errors
|
|
89
137
|
|
|
90
138
|
async def initialize(self) -> bool:
|
|
91
|
-
"""Initialize
|
|
139
|
+
"""Initialize with enhanced error handling and health monitoring."""
|
|
92
140
|
if self._initialized:
|
|
93
141
|
logger.warning("Transport already initialized")
|
|
94
142
|
return True
|
|
@@ -98,50 +146,41 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
98
146
|
try:
|
|
99
147
|
logger.debug("Initializing HTTP Streamable transport to %s", self.url)
|
|
100
148
|
|
|
101
|
-
#
|
|
102
|
-
|
|
103
|
-
"
|
|
104
|
-
"Accept": "application/json, text/event-stream",
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
# FIXED: Only set Authorization header, not both bearer_token and headers
|
|
108
|
-
bearer_token = None
|
|
109
|
-
if self.api_key:
|
|
110
|
-
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
111
|
-
logger.debug("API key configured for authentication")
|
|
149
|
+
# Test basic connectivity first (like SSE)
|
|
150
|
+
if not await self._test_connection_health():
|
|
151
|
+
logger.warning("Connection health test failed, proceeding anyway")
|
|
112
152
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
153
|
+
# Build headers properly
|
|
154
|
+
headers = self._get_headers()
|
|
155
|
+
logger.debug("Using headers: %s", list(headers.keys()))
|
|
116
156
|
|
|
117
|
-
#
|
|
157
|
+
# Create StreamableHTTPParameters with proper configuration
|
|
118
158
|
http_params = StreamableHTTPParameters(
|
|
119
159
|
url=self.url,
|
|
120
|
-
timeout=self.default_timeout,
|
|
160
|
+
timeout=self.default_timeout,
|
|
121
161
|
headers=headers,
|
|
122
|
-
bearer_token=
|
|
162
|
+
bearer_token=None, # Don't duplicate auth - it's in headers
|
|
123
163
|
session_id=self.session_id,
|
|
124
164
|
enable_streaming=True,
|
|
125
|
-
max_concurrent_requests=5,
|
|
126
|
-
max_retries=2,
|
|
127
|
-
retry_delay=1.0,
|
|
165
|
+
max_concurrent_requests=5,
|
|
166
|
+
max_retries=2,
|
|
167
|
+
retry_delay=1.0,
|
|
128
168
|
user_agent="chuk-tool-processor/1.0.0",
|
|
129
169
|
)
|
|
130
170
|
|
|
131
171
|
# Create and enter the HTTP context
|
|
132
172
|
self._http_context = http_client(http_params)
|
|
133
173
|
|
|
134
|
-
logger.debug("Establishing HTTP connection
|
|
174
|
+
logger.debug("Establishing HTTP connection...")
|
|
135
175
|
self._read_stream, self._write_stream = await asyncio.wait_for(
|
|
136
176
|
self._http_context.__aenter__(),
|
|
137
177
|
timeout=self.connection_timeout
|
|
138
178
|
)
|
|
139
179
|
|
|
140
|
-
#
|
|
180
|
+
# Enhanced MCP initialize sequence
|
|
141
181
|
logger.debug("Sending MCP initialize request...")
|
|
142
182
|
init_start = time.time()
|
|
143
183
|
|
|
144
|
-
# Send initialize request with default parameters
|
|
145
184
|
init_result = await asyncio.wait_for(
|
|
146
185
|
send_initialize(self._read_stream, self._write_stream),
|
|
147
186
|
timeout=self.default_timeout
|
|
@@ -150,54 +189,83 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
150
189
|
init_time = time.time() - init_start
|
|
151
190
|
logger.debug("MCP initialize completed in %.3fs", init_time)
|
|
152
191
|
|
|
153
|
-
# Verify
|
|
192
|
+
# Verify connection with ping (enhanced like SSE)
|
|
154
193
|
logger.debug("Verifying connection with ping...")
|
|
155
194
|
ping_start = time.time()
|
|
156
195
|
ping_success = await asyncio.wait_for(
|
|
157
196
|
send_ping(self._read_stream, self._write_stream),
|
|
158
|
-
timeout=
|
|
197
|
+
timeout=10.0 # Longer timeout for initial ping
|
|
159
198
|
)
|
|
160
199
|
ping_time = time.time() - ping_start
|
|
161
200
|
|
|
162
201
|
if ping_success:
|
|
163
202
|
self._initialized = True
|
|
203
|
+
self._last_successful_ping = time.time()
|
|
204
|
+
self._consecutive_failures = 0
|
|
205
|
+
|
|
164
206
|
total_init_time = time.time() - start_time
|
|
165
207
|
if self.enable_metrics:
|
|
166
208
|
self._metrics["initialization_time"] = total_init_time
|
|
167
209
|
self._metrics["last_ping_time"] = ping_time
|
|
168
210
|
|
|
169
|
-
logger.debug("HTTP Streamable transport initialized successfully in %.3fs (ping: %.3fs)",
|
|
211
|
+
logger.debug("HTTP Streamable transport initialized successfully in %.3fs (ping: %.3fs)",
|
|
212
|
+
total_init_time, ping_time)
|
|
170
213
|
return True
|
|
171
214
|
else:
|
|
172
215
|
logger.warning("HTTP connection established but ping failed")
|
|
173
216
|
# Still consider it initialized since connection was established
|
|
174
217
|
self._initialized = True
|
|
218
|
+
self._consecutive_failures = 1 # Mark one failure
|
|
175
219
|
if self.enable_metrics:
|
|
176
220
|
self._metrics["initialization_time"] = time.time() - start_time
|
|
177
221
|
return True
|
|
178
222
|
|
|
179
223
|
except asyncio.TimeoutError:
|
|
180
224
|
logger.error("HTTP Streamable initialization timed out after %ss", self.connection_timeout)
|
|
181
|
-
logger.error("This may indicate the server is not responding to MCP initialization")
|
|
182
225
|
await self._cleanup()
|
|
226
|
+
if self.enable_metrics:
|
|
227
|
+
self._metrics["connection_errors"] += 1
|
|
183
228
|
return False
|
|
184
229
|
except Exception as e:
|
|
185
230
|
logger.error("Error initializing HTTP Streamable transport: %s", e, exc_info=True)
|
|
186
231
|
await self._cleanup()
|
|
232
|
+
if self.enable_metrics:
|
|
233
|
+
self._metrics["connection_errors"] += 1
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
async def _attempt_recovery(self) -> bool:
|
|
237
|
+
"""Attempt to recover from connection issues (NEW - like SSE resilience)."""
|
|
238
|
+
if self.enable_metrics:
|
|
239
|
+
self._metrics["recovery_attempts"] += 1
|
|
240
|
+
|
|
241
|
+
logger.debug("Attempting HTTP connection recovery...")
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
# Clean up existing connection
|
|
245
|
+
await self._cleanup()
|
|
246
|
+
|
|
247
|
+
# Re-initialize
|
|
248
|
+
return await self.initialize()
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.warning("Recovery attempt failed: %s", e)
|
|
187
251
|
return False
|
|
188
252
|
|
|
189
253
|
async def close(self) -> None:
|
|
190
|
-
"""Close
|
|
254
|
+
"""Close with enhanced cleanup and metrics reporting."""
|
|
191
255
|
if not self._initialized:
|
|
192
256
|
return
|
|
193
257
|
|
|
194
|
-
#
|
|
258
|
+
# Enhanced metrics logging (like SSE)
|
|
195
259
|
if self.enable_metrics and self._metrics["total_calls"] > 0:
|
|
260
|
+
success_rate = (self._metrics["successful_calls"] / self._metrics["total_calls"] * 100)
|
|
196
261
|
logger.debug(
|
|
197
|
-
"HTTP Streamable transport closing -
|
|
262
|
+
"HTTP Streamable transport closing - Calls: %d, Success: %.1f%%, "
|
|
263
|
+
"Avg time: %.3fs, Recoveries: %d, Errors: %d",
|
|
198
264
|
self._metrics["total_calls"],
|
|
199
|
-
|
|
200
|
-
self._metrics["avg_response_time"]
|
|
265
|
+
success_rate,
|
|
266
|
+
self._metrics["avg_response_time"],
|
|
267
|
+
self._metrics["recovery_attempts"],
|
|
268
|
+
self._metrics["connection_errors"]
|
|
201
269
|
)
|
|
202
270
|
|
|
203
271
|
try:
|
|
@@ -211,14 +279,14 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
211
279
|
await self._cleanup()
|
|
212
280
|
|
|
213
281
|
async def _cleanup(self) -> None:
|
|
214
|
-
"""
|
|
282
|
+
"""Enhanced cleanup with state reset."""
|
|
215
283
|
self._http_context = None
|
|
216
284
|
self._read_stream = None
|
|
217
285
|
self._write_stream = None
|
|
218
286
|
self._initialized = False
|
|
219
287
|
|
|
220
288
|
async def send_ping(self) -> bool:
|
|
221
|
-
"""
|
|
289
|
+
"""Enhanced ping with health monitoring (like SSE)."""
|
|
222
290
|
if not self._initialized or not self._read_stream:
|
|
223
291
|
logger.error("Cannot send ping: transport not initialized")
|
|
224
292
|
return False
|
|
@@ -230,27 +298,45 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
230
298
|
timeout=self.default_timeout
|
|
231
299
|
)
|
|
232
300
|
|
|
301
|
+
success = bool(result)
|
|
302
|
+
|
|
303
|
+
if success:
|
|
304
|
+
self._last_successful_ping = time.time()
|
|
305
|
+
self._consecutive_failures = 0
|
|
306
|
+
else:
|
|
307
|
+
self._consecutive_failures += 1
|
|
308
|
+
|
|
233
309
|
if self.enable_metrics:
|
|
234
310
|
ping_time = time.time() - start_time
|
|
235
311
|
self._metrics["last_ping_time"] = ping_time
|
|
236
|
-
logger.debug("HTTP Streamable ping completed in %.3fs: %s", ping_time,
|
|
312
|
+
logger.debug("HTTP Streamable ping completed in %.3fs: %s", ping_time, success)
|
|
237
313
|
|
|
238
|
-
return
|
|
314
|
+
return success
|
|
239
315
|
except asyncio.TimeoutError:
|
|
240
316
|
logger.error("HTTP Streamable ping timed out")
|
|
317
|
+
self._consecutive_failures += 1
|
|
241
318
|
return False
|
|
242
319
|
except Exception as e:
|
|
243
320
|
logger.error("HTTP Streamable ping failed: %s", e)
|
|
321
|
+
self._consecutive_failures += 1
|
|
244
322
|
if self.enable_metrics:
|
|
245
323
|
self._metrics["stream_errors"] += 1
|
|
246
324
|
return False
|
|
247
325
|
|
|
248
326
|
def is_connected(self) -> bool:
|
|
249
|
-
"""
|
|
250
|
-
|
|
327
|
+
"""Enhanced connection status check (like SSE)."""
|
|
328
|
+
if not self._initialized or not self._read_stream or not self._write_stream:
|
|
329
|
+
return False
|
|
330
|
+
|
|
331
|
+
# Check if we've had too many consecutive failures (like SSE)
|
|
332
|
+
if self._consecutive_failures >= self._max_consecutive_failures:
|
|
333
|
+
logger.warning("Connection marked unhealthy after %d failures", self._consecutive_failures)
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
return True
|
|
251
337
|
|
|
252
338
|
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
253
|
-
"""
|
|
339
|
+
"""Enhanced tools retrieval with error handling."""
|
|
254
340
|
if not self._initialized:
|
|
255
341
|
logger.error("Cannot get tools: transport not initialized")
|
|
256
342
|
return []
|
|
@@ -271,6 +357,9 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
271
357
|
logger.warning("Unexpected tools response type: %s", type(tools_response))
|
|
272
358
|
tools = []
|
|
273
359
|
|
|
360
|
+
# Reset failure count on success
|
|
361
|
+
self._consecutive_failures = 0
|
|
362
|
+
|
|
274
363
|
if self.enable_metrics:
|
|
275
364
|
response_time = time.time() - start_time
|
|
276
365
|
logger.debug("Retrieved %d tools in %.3fs", len(tools), response_time)
|
|
@@ -279,16 +368,18 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
279
368
|
|
|
280
369
|
except asyncio.TimeoutError:
|
|
281
370
|
logger.error("Get tools timed out")
|
|
371
|
+
self._consecutive_failures += 1
|
|
282
372
|
return []
|
|
283
373
|
except Exception as e:
|
|
284
374
|
logger.error("Error getting tools: %s", e)
|
|
375
|
+
self._consecutive_failures += 1
|
|
285
376
|
if self.enable_metrics:
|
|
286
377
|
self._metrics["stream_errors"] += 1
|
|
287
378
|
return []
|
|
288
379
|
|
|
289
380
|
async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
|
|
290
381
|
timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
291
|
-
"""
|
|
382
|
+
"""Enhanced tool calling with recovery and health monitoring."""
|
|
292
383
|
if not self._initialized:
|
|
293
384
|
return {
|
|
294
385
|
"isError": True,
|
|
@@ -304,13 +395,15 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
304
395
|
try:
|
|
305
396
|
logger.debug("Calling tool '%s' with timeout %ss", tool_name, tool_timeout)
|
|
306
397
|
|
|
307
|
-
#
|
|
398
|
+
# Enhanced connection check with recovery attempt
|
|
308
399
|
if not self.is_connected():
|
|
309
|
-
logger.warning("Connection
|
|
310
|
-
if not await self.
|
|
400
|
+
logger.warning("Connection unhealthy, attempting recovery...")
|
|
401
|
+
if not await self._attempt_recovery():
|
|
402
|
+
if self.enable_metrics:
|
|
403
|
+
self._update_metrics(time.time() - start_time, False)
|
|
311
404
|
return {
|
|
312
405
|
"isError": True,
|
|
313
|
-
"error": "Failed to
|
|
406
|
+
"error": "Failed to recover connection"
|
|
314
407
|
}
|
|
315
408
|
|
|
316
409
|
raw_response = await asyncio.wait_for(
|
|
@@ -326,18 +419,24 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
326
419
|
response_time = time.time() - start_time
|
|
327
420
|
result = self._normalize_mcp_response(raw_response)
|
|
328
421
|
|
|
422
|
+
# Reset failure count on success
|
|
423
|
+
self._consecutive_failures = 0
|
|
424
|
+
self._last_successful_ping = time.time() # Update health timestamp
|
|
425
|
+
|
|
329
426
|
if self.enable_metrics:
|
|
330
427
|
self._update_metrics(response_time, not result.get("isError", False))
|
|
331
428
|
|
|
332
429
|
if not result.get("isError", False):
|
|
333
430
|
logger.debug("Tool '%s' completed successfully in %.3fs", tool_name, response_time)
|
|
334
431
|
else:
|
|
335
|
-
logger.warning("Tool '%s' failed in %.3fs: %s", tool_name, response_time,
|
|
432
|
+
logger.warning("Tool '%s' failed in %.3fs: %s", tool_name, response_time,
|
|
433
|
+
result.get('error', 'Unknown error'))
|
|
336
434
|
|
|
337
435
|
return result
|
|
338
436
|
|
|
339
437
|
except asyncio.TimeoutError:
|
|
340
438
|
response_time = time.time() - start_time
|
|
439
|
+
self._consecutive_failures += 1
|
|
341
440
|
if self.enable_metrics:
|
|
342
441
|
self._update_metrics(response_time, False)
|
|
343
442
|
|
|
@@ -349,14 +448,19 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
349
448
|
}
|
|
350
449
|
except Exception as e:
|
|
351
450
|
response_time = time.time() - start_time
|
|
451
|
+
self._consecutive_failures += 1
|
|
352
452
|
if self.enable_metrics:
|
|
353
453
|
self._update_metrics(response_time, False)
|
|
354
454
|
self._metrics["stream_errors"] += 1
|
|
355
455
|
|
|
356
|
-
#
|
|
357
|
-
|
|
358
|
-
|
|
456
|
+
# Enhanced connection error detection
|
|
457
|
+
error_str = str(e).lower()
|
|
458
|
+
if any(indicator in error_str for indicator in
|
|
459
|
+
["connection", "disconnected", "broken pipe", "eof"]):
|
|
460
|
+
logger.warning("Connection error detected: %s", e)
|
|
359
461
|
self._initialized = False
|
|
462
|
+
if self.enable_metrics:
|
|
463
|
+
self._metrics["connection_errors"] += 1
|
|
360
464
|
|
|
361
465
|
error_msg = f"Tool execution failed: {str(e)}"
|
|
362
466
|
logger.error("Tool '%s' error: %s", tool_name, error_msg)
|
|
@@ -366,7 +470,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
366
470
|
}
|
|
367
471
|
|
|
368
472
|
def _update_metrics(self, response_time: float, success: bool) -> None:
|
|
369
|
-
"""
|
|
473
|
+
"""Enhanced metrics tracking (like SSE)."""
|
|
370
474
|
if success:
|
|
371
475
|
self._metrics["successful_calls"] += 1
|
|
372
476
|
else:
|
|
@@ -379,7 +483,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
379
483
|
)
|
|
380
484
|
|
|
381
485
|
async def list_resources(self) -> Dict[str, Any]:
|
|
382
|
-
"""
|
|
486
|
+
"""Enhanced resource listing with error handling."""
|
|
383
487
|
if not self._initialized:
|
|
384
488
|
return {}
|
|
385
489
|
|
|
@@ -391,13 +495,15 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
391
495
|
return response if isinstance(response, dict) else {}
|
|
392
496
|
except asyncio.TimeoutError:
|
|
393
497
|
logger.error("List resources timed out")
|
|
498
|
+
self._consecutive_failures += 1
|
|
394
499
|
return {}
|
|
395
500
|
except Exception as e:
|
|
396
501
|
logger.debug("Error listing resources: %s", e)
|
|
502
|
+
self._consecutive_failures += 1
|
|
397
503
|
return {}
|
|
398
504
|
|
|
399
505
|
async def list_prompts(self) -> Dict[str, Any]:
|
|
400
|
-
"""
|
|
506
|
+
"""Enhanced prompt listing with error handling."""
|
|
401
507
|
if not self._initialized:
|
|
402
508
|
return {}
|
|
403
509
|
|
|
@@ -409,51 +515,96 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
409
515
|
return response if isinstance(response, dict) else {}
|
|
410
516
|
except asyncio.TimeoutError:
|
|
411
517
|
logger.error("List prompts timed out")
|
|
518
|
+
self._consecutive_failures += 1
|
|
412
519
|
return {}
|
|
413
520
|
except Exception as e:
|
|
414
521
|
logger.debug("Error listing prompts: %s", e)
|
|
522
|
+
self._consecutive_failures += 1
|
|
523
|
+
return {}
|
|
524
|
+
|
|
525
|
+
async def read_resource(self, uri: str) -> Dict[str, Any]:
|
|
526
|
+
"""Read a specific resource."""
|
|
527
|
+
if not self._initialized:
|
|
528
|
+
return {}
|
|
529
|
+
|
|
530
|
+
try:
|
|
531
|
+
response = await asyncio.wait_for(
|
|
532
|
+
send_resources_read(self._read_stream, self._write_stream, uri),
|
|
533
|
+
timeout=self.default_timeout
|
|
534
|
+
)
|
|
535
|
+
return response if isinstance(response, dict) else {}
|
|
536
|
+
except asyncio.TimeoutError:
|
|
537
|
+
logger.error("Read resource timed out")
|
|
538
|
+
self._consecutive_failures += 1
|
|
539
|
+
return {}
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.debug("Error reading resource: %s", e)
|
|
542
|
+
self._consecutive_failures += 1
|
|
543
|
+
return {}
|
|
544
|
+
|
|
545
|
+
async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
546
|
+
"""Get a specific prompt."""
|
|
547
|
+
if not self._initialized:
|
|
548
|
+
return {}
|
|
549
|
+
|
|
550
|
+
try:
|
|
551
|
+
response = await asyncio.wait_for(
|
|
552
|
+
send_prompts_get(self._read_stream, self._write_stream, name, arguments or {}),
|
|
553
|
+
timeout=self.default_timeout
|
|
554
|
+
)
|
|
555
|
+
return response if isinstance(response, dict) else {}
|
|
556
|
+
except asyncio.TimeoutError:
|
|
557
|
+
logger.error("Get prompt timed out")
|
|
558
|
+
self._consecutive_failures += 1
|
|
559
|
+
return {}
|
|
560
|
+
except Exception as e:
|
|
561
|
+
logger.debug("Error getting prompt: %s", e)
|
|
562
|
+
self._consecutive_failures += 1
|
|
415
563
|
return {}
|
|
416
564
|
|
|
417
|
-
# ------------------------------------------------------------------ #
|
|
418
|
-
# Metrics and monitoring (consistent with other transports) #
|
|
419
|
-
# ------------------------------------------------------------------ #
|
|
420
565
|
def get_metrics(self) -> Dict[str, Any]:
|
|
421
|
-
"""
|
|
422
|
-
|
|
566
|
+
"""Enhanced metrics with health information."""
|
|
567
|
+
metrics = self._metrics.copy()
|
|
568
|
+
metrics.update({
|
|
569
|
+
"is_connected": self.is_connected(),
|
|
570
|
+
"consecutive_failures": self._consecutive_failures,
|
|
571
|
+
"last_successful_ping": self._last_successful_ping,
|
|
572
|
+
"max_consecutive_failures": self._max_consecutive_failures,
|
|
573
|
+
})
|
|
574
|
+
return metrics
|
|
423
575
|
|
|
424
576
|
def reset_metrics(self) -> None:
|
|
425
|
-
"""
|
|
577
|
+
"""Enhanced metrics reset preserving health state."""
|
|
578
|
+
preserved_init_time = self._metrics.get("initialization_time")
|
|
579
|
+
preserved_last_ping = self._metrics.get("last_ping_time")
|
|
580
|
+
|
|
426
581
|
self._metrics = {
|
|
427
582
|
"total_calls": 0,
|
|
428
583
|
"successful_calls": 0,
|
|
429
584
|
"failed_calls": 0,
|
|
430
585
|
"total_time": 0.0,
|
|
431
586
|
"avg_response_time": 0.0,
|
|
432
|
-
"last_ping_time":
|
|
433
|
-
"initialization_time":
|
|
587
|
+
"last_ping_time": preserved_last_ping,
|
|
588
|
+
"initialization_time": preserved_init_time,
|
|
434
589
|
"connection_resets": self._metrics.get("connection_resets", 0),
|
|
435
|
-
"stream_errors": 0
|
|
590
|
+
"stream_errors": 0,
|
|
591
|
+
"connection_errors": 0,
|
|
592
|
+
"recovery_attempts": 0,
|
|
436
593
|
}
|
|
437
594
|
|
|
438
|
-
# ------------------------------------------------------------------ #
|
|
439
|
-
# Backward compatibility #
|
|
440
|
-
# ------------------------------------------------------------------ #
|
|
441
595
|
def get_streams(self) -> List[tuple]:
|
|
442
|
-
"""
|
|
596
|
+
"""Enhanced streams access with connection check."""
|
|
443
597
|
if self._initialized and self._read_stream and self._write_stream:
|
|
444
598
|
return [(self._read_stream, self._write_stream)]
|
|
445
599
|
return []
|
|
446
600
|
|
|
447
|
-
# ------------------------------------------------------------------ #
|
|
448
|
-
# Context manager support #
|
|
449
|
-
# ------------------------------------------------------------------ #
|
|
450
601
|
async def __aenter__(self):
|
|
451
|
-
"""
|
|
602
|
+
"""Enhanced context manager entry."""
|
|
452
603
|
success = await self.initialize()
|
|
453
604
|
if not success:
|
|
454
605
|
raise RuntimeError("Failed to initialize HTTPStreamableTransport")
|
|
455
606
|
return self
|
|
456
607
|
|
|
457
608
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
458
|
-
"""
|
|
609
|
+
"""Enhanced context manager cleanup."""
|
|
459
610
|
await self.close()
|