chuk-tool-processor 0.5__tar.gz → 0.5.1__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.
Files changed (64) hide show
  1. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/PKG-INFO +2 -2
  2. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/pyproject.toml +2 -2
  3. chuk_tool_processor-0.5.1/src/chuk_tool_processor/mcp/transport/stdio_transport.py +392 -0
  4. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor.egg-info/PKG-INFO +2 -2
  5. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor.egg-info/requires.txt +1 -1
  6. chuk_tool_processor-0.5/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -195
  7. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/README.md +0 -0
  8. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/setup.cfg +0 -0
  9. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/__init__.py +0 -0
  10. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/core/__init__.py +0 -0
  11. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/core/exceptions.py +0 -0
  12. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/core/processor.py +0 -0
  13. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/execution/__init__.py +0 -0
  14. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
  15. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
  16. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
  17. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
  18. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
  19. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
  20. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
  21. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
  22. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/logging/__init__.py +0 -0
  23. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/logging/context.py +0 -0
  24. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/logging/formatter.py +0 -0
  25. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/logging/helpers.py +0 -0
  26. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/logging/metrics.py +0 -0
  27. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/mcp/__init__.py +0 -0
  28. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
  29. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
  30. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
  31. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
  32. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
  33. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
  34. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
  35. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
  36. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/models/__init__.py +0 -0
  37. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
  38. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
  39. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/models/tool_call.py +0 -0
  40. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
  41. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/models/tool_result.py +0 -0
  42. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/models/validated_tool.py +0 -0
  43. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/plugins/__init__.py +0 -0
  44. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/plugins/discovery.py +0 -0
  45. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
  46. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
  47. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
  48. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
  49. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
  50. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
  51. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/registry/__init__.py +0 -0
  52. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/registry/auto_register.py +0 -0
  53. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/registry/decorators.py +0 -0
  54. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/registry/interface.py +0 -0
  55. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/registry/metadata.py +0 -0
  56. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/registry/provider.py +0 -0
  57. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
  58. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
  59. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/registry/tool_export.py +0 -0
  60. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/utils/__init__.py +0 -0
  61. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor/utils/validation.py +0 -0
  62. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
  63. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
  64. {chuk_tool_processor-0.5 → chuk_tool_processor-0.5.1}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.5
3
+ Version: 0.5.1
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: chuk-mcp>=0.2
7
+ Requires-Dist: chuk-mcp>=0.2.3
8
8
  Requires-Dist: dotenv>=0.9.9
9
9
  Requires-Dist: pydantic>=2.11.3
