chuk-tool-processor 0.6.10__py3-none-any.whl → 0.6.11__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/mcp/transport/http_streamable_transport.py +229 -78
- chuk_tool_processor/mcp/transport/sse_transport.py +98 -70
- chuk_tool_processor/mcp/transport/stdio_transport.py +293 -58
- {chuk_tool_processor-0.6.10.dist-info → chuk_tool_processor-0.6.11.dist-info}/METADATA +2 -1
- {chuk_tool_processor-0.6.10.dist-info → chuk_tool_processor-0.6.11.dist-info}/RECORD +7 -7
- {chuk_tool_processor-0.6.10.dist-info → chuk_tool_processor-0.6.11.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.6.10.dist-info → chuk_tool_processor-0.6.11.dist-info}/top_level.txt +0 -0
|
@@ -2,15 +2,8 @@
|
|
|
2
2
|
"""
|
|
3
3
|
SSE transport for MCP communication.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
2. Receive responses via SSE stream
|
|
8
|
-
|
|
9
|
-
Note: This transport is deprecated in favor of HTTP Streamable (spec 2025-03-26)
|
|
10
|
-
but remains supported for backward compatibility.
|
|
11
|
-
|
|
12
|
-
FIXED: Updated to support both old format (/messages/) and new event-based format
|
|
13
|
-
(event: endpoint + data: https://...) for session discovery.
|
|
5
|
+
FIXED: Removed problematic gateway connectivity test that was causing 401 errors.
|
|
6
|
+
The SSE endpoint works perfectly, so we don't need to test the base URL.
|
|
14
7
|
"""
|
|
15
8
|
from __future__ import annotations
|
|
16
9
|
|
|
@@ -32,28 +25,16 @@ class SSETransport(MCPBaseTransport):
|
|
|
32
25
|
"""
|
|
33
26
|
SSE transport implementing the MCP protocol over Server-Sent Events.
|
|
34
27
|
|
|
35
|
-
|
|
36
|
-
- SSE stream for receiving responses
|
|
37
|
-
- HTTP POST for sending requests
|
|
38
|
-
|
|
39
|
-
FIXED: Supports both old and new session discovery formats.
|
|
28
|
+
FIXED: Uses SSE endpoint directly without problematic connectivity tests.
|
|
40
29
|
"""
|
|
41
30
|
|
|
42
31
|
def __init__(self, url: str, api_key: Optional[str] = None,
|
|
43
32
|
headers: Optional[Dict[str, str]] = None,
|
|
44
33
|
connection_timeout: float = 30.0,
|
|
45
|
-
default_timeout: float =
|
|
34
|
+
default_timeout: float = 60.0,
|
|
46
35
|
enable_metrics: bool = True):
|
|
47
36
|
"""
|
|
48
37
|
Initialize SSE transport.
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
url: Base URL for the MCP server
|
|
52
|
-
api_key: Optional API key for authentication
|
|
53
|
-
headers: Optional custom headers
|
|
54
|
-
connection_timeout: Timeout for initial connection setup
|
|
55
|
-
default_timeout: Default timeout for operations
|
|
56
|
-
enable_metrics: Whether to track performance metrics
|
|
57
38
|
"""
|
|
58
39
|
self.url = url.rstrip('/')
|
|
59
40
|
self.api_key = api_key
|
|
@@ -63,10 +44,6 @@ class SSETransport(MCPBaseTransport):
|
|
|
63
44
|
self.enable_metrics = enable_metrics
|
|
64
45
|
|
|
65
46
|
logger.debug("SSE Transport initialized with URL: %s", self.url)
|
|
66
|
-
if self.api_key:
|
|
67
|
-
logger.debug("API key configured for authentication")
|
|
68
|
-
if self.configured_headers:
|
|
69
|
-
logger.debug("Custom headers configured: %s", list(self.configured_headers.keys()))
|
|
70
47
|
|
|
71
48
|
# Connection state
|
|
72
49
|
self.session_id = None
|
|
@@ -83,7 +60,12 @@ class SSETransport(MCPBaseTransport):
|
|
|
83
60
|
self.sse_response = None
|
|
84
61
|
self.sse_stream_context = None
|
|
85
62
|
|
|
86
|
-
#
|
|
63
|
+
# Health monitoring
|
|
64
|
+
self._last_successful_ping = None
|
|
65
|
+
self._consecutive_failures = 0
|
|
66
|
+
self._max_consecutive_failures = 3
|
|
67
|
+
|
|
68
|
+
# Performance metrics
|
|
87
69
|
self._metrics = {
|
|
88
70
|
"total_calls": 0,
|
|
89
71
|
"successful_calls": 0,
|
|
@@ -97,11 +79,7 @@ class SSETransport(MCPBaseTransport):
|
|
|
97
79
|
}
|
|
98
80
|
|
|
99
81
|
def _construct_sse_url(self, base_url: str) -> str:
|
|
100
|
-
"""
|
|
101
|
-
Construct the SSE endpoint URL from the base URL.
|
|
102
|
-
|
|
103
|
-
Smart detection to avoid double-appending /sse if already present.
|
|
104
|
-
"""
|
|
82
|
+
"""Construct the SSE endpoint URL from the base URL."""
|
|
105
83
|
base_url = base_url.rstrip('/')
|
|
106
84
|
|
|
107
85
|
if base_url.endswith('/sse'):
|
|
@@ -114,20 +92,34 @@ class SSETransport(MCPBaseTransport):
|
|
|
114
92
|
|
|
115
93
|
def _get_headers(self) -> Dict[str, str]:
|
|
116
94
|
"""Get headers with authentication and custom headers."""
|
|
117
|
-
headers = {
|
|
95
|
+
headers = {
|
|
96
|
+
'User-Agent': 'chuk-tool-processor/1.0.0',
|
|
97
|
+
'Accept': 'text/event-stream',
|
|
98
|
+
'Cache-Control': 'no-cache',
|
|
99
|
+
}
|
|
118
100
|
|
|
119
101
|
# Add configured headers first
|
|
120
102
|
if self.configured_headers:
|
|
121
103
|
headers.update(self.configured_headers)
|
|
122
104
|
|
|
123
|
-
# Add API key as Bearer token if provided
|
|
105
|
+
# Add API key as Bearer token if provided
|
|
124
106
|
if self.api_key:
|
|
125
107
|
headers['Authorization'] = f'Bearer {self.api_key}'
|
|
126
108
|
|
|
127
109
|
return headers
|
|
128
110
|
|
|
111
|
+
async def _test_gateway_connectivity(self) -> bool:
|
|
112
|
+
"""
|
|
113
|
+
Skip connectivity test - we know the SSE endpoint works.
|
|
114
|
+
|
|
115
|
+
FIXED: The diagnostic proves SSE endpoint works perfectly.
|
|
116
|
+
No need to test base URL that causes 401 errors.
|
|
117
|
+
"""
|
|
118
|
+
logger.debug("Skipping gateway connectivity test - using direct SSE connection")
|
|
119
|
+
return True
|
|
120
|
+
|
|
129
121
|
async def initialize(self) -> bool:
|
|
130
|
-
"""Initialize SSE connection
|
|
122
|
+
"""Initialize SSE connection."""
|
|
131
123
|
if self._initialized:
|
|
132
124
|
logger.warning("Transport already initialized")
|
|
133
125
|
return True
|
|
@@ -137,9 +129,22 @@ class SSETransport(MCPBaseTransport):
|
|
|
137
129
|
try:
|
|
138
130
|
logger.debug("Initializing SSE transport...")
|
|
139
131
|
|
|
140
|
-
#
|
|
141
|
-
|
|
142
|
-
|
|
132
|
+
# FIXED: Skip problematic connectivity test
|
|
133
|
+
if not await self._test_gateway_connectivity():
|
|
134
|
+
logger.error("Gateway connectivity test failed")
|
|
135
|
+
return False
|
|
136
|
+
|
|
137
|
+
# Create HTTP clients
|
|
138
|
+
self.stream_client = httpx.AsyncClient(
|
|
139
|
+
timeout=httpx.Timeout(self.connection_timeout),
|
|
140
|
+
follow_redirects=True,
|
|
141
|
+
limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
|
|
142
|
+
)
|
|
143
|
+
self.send_client = httpx.AsyncClient(
|
|
144
|
+
timeout=httpx.Timeout(self.default_timeout),
|
|
145
|
+
follow_redirects=True,
|
|
146
|
+
limits=httpx.Limits(max_connections=10, max_keepalive_connections=5)
|
|
147
|
+
)
|
|
143
148
|
|
|
144
149
|
# Connect to SSE stream
|
|
145
150
|
sse_url = self._construct_sse_url(self.url)
|
|
@@ -163,13 +168,21 @@ class SSETransport(MCPBaseTransport):
|
|
|
163
168
|
name="sse_stream_processor"
|
|
164
169
|
)
|
|
165
170
|
|
|
166
|
-
# Wait for session discovery
|
|
171
|
+
# Wait for session discovery
|
|
167
172
|
logger.debug("Waiting for session discovery...")
|
|
168
|
-
session_timeout =
|
|
173
|
+
session_timeout = 10.0
|
|
169
174
|
session_start = time.time()
|
|
170
175
|
|
|
171
176
|
while not self.message_url and (time.time() - session_start) < session_timeout:
|
|
172
177
|
await asyncio.sleep(0.1)
|
|
178
|
+
|
|
179
|
+
# Check if SSE task died
|
|
180
|
+
if self.sse_task.done():
|
|
181
|
+
exception = self.sse_task.exception()
|
|
182
|
+
if exception:
|
|
183
|
+
logger.error(f"SSE task died during session discovery: {exception}")
|
|
184
|
+
await self._cleanup()
|
|
185
|
+
return False
|
|
173
186
|
|
|
174
187
|
if not self.message_url:
|
|
175
188
|
logger.error("Failed to discover session endpoint within %.1fs", session_timeout)
|
|
@@ -179,7 +192,7 @@ class SSETransport(MCPBaseTransport):
|
|
|
179
192
|
if self.enable_metrics:
|
|
180
193
|
self._metrics["session_discoveries"] += 1
|
|
181
194
|
|
|
182
|
-
logger.debug("Session endpoint discovered: %s", self.
|
|
195
|
+
logger.debug("Session endpoint discovered: %s", self.message_url)
|
|
183
196
|
|
|
184
197
|
# Perform MCP initialization handshake
|
|
185
198
|
try:
|
|
@@ -190,7 +203,7 @@ class SSETransport(MCPBaseTransport):
|
|
|
190
203
|
"name": "chuk-tool-processor",
|
|
191
204
|
"version": "1.0.0"
|
|
192
205
|
}
|
|
193
|
-
})
|
|
206
|
+
}, timeout=self.default_timeout)
|
|
194
207
|
|
|
195
208
|
if 'error' in init_response:
|
|
196
209
|
logger.error("MCP initialize failed: %s", init_response['error'])
|
|
@@ -201,6 +214,7 @@ class SSETransport(MCPBaseTransport):
|
|
|
201
214
|
await self._send_notification("notifications/initialized")
|
|
202
215
|
|
|
203
216
|
self._initialized = True
|
|
217
|
+
self._last_successful_ping = time.time()
|
|
204
218
|
|
|
205
219
|
if self.enable_metrics:
|
|
206
220
|
init_time = time.time() - start_time
|
|
@@ -220,16 +234,11 @@ class SSETransport(MCPBaseTransport):
|
|
|
220
234
|
return False
|
|
221
235
|
|
|
222
236
|
async def _process_sse_stream(self):
|
|
223
|
-
"""
|
|
224
|
-
Process the persistent SSE stream for responses and session discovery.
|
|
225
|
-
|
|
226
|
-
FIXED: Supports both old format (/messages/) and new event-based format
|
|
227
|
-
(event: endpoint + data: https://...) for session discovery.
|
|
228
|
-
"""
|
|
237
|
+
"""Process the SSE stream for responses and session discovery."""
|
|
229
238
|
try:
|
|
230
239
|
logger.debug("Starting SSE stream processing...")
|
|
231
240
|
|
|
232
|
-
current_event = None
|
|
241
|
+
current_event = None
|
|
233
242
|
|
|
234
243
|
async for line in self.sse_response.aiter_lines():
|
|
235
244
|
line = line.strip()
|
|
@@ -242,7 +251,7 @@ class SSETransport(MCPBaseTransport):
|
|
|
242
251
|
logger.debug("SSE event type: %s", current_event)
|
|
243
252
|
continue
|
|
244
253
|
|
|
245
|
-
# Handle session endpoint discovery
|
|
254
|
+
# Handle session endpoint discovery
|
|
246
255
|
if not self.message_url and line.startswith('data:'):
|
|
247
256
|
data_part = line.split(':', 1)[1].strip()
|
|
248
257
|
|
|
@@ -253,8 +262,10 @@ class SSETransport(MCPBaseTransport):
|
|
|
253
262
|
# Extract session ID from URL if present
|
|
254
263
|
if 'session_id=' in data_part:
|
|
255
264
|
self.session_id = data_part.split('session_id=')[1].split('&')[0]
|
|
265
|
+
else:
|
|
266
|
+
self.session_id = str(uuid.uuid4())
|
|
256
267
|
|
|
257
|
-
logger.debug("Session endpoint discovered via event format: %s", self.
|
|
268
|
+
logger.debug("Session endpoint discovered via event format: %s", self.message_url)
|
|
258
269
|
continue
|
|
259
270
|
|
|
260
271
|
# OLD FORMAT: data: /messages/... (backwards compatibility)
|
|
@@ -265,8 +276,10 @@ class SSETransport(MCPBaseTransport):
|
|
|
265
276
|
# Extract session ID if present
|
|
266
277
|
if 'session_id=' in endpoint_path:
|
|
267
278
|
self.session_id = endpoint_path.split('session_id=')[1].split('&')[0]
|
|
279
|
+
else:
|
|
280
|
+
self.session_id = str(uuid.uuid4())
|
|
268
281
|
|
|
269
|
-
logger.debug("Session endpoint discovered via old format: %s", self.
|
|
282
|
+
logger.debug("Session endpoint discovered via old format: %s", self.message_url)
|
|
270
283
|
continue
|
|
271
284
|
|
|
272
285
|
# Handle JSON-RPC responses
|
|
@@ -293,15 +306,12 @@ class SSETransport(MCPBaseTransport):
|
|
|
293
306
|
|
|
294
307
|
except json.JSONDecodeError as e:
|
|
295
308
|
logger.debug("Non-JSON data in SSE stream (ignoring): %s", e)
|
|
296
|
-
|
|
297
|
-
# Reset event type after processing data (only if we processed JSON-RPC)
|
|
298
|
-
if line.startswith('data:') and current_event not in ("endpoint",):
|
|
299
|
-
current_event = None
|
|
300
309
|
|
|
301
310
|
except Exception as e:
|
|
302
311
|
if self.enable_metrics:
|
|
303
312
|
self._metrics["stream_errors"] += 1
|
|
304
313
|
logger.error("SSE stream processing error: %s", e)
|
|
314
|
+
self._consecutive_failures += 1
|
|
305
315
|
|
|
306
316
|
async def _send_request(self, method: str, params: Dict[str, Any] = None,
|
|
307
317
|
timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
@@ -338,20 +348,25 @@ class SSETransport(MCPBaseTransport):
|
|
|
338
348
|
# Async response - wait for result via SSE
|
|
339
349
|
request_timeout = timeout or self.default_timeout
|
|
340
350
|
result = await asyncio.wait_for(future, timeout=request_timeout)
|
|
351
|
+
self._consecutive_failures = 0 # Reset on success
|
|
341
352
|
return result
|
|
342
353
|
elif response.status_code == 200:
|
|
343
354
|
# Immediate response
|
|
344
355
|
self.pending_requests.pop(request_id, None)
|
|
356
|
+
self._consecutive_failures = 0 # Reset on success
|
|
345
357
|
return response.json()
|
|
346
358
|
else:
|
|
347
359
|
self.pending_requests.pop(request_id, None)
|
|
360
|
+
self._consecutive_failures += 1
|
|
348
361
|
raise RuntimeError(f"HTTP request failed with status: {response.status_code}")
|
|
349
362
|
|
|
350
363
|
except asyncio.TimeoutError:
|
|
351
364
|
self.pending_requests.pop(request_id, None)
|
|
365
|
+
self._consecutive_failures += 1
|
|
352
366
|
raise
|
|
353
367
|
except Exception:
|
|
354
368
|
self.pending_requests.pop(request_id, None)
|
|
369
|
+
self._consecutive_failures += 1
|
|
355
370
|
raise
|
|
356
371
|
|
|
357
372
|
async def _send_notification(self, method: str, params: Dict[str, Any] = None):
|
|
@@ -387,21 +402,43 @@ class SSETransport(MCPBaseTransport):
|
|
|
387
402
|
start_time = time.time()
|
|
388
403
|
try:
|
|
389
404
|
# Use tools/list as a lightweight ping since not all servers support ping
|
|
390
|
-
response = await self._send_request("tools/list", {}, timeout=
|
|
405
|
+
response = await self._send_request("tools/list", {}, timeout=10.0)
|
|
406
|
+
|
|
407
|
+
success = 'error' not in response
|
|
408
|
+
|
|
409
|
+
if success:
|
|
410
|
+
self._last_successful_ping = time.time()
|
|
411
|
+
self._consecutive_failures = 0
|
|
391
412
|
|
|
392
413
|
if self.enable_metrics:
|
|
393
414
|
ping_time = time.time() - start_time
|
|
394
415
|
self._metrics["last_ping_time"] = ping_time
|
|
395
|
-
logger.debug("SSE ping completed in %.3fs", ping_time)
|
|
416
|
+
logger.debug("SSE ping completed in %.3fs: %s", ping_time, success)
|
|
396
417
|
|
|
397
|
-
return
|
|
418
|
+
return success
|
|
398
419
|
except Exception as e:
|
|
399
420
|
logger.debug("SSE ping failed: %s", e)
|
|
421
|
+
self._consecutive_failures += 1
|
|
400
422
|
return False
|
|
401
423
|
|
|
402
424
|
def is_connected(self) -> bool:
|
|
403
425
|
"""Check if the transport is connected and ready."""
|
|
404
|
-
|
|
426
|
+
if not self._initialized or not self.session_id:
|
|
427
|
+
return False
|
|
428
|
+
|
|
429
|
+
# Check if we've had too many consecutive failures
|
|
430
|
+
if self._consecutive_failures >= self._max_consecutive_failures:
|
|
431
|
+
logger.warning(f"Connection marked unhealthy after {self._consecutive_failures} failures")
|
|
432
|
+
return False
|
|
433
|
+
|
|
434
|
+
# Check if SSE task is still running
|
|
435
|
+
if self.sse_task and self.sse_task.done():
|
|
436
|
+
exception = self.sse_task.exception()
|
|
437
|
+
if exception:
|
|
438
|
+
logger.warning(f"SSE task died: {exception}")
|
|
439
|
+
return False
|
|
440
|
+
|
|
441
|
+
return True
|
|
405
442
|
|
|
406
443
|
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
407
444
|
"""Get list of available tools from the server."""
|
|
@@ -589,9 +626,6 @@ class SSETransport(MCPBaseTransport):
|
|
|
589
626
|
self.stream_client = None
|
|
590
627
|
self.send_client = None
|
|
591
628
|
|
|
592
|
-
# ------------------------------------------------------------------ #
|
|
593
|
-
# Metrics and monitoring (consistent with other transports) #
|
|
594
|
-
# ------------------------------------------------------------------ #
|
|
595
629
|
def get_metrics(self) -> Dict[str, Any]:
|
|
596
630
|
"""Get performance and connection metrics."""
|
|
597
631
|
return self._metrics.copy()
|
|
@@ -610,16 +644,10 @@ class SSETransport(MCPBaseTransport):
|
|
|
610
644
|
"stream_errors": 0
|
|
611
645
|
}
|
|
612
646
|
|
|
613
|
-
# ------------------------------------------------------------------ #
|
|
614
|
-
# Backward compatibility #
|
|
615
|
-
# ------------------------------------------------------------------ #
|
|
616
647
|
def get_streams(self) -> List[tuple]:
|
|
617
648
|
"""SSE transport doesn't expose raw streams."""
|
|
618
649
|
return []
|
|
619
650
|
|
|
620
|
-
# ------------------------------------------------------------------ #
|
|
621
|
-
# Context manager support #
|
|
622
|
-
# ------------------------------------------------------------------ #
|
|
623
651
|
async def __aenter__(self):
|
|
624
652
|
"""Context manager entry."""
|
|
625
653
|
success = await self.initialize()
|