chuk-tool-processor 0.6.2__tar.gz → 0.6.4__tar.gz

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.

Files changed (66) hide show
  1. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/PKG-INFO +2 -2
  2. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/pyproject.toml +2 -2
  3. chuk_tool_processor-0.6.4/src/chuk_tool_processor/mcp/transport/sse_transport.py +439 -0
  4. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor.egg-info/PKG-INFO +2 -2
  5. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor.egg-info/requires.txt +1 -1
  6. chuk_tool_processor-0.6.2/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -377
  7. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/README.md +0 -0
  8. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/setup.cfg +0 -0
  9. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/__init__.py +0 -0
  10. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/core/__init__.py +0 -0
  11. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/core/exceptions.py +0 -0
  12. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/core/processor.py +0 -0
  13. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/execution/__init__.py +0 -0
  14. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
  15. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
  16. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
  17. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
  18. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
  19. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
  20. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
  21. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
  22. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/logging/__init__.py +0 -0
  23. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/logging/context.py +0 -0
  24. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/logging/formatter.py +0 -0
  25. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/logging/helpers.py +0 -0
  26. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/logging/metrics.py +0 -0
  27. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/__init__.py +0 -0
  28. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
  29. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
  30. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
  31. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
  32. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
  33. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
  34. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
  35. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
  36. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
  37. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
  38. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/models/__init__.py +0 -0
  39. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
  40. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
  41. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/models/tool_call.py +0 -0
  42. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
  43. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/models/tool_result.py +0 -0
  44. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/models/validated_tool.py +0 -0
  45. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/plugins/__init__.py +0 -0
  46. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/plugins/discovery.py +0 -0
  47. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
  48. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
  49. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
  50. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
  51. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
  52. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
  53. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/registry/__init__.py +0 -0
  54. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/registry/auto_register.py +0 -0
  55. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/registry/decorators.py +0 -0
  56. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/registry/interface.py +0 -0
  57. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/registry/metadata.py +0 -0
  58. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/registry/provider.py +0 -0
  59. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
  60. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
  61. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/registry/tool_export.py +0 -0
  62. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/utils/__init__.py +0 -0
  63. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor/utils/validation.py +0 -0
  64. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
  65. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
  66. {chuk_tool_processor-0.6.2 → chuk_tool_processor-0.6.4}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.6.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chuk-tool-processor"
7
- version = "0.6.2"
7
+ version = "0.6.4"
8
8
  description = "Async-native framework for registering, discovering, and executing tools referenced in LLM responses"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -41,7 +41,7 @@ classifiers = [
41
41
  "Typing :: Typed",
42
42
  ]
