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

@@ -1,4 +1,4 @@
1
- # chuk_tool_processor/mcp/transport/http_streamable_transport.py
1
+ # chuk_tool_processor/mcp/transport/http_streamable_transport.py - FIXED
2
2
  from __future__ import annotations
3
3
 
4
4
  import asyncio
@@ -10,30 +10,18 @@ import logging
10
10
  from .base_transport import MCPBaseTransport
11
11
 
12
12
  # Import chuk-mcp HTTP transport components
13
- try:
14
- from chuk_mcp.transports.http import http_client
15
- from chuk_mcp.transports.http.parameters import StreamableHTTPParameters
16
- from chuk_mcp.protocol.messages import (
17
- send_initialize,
18
- send_ping,
19
- send_tools_list,
20
- send_tools_call,
21
- )
22
- HAS_HTTP_SUPPORT = True
23
- except ImportError:
24
- HAS_HTTP_SUPPORT = False
25
-
26
- # Import optional resource and prompt support
27
- try:
28
- from chuk_mcp.protocol.messages import (
29
- send_resources_list,
30
- send_resources_read,
31
- send_prompts_list,
32
- send_prompts_get,
33
- )
34
- HAS_RESOURCES_PROMPTS = True
35
- except ImportError:
36
- HAS_RESOURCES_PROMPTS = False
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 (
16
+ send_initialize,
17
+ send_ping,
18
+ send_tools_list,
19
+ send_tools_call,
20
+ send_resources_list,
21
+ send_resources_read,
22
+ send_prompts_list,
23
+ send_prompts_get,
24
+ )
37
25
 
38
26
  logger = logging.getLogger(__name__)
39
27
 
@@ -42,13 +30,15 @@ class HTTPStreamableTransport(MCPBaseTransport):
42
30
  """
43
31
  HTTP Streamable transport using chuk-mcp HTTP client.
44
32
 
45
- This implements the modern MCP spec (2025-03-26) replacement for SSE transport.
46
- Follows the same patterns as SSETransport but uses HTTP requests instead of SSE.
33
+ FIXED: Improved connection management and parameter configuration
34
+ to eliminate "server disconnected" errors.
47
35
  """
48
36
 
