chuk-tool-processor 0.1.3__py3-none-any.whl → 0.1.5__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,59 +1,189 @@
1
1
  # chuk_tool_processor/mcp/transport/sse_transport.py
2
2
  """
3
- Server-Sent Events (SSE) transport for MCP communication.
3
+ Server-Sent Events (SSE) transport for MCP communication – implemented with **httpx**.
4
4
  """
5
+ from __future__ import annotations
6
+
7
+ import asyncio
8
+ import contextlib
9
+ import json
5
10
  from typing import Any, Dict, List, Optional
6
11
 
7
- # imports
12
+ import httpx
13
+
8
14
  from .base_transport import MCPBaseTransport
9
15
 
16
+ # --------------------------------------------------------------------------- #
17
+ # Helpers #
18
+ # --------------------------------------------------------------------------- #
19
+ DEFAULT_TIMEOUT = 5.0 # seconds
20
+ HEADERS_JSON: Dict[str, str] = {"accept": "application/json"}
21
+
22
+
23
+ def _url(base: str, path: str) -> str:
24
+ """Join *base* and *path* with exactly one slash."""
25
+ return f"{base.rstrip('/')}/{path.lstrip('/')}"
26
+
27
+
28
+ # --------------------------------------------------------------------------- #
29
+ # Transport #
30
+ # --------------------------------------------------------------------------- #
10
31
  class SSETransport(MCPBaseTransport):
11
32
  """
12
- Server-Sent Events (SSE) transport for MCP communication.
33
+ Minimal SSE/REST transport. It speaks a simple REST dialect:
34
+
35
+ GET /ping → 200 OK
36
+ GET /tools/list → {"tools": [...]}
37
+ POST /tools/call → {"name": ..., "result": ...}
38
+ GET /resources/list → {"resources": [...]}
39
+ GET /prompts/list → {"prompts": [...]}
40
+ GET /events → <text/event-stream>
13
41
  """
14
-
15
- def __init__(self, url: str, api_key: Optional[str] = None):
16
- """
17
- Initialize the SSE transport.
18
-
19
- Args:
20
- url: Server URL
21
- api_key: Optional API key
22
- """
23
- self.url = url
42
+
43
+ EVENTS_PATH = "/events"
44
+
45
+ # ------------------------------------------------------------------ #
46
+ # Construction #
47
+ # ------------------------------------------------------------------ #
48
+ def __init__(self, url: str, api_key: Optional[str] = None) -> None:
49
+ self.base_url = url.rstrip("/")
24
50
  self.api_key = api_key
25
- self.session = None
26
- self.connection_id = None
27
-
51
+
52
+ # httpx client (None until initialise)
53
+ self._client: httpx.AsyncClient | None = None
54
+ self.session: httpx.AsyncClient | None = None # ← kept for legacy tests
55
+
56
+ # background reader
57
+ self._reader_task: asyncio.Task | None = None
58
+ self._incoming_queue: "asyncio.Queue[dict[str, Any]]" = asyncio.Queue()
59
+
60
+ # ------------------------------------------------------------------ #
61
+ # Life-cycle #
62
+ # ------------------------------------------------------------------ #
28
63
  async def initialize(self) -> bool:
