chuk-tool-processor 0.5__py3-none-any.whl → 0.5.2__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.

@@ -1,10 +1,10 @@
1
1
  # chuk_tool_processor/mcp/transport/stdio_transport.py
2
2
  from __future__ import annotations
3
3
 
4
- from contextlib import AsyncExitStack
4
+ import asyncio
5
5
  import json
6
6
  from typing import Dict, Any, List, Optional
7
- import asyncio
7
+ import logging
8
8
 
9
9
  # ------------------------------------------------------------------ #
10
10
  # Local import #
@@ -12,184 +12,382 @@ import asyncio
12
12
  from .base_transport import MCPBaseTransport
13
13
 
14
14
  # ------------------------------------------------------------------ #
15
- # chuk-protocol imports #
15
+ # New chuk-mcp API imports only #
16
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
17
+ from chuk_mcp.transports.stdio import stdio_client
18
+ from chuk_mcp.transports.stdio.parameters import StdioParameters
20
19
 
21
- # tools
22
- from chuk_mcp.mcp_client.messages.tools.send_messages import (
23
- send_tools_call,
20
+ from chuk_mcp.protocol.messages import (
21
+ send_initialize,
22
+ send_ping,
24
23
  send_tools_list,
24
+ send_tools_call,
25
25
  )
26
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
- )
27
+ # Try to import resources and prompts if available
28
+ try:
29
+ from chuk_mcp.protocol.messages import (
30
+ send_resources_list,
31
+ send_resources_read,
32
+ )
33
+ HAS_RESOURCES = True
34
+ except ImportError:
35
+ HAS_RESOURCES = False
36
+
37
+ try:
38
+ from chuk_mcp.protocol.messages import (
39
+ send_prompts_list,
40
+ send_prompts_get,
41
+ )
42
+ HAS_PROMPTS = True
43
+ except ImportError:
44
+ HAS_PROMPTS = False
45
+
46
+ logger = logging.getLogger(__name__)
34
47
 
35
48
 
36
49
  class StdioTransport(MCPBaseTransport):
37
50
  """
38
- Stdio transport for MCP communication.
51
+ STDIO transport for MCP communication using new chuk-mcp APIs.
39
52
  """
40
53
 
41
54
  def __init__(self, server_params):
42
- self.server_params = server_params
55
+ """
56
+ Initialize STDIO transport.
57
+
58
+ Args:
59
+ server_params: Either a dict with 'command' and 'args',
60
+ or a StdioParameters object
61
+ """
62
+ # Convert dict format to StdioParameters
63
+ if isinstance(server_params, dict):
64
+ self.server_params = StdioParameters(
65
+ command=server_params.get('command', 'python'),
66
+ args=server_params.get('args', [])
67
+ )
68
+ else:
69
+ self.server_params = server_params
70
+
43
71
  self.read_stream = None
44
72
  self.write_stream = None
45
- self._context_stack: Optional[AsyncExitStack] = None
73
+ self._client_context = None
46
74
 
47
75
  # --------------------------------------------------------------------- #
48
76
  # Connection management #
49
77
  # --------------------------------------------------------------------- #
50
78
  async def initialize(self) -> bool:
79
+ """Initialize the STDIO transport."""
51
80
  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
-
81
+ logger.info("Initializing STDIO transport...")
82
+
83
+ # Use the new stdio_client context manager
84
+ self._client_context = stdio_client(self.server_params)
85
+ self.read_stream, self.write_stream = await self._client_context.__aenter__()
86
+
87
+ # Send initialize message
88
+ logger.debug("Sending initialize message...")
60
89
  init_result = await send_initialize(self.read_stream, self.write_stream)
61
- return bool(init_result)
90
+
91
+ if init_result:
92
+ logger.info("STDIO transport initialized successfully")
93
+ return True
94
+ else:
95
+ logger.error("Initialize message failed")
96
+ await self._cleanup()
97
+ return False
62
98
 
63
- except Exception as e: # pragma: no cover
64
- import logging
99
+ except Exception as e:
100
+ logger.error(f"Error initializing STDIO transport: {e}", exc_info=True)
101
+ await self._cleanup()
102
+ return False
65
103
 
66
- logging.error(f"Error initializing stdio transport: {e}")
67
- if self._context_stack:
104
+ async def close(self) -> None:
105
+ """Close the transport with proper error handling."""
106
+ try:
107
+ # Handle both old _context_stack and new _client_context for test compatibility
108
+ if hasattr(self, '_context_stack') and self._context_stack:
68
109
  try:
69
110
  await self._context_stack.__aexit__(None, None, None)