10
10
  Requires-Dist: uuid>=1.30
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chuk-tool-processor"
7
- version = "0.5"
7
+ version = "0.5.1"
8
8
  description = "Add your description here"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
  dependencies = [
12
- "chuk-mcp>=0.2",
12
+ "chuk-mcp>=0.2.3",
13
13
  "dotenv>=0.9.9",
14
14
  "pydantic>=2.11.3",
15
15
  "uuid>=1.30",
@@ -0,0 +1,392 @@
1
+ # chuk_tool_processor/mcp/transport/stdio_transport.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from typing import Dict, Any, List, Optional
6
+ import logging
7
+
8
+ # ------------------------------------------------------------------ #
9
+ # Local import #
10
+ # ------------------------------------------------------------------ #
11
+ from .base_transport import MCPBaseTransport
12
+
13
+ # ------------------------------------------------------------------ #
14
+ # New chuk-mcp API imports only #
15
+ # ------------------------------------------------------------------ #
16
+ from chuk_mcp.transports.stdio import stdio_client
17
+ from chuk_mcp.transports.stdio.parameters import StdioParameters
18
+
19
+ from chuk_mcp.protocol.messages import (
20
+ send_initialize,
21
+ send_ping,
22
+ send_tools_list,
23
+ send_tools_call,
24
+ )
25
+
26
+ # Try to import resources and prompts if available
27
+ try:
28
+ from chuk_mcp.protocol.messages import (
29
+ send_resources_list,
30
+ send_resources_read,
31
+ )
32
+ HAS_RESOURCES = True
33
+ except ImportError:
34
+ HAS_RESOURCES = False
35
+
36
+ try:
37
+ from chuk_mcp.protocol.messages import (
38
+ send_prompts_list,
39
+ send_prompts_get,
40
+ )
41
+ HAS_PROMPTS = True
42
+ except ImportError:
43
+ HAS_PROMPTS = False
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ class StdioTransport(MCPBaseTransport):
49
+ """
50
+ STDIO transport for MCP communication using new chuk-mcp APIs.
51
+ """
52
+
53
+ def __init__(self, server_params):
54
+ """
55
+ Initialize STDIO transport.
56
+
57
+ Args:
58
+ server_params: Either a dict with 'command' and 'args',
59
+ or a StdioParameters object
60
+ """
61
+ # Convert dict format to StdioParameters
62
+ if isinstance(server_params, dict):
63
+ self.server_params = StdioParameters(
64
+ command=server_params.get('command', 'python'),
65
+ args=server_params.get('args', [])
66
+ )
67
+ else:
68
+ self.server_params = server_params
69
+
70
+ self.read_stream = None
71
+ self.write_stream = None
72
+ self._client_context = None
73
+
74
+ # --------------------------------------------------------------------- #
75
+ # Connection management #
76
+ # --------------------------------------------------------------------- #
77
+ async def initialize(self) -> bool:
78
+ """Initialize the STDIO transport."""
79
+ try:
80
+ logger.info("Initializing STDIO transport...")
81
+
82
+ # Use the new stdio_client context manager
83
+ self._client_context = stdio_client(self.server_params)
84
+ self.read_stream, self.write_stream = await self._client_context.__aenter__()
85
+
86
+ # Send initialize message
87
+ logger.debug("Sending initialize message...")
88
+ init_result = await send_initialize(self.read_stream, self.write_stream)
89
+
90
+ if init_result:
91
+ logger.info("STDIO transport initialized successfully")
92
+ return True
93
+ else:
94
+ logger.error("Initialize message failed")
95
+ await self._cleanup()
96
+ return False
97
+
98
+ except Exception as e:
99
+ logger.error(f"Error initializing STDIO transport: {e}", exc_info=True)
100
+ await self._cleanup()
101
+ return False
102
+
103
+ async def close(self) -> None:
104
+ """Close the transport with proper error handling."""
105
+ try:
106
+ # Handle both old _context_stack and new _client_context for test compatibility
107
+ if hasattr(self, '_context_stack') and self._context_stack:
108
+ try:
109
+ await self._context_stack.__aexit__(None, None, None)
110
+ logger.debug("Context stack closed")
111
+ except asyncio.CancelledError:
112
+ # Expected during shutdown - don't log as error
113
+ logger.debug("Context stack close cancelled during shutdown")
114
+ except Exception as e:
115
+ logger.error(f"Error closing context stack: {e}")
116
+ elif self._client_context:
117
+ try:
118
+ await self._client_context.__aexit__(None, None, None)
119
+ logger.debug("STDIO client context closed")
120
+ except asyncio.CancelledError:
121
+ # Expected during shutdown - don't log as error
122
+ logger.debug("Client context close cancelled during shutdown")
123
+ except Exception as e:
124
+ logger.error(f"Error closing client context: {e}")
125
+ except Exception as e:
126
+ logger.error(f"Error during transport cleanup: {e}")
127
+ finally:
128
+ await self._cleanup()
129
+
130
+ async def _cleanup(self) -> None:
131
+ """Internal cleanup method."""
132
+ # Clean up both old and new context attributes for test compatibility
133
+ if hasattr(self, '_context_stack'):
134
+ self._context_stack = None
135
+ self._client_context = None
136
+ self.read_stream = None
137
+ self.write_stream = None
138
+
139
+ # --------------------------------------------------------------------- #
140
+ # Core MCP Operations #
141
+ # --------------------------------------------------------------------- #
142
+ async def send_ping(self) -> bool:
143
+ """Send a ping."""
144
+ if not self.read_stream or not self.write_stream:
145
+ logger.error("Cannot send ping: streams not available")
146
+ return False
147
+
148
+ try:
149
+ result = await send_ping(self.read_stream, self.write_stream)
150
+ logger.debug(f"Ping result: {result}")
151
+ return bool(result)
152
+ except Exception as e:
153
+ logger.error(f"Ping failed: {e}")
154
+ return False
155
+
156
+ async def get_tools(self) -> List[Dict[str, Any]]:
157
+ """Get list of available tools."""
158
+ if not self.read_stream or not self.write_stream:
159
+ logger.error("Cannot get tools: streams not available")
160
+ return []
161
+
162
+ try:
163
+ tools_response = await send_tools_list(self.read_stream, self.write_stream)
164
+
165
+ # Handle both dict response and direct tools list
166
+ if isinstance(tools_response, dict):
167
+ tools = tools_response.get("tools", [])
168
+ elif isinstance(tools_response, list):
169
+ tools = tools_response
170
+ else:
171
+ logger.warning(f"Unexpected tools response type: {type(tools_response)}")
172
+ tools = []
173
+
174
+ logger.debug(f"Retrieved {len(tools)} tools")
175
+ return tools
176
+
177
+ except Exception as e:
178
+ logger.error(f"Error getting tools: {e}")
179
+ return []
180
+
181
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
182
+ """
183
+ Execute a tool.
184
+
185
+ Returns normalized response in format:
186
+ {
187
+ "isError": bool,
188
+ "content": Any, # Result data if successful
189
+ "error": str # Error message if failed
190
+ }
191
+ """
192
+ if not self.read_stream or not self.write_stream:
193
+ return {
194
+ "isError": True,
195
+ "error": "Transport not initialized"
196
+ }
197
+
198
+ try:
199
+ logger.debug(f"Calling tool {tool_name} with args: {arguments}")
200
+
201
+ raw_response = await send_tools_call(
202
+ self.read_stream,
203
+ self.write_stream,
204
+ tool_name,
205
+ arguments
206
+ )
207
+
208
+ logger.debug(f"Tool {tool_name} raw response: {raw_response}")
209
+ return self._normalize_tool_response(raw_response)
210
+
211
+ except Exception as e:
212
+ logger.error(f"Error calling tool {tool_name}: {e}")
213
+ return {
214
+ "isError": True,
215
+ "error": f"Tool execution failed: {str(e)}"
216
+ }
217
+
218
+ def _normalize_tool_response(self, raw_response: Dict[str, Any]) -> Dict[str, Any]:
219
+ """Normalize tool response to consistent format."""
220
+ # Handle explicit error in response
221
+ if "error" in raw_response:
222
+ error_info = raw_response["error"]
223
+ if isinstance(error_info, dict):
224
+ error_msg = error_info.get("message", "Unknown error")
225
+ else:
226
+ error_msg = str(error_info)
227
+
228
+ return {
229
+ "isError": True,
230
+ "error": error_msg
231
+ }
232
+
233
+ # Handle successful response with result (MCP standard)
234
+ if "result" in raw_response:
235
+ result = raw_response["result"]
236
+
237
+ # If result has content, extract it
238
+ if isinstance(result, dict) and "content" in result:
239
+ return {
240
+ "isError": False,
241
+ "content": self._extract_content(result["content"])
242
+ }
243
+ else:
244
+ return {
245
+ "isError": False,
246
+ "content": result
247
+ }
248
+
249
+ # Handle direct content-based response
250
+ if "content" in raw_response:
251
+ return {
252
+ "isError": False,
253
+ "content": self._extract_content(raw_response["content"])
254
+ }
255
+
256
+ # Fallback: return whatever the server sent
257
+ return {
258
+ "isError": False,
259
+ "content": raw_response
260
+ }
261
+
262
+ def _extract_content(self, content_list: Any) -> Any:
263
+ """Extract content from MCP content list format."""
264
+ if not isinstance(content_list, list) or not content_list:
265
+ return content_list
266
+
267
+ # Handle single content item (most common)
268
+ if len(content_list) == 1:
269
+ content_item = content_list[0]
270
+ if isinstance(content_item, dict):
271
+ if content_item.get("type") == "text":
272
+ text_content = content_item.get("text", "")
273
+ # Try to parse JSON, fall back to plain text
274
+ try:
275
+ return json.loads(text_content)
276
+ except json.JSONDecodeError:
277
+ return text_content
278
+ else:
279
+ # Non-text content (image, audio, etc.)
280
+ return content_item
281
+
282
+ # Multiple content items - return the list
283
+ return content_list
284
+
285
+ # --------------------------------------------------------------------- #
286
+ # Resources Operations (if available) #
287
+ # --------------------------------------------------------------------- #
288
+ async def list_resources(self) -> Dict[str, Any]:
289
+ """Get list of available resources."""
290
+ if not HAS_RESOURCES:
291
+ logger.warning("Resources not supported in current chuk-mcp version")
292
+ return {}
293
+
294
+ if not self.read_stream or not self.write_stream:
295
+ logger.error("Cannot list resources: streams not available")
296
+ return {}
297
+
298
+ try:
299
+ response = await send_resources_list(self.read_stream, self.write_stream)
300
+ return response if isinstance(response, dict) else {}
301
+ except Exception as e:
302
+ logger.error(f"Error listing resources: {e}")
303
+ return {}
304
+
305
+ async def read_resource(self, uri: str) -> Dict[str, Any]:
306
+ """Read a specific resource by URI."""
307
+ if not HAS_RESOURCES:
308
+ logger.warning("Resources not supported in current chuk-mcp version")
309
+ return {}
310
+
311
+ if not self.read_stream or not self.write_stream:
312
+ logger.error("Cannot read resource: streams not available")
313
+ return {}
314
+
315
+ try:
316
+ response = await send_resources_read(self.read_stream, self.write_stream, uri)
317
+ return response if isinstance(response, dict) else {}
318
+ except Exception as e:
319
+ logger.error(f"Error reading resource {uri}: {e}")
320
+ return {}
321
+
322
+ # --------------------------------------------------------------------- #
323
+ # Prompts Operations (if available) #
324
+ # --------------------------------------------------------------------- #
325
+ async def list_prompts(self) -> Dict[str, Any]:
326
+ """Get list of available prompts."""
327
+ if not HAS_PROMPTS:
328
+ logger.warning("Prompts not supported in current chuk-mcp version")
329
+ return {}
330
+
331
+ if not self.read_stream or not self.write_stream:
332
+ logger.error("Cannot list prompts: streams not available")
333
+ return {}
334
+
335
+ try:
336
+ response = await send_prompts_list(self.read_stream, self.write_stream)
337
+ return response if isinstance(response, dict) else {}
338
+ except Exception as e:
339
+ logger.error(f"Error listing prompts: {e}")
340
+ return {}
341
+
342
+ async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
343
+ """Get a specific prompt by name."""
344
+ if not HAS_PROMPTS:
345
+ logger.warning("Prompts not supported in current chuk-mcp version")
346
+ return {}
347
+
348
+ if not self.read_stream or not self.write_stream:
349
+ logger.error("Cannot get prompt: streams not available")
350
+ return {}
351
+
352
+ try:
353
+ response = await send_prompts_get(
354
+ self.read_stream,
355
+ self.write_stream,
356
+ name,
357
+ arguments or {}
358
+ )
359
+ return response if isinstance(response, dict) else {}
360
+ except Exception as e:
361
+ logger.error(f"Error getting prompt {name}: {e}")
362
+ return {}
363
+
364
+ # --------------------------------------------------------------------- #
365
+ # Utility Methods #
366
+ # --------------------------------------------------------------------- #
367
+ def get_streams(self) -> List[tuple]:
368
+ """Get the underlying streams for advanced usage."""
369
+ if self.read_stream and self.write_stream:
370
+ return [(self.read_stream, self.write_stream)]
371
+ return []
372
+
373
+ def is_connected(self) -> bool:
374
+ """Check if transport is connected and ready."""
375
+ return self.read_stream is not None and self.write_stream is not None
376
+
377
+ async def __aenter__(self):
378
+ """Async context manager entry."""
379
+ success = await self.initialize()
380
+ if not success:
381
+ raise RuntimeError("Failed to initialize STDIO transport")
382
+ return self
383
+
384
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
385
+ """Async context manager exit."""
386
+ await self.close()
387
+
388
+ def __repr__(self) -> str:
389
+ """String representation for debugging."""
390
+ status = "connected" if self.is_connected() else "disconnected"
391
+ cmd_info = f"command={getattr(self.server_params, 'command', 'unknown')}"
392
+ return f"StdioTransport(status={status}, {cmd_info})"
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.5
3
+ Version: 0.5.1
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: chuk-mcp>=0.2
7
+ Requires-Dist: chuk-mcp>=0.2.3
8
8
  Requires-Dist: dotenv>=0.9.9
9
9
  Requires-Dist: pydantic>=2.11.3
10
10
  Requires-Dist: uuid>=1.30
@@ -1,4 +1,4 @@
1
- chuk-mcp>=0.2
1
+ chuk-mcp>=0.2.3
2
2
  dotenv>=0.9.9
3
3
  pydantic>=2.11.3
4
4
  uuid>=1.30
@@ -1,195 +0,0 @@
1
- # chuk_tool_processor/mcp/transport/stdio_transport.py
2
- from __future__ import annotations
3
-
4
- from contextlib import AsyncExitStack
5
- import json
6
- from typing import Dict, Any, List, Optional
7
- import asyncio
8
-
9
- # ------------------------------------------------------------------ #
10
- # Local import #
11
- # ------------------------------------------------------------------ #
12
- from .base_transport import MCPBaseTransport
13
-
14
- # ------------------------------------------------------------------ #
15
- # chuk-protocol imports #
16
- # ------------------------------------------------------------------ #
17
- from chuk_mcp.mcp_client.transport.stdio.stdio_client import stdio_client
18
- from chuk_mcp.mcp_client.messages.initialize.send_messages import send_initialize
19
- from chuk_mcp.mcp_client.messages.ping.send_messages import send_ping
20
-
21
- # tools
22
- from chuk_mcp.mcp_client.messages.tools.send_messages import (
23
- send_tools_call,
24
- send_tools_list,
25
- )
26
-
27
- # NEW: resources & prompts
28
- from chuk_mcp.mcp_client.messages.resources.send_messages import (
29
- send_resources_list,
30
- )
31
- from chuk_mcp.mcp_client.messages.prompts.send_messages import (
32
- send_prompts_list,
33
- )
34
-
35
-
36
- class StdioTransport(MCPBaseTransport):
37
- """
38
- Stdio transport for MCP communication.
39
- """
40
-
41
- def __init__(self, server_params):
42
- self.server_params = server_params
43
- self.read_stream = None
44
- self.write_stream = None
45
- self._context_stack: Optional[AsyncExitStack] = None
46
-
47
- # --------------------------------------------------------------------- #
48
- # Connection management #
49
- # --------------------------------------------------------------------- #
50
- async def initialize(self) -> bool:
51
- try:
52
- self._context_stack = AsyncExitStack()
53
- await self._context_stack.__aenter__()
54
-
55
- ctx = stdio_client(self.server_params)
56
- self.read_stream, self.write_stream = await self._context_stack.enter_async_context(
57
- ctx
58
- )
59
-
60
- init_result = await send_initialize(self.read_stream, self.write_stream)
61
- return bool(init_result)
62
-
63
- except Exception as e: # pragma: no cover
64
- import logging
65
-
66
- logging.error(f"Error initializing stdio transport: {e}")
67
- if self._context_stack:
68
- try:
69
- await self._context_stack.__aexit__(None, None, None)
70
- except Exception:
71
- pass
72
- return False
73
-
74
- async def close(self) -> None:
75
- """Minimal close method with zero async operations."""
76
- # Just clear references - no async operations at all
77
- self._context_stack = None
78
- self.read_stream = None
79
- self.write_stream = None
80
-
81
- # --------------------------------------------------------------------- #
82
- # Utility #
83
- # --------------------------------------------------------------------- #
84
- async def send_ping(self) -> bool:
85
- if not self.read_stream or not self.write_stream:
86
- return False
87
- return await send_ping(self.read_stream, self.write_stream)
88
-
89
- async def get_tools(self) -> List[Dict[str, Any]]:
90
- if not self.read_stream or not self.write_stream:
91
- return []
92
- tools_response = await send_tools_list(self.read_stream, self.write_stream)
93
- return tools_response.get("tools", [])
94
-
95
- # NEW ------------------------------------------------------------------ #
96
- # Resources / Prompts #
97
- # --------------------------------------------------------------------- #
98
- async def list_resources(self) -> Dict[str, Any]:
99
- """
100
- Return the result of *resources/list*. If the connection is not yet
101
- initialised an empty dict is returned.
102
- """
103
- if not self.read_stream or not self.write_stream:
104
- return {}
105
- try:
106
- return await send_resources_list(self.read_stream, self.write_stream)
107
- except Exception as exc: # pragma: no cover
108
- import logging
109
-
110
- logging.error(f"Error listing resources: {exc}")
111
- return {}
112
-
113
- async def list_prompts(self) -> Dict[str, Any]:
114
- """
115
- Return the result of *prompts/list*. If the connection is not yet
116
- initialised an empty dict is returned.
117
- """
118
- if not self.read_stream or not self.write_stream:
119
- return {}
120
- try:
121
- return await send_prompts_list(self.read_stream, self.write_stream)
122
- except Exception as exc: # pragma: no cover
123
- import logging
124
-
125
- logging.error(f"Error listing prompts: {exc}")
126
- return {}
127
-
128
- # OPTIONAL helper ------------------------------------------------------ #
129
- def get_streams(self):
130
- """
131
- Expose the low-level streams so legacy callers can access them
132
- directly. The base-class' default returns an empty list; here we
133
- return a single-element list when the transport is active.
134
- """
135
- if self.read_stream and self.write_stream:
136
- return [(self.read_stream, self.write_stream)]
137
- return []
138
-
139
- # --------------------------------------------------------------------- #
140
- # Main entry-point #
141
- # --------------------------------------------------------------------- #
142
- async def call_tool(
143
- self, tool_name: str, arguments: Dict[str, Any]
144
- ) -> Dict[str, Any]:
145
- """
146
- Execute *tool_name* with *arguments* and normalise the server's reply.
147
-
148
- The echo-server often returns:
149
- {
150
- "content": [{"type":"text","text":"{\"message\":\"…\"}"}],
151
- "isError": false
152
- }
153
- We unwrap that so callers just receive either a dict or a plain string.
154
- """
155
- if not self.read_stream or not self.write_stream:
156
- return {"isError": True, "error": "Transport not initialized"}
157
-
158
- try:
159
- raw = await send_tools_call(
160
- self.read_stream, self.write_stream, tool_name, arguments
161
- )
162
-
163
- # Handle explicit error wrapper
164
- if "error" in raw:
165
- return {
166
- "isError": True,
167
- "error": raw["error"].get("message", "Unknown error"),
168
- }
169
-
170
- # Preferred: servers that put the answer under "result"
171
- if "result" in raw:
172
- return {"isError": False, "content": raw["result"]}
173
-
174
- # Common echo-server shape: top-level "content" list
175
- if "content" in raw:
176
- clist = raw["content"]
177
- if isinstance(clist, list) and clist:
178
- first = clist[0]
179
- if isinstance(first, dict) and first.get("type") == "text":
180
- text = first.get("text", "")
181
- # Try to parse as JSON; fall back to plain string
182
- try:
183
- parsed = json.loads(text)
184
- return {"isError": False, "content": parsed}
185
- except json.JSONDecodeError:
186
- return {"isError": False, "content": text}
187
-
188
- # Fallback: give caller whatever the server sent
189
- return {"isError": False, "content": raw}
190
-
191
- except Exception as e: # pragma: no cover
192
- import logging
193
-
194
- logging.error(f"Error calling tool {tool_name}: {e}")
195
- return {"isError": True, "error": str(e)}