29
- """
30
- Initialize the SSE connection.
31
-
32
- Returns:
33
- True if successful, False otherwise
34
- """
35
- # TODO: Implement SSE connection logic
36
- # This is currently a placeholder
37
- import logging
38
- logging.info(f"SSE transport not yet implemented for {self.url}")
39
- return False
40
-
64
+ """Open the httpx client and start the /events consumer."""
65
+ if self._client: # already initialised
66
+ return True
67
+
68
+ self._client = httpx.AsyncClient(
69
+ headers={"authorization": self.api_key} if self.api_key else None,
70
+ timeout=DEFAULT_TIMEOUT,
71
+ )
72
+ self.session = self._client # legacy attribute for tests
73
+
74
+ # spawn reader (best-effort reconnect)
75
+ self._reader_task = asyncio.create_task(self._consume_events(), name="sse-reader")
76
+
77
+ # verify connection
78
+ return await self.send_ping()
79
+
80
+ async def close(self) -> None:
81
+ """Stop background reader and close the httpx client."""
82
+ if self._reader_task:
83
+ self._reader_task.cancel()
84
+ with contextlib.suppress(asyncio.CancelledError):
85
+ await self._reader_task
86
+ self._reader_task = None
87
+
88
+ if self._client:
89
+ await self._client.aclose()
90
+ self._client = None
91
+ self.session = None # keep tests happy
92
+
93
+ # ------------------------------------------------------------------ #
94
+ # Internal helpers #
95
+ # ------------------------------------------------------------------ #
96
+ async def _get_json(self, path: str) -> Any:
97
+ if not self._client:
98
+ raise RuntimeError("Transport not initialised")
99
+
100
+ resp = await self._client.get(_url(self.base_url, path), headers=HEADERS_JSON)
101
+ resp.raise_for_status()
102
+ return resp.json()
103
+
104
+ async def _post_json(self, path: str, payload: Dict[str, Any]) -> Any:
105
+ if not self._client:
106
+ raise RuntimeError("Transport not initialised")
107
+
108
+ resp = await self._client.post(
109
+ _url(self.base_url, path), json=payload, headers=HEADERS_JSON
110
+ )
111
+ resp.raise_for_status()
112
+ return resp.json()
113
+
114
+ # ------------------------------------------------------------------ #
115
+ # Public API (implements MCPBaseTransport) #
116
+ # ------------------------------------------------------------------ #
41
117
  async def send_ping(self) -> bool:
42
- """Send a ping message."""
43
- # TODO: Implement SSE ping logic
44
- return False
45
-
118
+ if not self._client:
119
+ return False
120
+ try:
121
+ await self._get_json("/ping")
122
+ return True
123
+ except Exception: # pragma: no cover
124
+ return False
125
+
46
126
  async def get_tools(self) -> List[Dict[str, Any]]:
47
- """Get available tools."""
48
- # TODO: Implement SSE tool retrieval logic
49
- return []
50
-
127
+ if not self._client:
128
+ return []
129
+ try:
130
+ data = await self._get_json("/tools/list")
131
+ return data.get("tools", []) if isinstance(data, dict) else []
132
+ except Exception: # pragma: no cover
133
+ return []
134
+
51
135
  async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
52
- """Call a tool via SSE."""
53
- # TODO: Implement SSE tool calling logic
54
- return {"isError": True, "error": "SSE transport not implemented"}
55
-
56
- async def close(self) -> None:
57
- """Close the SSE connection."""
58
- # TODO: Implement SSE connection closure logic
59
- pass
136
+ # ─── tests expect this specific message if *not* initialised ───
137
+ if not self._client:
138
+ return {"isError": True, "error": "SSE transport not implemented"}
139
+
140
+ try:
141
+ payload = {"name": tool_name, "arguments": arguments}
142
+ return await self._post_json("/tools/call", payload)
143
+ except Exception as exc: # pragma: no cover
144
+ return {"isError": True, "error": str(exc)}
145
+
146
+ # ----------------------- extras used by StreamManager ------------- #
147
+ async def list_resources(self) -> List[Dict[str, Any]]:
148
+ if not self._client:
149
+ return []
150
+ try:
151
+ data = await self._get_json("/resources/list")
152
+ return data.get("resources", []) if isinstance(data, dict) else []
153
+ except Exception: # pragma: no cover
154
+ return []
155
+
156
+ async def list_prompts(self) -> List[Dict[str, Any]]:
157
+ if not self._client:
158
+ return []
159
+ try:
160
+ data = await self._get_json("/prompts/list")
161
+ return data.get("prompts", []) if isinstance(data, dict) else []
162
+ except Exception: # pragma: no cover
163
+ return []
164
+
165
+ # ------------------------------------------------------------------ #
166
+ # Background event-stream reader #
167
+ # ------------------------------------------------------------------ #
168
+ async def _consume_events(self) -> None: # pragma: no cover
169
+ """Continuously read `/events` and push JSON objects onto a queue."""
170
+ if not self._client:
171
+ return
172
+
173
+ while True:
174
+ try:
175
+ async with self._client.stream(
176
+ "GET", _url(self.base_url, self.EVENTS_PATH), headers=HEADERS_JSON
177
+ ) as resp:
178
+ resp.raise_for_status()
179
+ async for line in resp.aiter_lines():
180
+ if not line:
181
+ continue
182
+ try:
183
+ await self._incoming_queue.put(json.loads(line))
184
+ except json.JSONDecodeError:
185
+ continue
186
+ except asyncio.CancelledError:
187
+ break
188
+ except Exception:
189
+ await asyncio.sleep(1.0) # back-off and retry
@@ -1,15 +1,35 @@
1
1
  # chuk_tool_processor/mcp/transport/stdio_transport.py
