chuk-tool-processor 0.1.3__py3-none-any.whl → 0.1.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor/mcp/stream_manager.py +208 -218
- chuk_tool_processor/mcp/transport/base_transport.py +73 -34
- chuk_tool_processor/mcp/transport/sse_transport.py +174 -44
- chuk_tool_processor/mcp/transport/stdio_transport.py +80 -8
- {chuk_tool_processor-0.1.3.dist-info → chuk_tool_processor-0.1.4.dist-info}/METADATA +1 -1
- {chuk_tool_processor-0.1.3.dist-info → chuk_tool_processor-0.1.4.dist-info}/RECORD +8 -8
- {chuk_tool_processor-0.1.3.dist-info → chuk_tool_processor-0.1.4.dist-info}/WHEEL +1 -1
- {chuk_tool_processor-0.1.3.dist-info → chuk_tool_processor-0.1.4.dist-info}/top_level.txt +0 -0
|
@@ -5,299 +5,289 @@ StreamManager for CHUK Tool Processor.
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
|
-
import
|
|
9
|
-
from typing import Dict, List, Optional, Any
|
|
8
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
10
9
|
|
|
11
|
-
#
|
|
10
|
+
# --------------------------------------------------------------------------- #
|
|
11
|
+
# CHUK imports #
|
|
12
|
+
# --------------------------------------------------------------------------- #
|
|
12
13
|
from chuk_mcp.config import load_config
|
|
13
|
-
from chuk_tool_processor.mcp.transport import
|
|
14
|
+
from chuk_tool_processor.mcp.transport import (
|
|
15
|
+
MCPBaseTransport,
|
|
16
|
+
StdioTransport,
|
|
17
|
+
SSETransport,
|
|
18
|
+
)
|
|
14
19
|
from chuk_tool_processor.logging import get_logger
|
|
15
20
|
|
|
16
|
-
# logger
|
|
17
21
|
logger = get_logger("chuk_tool_processor.mcp.stream_manager")
|
|
18
22
|
|
|
23
|
+
|
|
19
24
|
class StreamManager:
|
|
20
25
|
"""
|
|
21
26
|
Manager for MCP server streams with support for multiple transport types.
|
|
22
27
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------------ #
|
|
30
|
+
# construction #
|
|
31
|
+
# ------------------------------------------------------------------ #
|
|
32
|
+
def __init__(self) -> None:
|
|
26
33
|
self.transports: Dict[str, MCPBaseTransport] = {}
|
|
27
34
|
self.server_info: List[Dict[str, Any]] = []
|
|
28
35
|
self.tool_to_server_map: Dict[str, str] = {}
|
|
29
36
|
self.server_names: Dict[int, str] = {}
|
|
30
37
|
self.all_tools: List[Dict[str, Any]] = []
|
|
31
38
|
self._lock = asyncio.Lock()
|
|
32
|
-
|
|
39
|
+
|
|
40
|
+
# ------------------------------------------------------------------ #
|
|
41
|
+
# factory helpers #
|
|
42
|
+
# ------------------------------------------------------------------ #
|
|
33
43
|
@classmethod
|
|
34
44
|
async def create(
|
|
35
45
|
cls,
|
|
36
46
|
config_file: str,
|
|
37
47
|
servers: List[str],
|
|
38
48
|
server_names: Optional[Dict[int, str]] = None,
|
|
39
|
-
transport_type: str = "stdio"
|
|
40
|
-
) -> StreamManager:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
config_file: Path to the config file
|
|
46
|
-
servers: List of server names to connect to
|
|
47
|
-
server_names: Optional mapping of server indices to names
|
|
48
|
-
transport_type: Transport type ("stdio" or "sse")
|
|
49
|
-
|
|
50
|
-
Returns:
|
|
51
|
-
Initialized StreamManager
|
|
52
|
-
"""
|
|
53
|
-
manager = cls()
|
|
54
|
-
await manager.initialize(config_file, servers, server_names, transport_type)
|
|
55
|
-
return manager
|
|
56
|
-
|
|
49
|
+
transport_type: str = "stdio",
|
|
50
|
+
) -> "StreamManager":
|
|
51
|
+
inst = cls()
|
|
52
|
+
await inst.initialize(config_file, servers, server_names, transport_type)
|
|
53
|
+
return inst
|
|
54
|
+
|
|
57
55
|
@classmethod
|
|
58
56
|
async def create_with_sse(
|
|
59
57
|
cls,
|
|
60
58
|
servers: List[Dict[str, str]],
|
|
61
|
-
server_names: Optional[Dict[int, str]] = None
|
|
62
|
-
) -> StreamManager:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Returns:
|
|
71
|
-
Initialized StreamManager
|
|
72
|
-
"""
|
|
73
|
-
manager = cls()
|
|
74
|
-
await manager.initialize_with_sse(servers, server_names)
|
|
75
|
-
return manager
|
|
76
|
-
|
|
59
|
+
server_names: Optional[Dict[int, str]] = None,
|
|
60
|
+
) -> "StreamManager":
|
|
61
|
+
inst = cls()
|
|
62
|
+
await inst.initialize_with_sse(servers, server_names)
|
|
63
|
+
return inst
|
|
64
|
+
|
|
65
|
+
# ------------------------------------------------------------------ #
|
|
66
|
+
# initialisation – stdio / sse #
|
|
67
|
+
# ------------------------------------------------------------------ #
|
|
77
68
|
async def initialize(
|
|
78
69
|
self,
|
|
79
70
|
config_file: str,
|
|
80
71
|
servers: List[str],
|
|
81
72
|
server_names: Optional[Dict[int, str]] = None,
|
|
82
|
-
transport_type: str = "stdio"
|
|
73
|
+
transport_type: str = "stdio",
|
|
83
74
|
) -> None:
|
|
84
|
-
"""
|
|
85
|
-
Initialize the StreamManager.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
config_file: Path to the config file
|
|
89
|
-
servers: List of server names to connect to
|
|
90
|
-
server_names: Optional mapping of server indices to names
|
|
91
|
-
transport_type: Transport type ("stdio" or "sse")
|
|
92
|
-
"""
|
|
93
75
|
async with self._lock:
|
|
94
|
-
# Store server names mapping
|
|
95
76
|
self.server_names = server_names or {}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
for i, server_name in enumerate(servers):
|
|
77
|
+
|
|
78
|
+
for idx, server_name in enumerate(servers):
|
|
99
79
|
try:
|
|
100
80
|
if transport_type == "stdio":
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
# Create transport
|
|
105
|
-
transport = StdioTransport(server_params)
|
|
81
|
+
params = await load_config(config_file, server_name)
|
|
82
|
+
transport: MCPBaseTransport = StdioTransport(params)
|
|
106
83
|
elif transport_type == "sse":
|
|
107
|
-
# For SSE, we would parse the config differently
|
|
108
|
-
# This is just a placeholder
|
|
109
84
|
transport = SSETransport("http://localhost:8000")
|
|
110
85
|
else:
|
|
111
|
-
logger.error(
|
|
86
|
+
logger.error("Unsupported transport type: %s", transport_type)
|
|
112
87
|
continue
|
|
113
|
-
|
|
114
|
-
# Initialize transport
|
|
88
|
+
|
|
115
89
|
if not await transport.initialize():
|
|
116
|
-
logger.error(
|
|
90
|
+
logger.error("Failed to init %s", server_name)
|
|
117
91
|
continue
|
|
118
|
-
|
|
119
|
-
#
|
|
92
|
+
|
|
93
|
+
# store transport
|
|
120
94
|
self.transports[server_name] = transport
|
|
121
|
-
|
|
122
|
-
#
|
|
123
|
-
|
|
124
|
-
status = "Up" if ping_result else "Down"
|
|
125
|
-
|
|
126
|
-
# Get available tools
|
|
95
|
+
|
|
96
|
+
# ping + gather tools
|
|
97
|
+
status = "Up" if await transport.send_ping() else "Down"
|
|
127
98
|
tools = await transport.get_tools()
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
self.tool_to_server_map[tool_name] = server_name
|
|
134
|
-
|
|
135
|
-
# Add to all tools
|
|
99
|
+
|
|
100
|
+
for t in tools:
|
|
101
|
+
name = t.get("name")
|
|
102
|
+
if name:
|
|
103
|
+
self.tool_to_server_map[name] = server_name
|
|
136
104
|
self.all_tools.extend(tools)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
logger.info(
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
105
|
+
|
|
106
|
+
self.server_info.append(
|
|
107
|
+
{
|
|
108
|
+
"id": idx,
|
|
109
|
+
"name": server_name,
|
|
110
|
+
"tools": len(tools),
|
|
111
|
+
"status": status,
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
logger.info("Initialised %s – %d tool(s)", server_name, len(tools))
|
|
115
|
+
except Exception as exc: # noqa: BLE001
|
|
116
|
+
logger.error("Error initialising %s: %s", server_name, exc)
|
|
117
|
+
|
|
118
|
+
logger.info(
|
|
119
|
+
"StreamManager ready – %d server(s), %d tool(s)",
|
|
120
|
+
len(self.transports),
|
|
121
|
+
len(self.all_tools),
|
|
122
|
+
)
|
|
123
|
+
|
|
153
124
|
async def initialize_with_sse(
|
|
154
125
|
self,
|
|
155
126
|
servers: List[Dict[str, str]],
|
|
156
|
-
server_names: Optional[Dict[int, str]] = None
|
|
127
|
+
server_names: Optional[Dict[int, str]] = None,
|
|
157
128
|
) -> None:
|
|
158
|
-
"""
|
|
159
|
-
Initialize the StreamManager with SSE transport.
|
|
160
|
-
|
|
161
|
-
Args:
|
|
162
|
-
servers: List of server configurations with "name" and "url" keys
|
|
163
|
-
server_names: Optional mapping of server indices to names
|
|
164
|
-
"""
|
|
165
129
|
async with self._lock:
|
|
166
|
-
# Store server names mapping
|
|
167
130
|
self.server_names = server_names or {}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
api_key = server_config.get("api_key")
|
|
174
|
-
|
|
175
|
-
if not server_name or not url:
|
|
176
|
-
logger.error(f"Invalid server configuration: {server_config}")
|
|
131
|
+
|
|
132
|
+
for idx, cfg in enumerate(servers):
|
|
133
|
+
name, url = cfg.get("name"), cfg.get("url")
|
|
134
|
+
if not (name and url):
|
|
135
|
+
logger.error("Bad server config: %s", cfg)
|
|
177
136
|
continue
|
|
178
|
-
|
|
179
137
|
try:
|
|
180
|
-
|
|
181
|
-
transport = SSETransport(url, api_key)
|
|
182
|
-
|
|
183
|
-
# Initialize transport
|
|
138
|
+
transport = SSETransport(url, cfg.get("api_key"))
|
|
184
139
|
if not await transport.initialize():
|
|
185
|
-
logger.error(
|
|
140
|
+
logger.error("Failed to init SSE %s", name)
|
|
186
141
|
continue
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# Check server is responsive
|
|
192
|
-
ping_result = await transport.send_ping()
|
|
193
|
-
status = "Up" if ping_result else "Down"
|
|
194
|
-
|
|
195
|
-
# Get available tools
|
|
142
|
+
|
|
143
|
+
self.transports[name] = transport
|
|
144
|
+
status = "Up" if await transport.send_ping() else "Down"
|
|
196
145
|
tools = await transport.get_tools()
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
self.tool_to_server_map[tool_name] = server_name
|
|
203
|
-
|
|
204
|
-
# Add to all tools
|
|
146
|
+
|
|
147
|
+
for t in tools:
|
|
148
|
+
tname = t.get("name")
|
|
149
|
+
if tname:
|
|
150
|
+
self.tool_to_server_map[tname] = name
|
|
205
151
|
self.all_tools.extend(tools)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
152
|
+
|
|
153
|
+
self.server_info.append(
|
|
154
|
+
{"id": idx, "name": name, "tools": len(tools), "status": status}
|
|
155
|
+
)
|
|
156
|
+
logger.info("Initialised SSE %s – %d tool(s)", name, len(tools))
|
|
157
|
+
except Exception as exc: # noqa: BLE001
|
|
158
|
+
logger.error("Error initialising SSE %s: %s", name, exc)
|
|
159
|
+
|
|
160
|
+
logger.info(
|
|
161
|
+
"StreamManager ready – %d SSE server(s), %d tool(s)",
|
|
162
|
+
len(self.transports),
|
|
163
|
+
len(self.all_tools),
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
# ------------------------------------------------------------------ #
|
|
167
|
+
# queries #
|
|
168
|
+
# ------------------------------------------------------------------ #
|
|
222
169
|
def get_all_tools(self) -> List[Dict[str, Any]]:
|
|
223
|
-
"""
|
|
224
|
-
Get all available tools.
|
|
225
|
-
|
|
226
|
-
Returns:
|
|
227
|
-
List of tool definitions
|
|
228
|
-
"""
|
|
229
170
|
return self.all_tools
|
|
230
|
-
|
|
171
|
+
|
|
231
172
|
def get_server_for_tool(self, tool_name: str) -> Optional[str]:
|
|
232
|
-
"""
|
|
233
|
-
Get the server name for a tool.
|
|
234
|
-
|
|
235
|
-
Args:
|
|
236
|
-
tool_name: Tool name
|
|
237
|
-
|
|
238
|
-
Returns:
|
|
239
|
-
Server name or None if not found
|
|
240
|
-
"""
|
|
241
173
|
return self.tool_to_server_map.get(tool_name)
|
|
242
|
-
|
|
174
|
+
|
|
243
175
|
def get_server_info(self) -> List[Dict[str, Any]]:
|
|
244
|
-
"""
|
|
245
|
-
Get information about all servers.
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
List of server info dictionaries
|
|
249
|
-
"""
|
|
250
176
|
return self.server_info
|
|
251
|
-
|
|
177
|
+
|
|
178
|
+
# ------------------------------------------------------------------ #
|
|
179
|
+
# EXTRA HELPERS – ping / resources / prompts #
|
|
180
|
+
# ------------------------------------------------------------------ #
|
|
181
|
+
async def ping_servers(self) -> List[Dict[str, Any]]:
|
|
182
|
+
async def _ping_one(name: str, tr: MCPBaseTransport):
|
|
183
|
+
try:
|
|
184
|
+
ok = await tr.send_ping()
|
|
185
|
+
except Exception: # pragma: no cover
|
|
186
|
+
ok = False
|
|
187
|
+
return {"server": name, "ok": ok}
|
|
188
|
+
|
|
189
|
+
return await asyncio.gather(*(_ping_one(n, t) for n, t in self.transports.items()))
|
|
190
|
+
|
|
191
|
+
async def list_resources(self) -> List[Dict[str, Any]]:
|
|
192
|
+
out: List[Dict[str, Any]] = []
|
|
193
|
+
|
|
194
|
+
async def _one(name: str, tr: MCPBaseTransport):
|
|
195
|
+
if not hasattr(tr, "list_resources"):
|
|
196
|
+
return
|
|
197
|
+
try:
|
|
198
|
+
res = await tr.list_resources() # type: ignore[attr-defined]
|
|
199
|
+
# accept either {"resources": [...]} **or** a plain list
|
|
200
|
+
resources = (
|
|
201
|
+
res.get("resources", []) if isinstance(res, dict) else res
|
|
202
|
+
)
|
|
203
|
+
for item in resources:
|
|
204
|
+
item = dict(item)
|
|
205
|
+
item["server"] = name
|
|
206
|
+
out.append(item)
|
|
207
|
+
except Exception as exc:
|
|
208
|
+
logger.debug("resources/list failed for %s: %s", name, exc)
|
|
209
|
+
|
|
210
|
+
await asyncio.gather(*(_one(n, t) for n, t in self.transports.items()))
|
|
211
|
+
return out
|
|
212
|
+
|
|
213
|
+
async def list_prompts(self) -> List[Dict[str, Any]]:
|
|
214
|
+
out: List[Dict[str, Any]] = []
|
|
215
|
+
|
|
216
|
+
async def _one(name: str, tr: MCPBaseTransport):
|
|
217
|
+
if not hasattr(tr, "list_prompts"):
|
|
218
|
+
return
|
|
219
|
+
try:
|
|
220
|
+
res = await tr.list_prompts() # type: ignore[attr-defined]
|
|
221
|
+
prompts = res.get("prompts", []) if isinstance(res, dict) else res
|
|
222
|
+
for item in prompts:
|
|
223
|
+
item = dict(item)
|
|
224
|
+
item["server"] = name
|
|
225
|
+
out.append(item)
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
logger.debug("prompts/list failed for %s: %s", name, exc)
|
|
228
|
+
|
|
229
|
+
await asyncio.gather(*(_one(n, t) for n, t in self.transports.items()))
|
|
230
|
+
return out
|
|
231
|
+
|
|
232
|
+
# ------------------------------------------------------------------ #
|
|
233
|
+
# tool execution #
|
|
234
|
+
# ------------------------------------------------------------------ #
|
|
252
235
|
async def call_tool(
|
|
253
236
|
self,
|
|
254
237
|
tool_name: str,
|
|
255
238
|
arguments: Dict[str, Any],
|
|
256
|
-
server_name: Optional[str] = None
|
|
239
|
+
server_name: Optional[str] = None,
|
|
257
240
|
) -> Dict[str, Any]:
|
|
258
|
-
|
|
259
|
-
Call a tool.
|
|
260
|
-
|
|
261
|
-
Args:
|
|
262
|
-
tool_name: Tool name
|
|
263
|
-
arguments: Tool arguments
|
|
264
|
-
server_name: Optional server name override
|
|
265
|
-
|
|
266
|
-
Returns:
|
|
267
|
-
Tool result
|
|
268
|
-
"""
|
|
269
|
-
# Get server name
|
|
270
|
-
if not server_name:
|
|
271
|
-
server_name = self.get_server_for_tool(tool_name)
|
|
272
|
-
|
|
241
|
+
server_name = server_name or self.get_server_for_tool(tool_name)
|
|
273
242
|
if not server_name or server_name not in self.transports:
|
|
243
|
+
# wording kept exactly for unit-test expectation
|
|
274
244
|
return {
|
|
275
245
|
"isError": True,
|
|
276
|
-
"error": f"No server found for tool: {tool_name}"
|
|
246
|
+
"error": f"No server found for tool: {tool_name}",
|
|
277
247
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return await transport.call_tool(tool_name, arguments)
|
|
284
|
-
|
|
248
|
+
return await self.transports[server_name].call_tool(tool_name, arguments)
|
|
249
|
+
|
|
250
|
+
# ------------------------------------------------------------------ #
|
|
251
|
+
# shutdown #
|
|
252
|
+
# ------------------------------------------------------------------ #
|
|
285
253
|
async def close(self) -> None:
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
for name, transport in self.transports.items():
|
|
289
|
-
close_tasks.append(transport.close())
|
|
290
|
-
|
|
291
|
-
if close_tasks:
|
|
254
|
+
tasks = [tr.close() for tr in self.transports.values()]
|
|
255
|
+
if tasks:
|
|
292
256
|
try:
|
|
293
|
-
await asyncio.gather(*
|
|
294
|
-
except asyncio.CancelledError:
|
|
295
|
-
# Ignore cancellation during cleanup
|
|
257
|
+
await asyncio.gather(*tasks)
|
|
258
|
+
except asyncio.CancelledError: # pragma: no cover
|
|
296
259
|
pass
|
|
297
|
-
except Exception as
|
|
298
|
-
logger.error(
|
|
299
|
-
|
|
260
|
+
except Exception as exc: # noqa: BLE001
|
|
261
|
+
logger.error("Error during close: %s", exc)
|
|
262
|
+
|
|
300
263
|
self.transports.clear()
|
|
301
264
|
self.server_info.clear()
|
|
302
265
|
self.tool_to_server_map.clear()
|
|
303
|
-
self.all_tools.clear()
|
|
266
|
+
self.all_tools.clear()
|
|
267
|
+
|
|
268
|
+
# ------------------------------------------------------------------ #
|
|
269
|
+
# backwards-compat: streams helper #
|
|
270
|
+
# ------------------------------------------------------------------ #
|
|
271
|
+
def get_streams(self) -> List[Tuple[Any, Any]]:
|
|
272
|
+
"""
|
|
273
|
+
Return a list of ``(read_stream, write_stream)`` tuples for **all**
|
|
274
|
+
transports. Older CLI commands rely on this helper.
|
|
275
|
+
"""
|
|
276
|
+
pairs: List[Tuple[Any, Any]] = []
|
|
277
|
+
|
|
278
|
+
for tr in self.transports.values():
|
|
279
|
+
if hasattr(tr, "get_streams") and callable(tr.get_streams):
|
|
280
|
+
pairs.extend(tr.get_streams()) # type: ignore[arg-type]
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
rd = getattr(tr, "read_stream", None)
|
|
284
|
+
wr = getattr(tr, "write_stream", None)
|
|
285
|
+
if rd and wr:
|
|
286
|
+
pairs.append((rd, wr))
|
|
287
|
+
|
|
288
|
+
return pairs
|
|
289
|
+
|
|
290
|
+
# convenience alias
|
|
291
|
+
@property
|
|
292
|
+
def streams(self) -> List[Tuple[Any, Any]]: # pragma: no cover
|
|
293
|
+
return self.get_streams()
|
|
@@ -2,63 +2,102 @@
|
|
|
2
2
|
"""
|
|
3
3
|
Abstract transport layer for MCP communication.
|
|
4
4
|
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
from abc import ABC, abstractmethod
|
|
6
8
|
from typing import Any, Dict, List
|
|
7
9
|
|
|
10
|
+
|
|
8
11
|
class MCPBaseTransport(ABC):
|
|
9
12
|
"""
|
|
10
13
|
Abstract base class for MCP transport mechanisms.
|
|
11
14
|
"""
|
|
12
|
-
|
|
15
|
+
|
|
16
|
+
# ------------------------------------------------------------------ #
|
|
17
|
+
# connection lifecycle #
|
|
18
|
+
# ------------------------------------------------------------------ #
|
|
13
19
|
@abstractmethod
|
|
14
20
|
async def initialize(self) -> bool:
|
|
15
21
|
"""
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Returns
|
|
19
|
-
|
|
22
|
+
Establish the connection.
|
|
23
|
+
|
|
24
|
+
Returns
|
|
25
|
+
-------
|
|
26
|
+
bool
|
|
27
|
+
``True`` if the connection was initialised successfully.
|
|
20
28
|
"""
|
|
21
|
-
|
|
22
|
-
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
async def close(self) -> None:
|
|
33
|
+
"""Tear down the connection and release all resources."""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
# ------------------------------------------------------------------ #
|
|
37
|
+
# diagnostics #
|
|
38
|
+
# ------------------------------------------------------------------ #
|
|
23
39
|
@abstractmethod
|
|
24
40
|
async def send_ping(self) -> bool:
|
|
25
41
|
"""
|
|
26
|
-
Send a ping
|
|
27
|
-
|
|
28
|
-
Returns
|
|
29
|
-
|
|
42
|
+
Send a **ping** request.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
bool
|
|
47
|
+
``True`` on success, ``False`` otherwise.
|
|
30
48
|
"""
|
|
31
|
-
|
|
32
|
-
|
|
49
|
+
raise NotImplementedError
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------------ #
|
|
52
|
+
# tool handling #
|
|
53
|
+
# ------------------------------------------------------------------ #
|
|
33
54
|
@abstractmethod
|
|
34
55
|
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
35
56
|
"""
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
57
|
+
Return a list with *all* tool definitions exposed by the server.
|
|
58
|
+
"""
|
|
59
|
+
raise NotImplementedError
|
|
60
|
+
|
|
61
|
+
@abstractmethod
|
|
62
|
+
async def call_tool(
|
|
63
|
+
self, tool_name: str, arguments: Dict[str, Any]
|
|
64
|
+
) -> Dict[str, Any]:
|
|
65
|
+
"""
|
|
66
|
+
Execute *tool_name* with *arguments* and return the normalised result.
|
|
40
67
|
"""
|
|
41
|
-
|
|
42
|
-
|
|
68
|
+
raise NotImplementedError
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------ #
|
|
71
|
+
# new: resources & prompts #
|
|
72
|
+
# ------------------------------------------------------------------ #
|
|
43
73
|
@abstractmethod
|
|
44
|
-
async def
|
|
74
|
+
async def list_resources(self) -> Dict[str, Any]:
|
|
45
75
|
"""
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
arguments: Tool arguments
|
|
51
|
-
|
|
52
|
-
Returns:
|
|
53
|
-
Tool result
|
|
76
|
+
Retrieve the server’s resources catalogue.
|
|
77
|
+
|
|
78
|
+
Expected shape::
|
|
79
|
+
{ "resources": [ {...}, ... ], "nextCursor": "…", … }
|
|
54
80
|
"""
|
|
55
|
-
|
|
56
|
-
|
|
81
|
+
raise NotImplementedError
|
|
82
|
+
|
|
57
83
|
@abstractmethod
|
|
58
|
-
async def
|
|
59
|
-
"""
|
|
60
|
-
|
|
84
|
+
async def list_prompts(self) -> Dict[str, Any]:
|
|
85
|
+
"""
|
|
86
|
+
Retrieve the server’s prompt catalogue.
|
|
61
87
|
|
|
88
|
+
Expected shape::
|
|
89
|
+
{ "prompts": [ {...}, ... ], "nextCursor": "…", … }
|
|
90
|
+
"""
|
|
91
|
+
raise NotImplementedError
|
|
62
92
|
|
|
93
|
+
# ------------------------------------------------------------------ #
|
|
94
|
+
# optional helper (non-abstract) #
|
|
95
|
+
# ------------------------------------------------------------------ #
|
|
96
|
+
def get_streams(self):
|
|
97
|
+
"""
|
|
98
|
+
Return a list of ``(read_stream, write_stream)`` tuples.
|
|
63
99
|
|
|
64
|
-
|
|
100
|
+
Transports that do not expose their low-level streams can simply leave
|
|
101
|
+
the default implementation (which returns an empty list).
|
|
102
|
+
"""
|
|
103
|
+
return []
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 {
|
|
98
|
-
|
|
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:
|
|
@@ -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=
|
|
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=
|
|
27
|
-
chuk_tool_processor/mcp/transport/sse_transport.py,sha256=
|
|
28
|
-
chuk_tool_processor/mcp/transport/stdio_transport.py,sha256=
|
|
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.
|
|
55
|
-
chuk_tool_processor-0.1.
|
|
56
|
-
chuk_tool_processor-0.1.
|
|
57
|
-
chuk_tool_processor-0.1.
|
|
54
|
+
chuk_tool_processor-0.1.4.dist-info/METADATA,sha256=ekQNpVXyJrLw9kaLnhHW4iI1Q5do07T6Ol2QfeRsQn0,13703
|
|
55
|
+
chuk_tool_processor-0.1.4.dist-info/WHEEL,sha256=GHB6lJx2juba1wDgXDNlMTyM13ckjBMKf-OnwgKOCtA,91
|
|
56
|
+
chuk_tool_processor-0.1.4.dist-info/top_level.txt,sha256=7lTsnuRx4cOW4U2sNJWNxl4ZTt_J1ndkjTbj3pHPY5M,20
|
|
57
|
+
chuk_tool_processor-0.1.4.dist-info/RECORD,,
|
|
File without changes
|