70
- except Exception:
71
- pass
72
- return False
111
+ logger.debug("Context stack closed")
112
+ except asyncio.CancelledError:
113
+ # Expected during shutdown - don't log as error
114
+ logger.debug("Context stack close cancelled during shutdown")
115
+ except Exception as e:
116
+ logger.error(f"Error closing context stack: {e}")
117
+ elif self._client_context:
118
+ try:
119
+ await self._client_context.__aexit__(None, None, None)
120
+ logger.debug("STDIO client context closed")
121
+ except asyncio.CancelledError:
122
+ # Expected during shutdown - don't log as error
123
+ logger.debug("Client context close cancelled during shutdown")
124
+ except Exception as e:
125
+ logger.error(f"Error closing client context: {e}")
126
+ except Exception as e:
127
+ logger.error(f"Error during transport cleanup: {e}")
128
+ finally:
129
+ await self._cleanup()
73
130
 
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
131
+ async def _cleanup(self) -> None:
132
+ """Internal cleanup method."""
133
+ # Clean up both old and new context attributes for test compatibility
134
+ if hasattr(self, '_context_stack'):
135
+ self._context_stack = None
136
+ self._client_context = None
78
137
  self.read_stream = None
79
138
  self.write_stream = None
80
139
 
81
140
  # --------------------------------------------------------------------- #
82
- # Utility #
141
+ # Core MCP Operations #
83
142
  # --------------------------------------------------------------------- #
84
143
  async def send_ping(self) -> bool:
144
+ """Send a ping."""
85
145
  if not self.read_stream or not self.write_stream:
146
+ logger.error("Cannot send ping: streams not available")
147
+ return False
148
+
149
+ try:
150
+ result = await send_ping(self.read_stream, self.write_stream)
151
+ logger.debug(f"Ping result: {result}")
152
+ return bool(result)
153
+ except Exception as e:
154
+ logger.error(f"Ping failed: {e}")
86
155
  return False
87
- return await send_ping(self.read_stream, self.write_stream)
88
156
 
89
157
  async def get_tools(self) -> List[Dict[str, Any]]:
158
+ """Get list of available tools."""
90
159
  if not self.read_stream or not self.write_stream:
160
+ logger.error("Cannot get tools: streams not available")
161
+ return []
162
+
163
+ try:
164
+ tools_response = await send_tools_list(self.read_stream, self.write_stream)
165
+
166
+ # Handle both dict response and direct tools list
167
+ if isinstance(tools_response, dict):
168
+ tools = tools_response.get("tools", [])
169
+ elif isinstance(tools_response, list):
170
+ tools = tools_response
171
+ else:
172
+ logger.warning(f"Unexpected tools response type: {type(tools_response)}")
173
+ tools = []
174
+
175
+ logger.debug(f"Retrieved {len(tools)} tools")
176
+ return tools
177
+
178
+ except Exception as e:
179
+ logger.error(f"Error getting tools: {e}")
91
180
  return []
92
- tools_response = await send_tools_list(self.read_stream, self.write_stream)
93
- return tools_response.get("tools", [])
94
181
 
95
- # NEW ------------------------------------------------------------------ #
96
- # Resources / Prompts #
97
- # --------------------------------------------------------------------- #
98
- async def list_resources(self) -> Dict[str, Any]:
182
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
99
183
  """
100
- Return the result of *resources/list*. If the connection is not yet
101
- initialised an empty dict is returned.
184
+ Execute a tool.
185
+
186
+ Returns normalized response in format:
187
+ {
188
+ "isError": bool,
189
+ "content": Any, # Result data if successful
190
+ "error": str # Error message if failed
191
+ }
102
192
  """
103
193
  if not self.read_stream or not self.write_stream:
194
+ return {
195
+ "isError": True,
196
+ "error": "Transport not initialized"
197
+ }
198
+
199
+ try:
200
+ logger.debug(f"Calling tool {tool_name} with args: {arguments}")
201
+
202
+ raw_response = await send_tools_call(
203
+ self.read_stream,
204
+ self.write_stream,
205
+ tool_name,
206
+ arguments
207
+ )
208
+
209
+ logger.debug(f"Tool {tool_name} raw response: {raw_response}")
210
+ return self._normalize_tool_response(raw_response)
211
+
212
+ except Exception as e:
213
+ logger.error(f"Error calling tool {tool_name}: {e}")
214
+ return {
215
+ "isError": True,
216
+ "error": f"Tool execution failed: {str(e)}"
217
+ }
218
+
219
+ def _normalize_tool_response(self, raw_response: Dict[str, Any]) -> Dict[str, Any]:
220
+ """Normalize tool response to consistent format."""
221
+ # Handle explicit error in response
222
+ if "error" in raw_response:
223
+ error_info = raw_response["error"]
224
+ if isinstance(error_info, dict):
225
+ error_msg = error_info.get("message", "Unknown error")
226
+ else:
227
+ error_msg = str(error_info)
228
+
229
+ return {
230
+ "isError": True,
231
+ "error": error_msg
232
+ }
233
+
234
+ # Handle successful response with result (MCP standard)
235
+ if "result" in raw_response:
236
+ result = raw_response["result"]
237
+
238
+ # If result has content, extract it
239
+ if isinstance(result, dict) and "content" in result:
240
+ return {
241
+ "isError": False,
242
+ "content": self._extract_content(result["content"])
243
+ }
244
+ else:
245
+ return {
246
+ "isError": False,
247
+ "content": result
248
+ }
249
+
250
+ # Handle direct content-based response
251
+ if "content" in raw_response:
252
+ return {
253
+ "isError": False,
254
+ "content": self._extract_content(raw_response["content"])
255
+ }
256
+
257
+ # Fallback: return whatever the server sent
258
+ return {
259
+ "isError": False,
260
+ "content": raw_response
261
+ }
262
+
263
+ def _extract_content(self, content_list: Any) -> Any:
264
+ """Extract content from MCP content list format."""
265
+ if not isinstance(content_list, list) or not content_list:
266
+ return content_list
267
+
268
+ # Handle single content item (most common)
269
+ if len(content_list) == 1:
270
+ content_item = content_list[0]
271
+ if isinstance(content_item, dict):
272
+ if content_item.get("type") == "text":
273
+ text_content = content_item.get("text", "")
274
+ # Try to parse JSON, fall back to plain text
275
+ try:
276
+ return json.loads(text_content)
277
+ except json.JSONDecodeError:
278
+ return text_content
279
+ else:
280
+ # Non-text content (image, audio, etc.)
281
+ return content_item
282
+
283
+ # Multiple content items - return the list
284
+ return content_list
285
+
286
+ # --------------------------------------------------------------------- #
287
+ # Resources Operations (if available) #
288
+ # --------------------------------------------------------------------- #
289
+ async def list_resources(self) -> Dict[str, Any]:
290
+ """Get list of available resources."""
291
+ if not HAS_RESOURCES:
292
+ logger.warning("Resources not supported in current chuk-mcp version")
293
+ return {}
294
+
295
+ if not self.read_stream or not self.write_stream:
296
+ logger.error("Cannot list resources: streams not available")
104
297
  return {}
298
+
105
299
  try:
106
- return await send_resources_list(self.read_stream, self.write_stream)
107
- except Exception as exc: # pragma: no cover
108
- import logging
300
+ response = await send_resources_list(self.read_stream, self.write_stream)
301
+ return response if isinstance(response, dict) else {}
302
+ except Exception as e:
303
+ logger.error(f"Error listing resources: {e}")
304
+ return {}
109
305
 
110
- logging.error(f"Error listing resources: {exc}")
306
+ async def read_resource(self, uri: str) -> Dict[str, Any]:
307
+ """Read a specific resource by URI."""
308
+ if not HAS_RESOURCES:
309
+ logger.warning("Resources not supported in current chuk-mcp version")
310
+ return {}
311
+
312
+ if not self.read_stream or not self.write_stream:
313
+ logger.error("Cannot read resource: streams not available")
314
+ return {}
315
+
316
+ try:
317
+ response = await send_resources_read(self.read_stream, self.write_stream, uri)
318
+ return response if isinstance(response, dict) else {}
319
+ except Exception as e:
320
+ logger.error(f"Error reading resource {uri}: {e}")
111
321
  return {}
112
322
 
323
+ # --------------------------------------------------------------------- #
324
+ # Prompts Operations (if available) #
325
+ # --------------------------------------------------------------------- #
113
326
  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
- """
327
+ """Get list of available prompts."""
328
+ if not HAS_PROMPTS:
329
+ logger.warning("Prompts not supported in current chuk-mcp version")
330
+ return {}
331
+
118
332
  if not self.read_stream or not self.write_stream:
333
+ logger.error("Cannot list prompts: streams not available")
119
334
  return {}
335
+
120
336
  try:
121
- return await send_prompts_list(self.read_stream, self.write_stream)
122
- except Exception as exc: # pragma: no cover
123
- import logging
337
+ response = await send_prompts_list(self.read_stream, self.write_stream)
338
+ return response if isinstance(response, dict) else {}
339
+ except Exception as e:
340
+ logger.error(f"Error listing prompts: {e}")
341
+ return {}
124
342
 
125
- logging.error(f"Error listing prompts: {exc}")
343
+ async def get_prompt(self, name: str, arguments: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
344
+ """Get a specific prompt by name."""
345
+ if not HAS_PROMPTS:
346
+ logger.warning("Prompts not supported in current chuk-mcp version")
347
+ return {}
348
+
349
+ if not self.read_stream or not self.write_stream:
350
+ logger.error("Cannot get prompt: streams not available")
351
+ return {}
352
+
353
+ try:
354
+ response = await send_prompts_get(
355
+ self.read_stream,
356
+ self.write_stream,
357
+ name,
358
+ arguments or {}
359
+ )
360
+ return response if isinstance(response, dict) else {}
361
+ except Exception as e:
362
+ logger.error(f"Error getting prompt {name}: {e}")
126
363
  return {}
127
364
 
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
- """
365
+ # --------------------------------------------------------------------- #
366
+ # Utility Methods #
367
+ # --------------------------------------------------------------------- #
368
+ def get_streams(self) -> List[tuple]:
369
+ """Get the underlying streams for advanced usage."""
135
370
  if self.read_stream and self.write_stream:
136
371
  return [(self.read_stream, self.write_stream)]
137
372
  return []
138
373
 
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"}
374
+ def is_connected(self) -> bool:
375
+ """Check if transport is connected and ready."""
376
+ return self.read_stream is not None and self.write_stream is not None
157
377
 
158
- try:
159
- raw = await send_tools_call(
160
- self.read_stream, self.write_stream, tool_name, arguments
161
- )
378
+ async def __aenter__(self):
379
+ """Async context manager entry."""
380
+ success = await self.initialize()
381
+ if not success:
382
+ raise RuntimeError("Failed to initialize STDIO transport")
383
+ return self
162
384
 
163
- # Handle explicit error wrapper
164
- if "error" in raw:
165
- return {
166
- "isError": True,
167
- "error": raw["error"].get("message", "Unknown error"),
168
- }
385
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
386
+ """Async context manager exit."""
387
+ await self.close()
169
388
 
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)}
389
+ def __repr__(self) -> str:
390
+ """String representation for debugging."""
391
+ status = "connected" if self.is_connected() else "disconnected"
392
+ cmd_info = f"command={getattr(self.server_params, 'command', 'unknown')}"
393
+ 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.2
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
@@ -25,7 +25,7 @@ chuk_tool_processor/mcp/stream_manager.py,sha256=FRTvvSBzBxU6-kPU1mZOjGCaqi8hHk5
25
25
  chuk_tool_processor/mcp/transport/__init__.py,sha256=7QQqeSKVKv0N9GcyJuYF0R4FDZeooii5RjggvFFg5GY,296
26
26
  chuk_tool_processor/mcp/transport/base_transport.py,sha256=bqId34OMQMxzMXtrKq_86sot0_x0NS_ecaIllsCyy6I,3423
27
27
  chuk_tool_processor/mcp/transport/sse_transport.py,sha256=nzzMgCgfUV_-Owga2rqJFEc5WLdgXZ922V9maLMwRBI,19408
28
- chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=5ocANj4wWnYxOZVuz2ttxfyMq3CHkznvIXBtCdBTJvo,7727
28
+ chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=YIXVd2Lwjl0wT1Jdi3MYzeaBmIE_ae-WgsAQQKMDCI8,15209
29
29
  chuk_tool_processor/models/__init__.py,sha256=TC__rdVa0lQsmJHM_hbLDPRgToa_pQT_UxRcPZk6iVw,40
30
30
  chuk_tool_processor/models/execution_strategy.py,sha256=UVW35YIeMY2B3mpIKZD2rAkyOPayI6ckOOUALyf0YiQ,2115
31
31
  chuk_tool_processor/models/streaming_tool.py,sha256=0v2PSPTgZ5TS_PpVdohvVhh99fPwPQM_R_z4RU0mlLM,3541
@@ -52,7 +52,7 @@ chuk_tool_processor/registry/providers/__init__.py,sha256=eigwG_So11j7WbDGSWaKd3
52
52
  chuk_tool_processor/registry/providers/memory.py,sha256=6cMtUwLO6zrk3pguQRgxJ2CReHAzewgZsizWZhsoStk,5184
53
53
  chuk_tool_processor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
54
54
  chuk_tool_processor/utils/validation.py,sha256=V5N1dH9sJlHepFIbiI2k2MU82o7nvnh0hKyIt2jdgww,4136
55
- chuk_tool_processor-0.5.dist-info/METADATA,sha256=fo__t2M-CSnUDOPMgmibn8oGbI_zmSEfmtBSRG40OYw,24279
56
- chuk_tool_processor-0.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
- chuk_tool_processor-0.5.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
58
- chuk_tool_processor-0.5.dist-info/RECORD,,
55
+ chuk_tool_processor-0.5.2.dist-info/METADATA,sha256=fy84gk4wov0A1cSWhXpb3AKsmxr8A4uOOn_XxHvLKIo,24283
56
+ chuk_tool_processor-0.5.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
57
+ chuk_tool_processor-0.5.2.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
58
+ chuk_tool_processor-0.5.2.dist-info/RECORD,,