2
- from typing import Dict, Any, List, Optional
2
+ from __future__ import annotations
3
+
3
4
  from contextlib import AsyncExitStack
4
5
  import json
6
+ from typing import Dict, Any, List, Optional
5
7
 
8
+ # ------------------------------------------------------------------ #
9
+ # Local import #
10
+ # ------------------------------------------------------------------ #
6
11
  from .base_transport import MCPBaseTransport
7
12
 
8
- # chuk-protocol imports
13
+ # ------------------------------------------------------------------ #
14
+ # chuk-protocol imports #
15
+ # ------------------------------------------------------------------ #
9
16
  from chuk_mcp.mcp_client.transport.stdio.stdio_client import stdio_client
10
17
  from chuk_mcp.mcp_client.messages.initialize.send_messages import send_initialize
11
18
  from chuk_mcp.mcp_client.messages.ping.send_messages import send_ping
12
- from chuk_mcp.mcp_client.messages.tools.send_messages import send_tools_call, send_tools_list
19
+
20
+ # tools
21
+ from chuk_mcp.mcp_client.messages.tools.send_messages import (
22
+ send_tools_call,
23
+ send_tools_list,
24
+ )
25
+
26
+ # NEW: resources & prompts
27
+ from chuk_mcp.mcp_client.messages.resources.send_messages import (
28
+ send_resources_list,
29
+ )
30
+ from chuk_mcp.mcp_client.messages.prompts.send_messages import (
31
+ send_prompts_list,
32
+ )
13
33
 
14
34
 
15
35
  class StdioTransport(MCPBaseTransport):
@@ -32,7 +52,9 @@ class StdioTransport(MCPBaseTransport):
32
52
  await self._context_stack.__aenter__()
33
53
 
34
54
  ctx = stdio_client(self.server_params)
35
- self.read_stream, self.write_stream = await self._context_stack.enter_async_context(ctx)
55
+ self.read_stream, self.write_stream = await self._context_stack.enter_async_context(
56
+ ctx
57
+ )
36
58
 
37
59
  init_result = await send_initialize(self.read_stream, self.write_stream)
38
60
  return bool(init_result)
@@ -72,10 +94,56 @@ class StdioTransport(MCPBaseTransport):
72
94
  tools_response = await send_tools_list(self.read_stream, self.write_stream)
73
95
  return tools_response.get("tools", [])
74
96
 
