chuk-tool-processor 0.6.6__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.
- chuk_tool_processor/mcp/stream_manager.py +67 -29
- chuk_tool_processor/mcp/transport/__init__.py +10 -27
- chuk_tool_processor/mcp/transport/base_transport.py +197 -46
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +134 -173
- chuk_tool_processor/mcp/transport/sse_transport.py +238 -119
- chuk_tool_processor/mcp/transport/stdio_transport.py +297 -81
- {chuk_tool_processor-0.6.6.dist-info → chuk_tool_processor-0.6.9.dist-info}/METADATA +1 -1
- {chuk_tool_processor-0.6.6.dist-info → chuk_tool_processor-0.6.9.dist-info}/RECORD +10 -10
- {chuk_tool_processor-0.6.6.dist-info → chuk_tool_processor-0.6.9.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.6.6.dist-info → chuk_tool_processor-0.6.9.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# chuk_tool_processor/mcp/stream_manager.py
|
|
2
2
|
"""
|
|
3
|
-
StreamManager for CHUK Tool Processor - Enhanced with robust shutdown handling
|
|
3
|
+
StreamManager for CHUK Tool Processor - Enhanced with robust shutdown handling and headers support
|
|
4
4
|
"""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
@@ -27,12 +27,12 @@ class StreamManager:
|
|
|
27
27
|
"""
|
|
28
28
|
Manager for MCP server streams with support for multiple transport types.
|
|
29
29
|
|
|
30
|
-
Enhanced with robust shutdown handling
|
|
30
|
+
Enhanced with robust shutdown handling and proper headers support.
|
|
31
31
|
|
|
32
32
|
Updated to support the latest transports:
|
|
33
33
|
- STDIO (process-based)
|
|
34
|
-
- SSE (Server-Sent Events)
|
|
35
|
-
- HTTP Streamable (modern replacement for SSE, spec 2025-03-26)
|
|
34
|
+
- SSE (Server-Sent Events) with headers support
|
|
35
|
+
- HTTP Streamable (modern replacement for SSE, spec 2025-03-26) with graceful headers handling
|
|
36
36
|
"""
|
|
37
37
|
|
|
38
38
|
def __init__(self) -> None:
|
|
@@ -175,6 +175,7 @@ class StreamManager:
|
|
|
175
175
|
transport_type: str = "stdio",
|
|
176
176
|
default_timeout: float = 30.0,
|
|
177
177
|
) -> None:
|
|
178
|
+
"""Initialize with graceful headers handling for all transport types."""
|
|
178
179
|
if self._closed:
|
|
179
180
|
raise RuntimeError("Cannot initialize a closed StreamManager")
|
|
180
181
|
|
|
@@ -193,16 +194,24 @@ class StreamManager:
|
|
|
193
194
|
if isinstance(params, dict) and 'url' in params:
|
|
194
195
|
sse_url = params['url']
|
|
195
196
|
api_key = params.get('api_key')
|
|
197
|
+
headers = params.get('headers', {})
|
|
196
198
|
else:
|
|
197
199
|
sse_url = "http://localhost:8000"
|
|
198
200
|
api_key = None
|
|
201
|
+
headers = {}
|
|
199
202
|
logger.warning("No URL configured for SSE transport, using default: %s", sse_url)
|
|
200
203
|
|
|
201
|
-
transport
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
204
|
+
# Build SSE transport with optional headers
|
|
205
|
+
transport_params = {
|
|
206
|
+
'url': sse_url,
|
|
207
|
+
'api_key': api_key,
|
|
208
|
+
'default_timeout': default_timeout
|
|
209
|
+
}
|
|
210
|
+
if headers:
|
|
211
|
+
transport_params['headers'] = headers
|
|
212
|
+
|
|
213
|
+
transport = SSETransport(**transport_params)
|
|
214
|
+
|
|
206
215
|
elif transport_type == "http_streamable":
|
|
207
216
|
logger.warning("Using HTTP Streamable transport in initialize() - consider using initialize_with_http_streamable() instead")
|
|
208
217
|
params = await load_config(config_file, server_name)
|
|
@@ -210,19 +219,28 @@ class StreamManager:
|
|
|
210
219
|
if isinstance(params, dict) and 'url' in params:
|
|
211
220
|
http_url = params['url']
|
|
212
221
|
api_key = params.get('api_key')
|
|
222
|
+
headers = params.get('headers', {})
|
|
213
223
|
session_id = params.get('session_id')
|
|
214
224
|
else:
|
|
215
225
|
http_url = "http://localhost:8000"
|
|
216
226
|
api_key = None
|
|
227
|
+
headers = {}
|
|
217
228
|
session_id = None
|
|
218
229
|
logger.warning("No URL configured for HTTP Streamable transport, using default: %s", http_url)
|
|
219
230
|
|
|
220
|
-
transport
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
231
|
+
# Build HTTP transport (headers not supported yet)
|
|
232
|
+
transport_params = {
|
|
233
|
+
'url': http_url,
|
|
234
|
+
'api_key': api_key,
|
|
235
|
+
'default_timeout': default_timeout,
|
|
236
|
+
'session_id': session_id
|
|
237
|
+
}
|
|
238
|
+
# Note: headers not added until HTTPStreamableTransport supports them
|
|
239
|
+
if headers:
|
|
240
|
+
logger.debug("Headers provided but not supported in HTTPStreamableTransport yet")
|
|
241
|
+
|
|
242
|
+
transport = HTTPStreamableTransport(**transport_params)
|
|
243
|
+
|
|
226
244
|
else:
|
|
227
245
|
logger.error("Unsupported transport type: %s", transport_type)
|
|
228
246
|
continue
|
|
@@ -271,6 +289,7 @@ class StreamManager:
|
|
|
271
289
|
connection_timeout: float = 10.0,
|
|
272
290
|
default_timeout: float = 30.0,
|
|
273
291
|
) -> None:
|
|
292
|
+
"""Initialize with SSE transport with optional headers support."""
|
|
274
293
|
if self._closed:
|
|
275
294
|
raise RuntimeError("Cannot initialize a closed StreamManager")
|
|
276
295
|
|
|
@@ -283,12 +302,21 @@ class StreamManager:
|
|
|
283
302
|
logger.error("Bad server config: %s", cfg)
|
|
284
303
|
continue
|
|
285
304
|
try:
|
|
286
|
-
transport
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
305
|
+
# Build SSE transport parameters with optional headers
|
|
306
|
+
transport_params = {
|
|
307
|
+
'url': url,
|
|
308
|
+
'api_key': cfg.get("api_key"),
|
|
309
|
+
'connection_timeout': connection_timeout,
|
|
310
|
+
'default_timeout': default_timeout
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Add headers if provided
|
|
314
|
+
headers = cfg.get("headers", {})
|
|
315
|
+
if headers:
|
|
316
|
+
logger.debug("SSE %s: Using configured headers: %s", name, list(headers.keys()))
|
|
317
|
+
transport_params['headers'] = headers
|
|
318
|
+
|
|
319
|
+
transport = SSETransport(**transport_params)
|
|
292
320
|
|
|
293
321
|
if not await asyncio.wait_for(transport.initialize(), timeout=connection_timeout):
|
|
294
322
|
logger.error("Failed to init SSE %s", name)
|
|
@@ -326,7 +354,7 @@ class StreamManager:
|
|
|
326
354
|
connection_timeout: float = 30.0,
|
|
327
355
|
default_timeout: float = 30.0,
|
|
328
356
|
) -> None:
|
|
329
|
-
"""Initialize with HTTP Streamable transport
|
|
357
|
+
"""Initialize with HTTP Streamable transport with graceful headers handling."""
|
|
330
358
|
if self._closed:
|
|
331
359
|
raise RuntimeError("Cannot initialize a closed StreamManager")
|
|
332
360
|
|
|
@@ -339,13 +367,23 @@ class StreamManager:
|
|
|
339
367
|
logger.error("Bad server config: %s", cfg)
|
|
340
368
|
continue
|
|
341
369
|
try:
|
|
342
|
-
transport
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
370
|
+
# Build HTTP Streamable transport parameters
|
|
371
|
+
transport_params = {
|
|
372
|
+
'url': url,
|
|
373
|
+
'api_key': cfg.get("api_key"),
|
|
374
|
+
'connection_timeout': connection_timeout,
|
|
375
|
+
'default_timeout': default_timeout,
|
|
376
|
+
'session_id': cfg.get("session_id")
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
# Handle headers if provided (for future HTTPStreamableTransport support)
|
|
380
|
+
headers = cfg.get("headers", {})
|
|
381
|
+
if headers:
|
|
382
|
+
logger.debug("HTTP Streamable %s: Headers provided but not yet supported in transport", name)
|
|
383
|
+
# TODO: Add headers support when HTTPStreamableTransport is updated
|
|
384
|
+
# transport_params['headers'] = headers
|
|
385
|
+
|
|
386
|
+
transport = HTTPStreamableTransport(**transport_params)
|
|
349
387
|
|
|
350
388
|
if not await asyncio.wait_for(transport.initialize(), timeout=connection_timeout):
|
|
351
389
|
logger.error("Failed to init HTTP Streamable %s", name)
|
|
@@ -1,39 +1,22 @@
|
|
|
1
1
|
# chuk_tool_processor/mcp/transport/__init__.py
|
|
2
2
|
"""
|
|
3
|
-
MCP Transport module providing
|
|
3
|
+
MCP Transport module providing consistent transport implementations.
|
|
4
|
+
|
|
5
|
+
All transports now follow the same interface and provide consistent behavior:
|
|
6
|
+
- Standardized initialization and cleanup
|
|
7
|
+
- Unified metrics and monitoring
|
|
8
|
+
- Consistent error handling and timeouts
|
|
9
|
+
- Shared response normalization
|
|
4
10
|
"""
|
|
5
11
|
|
|
6
12
|
from .base_transport import MCPBaseTransport
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
from .stdio_transport import StdioTransport
|
|
11
|
-
HAS_STDIO_TRANSPORT = True
|
|
12
|
-
except ImportError:
|
|
13
|
-
StdioTransport = None
|
|
14
|
-
HAS_STDIO_TRANSPORT = False
|
|
15
|
-
|
|
16
|
-
# Conditionally available transports
|
|
17
|
-
try:
|
|
18
|
-
from .sse_transport import SSETransport
|
|
19
|
-
HAS_SSE_TRANSPORT = True
|
|
20
|
-
except ImportError:
|
|
21
|
-
SSETransport = None
|
|
22
|
-
HAS_SSE_TRANSPORT = False
|
|
23
|
-
|
|
24
|
-
try:
|
|
25
|
-
from .http_streamable_transport import HTTPStreamableTransport
|
|
26
|
-
HAS_HTTP_STREAMABLE_TRANSPORT = True
|
|
27
|
-
except ImportError:
|
|
28
|
-
HTTPStreamableTransport = None
|
|
29
|
-
HAS_HTTP_STREAMABLE_TRANSPORT = False
|
|
13
|
+
from .stdio_transport import StdioTransport
|
|
14
|
+
from .sse_transport import SSETransport
|
|
15
|
+
from .http_streamable_transport import HTTPStreamableTransport
|
|
30
16
|
|
|
31
17
|
__all__ = [
|
|
32
18
|
"MCPBaseTransport",
|
|
33
19
|
"StdioTransport",
|
|
34
20
|
"SSETransport",
|
|
35
21
|
"HTTPStreamableTransport",
|
|
36
|
-
"HAS_STDIO_TRANSPORT",
|
|
37
|
-
"HAS_SSE_TRANSPORT",
|
|
38
|
-
"HAS_HTTP_STREAMABLE_TRANSPORT"
|
|
39
22
|
]
|
|
@@ -1,103 +1,254 @@
|
|
|
1
1
|
# chuk_tool_processor/mcp/transport/base_transport.py
|
|
2
2
|
"""
|
|
3
|
-
Abstract
|
|
3
|
+
Abstract base class for MCP transports with complete interface definition.
|
|
4
4
|
"""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
|
-
from typing import Any, Dict, List
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class MCPBaseTransport(ABC):
|
|
12
12
|
"""
|
|
13
|
-
Abstract base class for MCP transport
|
|
13
|
+
Abstract base class for all MCP transport implementations.
|
|
14
|
+
|
|
15
|
+
Defines the complete interface that all transports must implement
|
|
16
|
+
for consistency across stdio, SSE, and HTTP streamable transports.
|
|
14
17
|
"""
|
|
15
18
|
|
|
16
19
|
# ------------------------------------------------------------------ #
|
|
17
|
-
# connection lifecycle
|
|
20
|
+
# Core connection lifecycle #
|
|
18
21
|
# ------------------------------------------------------------------ #
|
|
19
22
|
@abstractmethod
|
|
20
23
|
async def initialize(self) -> bool:
|
|
21
24
|
"""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
Returns
|
|
25
|
-
|
|
26
|
-
bool
|
|
27
|
-
``True`` if the connection was initialised successfully.
|
|
25
|
+
Initialize the transport connection.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
True if initialization was successful, False otherwise.
|
|
28
29
|
"""
|
|
29
|
-
|
|
30
|
+
pass
|
|
30
31
|
|
|
31
32
|
@abstractmethod
|
|
32
33
|
async def close(self) -> None:
|
|
33
|
-
"""
|
|
34
|
-
|
|
34
|
+
"""Close the transport connection and clean up all resources."""
|
|
35
|
+
pass
|
|
35
36
|
|
|
36
37
|
# ------------------------------------------------------------------ #
|
|
37
|
-
# diagnostics
|
|
38
|
+
# Health and diagnostics #
|
|
38
39
|
# ------------------------------------------------------------------ #
|
|
39
40
|
@abstractmethod
|
|
40
41
|
async def send_ping(self) -> bool:
|
|
41
42
|
"""
|
|
42
|
-
Send a
|
|
43
|
+
Send a ping to verify the connection is alive.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
True if ping was successful, False otherwise.
|
|
47
|
+
"""
|
|
48
|
+
pass
|
|
43
49
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
@abstractmethod
|
|
51
|
+
def is_connected(self) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Check if the transport is connected and ready for operations.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
True if connected, False otherwise.
|
|
48
57
|
"""
|
|
49
|
-
|
|
58
|
+
pass
|
|
50
59
|
|
|
51
60
|
# ------------------------------------------------------------------ #
|
|
52
|
-
#
|
|
61
|
+
# Core MCP operations #
|
|
53
62
|
# ------------------------------------------------------------------ #
|
|
54
63
|
@abstractmethod
|
|
55
64
|
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
56
65
|
"""
|
|
57
|
-
|
|
66
|
+
Get the list of available tools from the server.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of tool definitions.
|
|
58
70
|
"""
|
|
59
|
-
|
|
71
|
+
pass
|
|
60
72
|
|
|
61
73
|
@abstractmethod
|
|
62
|
-
async def call_tool(
|
|
63
|
-
|
|
64
|
-
) -> Dict[str, Any]:
|
|
74
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
|
|
75
|
+
timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
65
76
|
"""
|
|
66
|
-
|
|
77
|
+
Call a tool with the given arguments.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
tool_name: Name of the tool to call
|
|
81
|
+
arguments: Arguments to pass to the tool
|
|
82
|
+
timeout: Optional timeout for the operation
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Dictionary with 'isError' boolean and either 'content' or 'error'
|
|
67
86
|
"""
|
|
68
|
-
|
|
87
|
+
pass
|
|
69
88
|
|
|
70
|
-
# ------------------------------------------------------------------ #
|
|
71
|
-
# new: resources & prompts #
|
|
72
|
-
# ------------------------------------------------------------------ #
|
|
73
89
|
@abstractmethod
|
|
74
90
|
async def list_resources(self) -> Dict[str, Any]:
|
|
75
91
|
"""
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
List available resources from the server.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Dictionary containing resources list or empty dict if not supported.
|
|
80
96
|
"""
|
|
81
|
-
|
|
97
|
+
pass
|
|
82
98
|
|
|
83
99
|
@abstractmethod
|
|
84
100
|
async def list_prompts(self) -> Dict[str, Any]:
|
|
85
101
|
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
List available prompts from the server.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Dictionary containing prompts list or empty dict if not supported.
|
|
90
106
|
"""
|
|
91
|
-
|
|
107
|
+
pass
|
|
92
108
|
|
|
93
109
|
# ------------------------------------------------------------------ #
|
|
94
|
-
#
|
|
110
|
+
# Metrics and monitoring (all transports should support these) #
|
|
95
111
|
# ------------------------------------------------------------------ #
|
|
96
|
-
|
|
112
|
+
@abstractmethod
|
|
113
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
114
|
+
"""
|
|
115
|
+
Get performance and connection metrics.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
Dictionary containing metrics data.
|
|
97
119
|
"""
|
|
98
|
-
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
@abstractmethod
|
|
123
|
+
def reset_metrics(self) -> None:
|
|
124
|
+
"""Reset performance metrics to initial state."""
|
|
125
|
+
pass
|
|
99
126
|
|
|
100
|
-
|
|
101
|
-
|
|
127
|
+
# ------------------------------------------------------------------ #
|
|
128
|
+
# Backward compatibility and utility methods #
|
|
129
|
+
# ------------------------------------------------------------------ #
|
|
130
|
+
def get_streams(self) -> List[tuple]:
|
|
131
|
+
"""
|
|
132
|
+
Get underlying stream objects for backward compatibility.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of (read_stream, write_stream) tuples, empty if not applicable.
|
|
102
136
|
"""
|
|
103
137
|
return []
|
|
138
|
+
|
|
139
|
+
# ------------------------------------------------------------------ #
|
|
140
|
+
# Context manager support (all transports should support this) #
|
|
141
|
+
# ------------------------------------------------------------------ #
|
|
142
|
+
async def __aenter__(self):
|
|
143
|
+
"""Context manager entry."""
|
|
144
|
+
success = await self.initialize()
|
|
145
|
+
if not success:
|
|
146
|
+
raise RuntimeError(f"Failed to initialize {self.__class__.__name__}")
|
|
147
|
+
return self
|
|
148
|
+
|
|
149
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
150
|
+
"""Context manager cleanup."""
|
|
151
|
+
await self.close()
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------ #
|
|
154
|
+
# Shared helper methods for response normalization #
|
|
155
|
+
# ------------------------------------------------------------------ #
|
|
156
|
+
def _normalize_mcp_response(self, response: Dict[str, Any]) -> Dict[str, Any]:
|
|
157
|
+
"""
|
|
158
|
+
Normalize MCP response to consistent format.
|
|
159
|
+
|
|
160
|
+
This provides shared logic for all transports to ensure consistent
|
|
161
|
+
response format regardless of transport type.
|
|
162
|
+
"""
|
|
163
|
+
# Handle explicit error in response
|
|
164
|
+
if "error" in response:
|
|
165
|
+
error_info = response["error"]
|
|
166
|
+
if isinstance(error_info, dict):
|
|
167
|
+
error_msg = error_info.get("message", "Unknown error")
|
|
168
|
+
else:
|
|
169
|
+
error_msg = str(error_info)
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
"isError": True,
|
|
173
|
+
"error": error_msg
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
# Handle successful response with result
|
|
177
|
+
if "result" in response:
|
|
178
|
+
result = response["result"]
|
|
179
|
+
|
|
180
|
+
if isinstance(result, dict) and "content" in result:
|
|
181
|
+
return {
|
|
182
|
+
"isError": False,
|
|
183
|
+
"content": self._extract_mcp_content(result["content"])
|
|
184
|
+
}
|
|
185
|
+
else:
|
|
186
|
+
return {
|
|
187
|
+
"isError": False,
|
|
188
|
+
"content": result
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
# Handle direct content-based response
|
|
192
|
+
if "content" in response:
|
|
193
|
+
return {
|
|
194
|
+
"isError": False,
|
|
195
|
+
"content": self._extract_mcp_content(response["content"])
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
# Fallback
|
|
199
|
+
return {
|
|
200
|
+
"isError": False,
|
|
201
|
+
"content": response
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
def _extract_mcp_content(self, content_list: Any) -> Any:
|
|
205
|
+
"""
|
|
206
|
+
Extract content from MCP content format.
|
|
207
|
+
|
|
208
|
+
Handles the standard MCP content format where content is a list
|
|
209
|
+
of content items with type and data.
|
|
210
|
+
"""
|
|
211
|
+
if not isinstance(content_list, list) or not content_list:
|
|
212
|
+
return content_list
|
|
213
|
+
|
|
214
|
+
# Handle single content item
|
|
215
|
+
if len(content_list) == 1:
|
|
216
|
+
content_item = content_list[0]
|
|
217
|
+
if isinstance(content_item, dict):
|
|
218
|
+
if content_item.get("type") == "text":
|
|
219
|
+
text_content = content_item.get("text", "")
|
|
220
|
+
# Try to parse JSON, fall back to plain text
|
|
221
|
+
try:
|
|
222
|
+
import json
|
|
223
|
+
return json.loads(text_content)
|
|
224
|
+
except json.JSONDecodeError:
|
|
225
|
+
return text_content
|
|
226
|
+
else:
|
|
227
|
+
return content_item
|
|
228
|
+
|
|
229
|
+
# Multiple content items - return as-is
|
|
230
|
+
return content_list
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------ #
|
|
233
|
+
# Standard string representation #
|
|
234
|
+
# ------------------------------------------------------------------ #
|
|
235
|
+
def __repr__(self) -> str:
|
|
236
|
+
"""Standard string representation for all transports."""
|
|
237
|
+
status = "initialized" if getattr(self, '_initialized', False) else "not initialized"
|
|
238
|
+
|
|
239
|
+
# Add metrics info if available
|
|
240
|
+
metrics_info = ""
|
|
241
|
+
if hasattr(self, 'enable_metrics') and getattr(self, 'enable_metrics', False):
|
|
242
|
+
metrics = self.get_metrics()
|
|
243
|
+
if metrics.get("total_calls", 0) > 0:
|
|
244
|
+
success_rate = (metrics.get("successful_calls", 0) / metrics["total_calls"]) * 100
|
|
245
|
+
metrics_info = f", calls: {metrics['total_calls']}, success: {success_rate:.1f}%"
|
|
246
|
+
|
|
247
|
+
# Add transport-specific info - FIXED FORMAT
|
|
248
|
+
transport_info = ""
|
|
249
|
+
if hasattr(self, 'url'):
|
|
250
|
+
transport_info = f", url={self.url}" # Fixed: was "url: "
|
|
251
|
+
elif hasattr(self, 'server_params') and hasattr(self.server_params, 'command'):
|
|
252
|
+
transport_info = f", command={self.server_params.command}" # Fixed: was "command: "
|
|
253
|
+
|
|
254
|
+
return f"{self.__class__.__name__}(status={status}{transport_info}{metrics_info})"
|