43
43
  dependencies = [
44
- "chuk-mcp>=0.5",
44
+ "chuk-mcp>=0.5.1",
45
45
  "dotenv>=0.9.9",
46
46
  "pydantic>=2.11.3",
47
47
  "uuid>=1.30",
@@ -0,0 +1,439 @@
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
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import json
10
+ import uuid
11
+ from typing import Dict, Any, List, Optional, Tuple
12
+ import logging
13
+
14
+ import httpx
15
+
16
+ from .base_transport import MCPBaseTransport
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class SSETransport(MCPBaseTransport):
22
+ """
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
26
+ """
27
+
28
+ def __init__(self, url: str, api_key: Optional[str] = None,
29
+ connection_timeout: float = 30.0, default_timeout: float = 30.0):
30
+ """Initialize SSE transport."""
31
+ self.url = url.rstrip('/')
32
+ self.api_key = api_key
33
+ self.connection_timeout = connection_timeout
34
+ self.default_timeout = default_timeout
35
+
36
+ # State
37
+ self.session_id = None
38
+ self.message_url = None
39
+ self.pending_requests: Dict[str, asyncio.Future] = {}
40
+ self._initialized = False
41
+
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
57
+
58
+ async def initialize(self) -> bool:
59
+ """Initialize SSE connection and MCP handshake."""
60
+ if self._initialized:
61
+ logger.warning("Transport already initialized")
62
+ return True
63
+
64
+ try:
65
+ logger.info("Initializing SSE transport...")
66
+
67
+ # Create HTTP clients
68
+ self.stream_client = httpx.AsyncClient(timeout=self.connection_timeout)
69
+ self.send_client = httpx.AsyncClient(timeout=self.default_timeout)
70
+
71
+ # Connect to SSE stream
72
+ sse_url = f"{self.url}/sse"
73
+ logger.debug(f"Connecting to SSE: {sse_url}")
74
+
75
+ self.sse_stream_context = self.stream_client.stream(
76
+ 'GET', sse_url, headers=self._get_headers()
77
+ )
78
+ self.sse_response = await self.sse_stream_context.__aenter__()
79
+
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)
95
+
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
+
120
+ self._initialized = True
121
+ logger.info("SSE transport initialized successfully")
122
+ return True
123
+
124
+ except Exception as e:
125
+ logger.error(f"MCP initialization failed: {e}")
126
+ return False
127
+
128
+ except Exception as e:
129
+ logger.error(f"Error initializing SSE transport: {e}", exc_info=True)
130
+ await self._cleanup()
131
+ return False
132
+
133
+ async def _process_sse_stream(self):
134
+ """Process the persistent SSE stream."""
135
+ try:
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
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
+
178
+ except Exception as e:
179
+ logger.error(f"SSE stream error: {e}")
180
+
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
+ )
253
+
254
+ async def send_ping(self) -> bool:
255
+ """Send ping to check connection."""
256
+ if not self._initialized:
257
+ return False
258
+
259
+ try:
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:
264
+ return False
265
+
266
+ async def get_tools(self) -> List[Dict[str, Any]]:
267
+ """Get tools list."""
268
+ if not self._initialized:
269
+ logger.error("Cannot get tools: transport not initialized")
270
+ return []
271
+
272
+ try:
273
+ response = await self._send_request("tools/list", {})
274
+
275
+ if 'error' in response:
276
+ logger.error(f"Error getting tools: {response['error']}")
277
+ return []
278
+
279
+ tools = response.get('result', {}).get('tools', [])
280
+ logger.debug(f"Retrieved {len(tools)} tools")
281
+ return tools
282
+
283
+ except Exception as e:
284
+ logger.error(f"Error getting tools: {e}")
285
+ return []
286
+
287
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
288
+ timeout: Optional[float] = None) -> Dict[str, Any]:
289
+ """Call a tool."""
290
+ if not self._initialized:
291
+ return {
292
+ "isError": True,
293
+ "error": "Transport not initialized"
294
+ }
295
+
296
+ try:
297
+ logger.debug(f"Calling tool {tool_name} with args: {arguments}")
298
+
299
+ response = await self._send_request(
300
+ "tools/call",
301
+ {
302
+ "name": tool_name,
303
+ "arguments": arguments
304
+ },
305
+ timeout=timeout
306
+ )
307
+
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
+
347
+ except asyncio.TimeoutError:
348
+ return {
349
+ "isError": True,
350
+ "error": f"Tool execution timed out"
351
+ }
352
+ except Exception as e:
353
+ logger.error(f"Error calling tool {tool_name}: {e}")
354
+ return {
355
+ "isError": True,
356
+ "error": str(e)
357
+ }
358
+
359
+ async def list_resources(self) -> Dict[str, Any]:
360
+ """List resources."""
361
+ if not self._initialized:
362
+ return {}
363
+
364
+ try:
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:
371
+ return {}
372
+
373
+ async def list_prompts(self) -> Dict[str, Any]:
374
+ """List prompts."""
375
+ if not self._initialized:
376
+ return {}
377
+
378
+ try:
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:
385
+ return {}
386
+
387
+ async def close(self) -> None:
388
+ """Close the transport."""
389
+ await self._cleanup()
390
+
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
399
+
400
+ if self.sse_stream_context:
401
+ try:
402
+ await self.sse_stream_context.__aexit__(None, None, None)
403
+ except Exception:
404
+ pass
405
+
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()
416
+
417
+ def get_streams(self) -> List[tuple]:
418
+ """Not applicable for this transport."""
419
+ return []
420
+
421
+ def is_connected(self) -> bool:
422
+ """Check if connected."""
423
+ return self._initialized and self.session_id is not None
424
+
425
+ async def __aenter__(self):
426
+ """Context manager support."""
427
+ success = await self.initialize()
428
+ if not success:
429
+ raise RuntimeError("Failed to initialize SSE transport")
430
+ return self
431
+
432
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
433
+ """Context manager cleanup."""
434
+ await self.close()
435
+
436
+ def __repr__(self) -> str:
437
+ """String representation."""
438
+ status = "initialized" if self._initialized else "not initialized"
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.2
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
@@ -1,4 +1,4 @@
1
- chuk-mcp>=0.5
1
+ chuk-mcp>=0.5.1
2
2
  dotenv>=0.9.9
3
3
  pydantic>=2.11.3
4
4
  uuid>=1.30
