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