chuk-tool-processor 0.5.2__py3-none-any.whl → 0.5.4__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/__init__.py +16 -3
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py +119 -0
- chuk_tool_processor/mcp/setup_mcp_sse.py +35 -36
- chuk_tool_processor/mcp/setup_mcp_stdio.py +3 -1
- chuk_tool_processor/mcp/stream_manager.py +157 -130
- chuk_tool_processor/mcp/transport/__init__.py +4 -2
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +496 -0
- chuk_tool_processor/mcp/transport/sse_transport.py +306 -425
- chuk_tool_processor/mcp/transport/stdio_transport.py +119 -276
- {chuk_tool_processor-0.5.2.dist-info → chuk_tool_processor-0.5.4.dist-info}/METADATA +2 -2
- {chuk_tool_processor-0.5.2.dist-info → chuk_tool_processor-0.5.4.dist-info}/RECORD +13 -11
- {chuk_tool_processor-0.5.2.dist-info → chuk_tool_processor-0.5.4.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.5.2.dist-info → chuk_tool_processor-0.5.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/transport/http_streamable_transport.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
from typing import Dict, Any, List, Optional
|
|
8
|
+
import logging
|
|
9
|
+
|
|
10
|
+
from .base_transport import MCPBaseTransport
|
|
11
|
+
|
|
12
|
+
# Import latest chuk-mcp HTTP Streamable transport
|
|
13
|
+
# Try different possible naming conventions
|
|
14
|
+
try:
|
|
15
|
+
# First try the expected naming
|
|
16
|
+
from chuk_mcp.transports.http_streamable import http_streamable_client
|
|
17
|
+
from chuk_mcp.transports.http_streamable.parameters import HTTPStreamableParameters
|
|
18
|
+
HAS_HTTP_STREAMABLE_SUPPORT = True
|
|
19
|
+
STREAMABLE_CLIENT = http_streamable_client
|
|
20
|
+
STREAMABLE_PARAMS = HTTPStreamableParameters
|
|
21
|
+
except ImportError:
|
|
22
|
+
try:
|
|
23
|
+
# Try alternative naming
|
|
24
|
+
from chuk_mcp.transports.streamable_http import streamable_http_client
|
|
25
|
+
from chuk_mcp.transports.streamable_http.parameters import StreamableHttpParameters
|
|
26
|
+
HAS_HTTP_STREAMABLE_SUPPORT = True
|
|
27
|
+
STREAMABLE_CLIENT = streamable_http_client
|
|
28
|
+
STREAMABLE_PARAMS = StreamableHttpParameters
|
|
29
|
+
except ImportError:
|
|
30
|
+
HAS_HTTP_STREAMABLE_SUPPORT = False
|
|
31
|
+
|
|
32
|
+
# Import protocol messages
|
|
33
|
+
try:
|
|
34
|
+
from chuk_mcp.protocol.messages import (
|
|
35
|
+
send_ping,
|
|
36
|
+
send_tools_list,
|
|
37
|
+
send_tools_call,
|
|
38
|
+
send_resources_list,
|
|
39
|
+
send_prompts_list,
|
|
40
|
+
)
|
|
41
|
+
HAS_PROTOCOL_MESSAGES = True
|
|
42
|
+
except ImportError:
|
|
43
|
+
HAS_PROTOCOL_MESSAGES = False
|
|
44
|
+
|
|
45
|
+
logger = logging.getLogger(__name__)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HTTPStreamableTransport(MCPBaseTransport):
|
|
49
|
+
"""
|
|
50
|
+
Enhanced HTTP Streamable transport using latest chuk-mcp APIs.
|
|
51
|
+
|
|
52
|
+
This implements the modern Streamable HTTP transport (spec 2025-03-26)
|
|
53
|
+
which replaces the deprecated SSE transport with a cleaner, more flexible approach.
|
|
54
|
+
|
|
55
|
+
Key features:
|
|
56
|
+
- Single /mcp endpoint for all communication
|
|
57
|
+
- Works with standard HTTP infrastructure
|
|
58
|
+
- Supports both immediate and streaming responses
|
|
59
|
+
- Better error handling and retry logic
|
|
60
|
+
- Stateless operation when streaming not needed
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, url: str, api_key: Optional[str] = None,
|
|
64
|
+
connection_timeout: float = 30.0, default_timeout: float = 30.0,
|
|
65
|
+
session_id: Optional[str] = None, enable_metrics: bool = True):
|
|
66
|
+
"""
|
|
67
|
+
Initialize HTTP Streamable transport.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
url: Base URL of the MCP server (will append /mcp if needed)
|
|
71
|
+
api_key: Optional API key for authentication
|
|
72
|
+
connection_timeout: Timeout for initial connection
|
|
73
|
+
default_timeout: Default timeout for operations
|
|
74
|
+
session_id: Optional session ID for stateful connections
|
|
75
|
+
enable_metrics: Whether to track performance metrics
|
|
76
|
+
"""
|
|
77
|
+
# Ensure URL points to the /mcp endpoint
|
|
78
|
+
if not url.endswith('/mcp'):
|
|
79
|
+
self.url = f"{url.rstrip('/')}/mcp"
|
|
80
|
+
else:
|
|
81
|
+
self.url = url
|
|
82
|
+
|
|
83
|
+
self.api_key = api_key
|
|
84
|
+
self.connection_timeout = connection_timeout
|
|
85
|
+
self.default_timeout = default_timeout
|
|
86
|
+
self.session_id = session_id
|
|
87
|
+
self.enable_metrics = enable_metrics
|
|
88
|
+
|
|
89
|
+
# State tracking
|
|
90
|
+
self._streamable_context = None
|
|
91
|
+
self._read_stream = None
|
|
92
|
+
self._write_stream = None
|
|
93
|
+
self._initialized = False
|
|
94
|
+
|
|
95
|
+
# Performance metrics
|
|
96
|
+
self._metrics = {
|
|
97
|
+
"total_calls": 0,
|
|
98
|
+
"successful_calls": 0,
|
|
99
|
+
"failed_calls": 0,
|
|
100
|
+
"total_time": 0.0,
|
|
101
|
+
"avg_response_time": 0.0,
|
|
102
|
+
"last_ping_time": None,
|
|
103
|
+
"initialization_time": None
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if not HAS_HTTP_STREAMABLE_SUPPORT:
|
|
107
|
+
logger.warning("HTTP Streamable transport not available - operations will fail")
|
|
108
|
+
if not HAS_PROTOCOL_MESSAGES:
|
|
109
|
+
logger.warning("Protocol messages not available - operations will fail")
|
|
110
|
+
|
|
111
|
+
async def initialize(self) -> bool:
|
|
112
|
+
"""Initialize using latest chuk-mcp streamable http client with enhanced monitoring."""
|
|
113
|
+
if not HAS_HTTP_STREAMABLE_SUPPORT or not HAS_PROTOCOL_MESSAGES:
|
|
114
|
+
logger.error("HTTP Streamable transport or protocol messages not available in chuk-mcp")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
if self._initialized:
|
|
118
|
+
logger.warning("Transport already initialized")
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
start_time = time.time()
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
logger.info(f"Initializing HTTP Streamable transport to {self.url}")
|
|
125
|
+
|
|
126
|
+
# Create HTTP Streamable parameters
|
|
127
|
+
streamable_params = STREAMABLE_PARAMS(
|
|
128
|
+
url=self.url,
|
|
129
|
+
timeout=self.connection_timeout,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Add session ID if provided
|
|
133
|
+
if self.session_id:
|
|
134
|
+
streamable_params.session_id = self.session_id
|
|
135
|
+
logger.debug(f"Using session ID: {self.session_id}")
|
|
136
|
+
|
|
137
|
+
# Add API key via headers if provided
|
|
138
|
+
if self.api_key:
|
|
139
|
+
streamable_params.headers = {"Authorization": f"Bearer {self.api_key}"}
|
|
140
|
+
logger.debug("API key configured for authentication")
|
|
141
|
+
|
|
142
|
+
# Create and enter the context - let chuk-mcp handle MCP handshake
|
|
143
|
+
self._streamable_context = STREAMABLE_CLIENT(streamable_params)
|
|
144
|
+
|
|
145
|
+
logger.debug("Establishing HTTP Streamable connection...")
|
|
146
|
+
self._read_stream, self._write_stream = await asyncio.wait_for(
|
|
147
|
+
self._streamable_context.__aenter__(),
|
|
148
|
+
timeout=self.connection_timeout
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Verify the connection works with a simple ping
|
|
152
|
+
logger.debug("Verifying connection with ping...")
|
|
153
|
+
ping_start = time.time()
|
|
154
|
+
ping_success = await asyncio.wait_for(
|
|
155
|
+
send_ping(self._read_stream, self._write_stream),
|
|
156
|
+
timeout=5.0
|
|
157
|
+
)
|
|
158
|
+
ping_time = time.time() - ping_start
|
|
159
|
+
|
|
160
|
+
if ping_success:
|
|
161
|
+
self._initialized = True
|
|
162
|
+
init_time = time.time() - start_time
|
|
163
|
+
self._metrics["initialization_time"] = init_time
|
|
164
|
+
self._metrics["last_ping_time"] = ping_time
|
|
165
|
+
|
|
166
|
+
logger.info(f"HTTP Streamable transport initialized successfully in {init_time:.3f}s (ping: {ping_time:.3f}s)")
|
|
167
|
+
return True
|
|
168
|
+
else:
|
|
169
|
+
logger.warning("HTTP Streamable connection established but ping failed")
|
|
170
|
+
# Still consider it initialized since connection was established
|
|
171
|
+
self._initialized = True
|
|
172
|
+
self._metrics["initialization_time"] = time.time() - start_time
|
|
173
|
+
return True
|
|
174
|
+
|
|
175
|
+
except asyncio.TimeoutError:
|
|
176
|
+
logger.error(f"HTTP Streamable initialization timed out after {self.connection_timeout}s")
|
|
177
|
+
await self._cleanup()
|
|
178
|
+
return False
|
|
179
|
+
except Exception as e:
|
|
180
|
+
logger.error(f"Error initializing HTTP Streamable transport: {e}", exc_info=True)
|
|
181
|
+
await self._cleanup()
|
|
182
|
+
return False
|
|
183
|
+
|
|
184
|
+
async def close(self) -> None:
|
|
185
|
+
"""Close the HTTP Streamable transport properly with metrics summary."""
|
|
186
|
+
if not self._initialized:
|
|
187
|
+
return
|
|
188
|
+
|
|
189
|
+
# Log final metrics
|
|
190
|
+
if self.enable_metrics and self._metrics["total_calls"] > 0:
|
|
191
|
+
logger.info(
|
|
192
|
+
f"HTTP Streamable transport closing - Total calls: {self._metrics['total_calls']}, "
|
|
193
|
+
f"Success rate: {(self._metrics['successful_calls']/self._metrics['total_calls']*100):.1f}%, "
|
|
194
|
+
f"Avg response time: {self._metrics['avg_response_time']:.3f}s"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
try:
|
|
198
|
+
if self._streamable_context is not None:
|
|
199
|
+
await self._streamable_context.__aexit__(None, None, None)
|
|
200
|
+
logger.debug("HTTP Streamable context closed")
|
|
201
|
+
|
|
202
|
+
except Exception as e:
|
|
203
|
+
logger.debug(f"Error during transport close: {e}")
|
|
204
|
+
finally:
|
|
205
|
+
await self._cleanup()
|
|
206
|
+
|
|
207
|
+
async def _cleanup(self) -> None:
|
|
208
|
+
"""Clean up internal state."""
|
|
209
|
+
self._streamable_context = None
|
|
210
|
+
self._read_stream = None
|
|
211
|
+
self._write_stream = None
|
|
212
|
+
self._initialized = False
|
|
213
|
+
|
|
214
|
+
async def send_ping(self) -> bool:
|
|
215
|
+
"""Send ping with performance tracking."""
|
|
216
|
+
if not self._initialized or not self._read_stream:
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
start_time = time.time()
|
|
220
|
+
try:
|
|
221
|
+
result = await asyncio.wait_for(
|
|
222
|
+
send_ping(self._read_stream, self._write_stream),
|
|
223
|
+
timeout=5.0
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if self.enable_metrics:
|
|
227
|
+
ping_time = time.time() - start_time
|
|
228
|
+
self._metrics["last_ping_time"] = ping_time
|
|
229
|
+
logger.debug(f"Ping completed in {ping_time:.3f}s: {result}")
|
|
230
|
+
|
|
231
|
+
return bool(result)
|
|
232
|
+
except asyncio.TimeoutError:
|
|
233
|
+
logger.error("Ping timed out")
|
|
234
|
+
return False
|
|
235
|
+
except Exception as e:
|
|
236
|
+
logger.error(f"Ping failed: {e}")
|
|
237
|
+
return False
|
|
238
|
+
|
|
239
|
+
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
240
|
+
"""Get tools list with performance tracking."""
|
|
241
|
+
if not self._initialized:
|
|
242
|
+
logger.error("Cannot get tools: transport not initialized")
|
|
243
|
+
return []
|
|
244
|
+
|
|
245
|
+
start_time = time.time()
|
|
246
|
+
try:
|
|
247
|
+
tools_response = await asyncio.wait_for(
|
|
248
|
+
send_tools_list(self._read_stream, self._write_stream),
|
|
249
|
+
timeout=self.default_timeout
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Normalize response
|
|
253
|
+
if isinstance(tools_response, dict):
|
|
254
|
+
tools = tools_response.get("tools", [])
|
|
255
|
+
elif isinstance(tools_response, list):
|
|
256
|
+
tools = tools_response
|
|
257
|
+
else:
|
|
258
|
+
logger.warning(f"Unexpected tools response type: {type(tools_response)}")
|
|
259
|
+
tools = []
|
|
260
|
+
|
|
261
|
+
if self.enable_metrics:
|
|
262
|
+
response_time = time.time() - start_time
|
|
263
|
+
logger.debug(f"Retrieved {len(tools)} tools in {response_time:.3f}s")
|
|
264
|
+
|
|
265
|
+
return tools
|
|
266
|
+
|
|
267
|
+
except asyncio.TimeoutError:
|
|
268
|
+
logger.error("Get tools timed out")
|
|
269
|
+
return []
|
|
270
|
+
except Exception as e:
|
|
271
|
+
logger.error(f"Error getting tools: {e}")
|
|
272
|
+
return []
|
|
273
|
+
|
|
274
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
|
|
275
|
+
timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
276
|
+
"""Call tool with enhanced performance tracking and error handling."""
|
|
277
|
+
if not self._initialized:
|
|
278
|
+
return {
|
|
279
|
+
"isError": True,
|
|
280
|
+
"error": "Transport not initialized"
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
tool_timeout = timeout or self.default_timeout
|
|
284
|
+
start_time = time.time()
|
|
285
|
+
|
|
286
|
+
if self.enable_metrics:
|
|
287
|
+
self._metrics["total_calls"] += 1
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
logger.debug(f"Calling tool '{tool_name}' with timeout {tool_timeout}s")
|
|
291
|
+
|
|
292
|
+
raw_response = await asyncio.wait_for(
|
|
293
|
+
send_tools_call(
|
|
294
|
+
self._read_stream,
|
|
295
|
+
self._write_stream,
|
|
296
|
+
tool_name,
|
|
297
|
+
arguments
|
|
298
|
+
),
|
|
299
|
+
timeout=tool_timeout
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
response_time = time.time() - start_time
|
|
303
|
+
result = self._normalize_tool_response(raw_response)
|
|
304
|
+
|
|
305
|
+
if self.enable_metrics:
|
|
306
|
+
self._update_metrics(response_time, not result.get("isError", False))
|
|
307
|
+
|
|
308
|
+
if not result.get("isError", False):
|
|
309
|
+
logger.debug(f"Tool '{tool_name}' completed successfully in {response_time:.3f}s")
|
|
310
|
+
else:
|
|
311
|
+
logger.warning(f"Tool '{tool_name}' failed in {response_time:.3f}s: {result.get('error', 'Unknown error')}")
|
|
312
|
+
|
|
313
|
+
return result
|
|
314
|
+
|
|
315
|
+
except asyncio.TimeoutError:
|
|
316
|
+
response_time = time.time() - start_time
|
|
317
|
+
if self.enable_metrics:
|
|
318
|
+
self._update_metrics(response_time, False)
|
|
319
|
+
|
|
320
|
+
error_msg = f"Tool execution timed out after {tool_timeout}s"
|
|
321
|
+
logger.error(f"Tool '{tool_name}' {error_msg}")
|
|
322
|
+
return {
|
|
323
|
+
"isError": True,
|
|
324
|
+
"error": error_msg
|
|
325
|
+
}
|
|
326
|
+
except Exception as e:
|
|
327
|
+
response_time = time.time() - start_time
|
|
328
|
+
if self.enable_metrics:
|
|
329
|
+
self._update_metrics(response_time, False)
|
|
330
|
+
|
|
331
|
+
error_msg = f"Tool execution failed: {str(e)}"
|
|
332
|
+
logger.error(f"Tool '{tool_name}' error: {error_msg}")
|
|
333
|
+
return {
|
|
334
|
+
"isError": True,
|
|
335
|
+
"error": error_msg
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
def _update_metrics(self, response_time: float, success: bool) -> None:
|
|
339
|
+
"""Update performance metrics."""
|
|
340
|
+
if success:
|
|
341
|
+
self._metrics["successful_calls"] += 1
|
|
342
|
+
else:
|
|
343
|
+
self._metrics["failed_calls"] += 1
|
|
344
|
+
|
|
345
|
+
self._metrics["total_time"] += response_time
|
|
346
|
+
self._metrics["avg_response_time"] = (
|
|
347
|
+
self._metrics["total_time"] / self._metrics["total_calls"]
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
async def list_resources(self) -> Dict[str, Any]:
|
|
351
|
+
"""List resources using latest chuk-mcp."""
|
|
352
|
+
if not self._initialized:
|
|
353
|
+
return {}
|
|
354
|
+
|
|
355
|
+
try:
|
|
356
|
+
response = await asyncio.wait_for(
|
|
357
|
+
send_resources_list(self._read_stream, self._write_stream),
|
|
358
|
+
timeout=self.default_timeout
|
|
359
|
+
)
|
|
360
|
+
return response if isinstance(response, dict) else {}
|
|
361
|
+
except asyncio.TimeoutError:
|
|
362
|
+
logger.error("List resources timed out")
|
|
363
|
+
return {}
|
|
364
|
+
except Exception as e:
|
|
365
|
+
logger.debug(f"Error listing resources: {e}")
|
|
366
|
+
return {}
|
|
367
|
+
|
|
368
|
+
async def list_prompts(self) -> Dict[str, Any]:
|
|
369
|
+
"""List prompts using latest chuk-mcp."""
|
|
370
|
+
if not self._initialized:
|
|
371
|
+
return {}
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
response = await asyncio.wait_for(
|
|
375
|
+
send_prompts_list(self._read_stream, self._write_stream),
|
|
376
|
+
timeout=self.default_timeout
|
|
377
|
+
)
|
|
378
|
+
return response if isinstance(response, dict) else {}
|
|
379
|
+
except asyncio.TimeoutError:
|
|
380
|
+
logger.error("List prompts timed out")
|
|
381
|
+
return {}
|
|
382
|
+
except Exception as e:
|
|
383
|
+
logger.debug(f"Error listing prompts: {e}")
|
|
384
|
+
return {}
|
|
385
|
+
|
|
386
|
+
def _normalize_tool_response(self, raw_response: Dict[str, Any]) -> Dict[str, Any]:
|
|
387
|
+
"""Normalize response for backward compatibility."""
|
|
388
|
+
# Handle explicit error in response
|
|
389
|
+
if "error" in raw_response:
|
|
390
|
+
error_info = raw_response["error"]
|
|
391
|
+
if isinstance(error_info, dict):
|
|
392
|
+
error_msg = error_info.get("message", "Unknown error")
|
|
393
|
+
else:
|
|
394
|
+
error_msg = str(error_info)
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
"isError": True,
|
|
398
|
+
"error": error_msg
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
# Handle successful response with result
|
|
402
|
+
if "result" in raw_response:
|
|
403
|
+
result = raw_response["result"]
|
|
404
|
+
|
|
405
|
+
if isinstance(result, dict) and "content" in result:
|
|
406
|
+
return {
|
|
407
|
+
"isError": False,
|
|
408
|
+
"content": self._extract_content(result["content"])
|
|
409
|
+
}
|
|
410
|
+
else:
|
|
411
|
+
return {
|
|
412
|
+
"isError": False,
|
|
413
|
+
"content": result
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
# Handle direct content-based response
|
|
417
|
+
if "content" in raw_response:
|
|
418
|
+
return {
|
|
419
|
+
"isError": False,
|
|
420
|
+
"content": self._extract_content(raw_response["content"])
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
# Fallback
|
|
424
|
+
return {
|
|
425
|
+
"isError": False,
|
|
426
|
+
"content": raw_response
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
def _extract_content(self, content_list: Any) -> Any:
|
|
430
|
+
"""Extract content from MCP content format with enhanced error handling."""
|
|
431
|
+
if not isinstance(content_list, list) or not content_list:
|
|
432
|
+
return content_list
|
|
433
|
+
|
|
434
|
+
# Handle single content item
|
|
435
|
+
if len(content_list) == 1:
|
|
436
|
+
content_item = content_list[0]
|
|
437
|
+
if isinstance(content_item, dict):
|
|
438
|
+
if content_item.get("type") == "text":
|
|
439
|
+
text_content = content_item.get("text", "")
|
|
440
|
+
# Try to parse JSON, fall back to plain text
|
|
441
|
+
try:
|
|
442
|
+
return json.loads(text_content)
|
|
443
|
+
except json.JSONDecodeError:
|
|
444
|
+
return text_content
|
|
445
|
+
else:
|
|
446
|
+
return content_item
|
|
447
|
+
|
|
448
|
+
# Multiple content items
|
|
449
|
+
return content_list
|
|
450
|
+
|
|
451
|
+
def get_streams(self) -> List[tuple]:
|
|
452
|
+
"""Provide streams for backward compatibility."""
|
|
453
|
+
if self._initialized and self._read_stream and self._write_stream:
|
|
454
|
+
return [(self._read_stream, self._write_stream)]
|
|
455
|
+
return []
|
|
456
|
+
|
|
457
|
+
def is_connected(self) -> bool:
|
|
458
|
+
"""Check connection status."""
|
|
459
|
+
return self._initialized and self._read_stream is not None and self._write_stream is not None
|
|
460
|
+
|
|
461
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
462
|
+
"""Get performance metrics."""
|
|
463
|
+
return self._metrics.copy()
|
|
464
|
+
|
|
465
|
+
def reset_metrics(self) -> None:
|
|
466
|
+
"""Reset performance metrics."""
|
|
467
|
+
self._metrics = {
|
|
468
|
+
"total_calls": 0,
|
|
469
|
+
"successful_calls": 0,
|
|
470
|
+
"failed_calls": 0,
|
|
471
|
+
"total_time": 0.0,
|
|
472
|
+
"avg_response_time": 0.0,
|
|
473
|
+
"last_ping_time": self._metrics.get("last_ping_time"),
|
|
474
|
+
"initialization_time": self._metrics.get("initialization_time")
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
async def __aenter__(self):
|
|
478
|
+
"""Context manager support."""
|
|
479
|
+
success = await self.initialize()
|
|
480
|
+
if not success:
|
|
481
|
+
raise RuntimeError("Failed to initialize HTTP Streamable transport")
|
|
482
|
+
return self
|
|
483
|
+
|
|
484
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
485
|
+
"""Context manager cleanup."""
|
|
486
|
+
await self.close()
|
|
487
|
+
|
|
488
|
+
def __repr__(self) -> str:
|
|
489
|
+
"""Enhanced string representation for debugging."""
|
|
490
|
+
status = "initialized" if self._initialized else "not initialized"
|
|
491
|
+
metrics_info = ""
|
|
492
|
+
if self.enable_metrics and self._metrics["total_calls"] > 0:
|
|
493
|
+
success_rate = (self._metrics["successful_calls"] / self._metrics["total_calls"]) * 100
|
|
494
|
+
metrics_info = f", calls: {self._metrics['total_calls']}, success: {success_rate:.1f}%"
|
|
495
|
+
|
|
496
|
+
return f"HTTPStreamableTransport(status={status}, url={self.url}{metrics_info})"
|