@@ -1,377 +0,0 @@
1
- # chuk_tool_processor/mcp/transport/sse_transport.py
2
- from __future__ import annotations
3
-
4
- import asyncio
5
- import json
6
- from typing import Dict, Any, List, Optional
7
- import logging
8
-
9
- from .base_transport import MCPBaseTransport
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
24
-
25
- # Import optional resource and prompt support
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
36
-
37
- logger = logging.getLogger(__name__)
38
-
39
-
40
- class SSETransport(MCPBaseTransport):
41
- """
42
- Updated SSE transport using latest chuk-mcp APIs.
43
-
44
- Supports all required abstract methods and provides full MCP functionality.
45
- """
46
-
47
- def __init__(self, url: str, api_key: Optional[str] = None,
48
- connection_timeout: float = 30.0, default_timeout: float = 30.0):
49
- """
50
- Initialize SSE transport with latest chuk-mcp.
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
59
- self.api_key = api_key
60
- self.connection_timeout = connection_timeout
61
- self.default_timeout = default_timeout
62
-
63
- # State tracking
64
- self._sse_context = None
65
- self._read_stream = None
66
- self._write_stream = None
67
- self._initialized = False
68
-
69
- if not HAS_SSE_SUPPORT:
70
- logger.warning("SSE transport not available - operations will fail")
71
-
72
- async def initialize(self) -> bool:
73
- """Initialize using latest chuk-mcp sse_client."""
74
- if not HAS_SSE_SUPPORT:
75
- logger.error("SSE transport not available in chuk-mcp")
76
- return False
77
-
78
- if self._initialized:
79
- logger.warning("Transport already initialized")
80
- return True
81
-
82
- try:
83
- logger.info("Initializing SSE transport...")
84
-
85
- # Create SSE parameters for latest chuk-mcp
86
- sse_params = SSEParameters(
87
- url=self.url,
88
- timeout=self.connection_timeout,
89
- auto_reconnect=True,
90
- max_reconnect_attempts=3
91
- )
92
-
93
- # Create and enter the context - this should handle the full MCP handshake
94
- self._sse_context = sse_client(sse_params)
95
-
96
- # The sse_client should handle the entire initialization process
97
- logger.debug("Establishing SSE connection and MCP handshake...")
98
- self._read_stream, self._write_stream = await asyncio.wait_for(
99
- self._sse_context.__aenter__(),
100
- timeout=self.connection_timeout
101
- )
102
-
103
- # At this point, chuk-mcp should have already completed the MCP initialization
104
- # Let's verify the connection works with a simple ping
105
- logger.debug("Verifying connection with ping...")
106
- ping_success = await asyncio.wait_for(
107
- send_ping(self._read_stream, self._write_stream),
108
- timeout=5.0
109
- )
110
-
111
- if ping_success:
112
- self._initialized = True
113
- logger.info("SSE transport initialized successfully")
114
- return True
115
- else:
116
- logger.warning("SSE connection established but ping failed")
117
- # Still consider it initialized since connection was established
118
- self._initialized = True
119
- return True
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
126
- except Exception as e:
127
- logger.error(f"Error initializing SSE transport: {e}", exc_info=True)
128
- await self._cleanup()
129
- return False
130
-
131
- async def close(self) -> None:
132
- """Close the SSE transport properly."""
133
- if not self._initialized:
134
- return
135
-
136
- try:
137
- if self._sse_context is not None:
138
- await self._sse_context.__aexit__(None, None, None)
139
- logger.debug("SSE context closed")
140
-
141
- except Exception as e:
142
- logger.debug(f"Error during transport close: {e}")
143
- finally:
144
- await self._cleanup()
145
-
146
- async def _cleanup(self) -> None:
147
- """Clean up internal state."""
148
- self._sse_context = None
149
- self._read_stream = None
150
- self._write_stream = None
151
- self._initialized = False
152
-
153
- async def send_ping(self) -> bool:
154
- """Send ping using latest chuk-mcp."""
155
- if not self._initialized:
156
- logger.error("Cannot send ping: transport not initialized")
157
- return False
158
-
159
- try:
160
- result = await asyncio.wait_for(
161
- send_ping(self._read_stream, self._write_stream),
162
- timeout=self.default_timeout
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}")
171
- return False
172
-
173
- async def get_tools(self) -> List[Dict[str, Any]]:
174
- """Get tools list using latest chuk-mcp."""
175
- if not self._initialized:
176
- logger.error("Cannot get tools: transport not initialized")
177
- return []
178
-
179
- try:
180
- tools_response = await asyncio.wait_for(
181
- send_tools_list(self._read_stream, self._write_stream),
182
- timeout=self.default_timeout
183
- )
184
-
185
- # Normalize response
186
- if isinstance(tools_response, dict):
187
- tools = tools_response.get("tools", [])
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 = []
193
-
194
- logger.debug(f"Retrieved {len(tools)} tools")
195
- return tools
196
-
197
- except asyncio.TimeoutError:
198
- logger.error("Get tools timed out")
199
- return []
200
- except Exception as e:
201
- logger.error(f"Error getting tools: {e}")
202
- return []
203
-
204
- async def call_tool(self, tool_name: str, arguments: Dict[str, Any],
205
- timeout: Optional[float] = None) -> Dict[str, Any]:
206
- """Call tool using latest chuk-mcp."""
207
- if not self._initialized:
208
- return {
209
- "isError": True,
210
- "error": "Transport not initialized"
211
- }
212
-
213
- tool_timeout = timeout or self.default_timeout
214
-
215
- try:
216
- logger.debug(f"Calling tool {tool_name} with args: {arguments}")
217
-
218
- raw_response = await asyncio.wait_for(
219
- send_tools_call(
220
- self._read_stream,
221
- self._write_stream,
222
- tool_name,
223
- arguments
224
- ),
225
- timeout=tool_timeout
226
- )
227
-
228
- logger.debug(f"Tool {tool_name} raw response: {raw_response}")
229
- return self._normalize_tool_response(raw_response)
230
-
231
- except asyncio.TimeoutError:
232
- logger.error(f"Tool {tool_name} timed out after {tool_timeout}s")
233
- return {
234
- "isError": True,
235
- "error": f"Tool execution timed out after {tool_timeout}s"
236
- }
237
- except Exception as e:
238
- logger.error(f"Error calling tool {tool_name}: {e}")
239
- return {
240
- "isError": True,
241
- "error": f"Tool execution failed: {str(e)}"
242
- }
243
-
244
- async def list_resources(self) -> Dict[str, Any]:
245
- """List resources using latest chuk-mcp."""
246
- if not HAS_RESOURCES_PROMPTS:
247
- logger.debug("Resources/prompts not available in chuk-mcp")
248
- return {}
249
-
250
- if not self._initialized:
251
- return {}
252
-
253
- try:
254
- response = await asyncio.wait_for(
255
- send_resources_list(self._read_stream, self._write_stream),
256
- timeout=self.default_timeout
257
- )
258
- return response if isinstance(response, dict) else {}
259
- except asyncio.TimeoutError:
260
- logger.error("List resources timed out")
261
- return {}
262
- except Exception as e:
263
- logger.debug(f"Error listing resources: {e}")
264
- return {}
265
-
266
- async def list_prompts(self) -> Dict[str, Any]:
267
- """List prompts using latest chuk-mcp."""
268
- if not HAS_RESOURCES_PROMPTS:
269
- logger.debug("Resources/prompts not available in chuk-mcp")
270
- return {}
271
-
272
- if not self._initialized:
273
- return {}
274
-
275
- try:
276
- response = await asyncio.wait_for(
277
- send_prompts_list(self._read_stream, self._write_stream),
278
- timeout=self.default_timeout
279
- )
280
- return response if isinstance(response, dict) else {}
281
- except asyncio.TimeoutError:
282
- logger.error("List prompts timed out")
283
- 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
-
303
- # Handle successful response with result
304
- if "result" in raw_response:
305
- result = raw_response["result"]
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
- }
330
-
331
- def _extract_content(self, content_list: Any) -> Any:
332
- """Extract content from MCP content format."""
333
- if not isinstance(content_list, list) or not content_list:
334
- return content_list
335
-
336
- # Handle single content item
337
- if len(content_list) == 1:
338
- content_item = content_list[0]
339
- if isinstance(content_item, dict):
340
- if content_item.get("type") == "text":
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
349
-
350
- # Multiple content items
351
- return content_list
352
-
353
- def get_streams(self) -> List[tuple]:
354
- """Provide streams for backward compatibility."""
355
- if self._initialized and self._read_stream and self._write_stream:
356
- return [(self._read_stream, self._write_stream)]
357
- return []
358
-
359
- def is_connected(self) -> bool:
360
- """Check connection status."""
361
- return self._initialized and self._read_stream is not None and self._write_stream is not None
362
-
363
- async def __aenter__(self):
364
- """Context manager support."""
365
- success = await self.initialize()
366
- if not success:
367
- raise RuntimeError("Failed to initialize SSE transport")
368
- return self
369
-
370
- async def __aexit__(self, exc_type, exc_val, exc_tb):
371
- """Context manager cleanup."""
372
- await self.close()
373
-
374
- def __repr__(self) -> str:
375
- """String representation for debugging."""
376
- status = "initialized" if self._initialized else "not initialized"
377
- return f"SSETransport(status={status}, url={self.url})"