49
37
  def __init__(self, url: str, api_key: Optional[str] = None,
50
- connection_timeout: float = 30.0, default_timeout: float = 30.0,
51
- session_id: Optional[str] = None, enable_metrics: bool = True):
38
+ connection_timeout: float = 30.0,
39
+ default_timeout: float = 30.0,
40
+ session_id: Optional[str] = None,
41
+ enable_metrics: bool = True):
52
42
  """
53
43
  Initialize HTTP Streamable transport with chuk-mcp.
54
44
 
@@ -72,13 +62,19 @@ class HTTPStreamableTransport(MCPBaseTransport):
72
62
  self.session_id = session_id
73
63
  self.enable_metrics = enable_metrics
74
64
 
75
- # State tracking (following SSE pattern)
65
+ logger.debug("HTTP Streamable transport initialized with URL: %s", self.url)
66
+ if self.api_key:
67
+ logger.debug("API key configured for authentication")
68
+ if self.session_id:
69
+ logger.debug("Session ID configured: %s", self.session_id)
70
+
71
+ # State tracking
76
72
  self._http_context = None
77
73
  self._read_stream = None
78
74
  self._write_stream = None
79
75
  self._initialized = False
80
76
 
81
- # Performance metrics (enhanced from SSE version)
77
+ # Performance metrics (consistent with other transports)
82
78
  self._metrics = {
83
79
  "total_calls": 0,
84
80
  "successful_calls": 0,
@@ -86,20 +82,13 @@ class HTTPStreamableTransport(MCPBaseTransport):
86
82
  "total_time": 0.0,
87
83
  "avg_response_time": 0.0,
88
84
  "last_ping_time": None,
89
- "initialization_time": None
85
+ "initialization_time": None,
86
+ "connection_resets": 0,
87
+ "stream_errors": 0
90
88
  }
91
-
92
- if not HAS_HTTP_SUPPORT:
93
- logger.warning("HTTP Streamable transport not available - operations will fail")
94
- if not HAS_RESOURCES_PROMPTS:
95
- logger.debug("Resources/prompts not available in chuk-mcp")
96
89
 
97
90
  async def initialize(self) -> bool:
98
- """Initialize using chuk-mcp http_client (following SSE pattern)."""
99
- if not HAS_HTTP_SUPPORT:
100
- logger.error("HTTP Streamable transport not available in chuk-mcp")
101
- return False
102
-
91
+ """Initialize using chuk-mcp http_client with improved configuration."""
103
92
  if self._initialized:
104
93
  logger.warning("Transport already initialized")
105
94
  return True
@@ -109,8 +98,14 @@ class HTTPStreamableTransport(MCPBaseTransport):
109
98
  try:
110
99
  logger.debug("Initializing HTTP Streamable transport to %s", self.url)
111
100
 
112
- # Create HTTP parameters for chuk-mcp (following SSE pattern)
113
- headers = {}
101
+ # FIXED: Proper HTTP headers (match working diagnostic)
102
+ headers = {
103
+ "Content-Type": "application/json",
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
114
109
  if self.api_key:
115
110
  headers["Authorization"] = f"Bearer {self.api_key}"
116
111
  logger.debug("API key configured for authentication")
@@ -119,17 +114,21 @@ class HTTPStreamableTransport(MCPBaseTransport):
119
114
  headers["X-Session-ID"] = self.session_id
120
115
  logger.debug("Using session ID: %s", self.session_id)
121
116
 
117
+ # FIXED: Use only valid StreamableHTTPParameters
122
118
  http_params = StreamableHTTPParameters(
123
119
  url=self.url,
124
- timeout=self.connection_timeout,
120
+ timeout=self.default_timeout, # FIXED: Use default_timeout for operations
125
121
  headers=headers,
126
- bearer_token=self.api_key,
122
+ bearer_token=bearer_token, # FIXED: Don't duplicate auth
127
123
  session_id=self.session_id,
128
- enable_streaming=True, # Enable SSE streaming when available
129
- max_concurrent_requests=10
124
+ enable_streaming=True,
125
+ max_concurrent_requests=5, # FIXED: Reduce concurrency for stability
126
+ max_retries=2, # FIXED: Add retry configuration
127
+ retry_delay=1.0, # FIXED: Short retry delay
128
+ user_agent="chuk-tool-processor/1.0.0",
130
129
  )
131
130
 
132
- # Create and enter the HTTP context (same pattern as SSE)
131
+ # Create and enter the HTTP context
133
132
  self._http_context = http_client(http_params)
134
133
 
135
134
  logger.debug("Establishing HTTP connection and MCP handshake...")
@@ -138,8 +137,20 @@ class HTTPStreamableTransport(MCPBaseTransport):
138
137
  timeout=self.connection_timeout
139
138
  )
140
139
 
141
- # At this point, chuk-mcp should have established the HTTP connection
142
- # Verify the connection works with a simple ping (same as SSE)
140
+ # FIXED: Simplified MCP initialize sequence (match working diagnostic)
141
+ logger.debug("Sending MCP initialize request...")
142
+ init_start = time.time()
143
+
144
+ # Send initialize request with default parameters
145
+ init_result = await asyncio.wait_for(
146
+ send_initialize(self._read_stream, self._write_stream),
147
+ timeout=self.default_timeout
148
+ )
149
+
150
+ init_time = time.time() - init_start
151
+ logger.debug("MCP initialize completed in %.3fs", init_time)
152
+
153
+ # Verify the connection works with a simple ping
143
154
  logger.debug("Verifying connection with ping...")
144
155
  ping_start = time.time()
145
156
  ping_success = await asyncio.wait_for(
@@ -150,17 +161,19 @@ class HTTPStreamableTransport(MCPBaseTransport):
150
161
 
151
162
  if ping_success:
152
163
  self._initialized = True
153
- init_time = time.time() - start_time
154
- self._metrics["initialization_time"] = init_time
155
- self._metrics["last_ping_time"] = ping_time
164
+ total_init_time = time.time() - start_time
165
+ if self.enable_metrics:
166
+ self._metrics["initialization_time"] = total_init_time
167
+ self._metrics["last_ping_time"] = ping_time
156
168
 
157
- logger.debug("HTTP Streamable transport initialized successfully in %.3fs (ping: %.3fs)", init_time, ping_time)
169
+ logger.debug("HTTP Streamable transport initialized successfully in %.3fs (ping: %.3fs)", total_init_time, ping_time)
158
170
  return True
159
171
  else:
160
172
  logger.warning("HTTP connection established but ping failed")
161
- # Still consider it initialized since connection was established (same as SSE)
173
+ # Still consider it initialized since connection was established
162
174
  self._initialized = True
163
- self._metrics["initialization_time"] = time.time() - start_time
175
+ if self.enable_metrics:
176
+ self._metrics["initialization_time"] = time.time() - start_time
164
177
  return True
165
178
 
166
179
  except asyncio.TimeoutError:
@@ -174,11 +187,11 @@ class HTTPStreamableTransport(MCPBaseTransport):
174
187
  return False
175
188
 
176
189
  async def close(self) -> None:
177
- """Close the HTTP Streamable transport properly (same pattern as SSE)."""
190
+ """Close the HTTP Streamable transport properly."""
178
191
  if not self._initialized:
179
192
  return
180
193
 
181
- # Log final metrics (enhanced from SSE)
194
+ # Log final metrics
182
195
  if self.enable_metrics and self._metrics["total_calls"] > 0:
183
196
  logger.debug(
184
197
  "HTTP Streamable transport closing - Total calls: %d, Success rate: %.1f%%, Avg response time: %.3fs",
@@ -198,14 +211,14 @@ class HTTPStreamableTransport(MCPBaseTransport):
198
211
  await self._cleanup()
199
212
 
200
213
  async def _cleanup(self) -> None:
201
- """Clean up internal state (same as SSE)."""
214
+ """Clean up internal state."""
202
215
  self._http_context = None
203
216
  self._read_stream = None
204
217
  self._write_stream = None
205
218
  self._initialized = False
206
219
 
207
220
  async def send_ping(self) -> bool:
208
- """Send ping with performance tracking (enhanced from SSE)."""
221
+ """Send ping with performance tracking."""
209
222
  if not self._initialized or not self._read_stream:
210
223
  logger.error("Cannot send ping: transport not initialized")
211
224
  return False
@@ -220,18 +233,24 @@ class HTTPStreamableTransport(MCPBaseTransport):
220
233
  if self.enable_metrics:
221
234
  ping_time = time.time() - start_time
222
235
  self._metrics["last_ping_time"] = ping_time
223
- logger.debug("Ping completed in %.3fs: %s", ping_time, result)
236
+ logger.debug("HTTP Streamable ping completed in %.3fs: %s", ping_time, result)
224
237
 
225
238
  return bool(result)
226
239
  except asyncio.TimeoutError:
227
- logger.error("Ping timed out")
240
+ logger.error("HTTP Streamable ping timed out")
228
241
  return False
229
242
  except Exception as e:
230
- logger.error("Ping failed: %s", e)
243
+ logger.error("HTTP Streamable ping failed: %s", e)
244
+ if self.enable_metrics:
245
+ self._metrics["stream_errors"] += 1
231
246
  return False
232
247
 
248
+ def is_connected(self) -> bool:
249
+ """Check connection status."""
250
+ return self._initialized and self._read_stream is not None and self._write_stream is not None
251
+
233
252
  async def get_tools(self) -> List[Dict[str, Any]]:
234
- """Get tools list with performance tracking (enhanced from SSE)."""
253
+ """Get tools list with performance tracking."""
235
254
  if not self._initialized:
236
255
  logger.error("Cannot get tools: transport not initialized")
237
256
  return []
@@ -243,7 +262,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
243
262
  timeout=self.default_timeout
244
263
  )
245
264
 
246
- # Normalize response (same as SSE)
265
+ # Normalize response
247
266
  if isinstance(tools_response, dict):
248
267
  tools = tools_response.get("tools", [])
249
268
  elif isinstance(tools_response, list):
@@ -263,6 +282,8 @@ class HTTPStreamableTransport(MCPBaseTransport):
263
282
  return []
264
283
  except Exception as e:
265
284
  logger.error("Error getting tools: %s", e)
285
+ if self.enable_metrics:
286
+ self._metrics["stream_errors"] += 1
266
287
  return []
267
288
 
268
289
  async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
@@ -283,6 +304,15 @@ class HTTPStreamableTransport(MCPBaseTransport):
283
304
  try:
284
305
  logger.debug("Calling tool '%s' with timeout %ss", tool_name, tool_timeout)
285
306
 
307
+ # FIXED: Add connection state check before making call
308
+ if not self.is_connected():
309
+ logger.warning("Connection lost, attempting to reconnect...")
310
+ if not await self.initialize():
311
+ return {
312
+ "isError": True,
313
+ "error": "Failed to reconnect to server"
314
+ }
315
+
286
316
  raw_response = await asyncio.wait_for(
287
317
  send_tools_call(
288
318
  self._read_stream,
@@ -294,7 +324,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
294
324
  )
295
325
 
296
326
  response_time = time.time() - start_time
297
- result = self._normalize_tool_response(raw_response)
327
+ result = self._normalize_mcp_response(raw_response)
298
328
 
299
329
  if self.enable_metrics:
300
330
  self._update_metrics(response_time, not result.get("isError", False))
@@ -321,6 +351,12 @@ class HTTPStreamableTransport(MCPBaseTransport):
321
351
  response_time = time.time() - start_time
322
352
  if self.enable_metrics:
323
353
  self._update_metrics(response_time, False)
354
+ self._metrics["stream_errors"] += 1
355
+
356
+ # FIXED: Check if this is a connection error that should trigger reconnect
357
+ if "connection" in str(e).lower() or "disconnected" in str(e).lower():
358
+ logger.warning("Connection error detected, marking as disconnected: %s", e)
359
+ self._initialized = False
324
360
 
325
361
  error_msg = f"Tool execution failed: {str(e)}"
326
362
  logger.error("Tool '%s' error: %s", tool_name, error_msg)
@@ -330,23 +366,20 @@ class HTTPStreamableTransport(MCPBaseTransport):
330
366
  }
331
367
 
332
368
  def _update_metrics(self, response_time: float, success: bool) -> None:
333
- """Update performance metrics (new feature)."""
369
+ """Update performance metrics."""
334
370
  if success:
335
371
  self._metrics["successful_calls"] += 1
336
372
  else:
337
373
  self._metrics["failed_calls"] += 1
338
374
 
339
375
  self._metrics["total_time"] += response_time
340
- self._metrics["avg_response_time"] = (
341
- self._metrics["total_time"] / self._metrics["total_calls"]
342
- )
376
+ if self._metrics["total_calls"] > 0:
377
+ self._metrics["avg_response_time"] = (
378
+ self._metrics["total_time"] / self._metrics["total_calls"]
379
+ )
343
380
 
344
381
  async def list_resources(self) -> Dict[str, Any]:
345
- """List resources using chuk-mcp (same as SSE)."""
346
- if not HAS_RESOURCES_PROMPTS:
347
- logger.debug("Resources/prompts not available in chuk-mcp")
348
- return {}
349
-
382
+ """List resources using chuk-mcp."""
350
383
  if not self._initialized:
351
384
  return {}
352
385
 
@@ -364,11 +397,7 @@ class HTTPStreamableTransport(MCPBaseTransport):
364
397
  return {}
365
398
 
366
399
  async def list_prompts(self) -> Dict[str, Any]:
367
- """List prompts using chuk-mcp (same as SSE)."""
368
- if not HAS_RESOURCES_PROMPTS:
369
- logger.debug("Resources/prompts not available in chuk-mcp")
370
- return {}
371
-
400
+ """List prompts using chuk-mcp."""
372
401
  if not self._initialized:
373
402
  return {}
374
403
 
@@ -385,87 +414,15 @@ class HTTPStreamableTransport(MCPBaseTransport):
385
414
  logger.debug("Error listing prompts: %s", e)
386
415
  return {}
387
416
 
388
- def _normalize_tool_response(self, raw_response: Dict[str, Any]) -> Dict[str, Any]:
389
- """Normalize response for backward compatibility (same as SSE)."""
390
- # Handle explicit error in response
391
- if "error" in raw_response:
392
- error_info = raw_response["error"]
393
- if isinstance(error_info, dict):
394
- error_msg = error_info.get("message", "Unknown error")
395
- else:
396
- error_msg = str(error_info)
397
-
398
- return {
399
- "isError": True,
400
- "error": error_msg
401
- }
402
-
403
- # Handle successful response with result
404
- if "result" in raw_response:
405
- result = raw_response["result"]
406
-
407
- if isinstance(result, dict) and "content" in result:
408
- return {
409
- "isError": False,
410
- "content": self._extract_content(result["content"])
411
- }
412
- else:
413
- return {
414
- "isError": False,
415
- "content": result
416
- }
417
-
418
- # Handle direct content-based response
419
- if "content" in raw_response:
420
- return {
421
- "isError": False,
422
- "content": self._extract_content(raw_response["content"])
423
- }
424
-
425
- # Fallback
426
- return {
427
- "isError": False,
428
- "content": raw_response
429
- }
430
-
431
- def _extract_content(self, content_list: Any) -> Any:
432
- """Extract content from MCP content format (same as SSE)."""
433
- if not isinstance(content_list, list) or not content_list:
434
- return content_list
435
-
436
- # Handle single content item
437
- if len(content_list) == 1:
438
- content_item = content_list[0]
439
- if isinstance(content_item, dict):
440
- if content_item.get("type") == "text":
441
- text_content = content_item.get("text", "")
442
- # Try to parse JSON, fall back to plain text
443
- try:
444
- return json.loads(text_content)
445
- except json.JSONDecodeError:
446
- return text_content
447
- else:
448
- return content_item
449
-
450
- # Multiple content items
451
- return content_list
452
-
453
- def get_streams(self) -> List[tuple]:
454
- """Provide streams for backward compatibility (same as SSE)."""
455
- if self._initialized and self._read_stream and self._write_stream:
456
- return [(self._read_stream, self._write_stream)]
457
- return []
458
-
459
- def is_connected(self) -> bool:
460
- """Check connection status (same as SSE)."""
461
- return self._initialized and self._read_stream is not None and self._write_stream is not None
462
-
417
+ # ------------------------------------------------------------------ #
418
+ # Metrics and monitoring (consistent with other transports) #
419
+ # ------------------------------------------------------------------ #
463
420
  def get_metrics(self) -> Dict[str, Any]:
464
- """Get performance metrics (new feature)."""
421
+ """Get performance metrics."""
465
422
  return self._metrics.copy()
466
423
 
467
424
  def reset_metrics(self) -> None:
468
- """Reset performance metrics (new feature)."""
425
+ """Reset performance metrics."""
469
426
  self._metrics = {
470
427
  "total_calls": 0,
471
428
  "successful_calls": 0,
@@ -473,26 +430,30 @@ class HTTPStreamableTransport(MCPBaseTransport):
473
430
  "total_time": 0.0,
474
431
  "avg_response_time": 0.0,
475
432
  "last_ping_time": self._metrics.get("last_ping_time"),
476
- "initialization_time": self._metrics.get("initialization_time")
433
+ "initialization_time": self._metrics.get("initialization_time"),
434
+ "connection_resets": self._metrics.get("connection_resets", 0),
435
+ "stream_errors": 0
477
436
  }
478
437
 
438
+ # ------------------------------------------------------------------ #
439
+ # Backward compatibility #
440
+ # ------------------------------------------------------------------ #
441
+ def get_streams(self) -> List[tuple]:
442
+ """Provide streams for backward compatibility."""
443
+ if self._initialized and self._read_stream and self._write_stream:
444
+ return [(self._read_stream, self._write_stream)]
445
+ return []
446
+
447
+ # ------------------------------------------------------------------ #
448
+ # Context manager support #
449
+ # ------------------------------------------------------------------ #
479
450
  async def __aenter__(self):
480
- """Context manager support (same as SSE)."""
451
+ """Context manager support."""
481
452
  success = await self.initialize()
482
453
  if not success:
483
- raise RuntimeError("Failed to initialize HTTP Streamable transport")
454
+ raise RuntimeError("Failed to initialize HTTPStreamableTransport")
484
455
  return self
485
456
 
486
457
  async def __aexit__(self, exc_type, exc_val, exc_tb):
487
- """Context manager cleanup (same as SSE)."""
488
- await self.close()
489
-
490
- def __repr__(self) -> str:
491
- """Enhanced string representation for debugging."""
492
- status = "initialized" if self._initialized else "not initialized"
493
- metrics_info = ""
494
- if self.enable_metrics and self._metrics["total_calls"] > 0:
495
- success_rate = (self._metrics["successful_calls"] / self._metrics["total_calls"]) * 100
496
- metrics_info = f", calls: {self._metrics['total_calls']}, success: {success_rate:.1f}%"
497
-
498
- return f"HTTPStreamableTransport(status={status}, url={self.url}{metrics_info})"
458
+ """Context manager cleanup."""
459
+ await self.close()