97
+ # NEW ------------------------------------------------------------------ #
98
+ # Resources / Prompts #
99
+ # --------------------------------------------------------------------- #
100
+ async def list_resources(self) -> Dict[str, Any]:
101
+ """
102
+ Return the result of *resources/list*. If the connection is not yet
103
+ initialised an empty dict is returned.
104
+ """
105
+ if not self.read_stream or not self.write_stream:
106
+ return {}
107
+ try:
108
+ return await send_resources_list(self.read_stream, self.write_stream)
109
+ except Exception as exc: # pragma: no cover
110
+ import logging
111
+
112
+ logging.error(f"Error listing resources: {exc}")
113
+ return {}
114
+
115
+ async def list_prompts(self) -> Dict[str, Any]:
116
+ """
117
+ Return the result of *prompts/list*. If the connection is not yet
118
+ initialised an empty dict is returned.
119
+ """
120
+ if not self.read_stream or not self.write_stream:
121
+ return {}
122
+ try:
123
+ return await send_prompts_list(self.read_stream, self.write_stream)
124
+ except Exception as exc: # pragma: no cover
125
+ import logging
126
+
127
+ logging.error(f"Error listing prompts: {exc}")
128
+ return {}
129
+
130
+ # OPTIONAL helper ------------------------------------------------------ #
131
+ def get_streams(self):
132
+ """
133
+ Expose the low-level streams so legacy callers can access them
134
+ directly. The base-class’ default returns an empty list; here we
135
+ return a single-element list when the transport is active.
136
+ """
137
+ if self.read_stream and self.write_stream:
138
+ return [(self.read_stream, self.write_stream)]
139
+ return []
140
+
75
141
  # --------------------------------------------------------------------- #
76
142
  # Main entry-point #
77
143
  # --------------------------------------------------------------------- #
