chuk-tool-processor 0.6.12__py3-none-any.whl → 0.6.13__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor/core/__init__.py +1 -1
- chuk_tool_processor/core/exceptions.py +10 -4
- chuk_tool_processor/core/processor.py +97 -97
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +200 -205
- chuk_tool_processor/execution/tool_executor.py +82 -84
- chuk_tool_processor/execution/wrappers/caching.py +102 -103
- chuk_tool_processor/execution/wrappers/rate_limiting.py +45 -42
- chuk_tool_processor/execution/wrappers/retry.py +23 -25
- chuk_tool_processor/logging/__init__.py +23 -17
- chuk_tool_processor/logging/context.py +40 -45
- chuk_tool_processor/logging/formatter.py +22 -21
- chuk_tool_processor/logging/helpers.py +24 -38
- chuk_tool_processor/logging/metrics.py +11 -13
- chuk_tool_processor/mcp/__init__.py +8 -12
- chuk_tool_processor/mcp/mcp_tool.py +124 -112
- chuk_tool_processor/mcp/register_mcp_tools.py +17 -17
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py +11 -13
- chuk_tool_processor/mcp/setup_mcp_sse.py +11 -13
- chuk_tool_processor/mcp/setup_mcp_stdio.py +7 -9
- chuk_tool_processor/mcp/stream_manager.py +168 -204
- chuk_tool_processor/mcp/transport/__init__.py +4 -4
- chuk_tool_processor/mcp/transport/base_transport.py +43 -58
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +145 -163
- chuk_tool_processor/mcp/transport/sse_transport.py +217 -255
- chuk_tool_processor/mcp/transport/stdio_transport.py +171 -189
- chuk_tool_processor/models/__init__.py +1 -1
- chuk_tool_processor/models/execution_strategy.py +16 -21
- chuk_tool_processor/models/streaming_tool.py +28 -25
- chuk_tool_processor/models/tool_call.py +19 -34
- chuk_tool_processor/models/tool_export_mixin.py +22 -8
- chuk_tool_processor/models/tool_result.py +40 -77
- chuk_tool_processor/models/validated_tool.py +14 -16
- chuk_tool_processor/plugins/__init__.py +1 -1
- chuk_tool_processor/plugins/discovery.py +10 -10
- chuk_tool_processor/plugins/parsers/__init__.py +1 -1
- chuk_tool_processor/plugins/parsers/base.py +1 -2
- chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
- chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
- chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
- chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
- chuk_tool_processor/registry/__init__.py +12 -12
- chuk_tool_processor/registry/auto_register.py +22 -30
- chuk_tool_processor/registry/decorators.py +127 -129
- chuk_tool_processor/registry/interface.py +26 -23
- chuk_tool_processor/registry/metadata.py +27 -22
- chuk_tool_processor/registry/provider.py +17 -18
- chuk_tool_processor/registry/providers/__init__.py +16 -19
- chuk_tool_processor/registry/providers/memory.py +18 -25
- chuk_tool_processor/registry/tool_export.py +42 -51
- chuk_tool_processor/utils/validation.py +15 -16
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/METADATA +1 -1
- chuk_tool_processor-0.6.13.dist-info/RECORD +60 -0
- chuk_tool_processor-0.6.12.dist-info/RECORD +0 -60
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/top_level.txt +0 -0
|
@@ -2,47 +2,51 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import asyncio
|
|
5
|
-
import json
|
|
6
|
-
import time
|
|
7
|
-
from typing import Dict, Any, List, Optional
|
|
8
5
|
import logging
|
|
6
|
+
import time
|
|
7
|
+
from typing import Any
|
|
9
8
|
|
|
10
|
-
from .
|
|
11
|
-
|
|
12
|
-
# Import chuk-mcp HTTP transport components
|
|
13
|
-
from chuk_mcp.transports.http import http_client
|
|
14
|
-
from chuk_mcp.transports.http.parameters import StreamableHTTPParameters
|
|
15
|
-
from chuk_mcp.protocol.messages import (
|
|
9
|
+
from chuk_mcp.protocol.messages import ( # type: ignore[import-untyped]
|
|
16
10
|
send_initialize,
|
|
17
|
-
send_ping,
|
|
18
|
-
|
|
19
|
-
|
|
11
|
+
send_ping,
|
|
12
|
+
send_prompts_get,
|
|
13
|
+
send_prompts_list,
|
|
20
14
|
send_resources_list,
|
|
21
15
|
send_resources_read,
|
|
22
|
-
|
|
23
|
-
|
|
16
|
+
send_tools_call,
|
|
17
|
+
send_tools_list,
|
|
24
18
|
)
|
|
25
19
|
|
|
20
|
+
# Import chuk-mcp HTTP transport components
|
|
21
|
+
from chuk_mcp.transports.http import http_client # type: ignore[import-untyped]
|
|
22
|
+
from chuk_mcp.transports.http.parameters import StreamableHTTPParameters # type: ignore[import-untyped]
|
|
23
|
+
|
|
24
|
+
from .base_transport import MCPBaseTransport
|
|
25
|
+
|
|
26
26
|
logger = logging.getLogger(__name__)
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
class HTTPStreamableTransport(MCPBaseTransport):
|
|
30
30
|
"""
|
|
31
31
|
HTTP Streamable transport using chuk-mcp HTTP client.
|
|
32
|
-
|
|
33
|
-
ENHANCED: Now matches SSE transport robustness with improved connection
|
|
32
|
+
|
|
33
|
+
ENHANCED: Now matches SSE transport robustness with improved connection
|
|
34
34
|
management, health monitoring, and comprehensive error handling.
|
|
35
35
|
"""
|
|
36
36
|
|
|
37
|
-
def __init__(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
url: str,
|
|
40
|
+
api_key: str | None = None,
|
|
41
|
+
headers: dict[str, str] | None = None, # NEW: Headers support
|
|
42
|
+
connection_timeout: float = 30.0,
|
|
43
|
+
default_timeout: float = 30.0,
|
|
44
|
+
session_id: str | None = None,
|
|
45
|
+
enable_metrics: bool = True,
|
|
46
|
+
):
|
|
43
47
|
"""
|
|
44
48
|
Initialize HTTP Streamable transport with enhanced configuration.
|
|
45
|
-
|
|
49
|
+
|
|
46
50
|
Args:
|
|
47
51
|
url: HTTP server URL (should end with /mcp)
|
|
48
52
|
api_key: Optional API key for authentication
|
|
@@ -53,18 +57,18 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
53
57
|
enable_metrics: Whether to track performance metrics
|
|
54
58
|
"""
|
|
55
59
|
# Ensure URL points to the /mcp endpoint
|
|
56
|
-
if not url.endswith(
|
|
60
|
+
if not url.endswith("/mcp"):
|
|
57
61
|
self.url = f"{url.rstrip('/')}/mcp"
|
|
58
62
|
else:
|
|
59
63
|
self.url = url
|
|
60
|
-
|
|
64
|
+
|
|
61
65
|
self.api_key = api_key
|
|
62
66
|
self.configured_headers = headers or {} # NEW: Store configured headers
|
|
63
67
|
self.connection_timeout = connection_timeout
|
|
64
68
|
self.default_timeout = default_timeout
|
|
65
69
|
self.session_id = session_id
|
|
66
70
|
self.enable_metrics = enable_metrics
|
|
67
|
-
|
|
71
|
+
|
|
68
72
|
logger.debug("HTTP Streamable transport initialized with URL: %s", self.url)
|
|
69
73
|
if self.api_key:
|
|
70
74
|
logger.debug("API key configured for authentication")
|
|
@@ -72,18 +76,18 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
72
76
|
logger.debug("Custom headers configured: %s", list(self.configured_headers.keys()))
|
|
73
77
|
if self.session_id:
|
|
74
78
|
logger.debug("Session ID configured: %s", self.session_id)
|
|
75
|
-
|
|
79
|
+
|
|
76
80
|
# State tracking (enhanced like SSE)
|
|
77
81
|
self._http_context = None
|
|
78
82
|
self._read_stream = None
|
|
79
83
|
self._write_stream = None
|
|
80
84
|
self._initialized = False
|
|
81
|
-
|
|
85
|
+
|
|
82
86
|
# Health monitoring (NEW - like SSE)
|
|
83
87
|
self._last_successful_ping = None
|
|
84
88
|
self._consecutive_failures = 0
|
|
85
89
|
self._max_consecutive_failures = 3
|
|
86
|
-
|
|
90
|
+
|
|
87
91
|
# Performance metrics (enhanced like SSE)
|
|
88
92
|
self._metrics = {
|
|
89
93
|
"total_calls": 0,
|
|
@@ -99,35 +103,36 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
99
103
|
"recovery_attempts": 0, # NEW
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
def _get_headers(self) ->
|
|
106
|
+
def _get_headers(self) -> dict[str, str]:
|
|
103
107
|
"""Get headers with authentication and custom headers (like SSE)."""
|
|
104
108
|
headers = {
|
|
105
109
|
"Content-Type": "application/json",
|
|
106
110
|
"Accept": "application/json, text/event-stream",
|
|
107
|
-
|
|
111
|
+
"User-Agent": "chuk-tool-processor/1.0.0",
|
|
108
112
|
}
|
|
109
|
-
|
|
113
|
+
|
|
110
114
|
# Add configured headers first
|
|
111
115
|
if self.configured_headers:
|
|
112
116
|
headers.update(self.configured_headers)
|
|
113
|
-
|
|
117
|
+
|
|
114
118
|
# Add API key as Bearer token if provided
|
|
115
119
|
if self.api_key:
|
|
116
|
-
headers[
|
|
117
|
-
|
|
120
|
+
headers["Authorization"] = f"Bearer {self.api_key}"
|
|
121
|
+
|
|
118
122
|
# Add session ID if provided
|
|
119
123
|
if self.session_id:
|
|
120
124
|
headers["X-Session-ID"] = self.session_id
|
|
121
|
-
|
|
125
|
+
|
|
122
126
|
return headers
|
|
123
127
|
|
|
124
128
|
async def _test_connection_health(self) -> bool:
|
|
125
129
|
"""Test basic HTTP connectivity (like SSE's connectivity test)."""
|
|
126
130
|
try:
|
|
127
131
|
import httpx
|
|
132
|
+
|
|
128
133
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
129
134
|
# Test basic connectivity to base URL
|
|
130
|
-
base_url = self.url.replace(
|
|
135
|
+
base_url = self.url.replace("/mcp", "")
|
|
131
136
|
response = await client.get(f"{base_url}/health", headers=self._get_headers())
|
|
132
137
|
logger.debug("Health check response: %s", response.status_code)
|
|
133
138
|
return response.status_code < 500 # Accept any non-server-error
|
|
@@ -140,20 +145,20 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
140
145
|
if self._initialized:
|
|
141
146
|
logger.warning("Transport already initialized")
|
|
142
147
|
return True
|
|
143
|
-
|
|
148
|
+
|
|
144
149
|
start_time = time.time()
|
|
145
|
-
|
|
150
|
+
|
|
146
151
|
try:
|
|
147
152
|
logger.debug("Initializing HTTP Streamable transport to %s", self.url)
|
|
148
|
-
|
|
153
|
+
|
|
149
154
|
# Test basic connectivity first (like SSE)
|
|
150
155
|
if not await self._test_connection_health():
|
|
151
156
|
logger.warning("Connection health test failed, proceeding anyway")
|
|
152
|
-
|
|
157
|
+
|
|
153
158
|
# Build headers properly
|
|
154
159
|
headers = self._get_headers()
|
|
155
160
|
logger.debug("Using headers: %s", list(headers.keys()))
|
|
156
|
-
|
|
161
|
+
|
|
157
162
|
# Create StreamableHTTPParameters with proper configuration
|
|
158
163
|
http_params = StreamableHTTPParameters(
|
|
159
164
|
url=self.url,
|
|
@@ -167,49 +172,48 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
167
172
|
retry_delay=1.0,
|
|
168
173
|
user_agent="chuk-tool-processor/1.0.0",
|
|
169
174
|
)
|
|
170
|
-
|
|
175
|
+
|
|
171
176
|
# Create and enter the HTTP context
|
|
172
177
|
self._http_context = http_client(http_params)
|
|
173
|
-
|
|
178
|
+
|
|
174
179
|
logger.debug("Establishing HTTP connection...")
|
|
175
180
|
self._read_stream, self._write_stream = await asyncio.wait_for(
|
|
176
|
-
self._http_context.__aenter__(),
|
|
177
|
-
timeout=self.connection_timeout
|
|
181
|
+
self._http_context.__aenter__(), timeout=self.connection_timeout
|
|
178
182
|
)
|
|
179
|
-
|
|
183
|
+
|
|
180
184
|
# Enhanced MCP initialize sequence
|
|
181
185
|
logger.debug("Sending MCP initialize request...")
|
|
182
186
|
init_start = time.time()
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
timeout=self.default_timeout
|
|
187
|
-
)
|
|
188
|
-
|
|
187
|
+
|
|
188
|
+
await asyncio.wait_for(send_initialize(self._read_stream, self._write_stream), timeout=self.default_timeout)
|
|
189
|
+
|
|
189
190
|
init_time = time.time() - init_start
|
|
190
191
|
logger.debug("MCP initialize completed in %.3fs", init_time)
|
|
191
|
-
|
|
192
|
+
|
|
192
193
|
# Verify connection with ping (enhanced like SSE)
|
|
193
194
|
logger.debug("Verifying connection with ping...")
|
|
194
195
|
ping_start = time.time()
|
|
195
196
|
ping_success = await asyncio.wait_for(
|
|
196
197
|
send_ping(self._read_stream, self._write_stream),
|
|
197
|
-
timeout=10.0 # Longer timeout for initial ping
|
|
198
|
+
timeout=10.0, # Longer timeout for initial ping
|
|
198
199
|
)
|
|
199
200
|
ping_time = time.time() - ping_start
|
|
200
|
-
|
|
201
|
+
|
|
201
202
|
if ping_success:
|
|
202
203
|
self._initialized = True
|
|
203
204
|
self._last_successful_ping = time.time()
|
|
204
205
|
self._consecutive_failures = 0
|
|
205
|
-
|
|
206
|
+
|
|
206
207
|
total_init_time = time.time() - start_time
|
|
207
208
|
if self.enable_metrics:
|
|
208
209
|
self._metrics["initialization_time"] = total_init_time
|
|
209
210
|
self._metrics["last_ping_time"] = ping_time
|
|
210
|
-
|
|
211
|
-
logger.debug(
|
|
212
|
-
|
|
211
|
+
|
|
212
|
+
logger.debug(
|
|
213
|
+
"HTTP Streamable transport initialized successfully in %.3fs (ping: %.3fs)",
|
|
214
|
+
total_init_time,
|
|
215
|
+
ping_time,
|
|
216
|
+
)
|
|
213
217
|
return True
|
|
214
218
|
else:
|
|
215
219
|
logger.warning("HTTP connection established but ping failed")
|
|
@@ -220,7 +224,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
220
224
|
self._metrics["initialization_time"] = time.time() - start_time
|
|
221
225
|
return True
|
|
222
226
|
|
|
223
|
-
except
|
|
227
|
+
except TimeoutError:
|
|
224
228
|
logger.error("HTTP Streamable initialization timed out after %ss", self.connection_timeout)
|
|
225
229
|
await self._cleanup()
|
|
226
230
|
if self.enable_metrics:
|
|
@@ -237,13 +241,13 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
237
241
|
"""Attempt to recover from connection issues (NEW - like SSE resilience)."""
|
|
238
242
|
if self.enable_metrics:
|
|
239
243
|
self._metrics["recovery_attempts"] += 1
|
|
240
|
-
|
|
244
|
+
|
|
241
245
|
logger.debug("Attempting HTTP connection recovery...")
|
|
242
|
-
|
|
246
|
+
|
|
243
247
|
try:
|
|
244
248
|
# Clean up existing connection
|
|
245
249
|
await self._cleanup()
|
|
246
|
-
|
|
250
|
+
|
|
247
251
|
# Re-initialize
|
|
248
252
|
return await self.initialize()
|
|
249
253
|
except Exception as e:
|
|
@@ -254,10 +258,10 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
254
258
|
"""Close with enhanced cleanup and metrics reporting."""
|
|
255
259
|
if not self._initialized:
|
|
256
260
|
return
|
|
257
|
-
|
|
261
|
+
|
|
258
262
|
# Enhanced metrics logging (like SSE)
|
|
259
263
|
if self.enable_metrics and self._metrics["total_calls"] > 0:
|
|
260
|
-
success_rate =
|
|
264
|
+
success_rate = self._metrics["successful_calls"] / self._metrics["total_calls"] * 100
|
|
261
265
|
logger.debug(
|
|
262
266
|
"HTTP Streamable transport closing - Calls: %d, Success: %.1f%%, "
|
|
263
267
|
"Avg time: %.3fs, Recoveries: %d, Errors: %d",
|
|
@@ -265,14 +269,14 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
265
269
|
success_rate,
|
|
266
270
|
self._metrics["avg_response_time"],
|
|
267
271
|
self._metrics["recovery_attempts"],
|
|
268
|
-
self._metrics["connection_errors"]
|
|
272
|
+
self._metrics["connection_errors"],
|
|
269
273
|
)
|
|
270
|
-
|
|
274
|
+
|
|
271
275
|
try:
|
|
272
276
|
if self._http_context is not None:
|
|
273
277
|
await self._http_context.__aexit__(None, None, None)
|
|
274
278
|
logger.debug("HTTP Streamable context closed")
|
|
275
|
-
|
|
279
|
+
|
|
276
280
|
except Exception as e:
|
|
277
281
|
logger.debug("Error during transport close: %s", e)
|
|
278
282
|
finally:
|
|
@@ -290,29 +294,28 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
290
294
|
if not self._initialized or not self._read_stream:
|
|
291
295
|
logger.error("Cannot send ping: transport not initialized")
|
|
292
296
|
return False
|
|
293
|
-
|
|
297
|
+
|
|
294
298
|
start_time = time.time()
|
|
295
299
|
try:
|
|
296
300
|
result = await asyncio.wait_for(
|
|
297
|
-
send_ping(self._read_stream, self._write_stream),
|
|
298
|
-
timeout=self.default_timeout
|
|
301
|
+
send_ping(self._read_stream, self._write_stream), timeout=self.default_timeout
|
|
299
302
|
)
|
|
300
|
-
|
|
303
|
+
|
|
301
304
|
success = bool(result)
|
|
302
|
-
|
|
305
|
+
|
|
303
306
|
if success:
|
|
304
307
|
self._last_successful_ping = time.time()
|
|
305
308
|
self._consecutive_failures = 0
|
|
306
309
|
else:
|
|
307
310
|
self._consecutive_failures += 1
|
|
308
|
-
|
|
311
|
+
|
|
309
312
|
if self.enable_metrics:
|
|
310
313
|
ping_time = time.time() - start_time
|
|
311
314
|
self._metrics["last_ping_time"] = ping_time
|
|
312
315
|
logger.debug("HTTP Streamable ping completed in %.3fs: %s", ping_time, success)
|
|
313
|
-
|
|
316
|
+
|
|
314
317
|
return success
|
|
315
|
-
except
|
|
318
|
+
except TimeoutError:
|
|
316
319
|
logger.error("HTTP Streamable ping timed out")
|
|
317
320
|
self._consecutive_failures += 1
|
|
318
321
|
return False
|
|
@@ -327,27 +330,26 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
327
330
|
"""Enhanced connection status check (like SSE)."""
|
|
328
331
|
if not self._initialized or not self._read_stream or not self._write_stream:
|
|
329
332
|
return False
|
|
330
|
-
|
|
333
|
+
|
|
331
334
|
# Check if we've had too many consecutive failures (like SSE)
|
|
332
335
|
if self._consecutive_failures >= self._max_consecutive_failures:
|
|
333
336
|
logger.warning("Connection marked unhealthy after %d failures", self._consecutive_failures)
|
|
334
337
|
return False
|
|
335
|
-
|
|
338
|
+
|
|
336
339
|
return True
|
|
337
340
|
|
|
338
|
-
async def get_tools(self) ->
|
|
341
|
+
async def get_tools(self) -> list[dict[str, Any]]:
|
|
339
342
|
"""Enhanced tools retrieval with error handling."""
|
|
340
343
|
if not self._initialized:
|
|
341
344
|
logger.error("Cannot get tools: transport not initialized")
|
|
342
345
|
return []
|
|
343
|
-
|
|
346
|
+
|
|
344
347
|
start_time = time.time()
|
|
345
348
|
try:
|
|
346
349
|
tools_response = await asyncio.wait_for(
|
|
347
|
-
send_tools_list(self._read_stream, self._write_stream),
|
|
348
|
-
timeout=self.default_timeout
|
|
350
|
+
send_tools_list(self._read_stream, self._write_stream), timeout=self.default_timeout
|
|
349
351
|
)
|
|
350
|
-
|
|
352
|
+
|
|
351
353
|
# Normalize response
|
|
352
354
|
if isinstance(tools_response, dict):
|
|
353
355
|
tools = tools_response.get("tools", [])
|
|
@@ -356,17 +358,17 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
356
358
|
else:
|
|
357
359
|
logger.warning("Unexpected tools response type: %s", type(tools_response))
|
|
358
360
|
tools = []
|
|
359
|
-
|
|
361
|
+
|
|
360
362
|
# Reset failure count on success
|
|
361
363
|
self._consecutive_failures = 0
|
|
362
|
-
|
|
364
|
+
|
|
363
365
|
if self.enable_metrics:
|
|
364
366
|
response_time = time.time() - start_time
|
|
365
367
|
logger.debug("Retrieved %d tools in %.3fs", len(tools), response_time)
|
|
366
|
-
|
|
368
|
+
|
|
367
369
|
return tools
|
|
368
|
-
|
|
369
|
-
except
|
|
370
|
+
|
|
371
|
+
except TimeoutError:
|
|
370
372
|
logger.error("Get tools timed out")
|
|
371
373
|
self._consecutive_failures += 1
|
|
372
374
|
return []
|
|
@@ -377,97 +379,80 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
377
379
|
self._metrics["stream_errors"] += 1
|
|
378
380
|
return []
|
|
379
381
|
|
|
380
|
-
async def call_tool(
|
|
381
|
-
|
|
382
|
+
async def call_tool(
|
|
383
|
+
self, tool_name: str, arguments: dict[str, Any], timeout: float | None = None
|
|
384
|
+
) -> dict[str, Any]:
|
|
382
385
|
"""Enhanced tool calling with recovery and health monitoring."""
|
|
383
386
|
if not self._initialized:
|
|
384
|
-
return {
|
|
385
|
-
"isError": True,
|
|
386
|
-
"error": "Transport not initialized"
|
|
387
|
-
}
|
|
387
|
+
return {"isError": True, "error": "Transport not initialized"}
|
|
388
388
|
|
|
389
389
|
tool_timeout = timeout or self.default_timeout
|
|
390
390
|
start_time = time.time()
|
|
391
|
-
|
|
391
|
+
|
|
392
392
|
if self.enable_metrics:
|
|
393
393
|
self._metrics["total_calls"] += 1
|
|
394
394
|
|
|
395
395
|
try:
|
|
396
396
|
logger.debug("Calling tool '%s' with timeout %ss", tool_name, tool_timeout)
|
|
397
|
-
|
|
397
|
+
|
|
398
398
|
# Enhanced connection check with recovery attempt
|
|
399
399
|
if not self.is_connected():
|
|
400
400
|
logger.warning("Connection unhealthy, attempting recovery...")
|
|
401
401
|
if not await self._attempt_recovery():
|
|
402
402
|
if self.enable_metrics:
|
|
403
403
|
self._update_metrics(time.time() - start_time, False)
|
|
404
|
-
return {
|
|
405
|
-
|
|
406
|
-
"error": "Failed to recover connection"
|
|
407
|
-
}
|
|
408
|
-
|
|
404
|
+
return {"isError": True, "error": "Failed to recover connection"}
|
|
405
|
+
|
|
409
406
|
raw_response = await asyncio.wait_for(
|
|
410
|
-
send_tools_call(
|
|
411
|
-
self._read_stream,
|
|
412
|
-
self._write_stream,
|
|
413
|
-
tool_name,
|
|
414
|
-
arguments
|
|
415
|
-
),
|
|
416
|
-
timeout=tool_timeout
|
|
407
|
+
send_tools_call(self._read_stream, self._write_stream, tool_name, arguments), timeout=tool_timeout
|
|
417
408
|
)
|
|
418
|
-
|
|
409
|
+
|
|
419
410
|
response_time = time.time() - start_time
|
|
420
411
|
result = self._normalize_mcp_response(raw_response)
|
|
421
|
-
|
|
412
|
+
|
|
422
413
|
# Reset failure count on success
|
|
423
414
|
self._consecutive_failures = 0
|
|
424
415
|
self._last_successful_ping = time.time() # Update health timestamp
|
|
425
|
-
|
|
416
|
+
|
|
426
417
|
if self.enable_metrics:
|
|
427
418
|
self._update_metrics(response_time, not result.get("isError", False))
|
|
428
|
-
|
|
419
|
+
|
|
429
420
|
if not result.get("isError", False):
|
|
430
421
|
logger.debug("Tool '%s' completed successfully in %.3fs", tool_name, response_time)
|
|
431
422
|
else:
|
|
432
|
-
logger.warning(
|
|
433
|
-
|
|
434
|
-
|
|
423
|
+
logger.warning(
|
|
424
|
+
"Tool '%s' failed in %.3fs: %s", tool_name, response_time, result.get("error", "Unknown error")
|
|
425
|
+
)
|
|
426
|
+
|
|
435
427
|
return result
|
|
436
428
|
|
|
437
|
-
except
|
|
429
|
+
except TimeoutError:
|
|
438
430
|
response_time = time.time() - start_time
|
|
439
431
|
self._consecutive_failures += 1
|
|
440
432
|
if self.enable_metrics:
|
|
441
433
|
self._update_metrics(response_time, False)
|
|
442
|
-
|
|
434
|
+
|
|
443
435
|
error_msg = f"Tool execution timed out after {tool_timeout}s"
|
|
444
436
|
logger.error("Tool '%s' %s", tool_name, error_msg)
|
|
445
|
-
return {
|
|
446
|
-
"isError": True,
|
|
447
|
-
"error": error_msg
|
|
448
|
-
}
|
|
437
|
+
return {"isError": True, "error": error_msg}
|
|
449
438
|
except Exception as e:
|
|
450
439
|
response_time = time.time() - start_time
|
|
451
440
|
self._consecutive_failures += 1
|
|
452
441
|
if self.enable_metrics:
|
|
453
442
|
self._update_metrics(response_time, False)
|
|
454
443
|
self._metrics["stream_errors"] += 1
|
|
455
|
-
|
|
444
|
+
|
|
456
445
|
# Enhanced connection error detection
|
|
457
446
|
error_str = str(e).lower()
|
|
458
|
-
if any(indicator in error_str for indicator in
|
|
459
|
-
["connection", "disconnected", "broken pipe", "eof"]):
|
|
447
|
+
if any(indicator in error_str for indicator in ["connection", "disconnected", "broken pipe", "eof"]):
|
|
460
448
|
logger.warning("Connection error detected: %s", e)
|
|
461
449
|
self._initialized = False
|
|
462
450
|
if self.enable_metrics:
|
|
463
451
|
self._metrics["connection_errors"] += 1
|
|
464
|
-
|
|
452
|
+
|
|
465
453
|
error_msg = f"Tool execution failed: {str(e)}"
|
|
466
454
|
logger.error("Tool '%s' error: %s", tool_name, error_msg)
|
|
467
|
-
return {
|
|
468
|
-
"isError": True,
|
|
469
|
-
"error": error_msg
|
|
470
|
-
}
|
|
455
|
+
return {"isError": True, "error": error_msg}
|
|
471
456
|
|
|
472
457
|
def _update_metrics(self, response_time: float, success: bool) -> None:
|
|
473
458
|
"""Enhanced metrics tracking (like SSE)."""
|
|
@@ -475,25 +460,22 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
475
460
|
self._metrics["successful_calls"] += 1
|
|
476
461
|
else:
|
|
477
462
|
self._metrics["failed_calls"] += 1
|
|
478
|
-
|
|
463
|
+
|
|
479
464
|
self._metrics["total_time"] += response_time
|
|
480
465
|
if self._metrics["total_calls"] > 0:
|
|
481
|
-
self._metrics["avg_response_time"] =
|
|
482
|
-
self._metrics["total_time"] / self._metrics["total_calls"]
|
|
483
|
-
)
|
|
466
|
+
self._metrics["avg_response_time"] = self._metrics["total_time"] / self._metrics["total_calls"]
|
|
484
467
|
|
|
485
|
-
async def list_resources(self) ->
|
|
468
|
+
async def list_resources(self) -> dict[str, Any]:
|
|
486
469
|
"""Enhanced resource listing with error handling."""
|
|
487
470
|
if not self._initialized:
|
|
488
471
|
return {}
|
|
489
|
-
|
|
472
|
+
|
|
490
473
|
try:
|
|
491
474
|
response = await asyncio.wait_for(
|
|
492
|
-
send_resources_list(self._read_stream, self._write_stream),
|
|
493
|
-
timeout=self.default_timeout
|
|
475
|
+
send_resources_list(self._read_stream, self._write_stream), timeout=self.default_timeout
|
|
494
476
|
)
|
|
495
477
|
return response if isinstance(response, dict) else {}
|
|
496
|
-
except
|
|
478
|
+
except TimeoutError:
|
|
497
479
|
logger.error("List resources timed out")
|
|
498
480
|
self._consecutive_failures += 1
|
|
499
481
|
return {}
|
|
@@ -502,18 +484,17 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
502
484
|
self._consecutive_failures += 1
|
|
503
485
|
return {}
|
|
504
486
|
|
|
505
|
-
async def list_prompts(self) ->
|
|
487
|
+
async def list_prompts(self) -> dict[str, Any]:
|
|
506
488
|
"""Enhanced prompt listing with error handling."""
|
|
507
489
|
if not self._initialized:
|
|
508
490
|
return {}
|
|
509
|
-
|
|
491
|
+
|
|
510
492
|
try:
|
|
511
493
|
response = await asyncio.wait_for(
|
|
512
|
-
send_prompts_list(self._read_stream, self._write_stream),
|
|
513
|
-
timeout=self.default_timeout
|
|
494
|
+
send_prompts_list(self._read_stream, self._write_stream), timeout=self.default_timeout
|
|
514
495
|
)
|
|
515
496
|
return response if isinstance(response, dict) else {}
|
|
516
|
-
except
|
|
497
|
+
except TimeoutError:
|
|
517
498
|
logger.error("List prompts timed out")
|
|
518
499
|
self._consecutive_failures += 1
|
|
519
500
|
return {}
|
|
@@ -522,18 +503,17 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
522
503
|
self._consecutive_failures += 1
|
|
523
504
|
return {}
|
|
524
505
|
|
|
525
|
-
async def read_resource(self, uri: str) ->
|
|
506
|
+
async def read_resource(self, uri: str) -> dict[str, Any]:
|
|
526
507
|
"""Read a specific resource."""
|
|
527
508
|
if not self._initialized:
|
|
528
509
|
return {}
|
|
529
|
-
|
|
510
|
+
|
|
530
511
|
try:
|
|
531
512
|
response = await asyncio.wait_for(
|
|
532
|
-
send_resources_read(self._read_stream, self._write_stream, uri),
|
|
533
|
-
timeout=self.default_timeout
|
|
513
|
+
send_resources_read(self._read_stream, self._write_stream, uri), timeout=self.default_timeout
|
|
534
514
|
)
|
|
535
515
|
return response if isinstance(response, dict) else {}
|
|
536
|
-
except
|
|
516
|
+
except TimeoutError:
|
|
537
517
|
logger.error("Read resource timed out")
|
|
538
518
|
self._consecutive_failures += 1
|
|
539
519
|
return {}
|
|
@@ -542,18 +522,18 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
542
522
|
self._consecutive_failures += 1
|
|
543
523
|
return {}
|
|
544
524
|
|
|
545
|
-
async def get_prompt(self, name: str, arguments:
|
|
525
|
+
async def get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
546
526
|
"""Get a specific prompt."""
|
|
547
527
|
if not self._initialized:
|
|
548
528
|
return {}
|
|
549
|
-
|
|
529
|
+
|
|
550
530
|
try:
|
|
551
531
|
response = await asyncio.wait_for(
|
|
552
532
|
send_prompts_get(self._read_stream, self._write_stream, name, arguments or {}),
|
|
553
|
-
timeout=self.default_timeout
|
|
533
|
+
timeout=self.default_timeout,
|
|
554
534
|
)
|
|
555
535
|
return response if isinstance(response, dict) else {}
|
|
556
|
-
except
|
|
536
|
+
except TimeoutError:
|
|
557
537
|
logger.error("Get prompt timed out")
|
|
558
538
|
self._consecutive_failures += 1
|
|
559
539
|
return {}
|
|
@@ -562,22 +542,24 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
562
542
|
self._consecutive_failures += 1
|
|
563
543
|
return {}
|
|
564
544
|
|
|
565
|
-
def get_metrics(self) ->
|
|
545
|
+
def get_metrics(self) -> dict[str, Any]:
|
|
566
546
|
"""Enhanced metrics with health information."""
|
|
567
547
|
metrics = self._metrics.copy()
|
|
568
|
-
metrics.update(
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
548
|
+
metrics.update(
|
|
549
|
+
{
|
|
550
|
+
"is_connected": self.is_connected(),
|
|
551
|
+
"consecutive_failures": self._consecutive_failures,
|
|
552
|
+
"last_successful_ping": self._last_successful_ping,
|
|
553
|
+
"max_consecutive_failures": self._max_consecutive_failures,
|
|
554
|
+
}
|
|
555
|
+
)
|
|
574
556
|
return metrics
|
|
575
557
|
|
|
576
558
|
def reset_metrics(self) -> None:
|
|
577
559
|
"""Enhanced metrics reset preserving health state."""
|
|
578
560
|
preserved_init_time = self._metrics.get("initialization_time")
|
|
579
561
|
preserved_last_ping = self._metrics.get("last_ping_time")
|
|
580
|
-
|
|
562
|
+
|
|
581
563
|
self._metrics = {
|
|
582
564
|
"total_calls": 0,
|
|
583
565
|
"successful_calls": 0,
|
|
@@ -592,7 +574,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
592
574
|
"recovery_attempts": 0,
|
|
593
575
|
}
|
|
594
576
|
|
|
595
|
-
def get_streams(self) ->
|
|
577
|
+
def get_streams(self) -> list[tuple]:
|
|
596
578
|
"""Enhanced streams access with connection check."""
|
|
597
579
|
if self._initialized and self._read_stream and self._write_stream:
|
|
598
580
|
return [(self._read_stream, self._write_stream)]
|
|
@@ -607,4 +589,4 @@ class HTTPStreamableTransport(MCPBaseTransport):
|
|
|
607
589
|
|
|
608
590
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
609
591
|
"""Enhanced context manager cleanup."""
|
|
610
|
-
await self.close()
|
|
592
|
+
await self.close()
|