chuk-tool-processor 0.1.1__py3-none-any.whl → 0.1.3__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.
- chuk_tool_processor/__init__.py +0 -1
- chuk_tool_processor/core/processor.py +7 -7
- chuk_tool_processor/execution/wrappers/retry.py +1 -1
- chuk_tool_processor/logging/__init__.py +5 -7
- chuk_tool_processor/mcp/__init__.py +21 -0
- chuk_tool_processor/mcp/mcp_tool.py +53 -0
- chuk_tool_processor/mcp/register_mcp_tools.py +82 -0
- chuk_tool_processor/mcp/setup_mcp_sse.py +74 -0
- chuk_tool_processor/mcp/setup_mcp_stdio.py +78 -0
- chuk_tool_processor/mcp/stream_manager.py +303 -0
- chuk_tool_processor/mcp/transport/__init__.py +14 -0
- chuk_tool_processor/mcp/transport/base_transport.py +64 -0
- chuk_tool_processor/mcp/transport/sse_transport.py +59 -0
- chuk_tool_processor/mcp/transport/stdio_transport.py +125 -0
- {chuk_tool_processor-0.1.1.dist-info → chuk_tool_processor-0.1.3.dist-info}/METADATA +169 -3
- {chuk_tool_processor-0.1.1.dist-info → chuk_tool_processor-0.1.3.dist-info}/RECORD +18 -8
- {chuk_tool_processor-0.1.1.dist-info → chuk_tool_processor-0.1.3.dist-info}/WHEEL +1 -1
- {chuk_tool_processor-0.1.1.dist-info → chuk_tool_processor-0.1.3.dist-info}/top_level.txt +0 -0
chuk_tool_processor/__init__.py
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
# chuk_tool_processor/__init__.py
|
|
@@ -72,14 +72,14 @@ class ToolProcessor:
|
|
|
72
72
|
|
|
73
73
|
# Apply optional wrappers
|
|
74
74
|
if enable_retries:
|
|
75
|
-
self.logger.
|
|
75
|
+
self.logger.debug("Enabling retry logic")
|
|
76
76
|
self.executor = RetryableToolExecutor(
|
|
77
77
|
executor=self.executor,
|
|
78
78
|
default_config=RetryConfig(max_retries=max_retries)
|
|
79
79
|
)
|
|
80
80
|
|
|
81
81
|
if enable_rate_limiting:
|
|
82
|
-
self.logger.
|
|
82
|
+
self.logger.debug("Enabling rate limiting")
|
|
83
83
|
rate_limiter = RateLimiter(
|
|
84
84
|
global_limit=global_rate_limit,
|
|
85
85
|
tool_limits=tool_rate_limits
|
|
@@ -90,7 +90,7 @@ class ToolProcessor:
|
|
|
90
90
|
)
|
|
91
91
|
|
|
92
92
|
if enable_caching:
|
|
93
|
-
self.logger.
|
|
93
|
+
self.logger.debug("Enabling result caching")
|
|
94
94
|
cache = InMemoryCache(default_ttl=cache_ttl)
|
|
95
95
|
self.executor = CachingToolExecutor(
|
|
96
96
|
executor=self.executor,
|
|
@@ -116,7 +116,7 @@ class ToolProcessor:
|
|
|
116
116
|
for name in parser_names
|
|
117
117
|
]
|
|
118
118
|
|
|
119
|
-
self.logger.
|
|
119
|
+
self.logger.debug(f"Initialized with {len(self.parsers)} parser plugins")
|
|
120
120
|
|
|
121
121
|
async def process_text(
|
|
122
122
|
self,
|
|
@@ -139,16 +139,16 @@ class ToolProcessor:
|
|
|
139
139
|
"""
|
|
140
140
|
# Create request context
|
|
141
141
|
with request_logging(request_id) as req_id:
|
|
142
|
-
self.logger.
|
|
142
|
+
self.logger.debug(f"Processing text ({len(text)} chars)")
|
|
143
143
|
|
|
144
144
|
# Extract tool calls
|
|
145
145
|
calls = await self._extract_tool_calls(text)
|
|
146
146
|
|
|
147
147
|
if not calls:
|
|
148
|
-
self.logger.
|
|
148
|
+
self.logger.debug("No tool calls found")
|
|
149
149
|
return []
|
|
150
150
|
|
|
151
|
-
self.logger.
|
|
151
|
+
self.logger.debug(f"Found {len(calls)} tool calls")
|
|
152
152
|
|
|
153
153
|
# Execute tool calls
|
|
154
154
|
with log_context_span("tool_execution", {"num_calls": len(calls)}):
|
|
@@ -103,7 +103,7 @@ class RetryableToolExecutor:
|
|
|
103
103
|
if result.error:
|
|
104
104
|
last_error = result.error
|
|
105
105
|
if config.should_retry(attempt, error_str=result.error):
|
|
106
|
-
logger.
|
|
106
|
+
logger.debug(
|
|
107
107
|
f"Retrying tool {call.tool} after error: {result.error} (attempt {attempt + 1})"
|
|
108
108
|
)
|
|
109
109
|
await asyncio.sleep(config.get_delay(attempt))
|
|
@@ -6,14 +6,12 @@ Other modules can continue to import:
|
|
|
6
6
|
|
|
7
7
|
from chuk_tool_processor.logging import get_logger, log_context_span, ...
|
|
8
8
|
"""
|
|
9
|
-
|
|
10
9
|
from __future__ import annotations
|
|
11
|
-
import logging
|
|
12
|
-
import sys
|
|
10
|
+
import logging, sys
|
|
13
11
|
|
|
14
12
|
from .formatter import StructuredFormatter
|
|
15
|
-
from .context
|
|
16
|
-
from .helpers
|
|
13
|
+
from .context import get_logger, log_context, StructuredAdapter
|
|
14
|
+
from .helpers import log_context_span, request_logging, log_tool_call, metrics
|
|
17
15
|
|
|
18
16
|
__all__ = [
|
|
19
17
|
"get_logger",
|
|
@@ -27,9 +25,9 @@ __all__ = [
|
|
|
27
25
|
# root logger & handler wiring (done once at import time)
|
|
28
26
|
# --------------------------------------------------------------------------- #
|
|
29
27
|
root_logger = logging.getLogger("chuk_tool_processor")
|
|
30
|
-
root_logger.setLevel(logging.
|
|
28
|
+
root_logger.setLevel(logging.WARNING) # ← quieter default
|
|
31
29
|
|
|
32
30
|
_handler = logging.StreamHandler(sys.stderr)
|
|
33
|
-
_handler.setLevel(logging.
|
|
31
|
+
_handler.setLevel(logging.WARNING) # match the logger
|
|
34
32
|
_handler.setFormatter(StructuredFormatter())
|
|
35
33
|
root_logger.addHandler(_handler)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
MCP integration for CHUK Tool Processor.
|
|
4
|
+
"""
|
|
5
|
+
from chuk_tool_processor.mcp.transport import MCPBaseTransport, StdioTransport, SSETransport
|
|
6
|
+
from chuk_tool_processor.mcp.stream_manager import StreamManager
|
|
7
|
+
from chuk_tool_processor.mcp.mcp_tool import MCPTool
|
|
8
|
+
from chuk_tool_processor.mcp.register_mcp_tools import register_mcp_tools
|
|
9
|
+
from chuk_tool_processor.mcp.setup_mcp_stdio import setup_mcp_stdio
|
|
10
|
+
from chuk_tool_processor.mcp.setup_mcp_sse import setup_mcp_sse
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"MCPBaseTransport",
|
|
14
|
+
"StdioTransport",
|
|
15
|
+
"SSETransport",
|
|
16
|
+
"StreamManager",
|
|
17
|
+
"MCPTool",
|
|
18
|
+
"register_mcp_tools",
|
|
19
|
+
"setup_mcp_stdio",
|
|
20
|
+
"setup_mcp_sse"
|
|
21
|
+
]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/mcp_tool.py
|
|
2
|
+
"""
|
|
3
|
+
MCP tool that uses StreamManager for execution.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from chuk_tool_processor.mcp.stream_manager import StreamManager
|
|
9
|
+
from chuk_tool_processor.logging import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger("chuk_tool_processor.mcp.mcp_tool")
|
|
12
|
+
|
|
13
|
+
class MCPTool:
|
|
14
|
+
"""
|
|
15
|
+
MCP tool that uses StreamManager for execution.
|
|
16
|
+
|
|
17
|
+
This tool handles both namespaced and non-namespaced execution.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, tool_name: str, stream_manager: StreamManager):
|
|
21
|
+
"""
|
|
22
|
+
Initialize the MCP tool.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
tool_name: Name of the MCP tool
|
|
26
|
+
stream_manager: StreamManager instance
|
|
27
|
+
"""
|
|
28
|
+
self.tool_name = tool_name
|
|
29
|
+
self.stream_manager = stream_manager
|
|
30
|
+
|
|
31
|
+
async def execute(self, **kwargs: Any) -> Any:
|
|
32
|
+
"""
|
|
33
|
+
Execute the tool using StreamManager.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
**kwargs: Tool arguments
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tool result
|
|
40
|
+
"""
|
|
41
|
+
logger.debug(f"Executing MCP tool {self.tool_name}")
|
|
42
|
+
|
|
43
|
+
result = await self.stream_manager.call_tool(
|
|
44
|
+
tool_name=self.tool_name,
|
|
45
|
+
arguments=kwargs
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
if result.get("isError"):
|
|
49
|
+
error_msg = result.get("error", "Unknown error")
|
|
50
|
+
logger.error(f"Error executing MCP tool {self.tool_name}: {error_msg}")
|
|
51
|
+
raise RuntimeError(error_msg)
|
|
52
|
+
|
|
53
|
+
return result.get("content")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/register_mcp_tools.py
|
|
2
|
+
"""
|
|
3
|
+
Registration functions for MCP tools.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import List, Dict, Any
|
|
7
|
+
|
|
8
|
+
from chuk_tool_processor.mcp.mcp_tool import MCPTool
|
|
9
|
+
from chuk_tool_processor.mcp.stream_manager import StreamManager
|
|
10
|
+
from chuk_tool_processor.registry.provider import ToolRegistryProvider
|
|
11
|
+
from chuk_tool_processor.logging import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger("chuk_tool_processor.mcp.register")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def register_mcp_tools(
|
|
17
|
+
stream_manager: StreamManager,
|
|
18
|
+
namespace: str = "mcp"
|
|
19
|
+
) -> List[str]:
|
|
20
|
+
"""
|
|
21
|
+
Register MCP tools with the CHUK registry.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
stream_manager: StreamManager instance
|
|
25
|
+
namespace: Namespace for the tools
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of registered tool names
|
|
29
|
+
"""
|
|
30
|
+
registry = ToolRegistryProvider.get_registry()
|
|
31
|
+
registered_tools = []
|
|
32
|
+
|
|
33
|
+
# Get all tools from StreamManager
|
|
34
|
+
mcp_tools = stream_manager.get_all_tools()
|
|
35
|
+
|
|
36
|
+
for tool_def in mcp_tools:
|
|
37
|
+
tool_name = tool_def.get("name")
|
|
38
|
+
if not tool_name:
|
|
39
|
+
logger.warning("Tool definition missing name")
|
|
40
|
+
continue
|
|
41
|
+
|
|
42
|
+
description = tool_def.get("description", f"MCP tool: {tool_name}")
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
# Create tool
|
|
46
|
+
tool = MCPTool(tool_name, stream_manager)
|
|
47
|
+
|
|
48
|
+
# Register with registry under the original name in the given namespace
|
|
49
|
+
registry.register_tool(
|
|
50
|
+
tool,
|
|
51
|
+
name=tool_name,
|
|
52
|
+
namespace=namespace,
|
|
53
|
+
metadata={
|
|
54
|
+
"description": description,
|
|
55
|
+
"is_async": True,
|
|
56
|
+
"tags": {"mcp", "remote"},
|
|
57
|
+
"argument_schema": tool_def.get("inputSchema", {})
|
|
58
|
+
}
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Also register the tool in the default namespace with the namespaced name
|
|
62
|
+
# This allows calling the tool as either "echo" or "stdio.echo" from parsers
|
|
63
|
+
namespaced_tool_name = f"{namespace}.{tool_name}"
|
|
64
|
+
registry.register_tool(
|
|
65
|
+
tool,
|
|
66
|
+
name=namespaced_tool_name,
|
|
67
|
+
namespace="default",
|
|
68
|
+
metadata={
|
|
69
|
+
"description": description,
|
|
70
|
+
"is_async": True,
|
|
71
|
+
"tags": {"mcp", "remote", "namespaced"},
|
|
72
|
+
"argument_schema": tool_def.get("inputSchema", {})
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
registered_tools.append(tool_name)
|
|
77
|
+
logger.info(f"Registered MCP tool '{tool_name}' in namespace '{namespace}' (also as '{namespaced_tool_name}' in default)")
|
|
78
|
+
|
|
79
|
+
except Exception as e:
|
|
80
|
+
logger.error(f"Error registering MCP tool '{tool_name}': {e}")
|
|
81
|
+
|
|
82
|
+
return registered_tools
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/setup_mcp_sse.py
|
|
2
|
+
"""
|
|
3
|
+
Setup function for SSE transport MCP integration.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from chuk_tool_processor.core.processor import ToolProcessor
|
|
10
|
+
from chuk_tool_processor.mcp.stream_manager import StreamManager
|
|
11
|
+
from chuk_tool_processor.mcp.register_mcp_tools import register_mcp_tools
|
|
12
|
+
from chuk_tool_processor.logging import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger("chuk_tool_processor.mcp.setup_sse")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def setup_mcp_sse(
|
|
18
|
+
servers: List[Dict[str, str]],
|
|
19
|
+
server_names: Optional[Dict[int, str]] = None,
|
|
20
|
+
default_timeout: float = 10.0,
|
|
21
|
+
max_concurrency: Optional[int] = None,
|
|
22
|
+
enable_caching: bool = True,
|
|
23
|
+
cache_ttl: int = 300,
|
|
24
|
+
enable_rate_limiting: bool = False,
|
|
25
|
+
global_rate_limit: Optional[int] = None,
|
|
26
|
+
tool_rate_limits: Optional[Dict[str, tuple]] = None,
|
|
27
|
+
enable_retries: bool = True,
|
|
28
|
+
max_retries: int = 3,
|
|
29
|
+
namespace: str = "mcp"
|
|
30
|
+
) -> tuple[ToolProcessor, StreamManager]:
|
|
31
|
+
"""
|
|
32
|
+
Set up MCP with SSE transport and CHUK Tool Processor.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
servers: List of server configurations with "name" and "url" keys
|
|
36
|
+
server_names: Optional mapping of server indices to names
|
|
37
|
+
default_timeout: Default timeout for tool execution
|
|
38
|
+
max_concurrency: Maximum concurrent executions
|
|
39
|
+
enable_caching: Whether to enable caching
|
|
40
|
+
cache_ttl: Cache TTL in seconds
|
|
41
|
+
enable_rate_limiting: Whether to enable rate limiting
|
|
42
|
+
global_rate_limit: Global rate limit (requests per minute)
|
|
43
|
+
tool_rate_limits: Per-tool rate limits
|
|
44
|
+
enable_retries: Whether to enable retries
|
|
45
|
+
max_retries: Maximum retry attempts
|
|
46
|
+
namespace: Namespace for MCP tools
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Tuple of (processor, stream_manager)
|
|
50
|
+
"""
|
|
51
|
+
# Create and initialize StreamManager with SSE transport
|
|
52
|
+
stream_manager = await StreamManager.create_with_sse(
|
|
53
|
+
servers=servers,
|
|
54
|
+
server_names=server_names
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Register MCP tools
|
|
58
|
+
registered_tools = register_mcp_tools(stream_manager, namespace)
|
|
59
|
+
|
|
60
|
+
# Create processor
|
|
61
|
+
processor = ToolProcessor(
|
|
62
|
+
default_timeout=default_timeout,
|
|
63
|
+
max_concurrency=max_concurrency,
|
|
64
|
+
enable_caching=enable_caching,
|
|
65
|
+
cache_ttl=cache_ttl,
|
|
66
|
+
enable_rate_limiting=enable_rate_limiting,
|
|
67
|
+
global_rate_limit=global_rate_limit,
|
|
68
|
+
tool_rate_limits=tool_rate_limits,
|
|
69
|
+
enable_retries=enable_retries,
|
|
70
|
+
max_retries=max_retries
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
logger.info(f"Set up MCP (SSE) with {len(registered_tools)} tools")
|
|
74
|
+
return processor, stream_manager
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/setup_mcp_stdio.py
|
|
2
|
+
"""
|
|
3
|
+
Setup function for stdio transport MCP integration.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from chuk_tool_processor.core.processor import ToolProcessor
|
|
10
|
+
from chuk_tool_processor.mcp.stream_manager import StreamManager
|
|
11
|
+
from chuk_tool_processor.mcp.register_mcp_tools import register_mcp_tools
|
|
12
|
+
from chuk_tool_processor.logging import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger("chuk_tool_processor.mcp.setup_stdio")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def setup_mcp_stdio(
|
|
18
|
+
config_file: str,
|
|
19
|
+
servers: List[str],
|
|
20
|
+
server_names: Optional[Dict[int, str]] = None,
|
|
21
|
+
default_timeout: float = 10.0,
|
|
22
|
+
max_concurrency: Optional[int] = None,
|
|
23
|
+
enable_caching: bool = True,
|
|
24
|
+
cache_ttl: int = 300,
|
|
25
|
+
enable_rate_limiting: bool = False,
|
|
26
|
+
global_rate_limit: Optional[int] = None,
|
|
27
|
+
tool_rate_limits: Optional[Dict[str, tuple]] = None,
|
|
28
|
+
enable_retries: bool = True,
|
|
29
|
+
max_retries: int = 3,
|
|
30
|
+
namespace: str = "mcp"
|
|
31
|
+
) -> tuple[ToolProcessor, StreamManager]:
|
|
32
|
+
"""
|
|
33
|
+
Set up MCP with stdio transport and CHUK Tool Processor.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config_file: Path to the config file
|
|
37
|
+
servers: List of server names to connect to
|
|
38
|
+
server_names: Optional mapping of server indices to names
|
|
39
|
+
default_timeout: Default timeout for tool execution
|
|
40
|
+
max_concurrency: Maximum concurrent executions
|
|
41
|
+
enable_caching: Whether to enable caching
|
|
42
|
+
cache_ttl: Cache TTL in seconds
|
|
43
|
+
enable_rate_limiting: Whether to enable rate limiting
|
|
44
|
+
global_rate_limit: Global rate limit (requests per minute)
|
|
45
|
+
tool_rate_limits: Per-tool rate limits
|
|
46
|
+
enable_retries: Whether to enable retries
|
|
47
|
+
max_retries: Maximum retry attempts
|
|
48
|
+
namespace: Namespace for MCP tools
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Tuple of (processor, stream_manager)
|
|
52
|
+
"""
|
|
53
|
+
# Create and initialize StreamManager with stdio transport
|
|
54
|
+
stream_manager = await StreamManager.create(
|
|
55
|
+
config_file=config_file,
|
|
56
|
+
servers=servers,
|
|
57
|
+
server_names=server_names,
|
|
58
|
+
transport_type="stdio"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Register MCP tools
|
|
62
|
+
registered_tools = register_mcp_tools(stream_manager, namespace)
|
|
63
|
+
|
|
64
|
+
# Create processor
|
|
65
|
+
processor = ToolProcessor(
|
|
66
|
+
default_timeout=default_timeout,
|
|
67
|
+
max_concurrency=max_concurrency,
|
|
68
|
+
enable_caching=enable_caching,
|
|
69
|
+
cache_ttl=cache_ttl,
|
|
70
|
+
enable_rate_limiting=enable_rate_limiting,
|
|
71
|
+
global_rate_limit=global_rate_limit,
|
|
72
|
+
tool_rate_limits=tool_rate_limits,
|
|
73
|
+
enable_retries=enable_retries,
|
|
74
|
+
max_retries=max_retries
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
logger.info(f"Set up MCP (stdio) with {len(registered_tools)} tools")
|
|
78
|
+
return processor, stream_manager
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/stream_manager.py
|
|
2
|
+
"""
|
|
3
|
+
StreamManager for CHUK Tool Processor.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import json
|
|
9
|
+
from typing import Dict, List, Optional, Any
|
|
10
|
+
|
|
11
|
+
# tool processor imports
|
|
12
|
+
from chuk_mcp.config import load_config
|
|
13
|
+
from chuk_tool_processor.mcp.transport import MCPBaseTransport, StdioTransport, SSETransport
|
|
14
|
+
from chuk_tool_processor.logging import get_logger
|
|
15
|
+
|
|
16
|
+
# logger
|
|
17
|
+
logger = get_logger("chuk_tool_processor.mcp.stream_manager")
|
|
18
|
+
|
|
19
|
+
class StreamManager:
|
|
20
|
+
"""
|
|
21
|
+
Manager for MCP server streams with support for multiple transport types.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
"""Initialize the StreamManager."""
|
|
26
|
+
self.transports: Dict[str, MCPBaseTransport] = {}
|
|
27
|
+
self.server_info: List[Dict[str, Any]] = []
|
|
28
|
+
self.tool_to_server_map: Dict[str, str] = {}
|
|
29
|
+
self.server_names: Dict[int, str] = {}
|
|
30
|
+
self.all_tools: List[Dict[str, Any]] = []
|
|
31
|
+
self._lock = asyncio.Lock()
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
async def create(
|
|
35
|
+
cls,
|
|
36
|
+
config_file: str,
|
|
37
|
+
servers: List[str],
|
|
38
|
+
server_names: Optional[Dict[int, str]] = None,
|
|
39
|
+
transport_type: str = "stdio"
|
|
40
|
+
) -> StreamManager:
|
|
41
|
+
"""
|
|
42
|
+
Create and initialize a StreamManager.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
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
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
async def create_with_sse(
|
|
59
|
+
cls,
|
|
60
|
+
servers: List[Dict[str, str]],
|
|
61
|
+
server_names: Optional[Dict[int, str]] = None
|
|
62
|
+
) -> StreamManager:
|
|
63
|
+
"""
|
|
64
|
+
Create and initialize a StreamManager with SSE transport.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
servers: List of server configurations with "name" and "url" keys
|
|
68
|
+
server_names: Optional mapping of server indices to names
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Initialized StreamManager
|
|
72
|
+
"""
|
|
73
|
+
manager = cls()
|
|
74
|
+
await manager.initialize_with_sse(servers, server_names)
|
|
75
|
+
return manager
|
|
76
|
+
|
|
77
|
+
async def initialize(
|
|
78
|
+
self,
|
|
79
|
+
config_file: str,
|
|
80
|
+
servers: List[str],
|
|
81
|
+
server_names: Optional[Dict[int, str]] = None,
|
|
82
|
+
transport_type: str = "stdio"
|
|
83
|
+
) -> 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
|
+
async with self._lock:
|
|
94
|
+
# Store server names mapping
|
|
95
|
+
self.server_names = server_names or {}
|
|
96
|
+
|
|
97
|
+
# Initialize servers
|
|
98
|
+
for i, server_name in enumerate(servers):
|
|
99
|
+
try:
|
|
100
|
+
if transport_type == "stdio":
|
|
101
|
+
# Load configuration
|
|
102
|
+
server_params = await load_config(config_file, server_name)
|
|
103
|
+
|
|
104
|
+
# Create transport
|
|
105
|
+
transport = StdioTransport(server_params)
|
|
106
|
+
elif transport_type == "sse":
|
|
107
|
+
# For SSE, we would parse the config differently
|
|
108
|
+
# This is just a placeholder
|
|
109
|
+
transport = SSETransport("http://localhost:8000")
|
|
110
|
+
else:
|
|
111
|
+
logger.error(f"Unsupported transport type: {transport_type}")
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
# Initialize transport
|
|
115
|
+
if not await transport.initialize():
|
|
116
|
+
logger.error(f"Failed to initialize transport for server: {server_name}")
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Store transport
|
|
120
|
+
self.transports[server_name] = transport
|
|
121
|
+
|
|
122
|
+
# Check server is responsive
|
|
123
|
+
ping_result = await transport.send_ping()
|
|
124
|
+
status = "Up" if ping_result else "Down"
|
|
125
|
+
|
|
126
|
+
# Get available tools
|
|
127
|
+
tools = await transport.get_tools()
|
|
128
|
+
|
|
129
|
+
# Map tools to server
|
|
130
|
+
for tool in tools:
|
|
131
|
+
tool_name = tool.get("name")
|
|
132
|
+
if tool_name:
|
|
133
|
+
self.tool_to_server_map[tool_name] = server_name
|
|
134
|
+
|
|
135
|
+
# Add to all tools
|
|
136
|
+
self.all_tools.extend(tools)
|
|
137
|
+
|
|
138
|
+
# Add server info
|
|
139
|
+
self.server_info.append({
|
|
140
|
+
"id": i,
|
|
141
|
+
"name": server_name,
|
|
142
|
+
"tools": len(tools),
|
|
143
|
+
"status": status
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
logger.info(f"Initialized server {server_name} with {len(tools)} tools")
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(f"Error initializing server {server_name}: {e}")
|
|
150
|
+
|
|
151
|
+
logger.info(f"StreamManager initialized with {len(self.transports)} servers and {len(self.all_tools)} tools")
|
|
152
|
+
|
|
153
|
+
async def initialize_with_sse(
|
|
154
|
+
self,
|
|
155
|
+
servers: List[Dict[str, str]],
|
|
156
|
+
server_names: Optional[Dict[int, str]] = None
|
|
157
|
+
) -> 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
|
+
async with self._lock:
|
|
166
|
+
# Store server names mapping
|
|
167
|
+
self.server_names = server_names or {}
|
|
168
|
+
|
|
169
|
+
# Initialize servers
|
|
170
|
+
for i, server_config in enumerate(servers):
|
|
171
|
+
server_name = server_config.get("name")
|
|
172
|
+
url = server_config.get("url")
|
|
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}")
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Create transport
|
|
181
|
+
transport = SSETransport(url, api_key)
|
|
182
|
+
|
|
183
|
+
# Initialize transport
|
|
184
|
+
if not await transport.initialize():
|
|
185
|
+
logger.error(f"Failed to initialize SSE transport for server: {server_name}")
|
|
186
|
+
continue
|
|
187
|
+
|
|
188
|
+
# Store transport
|
|
189
|
+
self.transports[server_name] = transport
|
|
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
|
|
196
|
+
tools = await transport.get_tools()
|
|
197
|
+
|
|
198
|
+
# Map tools to server
|
|
199
|
+
for tool in tools:
|
|
200
|
+
tool_name = tool.get("name")
|
|
201
|
+
if tool_name:
|
|
202
|
+
self.tool_to_server_map[tool_name] = server_name
|
|
203
|
+
|
|
204
|
+
# Add to all tools
|
|
205
|
+
self.all_tools.extend(tools)
|
|
206
|
+
|
|
207
|
+
# Add server info
|
|
208
|
+
self.server_info.append({
|
|
209
|
+
"id": i,
|
|
210
|
+
"name": server_name,
|
|
211
|
+
"tools": len(tools),
|
|
212
|
+
"status": status
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
logger.info(f"Initialized SSE server {server_name} with {len(tools)} tools")
|
|
216
|
+
|
|
217
|
+
except Exception as e:
|
|
218
|
+
logger.error(f"Error initializing SSE server {server_name}: {e}")
|
|
219
|
+
|
|
220
|
+
logger.info(f"StreamManager initialized with {len(self.transports)} SSE servers and {len(self.all_tools)} tools")
|
|
221
|
+
|
|
222
|
+
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
|
+
return self.all_tools
|
|
230
|
+
|
|
231
|
+
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
|
+
return self.tool_to_server_map.get(tool_name)
|
|
242
|
+
|
|
243
|
+
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
|
+
return self.server_info
|
|
251
|
+
|
|
252
|
+
async def call_tool(
|
|
253
|
+
self,
|
|
254
|
+
tool_name: str,
|
|
255
|
+
arguments: Dict[str, Any],
|
|
256
|
+
server_name: Optional[str] = None
|
|
257
|
+
) -> 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
|
+
|
|
273
|
+
if not server_name or server_name not in self.transports:
|
|
274
|
+
return {
|
|
275
|
+
"isError": True,
|
|
276
|
+
"error": f"No server found for tool: {tool_name}"
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
# Get transport
|
|
280
|
+
transport = self.transports[server_name]
|
|
281
|
+
|
|
282
|
+
# Call tool
|
|
283
|
+
return await transport.call_tool(tool_name, arguments)
|
|
284
|
+
|
|
285
|
+
async def close(self) -> None:
|
|
286
|
+
"""Close all transports."""
|
|
287
|
+
close_tasks = []
|
|
288
|
+
for name, transport in self.transports.items():
|
|
289
|
+
close_tasks.append(transport.close())
|
|
290
|
+
|
|
291
|
+
if close_tasks:
|
|
292
|
+
try:
|
|
293
|
+
await asyncio.gather(*close_tasks)
|
|
294
|
+
except asyncio.CancelledError:
|
|
295
|
+
# Ignore cancellation during cleanup
|
|
296
|
+
pass
|
|
297
|
+
except Exception as e:
|
|
298
|
+
logger.error(f"Error closing transports: {e}")
|
|
299
|
+
|
|
300
|
+
self.transports.clear()
|
|
301
|
+
self.server_info.clear()
|
|
302
|
+
self.tool_to_server_map.clear()
|
|
303
|
+
self.all_tools.clear()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/transport/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
MCP transport implementations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from .base_transport import MCPBaseTransport
|
|
7
|
+
from .stdio_transport import StdioTransport
|
|
8
|
+
from .sse_transport import SSETransport
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"MCPBaseTransport",
|
|
12
|
+
"StdioTransport",
|
|
13
|
+
"SSETransport"
|
|
14
|
+
]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/transport/base_transport.py
|
|
2
|
+
"""
|
|
3
|
+
Abstract transport layer for MCP communication.
|
|
4
|
+
"""
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import Any, Dict, List
|
|
7
|
+
|
|
8
|
+
class MCPBaseTransport(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for MCP transport mechanisms.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@abstractmethod
|
|
14
|
+
async def initialize(self) -> bool:
|
|
15
|
+
"""
|
|
16
|
+
Initialize the transport connection.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
True if successful, False otherwise
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
async def send_ping(self) -> bool:
|
|
25
|
+
"""
|
|
26
|
+
Send a ping message.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
True if successful, False otherwise
|
|
30
|
+
"""
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
@abstractmethod
|
|
34
|
+
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
35
|
+
"""
|
|
36
|
+
Get available tools.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
List of tool definitions
|
|
40
|
+
"""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
45
|
+
"""
|
|
46
|
+
Call a tool.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
tool_name: Tool name
|
|
50
|
+
arguments: Tool arguments
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
Tool result
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
@abstractmethod
|
|
58
|
+
async def close(self) -> None:
|
|
59
|
+
"""Close the transport connection."""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/transport/sse_transport.py
|
|
2
|
+
"""
|
|
3
|
+
Server-Sent Events (SSE) transport for MCP communication.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
# imports
|
|
8
|
+
from .base_transport import MCPBaseTransport
|
|
9
|
+
|
|
10
|
+
class SSETransport(MCPBaseTransport):
|
|
11
|
+
"""
|
|
12
|
+
Server-Sent Events (SSE) transport for MCP communication.
|
|
13
|
+
"""
|
|
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
|
|
24
|
+
self.api_key = api_key
|
|
25
|
+
self.session = None
|
|
26
|
+
self.connection_id = None
|
|
27
|
+
|
|
28
|
+
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
|
+
|
|
41
|
+
async def send_ping(self) -> bool:
|
|
42
|
+
"""Send a ping message."""
|
|
43
|
+
# TODO: Implement SSE ping logic
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
47
|
+
"""Get available tools."""
|
|
48
|
+
# TODO: Implement SSE tool retrieval logic
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
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
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# chuk_tool_processor/mcp/transport/stdio_transport.py
|
|
2
|
+
from typing import Dict, Any, List, Optional
|
|
3
|
+
from contextlib import AsyncExitStack
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
from .base_transport import MCPBaseTransport
|
|
7
|
+
|
|
8
|
+
# chuk-protocol imports
|
|
9
|
+
from chuk_mcp.mcp_client.transport.stdio.stdio_client import stdio_client
|
|
10
|
+
from chuk_mcp.mcp_client.messages.initialize.send_messages import send_initialize
|
|
11
|
+
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
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StdioTransport(MCPBaseTransport):
|
|
16
|
+
"""
|
|
17
|
+
Stdio transport for MCP communication.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, server_params):
|
|
21
|
+
self.server_params = server_params
|
|
22
|
+
self.read_stream = None
|
|
23
|
+
self.write_stream = None
|
|
24
|
+
self._context_stack: Optional[AsyncExitStack] = None
|
|
25
|
+
|
|
26
|
+
# --------------------------------------------------------------------- #
|
|
27
|
+
# Connection management #
|
|
28
|
+
# --------------------------------------------------------------------- #
|
|
29
|
+
async def initialize(self) -> bool:
|
|
30
|
+
try:
|
|
31
|
+
self._context_stack = AsyncExitStack()
|
|
32
|
+
await self._context_stack.__aenter__()
|
|
33
|
+
|
|
34
|
+
ctx = stdio_client(self.server_params)
|
|
35
|
+
self.read_stream, self.write_stream = await self._context_stack.enter_async_context(ctx)
|
|
36
|
+
|
|
37
|
+
init_result = await send_initialize(self.read_stream, self.write_stream)
|
|
38
|
+
return bool(init_result)
|
|
39
|
+
|
|
40
|
+
except Exception as e: # pragma: no cover
|
|
41
|
+
import logging
|
|
42
|
+
|
|
43
|
+
logging.error(f"Error initializing stdio transport: {e}")
|
|
44
|
+
if self._context_stack:
|
|
45
|
+
try:
|
|
46
|
+
await self._context_stack.__aexit__(None, None, None)
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
async def close(self) -> None:
|
|
52
|
+
if self._context_stack:
|
|
53
|
+
try:
|
|
54
|
+
await self._context_stack.__aexit__(None, None, None)
|
|
55
|
+
except Exception:
|
|
56
|
+
pass
|
|
57
|
+
self.read_stream = None
|
|
58
|
+
self.write_stream = None
|
|
59
|
+
self._context_stack = None
|
|
60
|
+
|
|
61
|
+
# --------------------------------------------------------------------- #
|
|
62
|
+
# Utility #
|
|
63
|
+
# --------------------------------------------------------------------- #
|
|
64
|
+
async def send_ping(self) -> bool:
|
|
65
|
+
if not self.read_stream or not self.write_stream:
|
|
66
|
+
return False
|
|
67
|
+
return await send_ping(self.read_stream, self.write_stream)
|
|
68
|
+
|
|
69
|
+
async def get_tools(self) -> List[Dict[str, Any]]:
|
|
70
|
+
if not self.read_stream or not self.write_stream:
|
|
71
|
+
return []
|
|
72
|
+
tools_response = await send_tools_list(self.read_stream, self.write_stream)
|
|
73
|
+
return tools_response.get("tools", [])
|
|
74
|
+
|
|
75
|
+
# --------------------------------------------------------------------- #
|
|
76
|
+
# Main entry-point #
|
|
77
|
+
# --------------------------------------------------------------------- #
|
|
78
|
+
async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
|
|
79
|
+
"""
|
|
80
|
+
Execute *tool_name* with *arguments* and normalise the server’s reply.
|
|
81
|
+
|
|
82
|
+
The echo-server often returns:
|
|
83
|
+
{
|
|
84
|
+
"content": [{"type":"text","text":"{\"message\":\"…\"}"}],
|
|
85
|
+
"isError": false
|
|
86
|
+
}
|
|
87
|
+
We unwrap that so callers just receive either a dict or a plain string.
|
|
88
|
+
"""
|
|
89
|
+
if not self.read_stream or not self.write_stream:
|
|
90
|
+
return {"isError": True, "error": "Transport not initialized"}
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
raw = await send_tools_call(self.read_stream, self.write_stream, tool_name, arguments)
|
|
94
|
+
|
|
95
|
+
# Handle explicit error wrapper
|
|
96
|
+
if "error" in raw:
|
|
97
|
+
return {"isError": True,
|
|
98
|
+
"error": raw["error"].get("message", "Unknown error")}
|
|
99
|
+
|
|
100
|
+
# Preferred: servers that put the answer under "result"
|
|
101
|
+
if "result" in raw:
|
|
102
|
+
return {"isError": False, "content": raw["result"]}
|
|
103
|
+
|
|
104
|
+
# Common echo-server shape: top-level "content" list
|
|
105
|
+
if "content" in raw:
|
|
106
|
+
clist = raw["content"]
|
|
107
|
+
if isinstance(clist, list) and clist:
|
|
108
|
+
first = clist[0]
|
|
109
|
+
if isinstance(first, dict) and first.get("type") == "text":
|
|
110
|
+
text = first.get("text", "")
|
|
111
|
+
# Try to parse as JSON; fall back to plain string
|
|
112
|
+
try:
|
|
113
|
+
parsed = json.loads(text)
|
|
114
|
+
return {"isError": False, "content": parsed}
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
return {"isError": False, "content": text}
|
|
117
|
+
|
|
118
|
+
# Fallback: give caller whatever the server sent
|
|
119
|
+
return {"isError": False, "content": raw}
|
|
120
|
+
|
|
121
|
+
except Exception as e: # pragma: no cover
|
|
122
|
+
import logging
|
|
123
|
+
|
|
124
|
+
logging.error(f"Error calling tool {tool_name}: {e}")
|
|
125
|
+
return {"isError": True, "error": str(e)}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-tool-processor
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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.1.12
|
|
7
8
|
Requires-Dist: dotenv>=0.9.9
|
|
8
9
|
Requires-Dist: openai>=1.76.0
|
|
9
10
|
Requires-Dist: pydantic>=2.11.3
|
|
@@ -21,6 +22,7 @@ The CHUK Tool Processor is a Python library designed to handle the execution of
|
|
|
21
22
|
2. **Executing tools** with proper isolation and error handling
|
|
22
23
|
3. **Managing tool executions** with retry logic, caching, and rate limiting
|
|
23
24
|
4. **Monitoring tool usage** with comprehensive logging
|
|
25
|
+
5. **MCP (Model Context Protocol) Integration** for remote tool execution
|
|
24
26
|
|
|
25
27
|
## Features
|
|
26
28
|
|
|
@@ -34,6 +36,7 @@ The CHUK Tool Processor is a Python library designed to handle the execution of
|
|
|
34
36
|
- **Retry Logic**: Automatically retry transient failures with exponential backoff
|
|
35
37
|
- **Structured Logging**: Comprehensive logging system for debugging and monitoring
|
|
36
38
|
- **Plugin Discovery**: Dynamically discover and load plugins from packages
|
|
39
|
+
- **MCP Integration**: Connect to and execute remote tools via Model Context Protocol
|
|
37
40
|
|
|
38
41
|
## Installation
|
|
39
42
|
|
|
@@ -101,6 +104,159 @@ if __name__ == "__main__":
|
|
|
101
104
|
asyncio.run(main())
|
|
102
105
|
```
|
|
103
106
|
|
|
107
|
+
## MCP Integration
|
|
108
|
+
|
|
109
|
+
The CHUK Tool Processor supports Model Context Protocol (MCP) for connecting to remote tool servers. This enables distributed tool execution and integration with third-party services.
|
|
110
|
+
|
|
111
|
+
### MCP with Stdio Transport
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
import asyncio
|
|
115
|
+
from chuk_tool_processor.mcp import setup_mcp_stdio
|
|
116
|
+
|
|
117
|
+
async def main():
|
|
118
|
+
# Configure MCP server
|
|
119
|
+
config_file = "server_config.json"
|
|
120
|
+
servers = ["echo", "calculator", "search"]
|
|
121
|
+
server_names = {0: "echo", 1: "calculator", 2: "search"}
|
|
122
|
+
|
|
123
|
+
# Setup MCP with stdio transport
|
|
124
|
+
processor, stream_manager = await setup_mcp_stdio(
|
|
125
|
+
config_file=config_file,
|
|
126
|
+
servers=servers,
|
|
127
|
+
server_names=server_names,
|
|
128
|
+
namespace="mcp", # All tools will be registered under this namespace
|
|
129
|
+
enable_caching=True,
|
|
130
|
+
enable_retries=True
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Process text with MCP tool calls
|
|
134
|
+
llm_text = """
|
|
135
|
+
Let me echo your message using the MCP server.
|
|
136
|
+
|
|
137
|
+
<tool name="mcp.echo" args='{"message": "Hello from MCP!"}'/>
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
results = await processor.process_text(llm_text)
|
|
141
|
+
|
|
142
|
+
for result in results:
|
|
143
|
+
print(f"Tool: {result.tool}")
|
|
144
|
+
print(f"Result: {result.result}")
|
|
145
|
+
|
|
146
|
+
# Clean up
|
|
147
|
+
await stream_manager.close()
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
asyncio.run(main())
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### MCP Server Configuration
|
|
154
|
+
|
|
155
|
+
Create a server configuration file (`server_config.json`):
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"mcpServers": {
|
|
160
|
+
"echo": {
|
|
161
|
+
"command": "uv",
|
|
162
|
+
"args": ["--directory", "/path/to/echo-server", "run", "src/echo_server/main.py"]
|
|
163
|
+
},
|
|
164
|
+
"calculator": {
|
|
165
|
+
"command": "node",
|
|
166
|
+
"args": ["/path/to/calculator-server/index.js"]
|
|
167
|
+
},
|
|
168
|
+
"search": {
|
|
169
|
+
"command": "python",
|
|
170
|
+
"args": ["/path/to/search-server/main.py"]
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Namespaced Tool Access
|
|
177
|
+
|
|
178
|
+
MCP tools are automatically registered in both their namespace and the default namespace:
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
# These are equivalent:
|
|
182
|
+
<tool name="echo" args='{"message": "Hello"}'/>
|
|
183
|
+
<tool name="mcp.echo" args='{"message": "Hello"}'/>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### MCP with SSE Transport
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
import asyncio
|
|
190
|
+
from chuk_tool_processor.mcp import setup_mcp_sse
|
|
191
|
+
|
|
192
|
+
async def main():
|
|
193
|
+
# Configure SSE servers
|
|
194
|
+
sse_servers = [
|
|
195
|
+
{
|
|
196
|
+
"name": "weather",
|
|
197
|
+
"url": "https://api.example.com/sse/weather",
|
|
198
|
+
"api_key": "your_api_key"
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "geocoding",
|
|
202
|
+
"url": "https://api.example.com/sse/geocoding"
|
|
203
|
+
}
|
|
204
|
+
]
|
|
205
|
+
|
|
206
|
+
# Setup MCP with SSE transport
|
|
207
|
+
processor, stream_manager = await setup_mcp_sse(
|
|
208
|
+
servers=sse_servers,
|
|
209
|
+
server_names={0: "weather", 1: "geocoding"},
|
|
210
|
+
namespace="remote",
|
|
211
|
+
enable_caching=True
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Process tool calls
|
|
215
|
+
llm_text = """
|
|
216
|
+
Get the weather for New York.
|
|
217
|
+
|
|
218
|
+
<tool name="remote.weather" args='{"location": "New York", "units": "imperial"}'/>
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
results = await processor.process_text(llm_text)
|
|
222
|
+
|
|
223
|
+
await stream_manager.close()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### MCP Stream Manager
|
|
227
|
+
|
|
228
|
+
The `StreamManager` class handles all MCP communication:
|
|
229
|
+
|
|
230
|
+
```python
|
|
231
|
+
from chuk_tool_processor.mcp.stream_manager import StreamManager
|
|
232
|
+
|
|
233
|
+
# Create and initialize
|
|
234
|
+
stream_manager = await StreamManager.create(
|
|
235
|
+
config_file="config.json",
|
|
236
|
+
servers=["echo", "search"],
|
|
237
|
+
transport_type="stdio"
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
# Get available tools
|
|
241
|
+
tools = stream_manager.get_all_tools()
|
|
242
|
+
for tool in tools:
|
|
243
|
+
print(f"Tool: {tool['name']}")
|
|
244
|
+
|
|
245
|
+
# Get server information
|
|
246
|
+
server_info = stream_manager.get_server_info()
|
|
247
|
+
for server in server_info:
|
|
248
|
+
print(f"Server: {server['name']}, Status: {server['status']}")
|
|
249
|
+
|
|
250
|
+
# Call a tool directly
|
|
251
|
+
result = await stream_manager.call_tool(
|
|
252
|
+
tool_name="echo",
|
|
253
|
+
arguments={"message": "Hello"}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Clean up
|
|
257
|
+
await stream_manager.close()
|
|
258
|
+
```
|
|
259
|
+
|
|
104
260
|
## Advanced Usage
|
|
105
261
|
|
|
106
262
|
### Using Decorators for Tool Configuration
|
|
@@ -259,11 +415,16 @@ The tool processor has several key components organized into a modular structure
|
|
|
259
415
|
- `plugins/discovery.py`: Plugin discovery mechanism
|
|
260
416
|
- `plugins/parsers/`: Parser plugins for different formats
|
|
261
417
|
|
|
262
|
-
5. **
|
|
418
|
+
5. **MCP Integration**: Model Context Protocol support
|
|
419
|
+
- `mcp/stream_manager.py`: Manages MCP server connections
|
|
420
|
+
- `mcp/transport/`: Transport implementations (stdio, SSE)
|
|
421
|
+
- `mcp/setup_mcp_*.py`: Easy setup functions for MCP integration
|
|
422
|
+
|
|
423
|
+
6. **Utils**: Shared utilities
|
|
263
424
|
- `utils/logging.py`: Structured logging system
|
|
264
425
|
- `utils/validation.py`: Argument and result validation
|
|
265
426
|
|
|
266
|
-
|
|
427
|
+
7. **Core**: Central components
|
|
267
428
|
- `core/processor.py`: Main processor for handling tool calls
|
|
268
429
|
- `core/exceptions.py`: Exception hierarchy
|
|
269
430
|
|
|
@@ -274,6 +435,8 @@ The repository includes several example scripts:
|
|
|
274
435
|
- `examples/tool_registry_example.py`: Demonstrates tool registration and usage
|
|
275
436
|
- `examples/plugin_example.py`: Shows how to create and use custom plugins
|
|
276
437
|
- `examples/tool_calling_example_usage.py`: Basic example demonstrating tool execution
|
|
438
|
+
- `examples/mcp_stdio_example.py`: MCP stdio transport demonstration
|
|
439
|
+
- `examples/mcp_stdio_example_calling_usage.py`: Complete MCP integration example
|
|
277
440
|
|
|
278
441
|
Run examples with:
|
|
279
442
|
|
|
@@ -287,6 +450,9 @@ uv run examples/plugin_example.py
|
|
|
287
450
|
# Tool execution example
|
|
288
451
|
uv run examples/tool_calling_example_usage.py
|
|
289
452
|
|
|
453
|
+
# MCP example
|
|
454
|
+
uv run examples/mcp_stdio_example.py
|
|
455
|
+
|
|
290
456
|
# Enable debug logging
|
|
291
457
|
LOGLEVEL=DEBUG uv run examples/tool_calling_example_usage.py
|
|
292
458
|
```
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
chuk_tool_processor/__init__.py,sha256=
|
|
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=
|
|
4
|
+
chuk_tool_processor/core/processor.py,sha256=ud7ezONnUFh_aDSapiBGNx-LtZfhAFpYjFuw2m_tFXk,10165
|
|
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
|
|
@@ -10,12 +10,22 @@ chuk_tool_processor/execution/strategies/subprocess_strategy.py,sha256=Er8z7x94E
|
|
|
10
10
|
chuk_tool_processor/execution/wrappers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
11
|
chuk_tool_processor/execution/wrappers/caching.py,sha256=dA2OULPQ9xCZj-r3ev5LtsCDFDPgoz8tr70YCX5A4Wg,7714
|
|
12
12
|
chuk_tool_processor/execution/wrappers/rate_limiting.py,sha256=pFqD1vLzOtJzsWzpEI7J786gOAbdFY0gVeiO7ElBXbA,4991
|
|
13
|
-
chuk_tool_processor/execution/wrappers/retry.py,sha256=
|
|
14
|
-
chuk_tool_processor/logging/__init__.py,sha256=
|
|
13
|
+
chuk_tool_processor/execution/wrappers/retry.py,sha256=L3lebFWf_IL7DaAhTR541BVIxns7amm5msqYjJcQtnk,6387
|
|
14
|
+
chuk_tool_processor/logging/__init__.py,sha256=kow8qhzuCShXQSpQ4c6OZ0dEok18iMikcYLJTXEluoA,1124
|
|
15
15
|
chuk_tool_processor/logging/context.py,sha256=hQFWGeraHX3DM28JDSiIuhQqep6TBfo1uaLlRRlGMVU,1521
|
|
16
16
|
chuk_tool_processor/logging/formatter.py,sha256=4pO-fLULkD3JPLjZOSiOZPGsjV3c4Ztr5ySda1RAvi4,1754
|
|
17
17
|
chuk_tool_processor/logging/helpers.py,sha256=Fk32BuecWZdyrmv8U0lh4W0AdNx4-gKcCaWBdId_rlI,3569
|
|
18
18
|
chuk_tool_processor/logging/metrics.py,sha256=ti_owuslT-x9cjcbP-_j7jivrlyY-Vb41mVhU-6W-2M,1537
|
|
19
|
+
chuk_tool_processor/mcp/__init__.py,sha256=vR9HHxLpXlKTIIwJJRr3QTmZegcdedR1YKyb46j6FIM,689
|
|
20
|
+
chuk_tool_processor/mcp/mcp_tool.py,sha256=TvZEudgQvaev2jaPw6OGsqAR5GNu6_cPaUCgqiT5ogU,1504
|
|
21
|
+
chuk_tool_processor/mcp/register_mcp_tools.py,sha256=ofE7pEn6sKDH8HWvNamVOaXsitLOaG48M5GhcpqCBbs,2801
|
|
22
|
+
chuk_tool_processor/mcp/setup_mcp_sse.py,sha256=Ep2IKRdH1Y299bCxt9G0NtwnsvguYP6mpraZyUJ8OKU,2643
|
|
23
|
+
chuk_tool_processor/mcp/setup_mcp_stdio.py,sha256=NjTvAFqQHxxN3XubsTgYY3lTrvPVWlnwCzkzbz7WE_M,2747
|
|
24
|
+
chuk_tool_processor/mcp/stream_manager.py,sha256=xdTXDJ08pVpHXvxao0ibXezGqqauMBLXpJhIXgGknOs,10847
|
|
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
|
|
19
29
|
chuk_tool_processor/models/__init__.py,sha256=TC__rdVa0lQsmJHM_hbLDPRgToa_pQT_UxRcPZk6iVw,40
|
|
20
30
|
chuk_tool_processor/models/execution_strategy.py,sha256=ZPHysmKNHqJmahTtUXAbt1ke09vxy7EhZcsrwTdla8o,508
|
|
21
31
|
chuk_tool_processor/models/tool_call.py,sha256=RZOnx2YczkJN6ym2PLiI4CRzP2qU_5hpMtHxMcFOxY4,298
|
|
@@ -41,7 +51,7 @@ chuk_tool_processor/registry/providers/__init__.py,sha256=_0dg4YhyfAV0TXuR_i4ewX
|
|
|
41
51
|
chuk_tool_processor/registry/providers/memory.py,sha256=29aI5uvykjDmn9ymIukEdUtmTC9SXOAsDu9hw36XF44,4474
|
|
42
52
|
chuk_tool_processor/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
43
53
|
chuk_tool_processor/utils/validation.py,sha256=7ezn_o-3IHDrzOD3j6ttsAn2s3zS-jIjeBTuqicrs6A,3775
|
|
44
|
-
chuk_tool_processor-0.1.
|
|
45
|
-
chuk_tool_processor-0.1.
|
|
46
|
-
chuk_tool_processor-0.1.
|
|
47
|
-
chuk_tool_processor-0.1.
|
|
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,,
|
|
File without changes
|