chuk-tool-processor 0.6.2__py3-none-any.whl → 0.6.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/transport/sse_transport.py +308 -246
- {chuk_tool_processor-0.6.2.dist-info → chuk_tool_processor-0.6.4.dist-info}/METADATA +2 -2
- {chuk_tool_processor-0.6.2.dist-info → chuk_tool_processor-0.6.4.dist-info}/RECORD +5 -5
- {chuk_tool_processor-0.6.2.dist-info → chuk_tool_processor-0.6.4.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.6.2.dist-info → chuk_tool_processor-0.6.4.dist-info}/top_level.txt +0 -0
|
@@ -1,364 +1,426 @@
|
|
|
1
1
|
# chuk_tool_processor/mcp/transport/sse_transport.py
|
|
2
|
+
"""
|
|
3
|
+
Fixed SSE transport that matches your server's actual behavior.
|
|
4
|
+
Based on your working debug script.
|
|
5
|
+
"""
|
|
2
6
|
from __future__ import annotations
|
|
3
7
|
|
|
4
8
|
import asyncio
|
|
5
9
|
import json
|
|
6
|
-
|
|
10
|
+
import uuid
|
|
11
|
+
from typing import Dict, Any, List, Optional, Tuple
|
|
7
12
|
import logging
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
# Import latest chuk-mcp SSE transport
|
|
12
|
-
try:
|
|
13
|
-
from chuk_mcp.transports.sse import sse_client
|
|
14
|
-
from chuk_mcp.transports.sse.parameters import SSEParameters
|
|
15
|
-
from chuk_mcp.protocol.messages import (
|
|
16
|
-
send_initialize,
|
|
17
|
-
send_ping,
|
|
18
|
-
send_tools_list,
|
|
19
|
-
send_tools_call,
|
|
20
|
-
)
|
|
21
|
-
HAS_SSE_SUPPORT = True
|
|
22
|
-
except ImportError:
|
|
23
|
-
HAS_SSE_SUPPORT = False
|
|
14
|
+
import httpx
|
|
24
15
|
|
|
25
|
-
|
|
26
|
-
try:
|
|
27
|
-
from chuk_mcp.protocol.messages import (
|
|
28
|
-
send_resources_list,
|
|
29
|
-
send_resources_read,
|
|
30
|
-
send_prompts_list,
|
|
31
|
-
send_prompts_get,
|
|
32
|
-
)
|
|
33
|
-
HAS_RESOURCES_PROMPTS = True
|
|
34
|
-
except ImportError:
|
|
35
|
-
HAS_RESOURCES_PROMPTS = False
|
|
16
|
+
from .base_transport import MCPBaseTransport
|
|
36
17
|
|
|
37
18
|
logger = logging.getLogger(__name__)
|
|
38
19
|
|
|
39
20
|
|
|
40
21
|
class SSETransport(MCPBaseTransport):
|
|
41
22
|
"""
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
23
|
+
SSE transport that works with your server's two-step async pattern:
|
|
24
|
+
1. POST messages to /messages endpoint
|
|
25
|
+
2. Receive responses via SSE stream
|
|
45
26
|
"""
|
|
46
27
|
|
|
47
28
|
def __init__(self, url: str, api_key: Optional[str] = None,
|
|
48
29
|
connection_timeout: float = 30.0, default_timeout: float = 30.0):
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Args:
|
|
53
|
-
url: SSE server URL
|
|
54
|
-
api_key: Optional API key for authentication
|
|
55
|
-
connection_timeout: Timeout for initial connection
|
|
56
|
-
default_timeout: Default timeout for operations
|
|
57
|
-
"""
|
|
58
|
-
self.url = url
|
|
30
|
+
"""Initialize SSE transport."""
|
|
31
|
+
self.url = url.rstrip('/')
|
|
59
32
|
self.api_key = api_key
|
|
60
33
|
self.connection_timeout = connection_timeout
|
|
61
34
|
self.default_timeout = default_timeout
|
|
62
35
|
|
|
63
|
-
# State
|
|
64
|
-
self.
|
|
65
|
-
self.
|
|
66
|
-
self.
|
|
36
|
+
# State
|
|
37
|
+
self.session_id = None
|
|
38
|
+
self.message_url = None
|
|
39
|
+
self.pending_requests: Dict[str, asyncio.Future] = {}
|
|
67
40
|
self._initialized = False
|
|
68
41
|
|
|
69
|
-
|
|
70
|
-
|
|
42
|
+
# HTTP clients
|
|
43
|
+
self.stream_client = None
|
|
44
|
+
self.send_client = None
|
|
45
|
+
|
|
46
|
+
# SSE stream
|
|
47
|
+
self.sse_task = None
|
|
48
|
+
self.sse_response = None
|
|
49
|
+
self.sse_stream_context = None
|
|
50
|
+
|
|
51
|
+
def _get_headers(self) -> Dict[str, str]:
|
|
52
|
+
"""Get headers with auth if available."""
|
|
53
|
+
headers = {}
|
|
54
|
+
if self.api_key:
|
|
55
|
+
headers['Authorization'] = f'Bearer {self.api_key}'
|
|
56
|
+
return headers
|
|
71
57
|
|
|
72
58
|
async def initialize(self) -> bool:
|
|
73
|
-
"""Initialize
|
|
74
|
-
if not HAS_SSE_SUPPORT:
|
|
75
|
-
logger.error("SSE transport not available in chuk-mcp")
|
|
76
|
-
return False
|
|
77
|
-
|
|
59
|
+
"""Initialize SSE connection and MCP handshake."""
|
|
78
60
|
if self._initialized:
|
|
79
61
|
logger.warning("Transport already initialized")
|
|
80
62
|
return True
|
|
81
|
-
|
|
63
|
+
|
|
82
64
|
try:
|
|
83
65
|
logger.info("Initializing SSE transport...")
|
|
84
66
|
|
|
85
|
-
# Create
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
timeout=self.connection_timeout,
|
|
89
|
-
auto_reconnect=True,
|
|
90
|
-
max_reconnect_attempts=3
|
|
91
|
-
)
|
|
67
|
+
# Create HTTP clients
|
|
68
|
+
self.stream_client = httpx.AsyncClient(timeout=self.connection_timeout)
|
|
69
|
+
self.send_client = httpx.AsyncClient(timeout=self.default_timeout)
|
|
92
70
|
|
|
93
|
-
#
|
|
94
|
-
|
|
71
|
+
# Connect to SSE stream
|
|
72
|
+
sse_url = f"{self.url}/sse"
|
|
73
|
+
logger.debug(f"Connecting to SSE: {sse_url}")
|
|
95
74
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
self._read_stream, self._write_stream = await asyncio.wait_for(
|
|
99
|
-
self._sse_context.__aenter__(),
|
|
100
|
-
timeout=self.connection_timeout
|
|
75
|
+
self.sse_stream_context = self.stream_client.stream(
|
|
76
|
+
'GET', sse_url, headers=self._get_headers()
|
|
101
77
|
)
|
|
78
|
+
self.sse_response = await self.sse_stream_context.__aenter__()
|
|
102
79
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
80
|
+
if self.sse_response.status_code != 200:
|
|
81
|
+
logger.error(f"SSE connection failed: {self.sse_response.status_code}")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
logger.info("SSE streaming connection established")
|
|
85
|
+
|
|
86
|
+
# Start SSE processing task
|
|
87
|
+
self.sse_task = asyncio.create_task(self._process_sse_stream())
|
|
88
|
+
|
|
89
|
+
# Wait for session discovery
|
|
90
|
+
logger.debug("Waiting for session discovery...")
|
|
91
|
+
for i in range(50): # 5 seconds max
|
|
92
|
+
if self.message_url:
|
|
93
|
+
break
|
|
94
|
+
await asyncio.sleep(0.1)
|
|
110
95
|
|
|
111
|
-
if
|
|
96
|
+
if not self.message_url:
|
|
97
|
+
logger.error("Failed to get session info from SSE")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
logger.info(f"Session ready: {self.session_id}")
|
|
101
|
+
|
|
102
|
+
# Now do MCP initialization
|
|
103
|
+
try:
|
|
104
|
+
init_response = await self._send_request("initialize", {
|
|
105
|
+
"protocolVersion": "2024-11-05",
|
|
106
|
+
"capabilities": {},
|
|
107
|
+
"clientInfo": {
|
|
108
|
+
"name": "chuk-tool-processor",
|
|
109
|
+
"version": "1.0.0"
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
if 'error' in init_response:
|
|
114
|
+
logger.error(f"Initialize failed: {init_response['error']}")
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
# Send initialized notification
|
|
118
|
+
await self._send_notification("notifications/initialized")
|
|
119
|
+
|
|
112
120
|
self._initialized = True
|
|
113
121
|
logger.info("SSE transport initialized successfully")
|
|
114
122
|
return True
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
except asyncio.TimeoutError:
|
|
122
|
-
logger.error(f"SSE initialization timed out after {self.connection_timeout}s")
|
|
123
|
-
logger.error("This may indicate the server is not responding to MCP initialization")
|
|
124
|
-
await self._cleanup()
|
|
125
|
-
return False
|
|
123
|
+
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.error(f"MCP initialization failed: {e}")
|
|
126
|
+
return False
|
|
127
|
+
|
|
126
128
|
except Exception as e:
|
|
127
129
|
logger.error(f"Error initializing SSE transport: {e}", exc_info=True)
|
|
128
130
|
await self._cleanup()
|
|
129
131
|
return False
|
|
130
132
|
|
|
131
|
-
async def
|
|
132
|
-
"""
|
|
133
|
-
if not self._initialized:
|
|
134
|
-
return
|
|
135
|
-
|
|
133
|
+
async def _process_sse_stream(self):
|
|
134
|
+
"""Process the persistent SSE stream."""
|
|
136
135
|
try:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
136
|
+
logger.debug("Starting SSE stream processing...")
|
|
137
|
+
|
|
138
|
+
async for line in self.sse_response.aiter_lines():
|
|
139
|
+
line = line.strip()
|
|
140
|
+
if not line:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
# Handle session endpoint discovery
|
|
144
|
+
if not self.message_url and line.startswith('data:') and '/messages/' in line:
|
|
145
|
+
endpoint_path = line.split(':', 1)[1].strip()
|
|
146
|
+
self.message_url = f"{self.url}{endpoint_path}"
|
|
147
|
+
|
|
148
|
+
if 'session_id=' in endpoint_path:
|
|
149
|
+
self.session_id = endpoint_path.split('session_id=')[1].split('&')[0]
|
|
150
|
+
|
|
151
|
+
logger.debug(f"Got session info: {self.session_id}")
|
|
152
|
+
continue
|
|
140
153
|
|
|
154
|
+
# Handle JSON-RPC responses
|
|
155
|
+
if line.startswith('data:'):
|
|
156
|
+
data_part = line.split(':', 1)[1].strip()
|
|
157
|
+
|
|
158
|
+
# Skip pings and empty data
|
|
159
|
+
if not data_part or data_part.startswith('ping'):
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
response_data = json.loads(data_part)
|
|
164
|
+
|
|
165
|
+
if 'jsonrpc' in response_data and 'id' in response_data:
|
|
166
|
+
request_id = str(response_data['id'])
|
|
167
|
+
|
|
168
|
+
# Resolve pending request
|
|
169
|
+
if request_id in self.pending_requests:
|
|
170
|
+
future = self.pending_requests.pop(request_id)
|
|
171
|
+
if not future.done():
|
|
172
|
+
future.set_result(response_data)
|
|
173
|
+
logger.debug(f"Resolved request: {request_id}")
|
|
174
|
+
|
|
175
|
+
except json.JSONDecodeError:
|
|
176
|
+
pass # Not JSON, ignore
|
|
177
|
+
|
|
141
178
|
except Exception as e:
|
|
142
|
-
logger.
|
|
143
|
-
finally:
|
|
144
|
-
await self._cleanup()
|
|
179
|
+
logger.error(f"SSE stream error: {e}")
|
|
145
180
|
|
|
146
|
-
async def
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
self.
|
|
150
|
-
|
|
151
|
-
|
|
181
|
+
async def _send_request(self, method: str, params: Dict[str, Any] = None,
|
|
182
|
+
timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
183
|
+
"""Send request and wait for async response."""
|
|
184
|
+
if not self.message_url:
|
|
185
|
+
raise RuntimeError("Not connected")
|
|
186
|
+
|
|
187
|
+
request_id = str(uuid.uuid4())
|
|
188
|
+
message = {
|
|
189
|
+
"jsonrpc": "2.0",
|
|
190
|
+
"id": request_id,
|
|
191
|
+
"method": method,
|
|
192
|
+
"params": params or {}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
# Create future for response
|
|
196
|
+
future = asyncio.Future()
|
|
197
|
+
self.pending_requests[request_id] = future
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
# Send message
|
|
201
|
+
headers = {
|
|
202
|
+
'Content-Type': 'application/json',
|
|
203
|
+
**self._get_headers()
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
response = await self.send_client.post(
|
|
207
|
+
self.message_url,
|
|
208
|
+
headers=headers,
|
|
209
|
+
json=message
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
if response.status_code == 202:
|
|
213
|
+
# Wait for async response
|
|
214
|
+
timeout = timeout or self.default_timeout
|
|
215
|
+
result = await asyncio.wait_for(future, timeout=timeout)
|
|
216
|
+
return result
|
|
217
|
+
elif response.status_code == 200:
|
|
218
|
+
# Immediate response
|
|
219
|
+
self.pending_requests.pop(request_id, None)
|
|
220
|
+
return response.json()
|
|
221
|
+
else:
|
|
222
|
+
self.pending_requests.pop(request_id, None)
|
|
223
|
+
raise RuntimeError(f"Request failed: {response.status_code}")
|
|
224
|
+
|
|
225
|
+
except asyncio.TimeoutError:
|
|
226
|
+
self.pending_requests.pop(request_id, None)
|
|
227
|
+
raise
|
|
228
|
+
except Exception:
|
|
229
|
+
self.pending_requests.pop(request_id, None)
|
|
230
|
+
raise
|
|
231
|
+
|
|
232
|
+
async def _send_notification(self, method: str, params: Dict[str, Any] = None):
|
|
233
|
+
"""Send notification (no response expected)."""
|
|
234
|
+
if not self.message_url:
|
|
235
|
+
raise RuntimeError("Not connected")
|
|
236
|
+
|
|
237
|
+
message = {
|
|
238
|
+
"jsonrpc": "2.0",
|
|
239
|
+
"method": method,
|
|
240
|
+
"params": params or {}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
headers = {
|
|
244
|
+
'Content-Type': 'application/json',
|
|
245
|
+
**self._get_headers()
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
await self.send_client.post(
|
|
249
|
+
self.message_url,
|
|
250
|
+
headers=headers,
|
|
251
|
+
json=message
|
|
252
|
+
)
|
|
152
253
|
|
|
153
254
|
async def send_ping(self) -> bool:
|
|
154
|
-
"""Send ping
|
|
255
|
+
"""Send ping to check connection."""
|
|
155
256
|
if not self._initialized:
|
|
156
|
-
logger.error("Cannot send ping: transport not initialized")
|
|
157
257
|
return False
|
|
158
258
|
|
|
159
259
|
try:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
logger.debug(f"Ping result: {result}")
|
|
165
|
-
return bool(result)
|
|
166
|
-
except asyncio.TimeoutError:
|
|
167
|
-
logger.error("Ping timed out")
|
|
168
|
-
return False
|
|
169
|
-
except Exception as e:
|
|
170
|
-
logger.error(f"Ping failed: {e}")
|
|
260
|
+
# Your server might not support ping, so we'll just check if we can list tools
|
|
261
|
+
response = await self._send_request("tools/list", {}, timeout=5.0)
|
|
262
|
+
return 'error' not in response
|
|
263
|
+
except Exception:
|
|
171
264
|
return False
|
|
172
265
|
|
|
173
266
|
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
174
|
-
"""Get tools list
|
|
267
|
+
"""Get tools list."""
|
|
175
268
|
if not self._initialized:
|
|
176
269
|
logger.error("Cannot get tools: transport not initialized")
|
|
177
270
|
return []
|
|
178
271
|
|
|
179
272
|
try:
|
|
180
|
-
|
|
181
|
-
send_tools_list(self._read_stream, self._write_stream),
|
|
182
|
-
timeout=self.default_timeout
|
|
183
|
-
)
|
|
273
|
+
response = await self._send_request("tools/list", {})
|
|
184
274
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
elif isinstance(tools_response, list):
|
|
189
|
-
tools = tools_response
|
|
190
|
-
else:
|
|
191
|
-
logger.warning(f"Unexpected tools response type: {type(tools_response)}")
|
|
192
|
-
tools = []
|
|
275
|
+
if 'error' in response:
|
|
276
|
+
logger.error(f"Error getting tools: {response['error']}")
|
|
277
|
+
return []
|
|
193
278
|
|
|
279
|
+
tools = response.get('result', {}).get('tools', [])
|
|
194
280
|
logger.debug(f"Retrieved {len(tools)} tools")
|
|
195
281
|
return tools
|
|
196
282
|
|
|
197
|
-
except asyncio.TimeoutError:
|
|
198
|
-
logger.error("Get tools timed out")
|
|
199
|
-
return []
|
|
200
283
|
except Exception as e:
|
|
201
284
|
logger.error(f"Error getting tools: {e}")
|
|
202
285
|
return []
|
|
203
286
|
|
|
204
287
|
async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
|
|
205
288
|
timeout: Optional[float] = None) -> Dict[str, Any]:
|
|
206
|
-
"""Call tool
|
|
289
|
+
"""Call a tool."""
|
|
207
290
|
if not self._initialized:
|
|
208
291
|
return {
|
|
209
292
|
"isError": True,
|
|
210
293
|
"error": "Transport not initialized"
|
|
211
294
|
}
|
|
212
295
|
|
|
213
|
-
tool_timeout = timeout or self.default_timeout
|
|
214
|
-
|
|
215
296
|
try:
|
|
216
297
|
logger.debug(f"Calling tool {tool_name} with args: {arguments}")
|
|
217
298
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
timeout=tool_timeout
|
|
299
|
+
response = await self._send_request(
|
|
300
|
+
"tools/call",
|
|
301
|
+
{
|
|
302
|
+
"name": tool_name,
|
|
303
|
+
"arguments": arguments
|
|
304
|
+
},
|
|
305
|
+
timeout=timeout
|
|
226
306
|
)
|
|
227
307
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
308
|
+
if 'error' in response:
|
|
309
|
+
return {
|
|
310
|
+
"isError": True,
|
|
311
|
+
"error": response['error'].get('message', 'Unknown error')
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
# Extract result
|
|
315
|
+
result = response.get('result', {})
|
|
316
|
+
|
|
317
|
+
# Handle content format
|
|
318
|
+
if 'content' in result:
|
|
319
|
+
content = result['content']
|
|
320
|
+
if isinstance(content, list) and len(content) == 1:
|
|
321
|
+
content_item = content[0]
|
|
322
|
+
if isinstance(content_item, dict) and content_item.get('type') == 'text':
|
|
323
|
+
text_content = content_item.get('text', '')
|
|
324
|
+
try:
|
|
325
|
+
# Try to parse as JSON
|
|
326
|
+
parsed_content = json.loads(text_content)
|
|
327
|
+
return {
|
|
328
|
+
"isError": False,
|
|
329
|
+
"content": parsed_content
|
|
330
|
+
}
|
|
331
|
+
except json.JSONDecodeError:
|
|
332
|
+
return {
|
|
333
|
+
"isError": False,
|
|
334
|
+
"content": text_content
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
"isError": False,
|
|
339
|
+
"content": content
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
"isError": False,
|
|
344
|
+
"content": result
|
|
345
|
+
}
|
|
346
|
+
|
|
231
347
|
except asyncio.TimeoutError:
|
|
232
|
-
logger.error(f"Tool {tool_name} timed out after {tool_timeout}s")
|
|
233
348
|
return {
|
|
234
349
|
"isError": True,
|
|
235
|
-
"error": f"Tool execution timed out
|
|
350
|
+
"error": f"Tool execution timed out"
|
|
236
351
|
}
|
|
237
352
|
except Exception as e:
|
|
238
353
|
logger.error(f"Error calling tool {tool_name}: {e}")
|
|
239
354
|
return {
|
|
240
355
|
"isError": True,
|
|
241
|
-
"error":
|
|
356
|
+
"error": str(e)
|
|
242
357
|
}
|
|
243
358
|
|
|
244
359
|
async def list_resources(self) -> Dict[str, Any]:
|
|
245
|
-
"""List resources
|
|
246
|
-
if not HAS_RESOURCES_PROMPTS:
|
|
247
|
-
logger.debug("Resources/prompts not available in chuk-mcp")
|
|
248
|
-
return {}
|
|
249
|
-
|
|
360
|
+
"""List resources."""
|
|
250
361
|
if not self._initialized:
|
|
251
362
|
return {}
|
|
252
363
|
|
|
253
364
|
try:
|
|
254
|
-
response = await
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return response
|
|
259
|
-
except
|
|
260
|
-
logger.error("List resources timed out")
|
|
261
|
-
return {}
|
|
262
|
-
except Exception as e:
|
|
263
|
-
logger.debug(f"Error listing resources: {e}")
|
|
365
|
+
response = await self._send_request("resources/list", {}, timeout=10.0)
|
|
366
|
+
if 'error' in response:
|
|
367
|
+
logger.debug(f"Resources not supported: {response['error']}")
|
|
368
|
+
return {}
|
|
369
|
+
return response.get('result', {})
|
|
370
|
+
except Exception:
|
|
264
371
|
return {}
|
|
265
372
|
|
|
266
373
|
async def list_prompts(self) -> Dict[str, Any]:
|
|
267
|
-
"""List prompts
|
|
268
|
-
if not HAS_RESOURCES_PROMPTS:
|
|
269
|
-
logger.debug("Resources/prompts not available in chuk-mcp")
|
|
270
|
-
return {}
|
|
271
|
-
|
|
374
|
+
"""List prompts."""
|
|
272
375
|
if not self._initialized:
|
|
273
376
|
return {}
|
|
274
377
|
|
|
275
378
|
try:
|
|
276
|
-
response = await
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return response
|
|
281
|
-
except
|
|
282
|
-
logger.error("List prompts timed out")
|
|
379
|
+
response = await self._send_request("prompts/list", {}, timeout=10.0)
|
|
380
|
+
if 'error' in response:
|
|
381
|
+
logger.debug(f"Prompts not supported: {response['error']}")
|
|
382
|
+
return {}
|
|
383
|
+
return response.get('result', {})
|
|
384
|
+
except Exception:
|
|
283
385
|
return {}
|
|
284
|
-
except Exception as e:
|
|
285
|
-
logger.debug(f"Error listing prompts: {e}")
|
|
286
|
-
return {}
|
|
287
|
-
|
|
288
|
-
def _normalize_tool_response(self, raw_response: Dict[str, Any]) -> Dict[str, Any]:
|
|
289
|
-
"""Normalize response for backward compatibility."""
|
|
290
|
-
# Handle explicit error in response
|
|
291
|
-
if "error" in raw_response:
|
|
292
|
-
error_info = raw_response["error"]
|
|
293
|
-
if isinstance(error_info, dict):
|
|
294
|
-
error_msg = error_info.get("message", "Unknown error")
|
|
295
|
-
else:
|
|
296
|
-
error_msg = str(error_info)
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
"isError": True,
|
|
300
|
-
"error": error_msg
|
|
301
|
-
}
|
|
302
386
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if isinstance(result, dict) and "content" in result:
|
|
308
|
-
return {
|
|
309
|
-
"isError": False,
|
|
310
|
-
"content": self._extract_content(result["content"])
|
|
311
|
-
}
|
|
312
|
-
else:
|
|
313
|
-
return {
|
|
314
|
-
"isError": False,
|
|
315
|
-
"content": result
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
# Handle direct content-based response
|
|
319
|
-
if "content" in raw_response:
|
|
320
|
-
return {
|
|
321
|
-
"isError": False,
|
|
322
|
-
"content": self._extract_content(raw_response["content"])
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
# Fallback
|
|
326
|
-
return {
|
|
327
|
-
"isError": False,
|
|
328
|
-
"content": raw_response
|
|
329
|
-
}
|
|
387
|
+
async def close(self) -> None:
|
|
388
|
+
"""Close the transport."""
|
|
389
|
+
await self._cleanup()
|
|
330
390
|
|
|
331
|
-
def
|
|
332
|
-
"""
|
|
333
|
-
if
|
|
334
|
-
|
|
391
|
+
async def _cleanup(self) -> None:
|
|
392
|
+
"""Clean up resources."""
|
|
393
|
+
if self.sse_task:
|
|
394
|
+
self.sse_task.cancel()
|
|
395
|
+
try:
|
|
396
|
+
await self.sse_task
|
|
397
|
+
except asyncio.CancelledError:
|
|
398
|
+
pass
|
|
335
399
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
text_content = content_item.get("text", "")
|
|
342
|
-
# Try to parse JSON, fall back to plain text
|
|
343
|
-
try:
|
|
344
|
-
return json.loads(text_content)
|
|
345
|
-
except json.JSONDecodeError:
|
|
346
|
-
return text_content
|
|
347
|
-
else:
|
|
348
|
-
return content_item
|
|
400
|
+
if self.sse_stream_context:
|
|
401
|
+
try:
|
|
402
|
+
await self.sse_stream_context.__aexit__(None, None, None)
|
|
403
|
+
except Exception:
|
|
404
|
+
pass
|
|
349
405
|
|
|
350
|
-
|
|
351
|
-
|
|
406
|
+
if self.stream_client:
|
|
407
|
+
await self.stream_client.aclose()
|
|
408
|
+
|
|
409
|
+
if self.send_client:
|
|
410
|
+
await self.send_client.aclose()
|
|
411
|
+
|
|
412
|
+
self._initialized = False
|
|
413
|
+
self.session_id = None
|
|
414
|
+
self.message_url = None
|
|
415
|
+
self.pending_requests.clear()
|
|
352
416
|
|
|
353
417
|
def get_streams(self) -> List[tuple]:
|
|
354
|
-
"""
|
|
355
|
-
if self._initialized and self._read_stream and self._write_stream:
|
|
356
|
-
return [(self._read_stream, self._write_stream)]
|
|
418
|
+
"""Not applicable for this transport."""
|
|
357
419
|
return []
|
|
358
420
|
|
|
359
421
|
def is_connected(self) -> bool:
|
|
360
|
-
"""Check
|
|
361
|
-
return self._initialized and self.
|
|
422
|
+
"""Check if connected."""
|
|
423
|
+
return self._initialized and self.session_id is not None
|
|
362
424
|
|
|
363
425
|
async def __aenter__(self):
|
|
364
426
|
"""Context manager support."""
|
|
@@ -372,6 +434,6 @@ class SSETransport(MCPBaseTransport):
|
|
|
372
434
|
await self.close()
|
|
373
435
|
|
|
374
436
|
def __repr__(self) -> str:
|
|
375
|
-
"""String representation
|
|
437
|
+
"""String representation."""
|
|
376
438
|
status = "initialized" if self._initialized else "not initialized"
|
|
377
|
-
return f"SSETransport(status={status}, url={self.url})"
|
|
439
|
+
return f"SSETransport(status={status}, url={self.url}, session={self.session_id})"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-tool-processor
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
4
4
|
Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
|
|
5
5
|
Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
|
|
6
6
|
Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
|
|
@@ -20,7 +20,7 @@ Classifier: Framework :: AsyncIO
|
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
|
-
Requires-Dist: chuk-mcp>=0.5
|
|
23
|
+
Requires-Dist: chuk-mcp>=0.5.1
|
|
24
24
|
Requires-Dist: dotenv>=0.9.9
|
|
25
25
|
Requires-Dist: pydantic>=2.11.3
|
|
26
26
|
Requires-Dist: uuid>=1.30
|
|
@@ -26,7 +26,7 @@ chuk_tool_processor/mcp/stream_manager.py,sha256=3JSxoVpvAI0_gZt7Njhp0vgpTnh4mLt
|
|
|
26
26
|
chuk_tool_processor/mcp/transport/__init__.py,sha256=0DX7m_VvlXPxijc-88_QTLhq4ZqAgUgzBjSMGL9C_lM,963
|
|
27
27
|
chuk_tool_processor/mcp/transport/base_transport.py,sha256=bqId34OMQMxzMXtrKq_86sot0_x0NS_ecaIllsCyy6I,3423
|
|
28
28
|
chuk_tool_processor/mcp/transport/http_streamable_transport.py,sha256=jtjv3RQU7753hV3QV3ZLhzJlP1w9zOy-_hI7OOjEC9A,19067
|
|
29
|
-
chuk_tool_processor/mcp/transport/sse_transport.py,sha256=
|
|
29
|
+
chuk_tool_processor/mcp/transport/sse_transport.py,sha256=BiIQBQUpU5XGdISiPZ_fWvsD54cv2wtFGQ6V9EEpeRM,15571
|
|
30
30
|
chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=QEpaGufkYmebrUZJMXHM-Q-Kj8TkkagorgUEqT17GwM,9095
|
|
31
31
|
chuk_tool_processor/models/__init__.py,sha256=TC__rdVa0lQsmJHM_hbLDPRgToa_pQT_UxRcPZk6iVw,40
|
|
32
32
|
chuk_tool_processor/models/execution_strategy.py,sha256=UVW35YIeMY2B3mpIKZD2rAkyOPayI6ckOOUALyf0YiQ,2115
|
|
@@ -54,7 +54,7 @@ chuk_tool_processor/registry/providers/__init__.py,sha256=eigwG_So11j7WbDGSWaKd3
|
|
|
54
54
|
chuk_tool_processor/registry/providers/memory.py,sha256=6cMtUwLO6zrk3pguQRgxJ2CReHAzewgZsizWZhsoStk,5184
|
|
55
55
|
chuk_tool_processor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
56
56
|
chuk_tool_processor/utils/validation.py,sha256=V5N1dH9sJlHepFIbiI2k2MU82o7nvnh0hKyIt2jdgww,4136
|
|
57
|
-
chuk_tool_processor-0.6.
|
|
58
|
-
chuk_tool_processor-0.6.
|
|
59
|
-
chuk_tool_processor-0.6.
|
|
60
|
-
chuk_tool_processor-0.6.
|
|
57
|
+
chuk_tool_processor-0.6.4.dist-info/METADATA,sha256=FHCcpjTWntOR3n5HMq_t2IiI49CfXQAvfCckDp_SWZU,23463
|
|
58
|
+
chuk_tool_processor-0.6.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
59
|
+
chuk_tool_processor-0.6.4.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
|
|
60
|
+
chuk_tool_processor-0.6.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|