78
- async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
144
+ async def call_tool(
145
+ self, tool_name: str, arguments: Dict[str, Any]
146
+ ) -> Dict[str, Any]:
79
147
  """
80
148
  Execute *tool_name* with *arguments* and normalise the server’s reply.
81
149
 
@@ -90,12 +158,16 @@ class StdioTransport(MCPBaseTransport):
90
158
  return {"isError": True, "error": "Transport not initialized"}
91
159
 
92
160
  try:
93
- raw = await send_tools_call(self.read_stream, self.write_stream, tool_name, arguments)
161
+ raw = await send_tools_call(
162
+ self.read_stream, self.write_stream, tool_name, arguments
163
+ )
94
164
 
95
165
  # Handle explicit error wrapper
96
166
  if "error" in raw:
97
- return {"isError": True,
98
- "error": raw["error"].get("message", "Unknown error")}
167
+ return {
168
+ "isError": True,
169
+ "error": raw["error"].get("message", "Unknown error"),
170
+ }
99
171
 
100
172
  # Preferred: servers that put the answer under "result"
101
173
  if "result" in raw:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -1,7 +1,7 @@
1
1
  chuk_tool_processor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  chuk_tool_processor/core/__init__.py,sha256=slM7pZna88tyZrF3KtN22ApYyCqGNt5Yscv-knsLOOA,38
3
3
  chuk_tool_processor/core/exceptions.py,sha256=h4zL1jpCY1Ud1wT8xDeMxZ8GR8ttmkObcv36peUHJEA,1571
4
- chuk_tool_processor/core/processor.py,sha256=ud7ezONnUFh_aDSapiBGNx-LtZfhAFpYjFuw2m_tFXk,10165
4
+ chuk_tool_processor/core/processor.py,sha256=fT3Qj8vmeUWoqOqHmWroi7lfJsMl52DpFQz8LT9UQME,10280
5
5
  chuk_tool_processor/execution/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  chuk_tool_processor/execution/tool_executor.py,sha256=e1EHE-744uJuB1XeZZF_6VT25Yg1RCd8XI3v8uOrOSo,1794
7
7
  chuk_tool_processor/execution/strategies/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -21,11 +21,11 @@ chuk_tool_processor/mcp/mcp_tool.py,sha256=TvZEudgQvaev2jaPw6OGsqAR5GNu6_cPaUCgq
21
21
  chuk_tool_processor/mcp/register_mcp_tools.py,sha256=ofE7pEn6sKDH8HWvNamVOaXsitLOaG48M5GhcpqCBbs,2801
22
22
  chuk_tool_processor/mcp/setup_mcp_sse.py,sha256=Ep2IKRdH1Y299bCxt9G0NtwnsvguYP6mpraZyUJ8OKU,2643
23
23
  chuk_tool_processor/mcp/setup_mcp_stdio.py,sha256=NjTvAFqQHxxN3XubsTgYY3lTrvPVWlnwCzkzbz7WE_M,2747
24
- chuk_tool_processor/mcp/stream_manager.py,sha256=xdTXDJ08pVpHXvxao0ibXezGqqauMBLXpJhIXgGknOs,10847
24
+ chuk_tool_processor/mcp/stream_manager.py,sha256=qIWzsQCTlu1SQQBExAdvBHGB3T5isQDyMhj29WkfbKQ,11779
25
25
  chuk_tool_processor/mcp/transport/__init__.py,sha256=7QQqeSKVKv0N9GcyJuYF0R4FDZeooii5RjggvFFg5GY,296
26
- chuk_tool_processor/mcp/transport/base_transport.py,sha256=uJcbyHYrw_zpE5Rc9wDo6yT0mmwqwhFXXbHIJxPoOac,1379
27
- chuk_tool_processor/mcp/transport/sse_transport.py,sha256=BcRRiOEDRiXiVK2rySB0Hm_dITDNHzrCd2h28Yv1r5c,1791
28
- chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=VxQYbN0jAyeOrQODZtTvityYRYUnbQHz3jc_eMTlv3I,5197
26
+ chuk_tool_processor/mcp/transport/base_transport.py,sha256=1E29LjWw5vLQrPUDF_9TJt63P5dxAAN7n6E_KiZbGUY,3427
27
+ chuk_tool_processor/mcp/transport/sse_transport.py,sha256=bryH9DOWOn5qr6LsimTriukDC4ix2kuRq6bUv9qOV20,7645
28
+ chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=lFXL7p8ca4z_J0RBL8UCHrQ1UH7C2-LbC0tZhpya4V4,7763
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=ZPHysmKNHqJmahTtUXAbt1ke09vxy7EhZcsrwTdla8o,508
31
31
  chuk_tool_processor/models/tool_call.py,sha256=RZOnx2YczkJN6ym2PLiI4CRzP2qU_5hpMtHxMcFOxY4,298
@@ -51,7 +51,7 @@ chuk_tool_processor/registry/providers/__init__.py,sha256=_0dg4YhyfAV0TXuR_i4ewX
51
51
  chuk_tool_processor/registry/providers/memory.py,sha256=29aI5uvykjDmn9ymIukEdUtmTC9SXOAsDu9hw36XF44,4474
52
52
  chuk_tool_processor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
53
  chuk_tool_processor/utils/validation.py,sha256=7ezn_o-3IHDrzOD3j6ttsAn2s3zS-jIjeBTuqicrs6A,3775
54
- chuk_tool_processor-0.1.3.dist-info/METADATA,sha256=lcYGBykoz2doY3Z8cR_SxyvdbXGgmDUUdPzIgLwP8wM,13703
55
- chuk_tool_processor-0.1.3.dist-info/WHEEL,sha256=wXxTzcEDnjrTwFYjLPcsW_7_XihufBwmpiBeiXNBGEA,91
56
- chuk_tool_processor-0.1.3.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
57
- chuk_tool_processor-0.1.3.dist-info/RECORD,,
54
+ chuk_tool_processor-0.1.5.dist-info/METADATA,sha256=GOztJXq4-Ro6ncQxUvtV8L97qXyn6_Qet0d1tzRe05g,13703
55
+ chuk_tool_processor-0.1.5.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
56
+ chuk_tool_processor-0.1.5.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
57
+ chuk_tool_processor-0.1.5.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.1.0)
2
+ Generator: setuptools (80.3.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5