agentrun-sdk 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of agentrun-sdk might be problematic. Click here for more details.
- agentrun_operation_sdk/cli/__init__.py +1 -0
- agentrun_operation_sdk/cli/cli.py +19 -0
- agentrun_operation_sdk/cli/common.py +21 -0
- agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
- agentrun_operation_sdk/cli/runtime/commands.py +203 -0
- agentrun_operation_sdk/client/client.py +75 -0
- agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
- agentrun_operation_sdk/operations/runtime/configure.py +101 -0
- agentrun_operation_sdk/operations/runtime/launch.py +82 -0
- agentrun_operation_sdk/operations/runtime/models.py +31 -0
- agentrun_operation_sdk/services/runtime.py +152 -0
- agentrun_operation_sdk/utils/logging_config.py +72 -0
- agentrun_operation_sdk/utils/runtime/config.py +94 -0
- agentrun_operation_sdk/utils/runtime/container.py +280 -0
- agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
- agentrun_operation_sdk/utils/runtime/schema.py +56 -0
- agentrun_sdk/__init__.py +7 -0
- agentrun_sdk/agent/__init__.py +25 -0
- agentrun_sdk/agent/agent.py +696 -0
- agentrun_sdk/agent/agent_result.py +46 -0
- agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
- agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
- agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
- agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
- agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
- agentrun_sdk/agent/state.py +97 -0
- agentrun_sdk/event_loop/__init__.py +9 -0
- agentrun_sdk/event_loop/event_loop.py +499 -0
- agentrun_sdk/event_loop/streaming.py +319 -0
- agentrun_sdk/experimental/__init__.py +4 -0
- agentrun_sdk/experimental/hooks/__init__.py +15 -0
- agentrun_sdk/experimental/hooks/events.py +123 -0
- agentrun_sdk/handlers/__init__.py +10 -0
- agentrun_sdk/handlers/callback_handler.py +70 -0
- agentrun_sdk/hooks/__init__.py +49 -0
- agentrun_sdk/hooks/events.py +80 -0
- agentrun_sdk/hooks/registry.py +247 -0
- agentrun_sdk/models/__init__.py +10 -0
- agentrun_sdk/models/anthropic.py +432 -0
- agentrun_sdk/models/bedrock.py +649 -0
- agentrun_sdk/models/litellm.py +225 -0
- agentrun_sdk/models/llamaapi.py +438 -0
- agentrun_sdk/models/mistral.py +539 -0
- agentrun_sdk/models/model.py +95 -0
- agentrun_sdk/models/ollama.py +357 -0
- agentrun_sdk/models/openai.py +436 -0
- agentrun_sdk/models/sagemaker.py +598 -0
- agentrun_sdk/models/writer.py +449 -0
- agentrun_sdk/multiagent/__init__.py +22 -0
- agentrun_sdk/multiagent/a2a/__init__.py +15 -0
- agentrun_sdk/multiagent/a2a/executor.py +148 -0
- agentrun_sdk/multiagent/a2a/server.py +252 -0
- agentrun_sdk/multiagent/base.py +92 -0
- agentrun_sdk/multiagent/graph.py +555 -0
- agentrun_sdk/multiagent/swarm.py +656 -0
- agentrun_sdk/py.typed +1 -0
- agentrun_sdk/session/__init__.py +18 -0
- agentrun_sdk/session/file_session_manager.py +216 -0
- agentrun_sdk/session/repository_session_manager.py +152 -0
- agentrun_sdk/session/s3_session_manager.py +272 -0
- agentrun_sdk/session/session_manager.py +73 -0
- agentrun_sdk/session/session_repository.py +51 -0
- agentrun_sdk/telemetry/__init__.py +21 -0
- agentrun_sdk/telemetry/config.py +194 -0
- agentrun_sdk/telemetry/metrics.py +476 -0
- agentrun_sdk/telemetry/metrics_constants.py +15 -0
- agentrun_sdk/telemetry/tracer.py +563 -0
- agentrun_sdk/tools/__init__.py +17 -0
- agentrun_sdk/tools/decorator.py +569 -0
- agentrun_sdk/tools/executor.py +137 -0
- agentrun_sdk/tools/loader.py +152 -0
- agentrun_sdk/tools/mcp/__init__.py +13 -0
- agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
- agentrun_sdk/tools/mcp/mcp_client.py +423 -0
- agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
- agentrun_sdk/tools/mcp/mcp_types.py +63 -0
- agentrun_sdk/tools/registry.py +607 -0
- agentrun_sdk/tools/structured_output.py +421 -0
- agentrun_sdk/tools/tools.py +217 -0
- agentrun_sdk/tools/watcher.py +136 -0
- agentrun_sdk/types/__init__.py +5 -0
- agentrun_sdk/types/collections.py +23 -0
- agentrun_sdk/types/content.py +188 -0
- agentrun_sdk/types/event_loop.py +48 -0
- agentrun_sdk/types/exceptions.py +81 -0
- agentrun_sdk/types/guardrails.py +254 -0
- agentrun_sdk/types/media.py +89 -0
- agentrun_sdk/types/session.py +152 -0
- agentrun_sdk/types/streaming.py +201 -0
- agentrun_sdk/types/tools.py +258 -0
- agentrun_sdk/types/traces.py +5 -0
- agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
- agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
- agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
- agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
- agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
- agentrun_wrapper/__init__.py +11 -0
- agentrun_wrapper/_utils/__init__.py +6 -0
- agentrun_wrapper/_utils/endpoints.py +16 -0
- agentrun_wrapper/identity/__init__.py +5 -0
- agentrun_wrapper/identity/auth.py +211 -0
- agentrun_wrapper/memory/__init__.py +6 -0
- agentrun_wrapper/memory/client.py +1697 -0
- agentrun_wrapper/memory/constants.py +103 -0
- agentrun_wrapper/memory/controlplane.py +626 -0
- agentrun_wrapper/py.typed +1 -0
- agentrun_wrapper/runtime/__init__.py +13 -0
- agentrun_wrapper/runtime/app.py +473 -0
- agentrun_wrapper/runtime/context.py +34 -0
- agentrun_wrapper/runtime/models.py +25 -0
- agentrun_wrapper/services/__init__.py +1 -0
- agentrun_wrapper/services/identity.py +192 -0
- agentrun_wrapper/tools/__init__.py +6 -0
- agentrun_wrapper/tools/browser_client.py +325 -0
- agentrun_wrapper/tools/code_interpreter_client.py +186 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Tool loading utilities."""
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import logging
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import cast
|
|
9
|
+
|
|
10
|
+
from ..types.tools import AgentTool
|
|
11
|
+
from .decorator import DecoratedFunctionTool
|
|
12
|
+
from .tools import PythonAgentTool
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ToolLoader:
|
|
18
|
+
"""Handles loading of tools from different sources."""
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def load_python_tool(tool_path: str, tool_name: str) -> AgentTool:
|
|
22
|
+
"""Load a Python tool module.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
tool_path: Path to the Python tool file.
|
|
26
|
+
tool_name: Name of the tool.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Tool instance.
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
AttributeError: If required attributes are missing from the tool module.
|
|
33
|
+
ImportError: If there are issues importing the tool module.
|
|
34
|
+
TypeError: If the tool function is not callable.
|
|
35
|
+
ValueError: If function in module is not a valid tool.
|
|
36
|
+
Exception: For other errors during tool loading.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# Check if tool_path is in the format "package.module:function"; but keep in mind windows whose file path
|
|
40
|
+
# could have a colon so also ensure that it's not a file
|
|
41
|
+
if not os.path.exists(tool_path) and ":" in tool_path:
|
|
42
|
+
module_path, function_name = tool_path.rsplit(":", 1)
|
|
43
|
+
logger.debug("tool_name=<%s>, module_path=<%s> | importing tool from path", function_name, module_path)
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
# Import the module
|
|
47
|
+
module = __import__(module_path, fromlist=["*"])
|
|
48
|
+
|
|
49
|
+
# Get the function
|
|
50
|
+
if not hasattr(module, function_name):
|
|
51
|
+
raise AttributeError(f"Module {module_path} has no function named {function_name}")
|
|
52
|
+
|
|
53
|
+
func = getattr(module, function_name)
|
|
54
|
+
|
|
55
|
+
if isinstance(func, DecoratedFunctionTool):
|
|
56
|
+
logger.debug(
|
|
57
|
+
"tool_name=<%s>, module_path=<%s> | found function-based tool", function_name, module_path
|
|
58
|
+
)
|
|
59
|
+
# mypy has problems converting between DecoratedFunctionTool <-> AgentTool
|
|
60
|
+
return cast(AgentTool, func)
|
|
61
|
+
else:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Function {function_name} in {module_path} is not a valid tool (missing @tool decorator)"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
except ImportError as e:
|
|
67
|
+
raise ImportError(f"Failed to import module {module_path}: {str(e)}") from e
|
|
68
|
+
|
|
69
|
+
# Normal file-based tool loading
|
|
70
|
+
abs_path = str(Path(tool_path).resolve())
|
|
71
|
+
|
|
72
|
+
logger.debug("tool_path=<%s> | loading python tool from path", abs_path)
|
|
73
|
+
|
|
74
|
+
# First load the module to get TOOL_SPEC and check for Lambda deployment
|
|
75
|
+
spec = importlib.util.spec_from_file_location(tool_name, abs_path)
|
|
76
|
+
if not spec:
|
|
77
|
+
raise ImportError(f"Could not create spec for {tool_name}")
|
|
78
|
+
if not spec.loader:
|
|
79
|
+
raise ImportError(f"No loader available for {tool_name}")
|
|
80
|
+
|
|
81
|
+
module = importlib.util.module_from_spec(spec)
|
|
82
|
+
sys.modules[tool_name] = module
|
|
83
|
+
spec.loader.exec_module(module)
|
|
84
|
+
|
|
85
|
+
# First, check for function-based tools with @tool decorator
|
|
86
|
+
for attr_name in dir(module):
|
|
87
|
+
attr = getattr(module, attr_name)
|
|
88
|
+
if isinstance(attr, DecoratedFunctionTool):
|
|
89
|
+
logger.debug(
|
|
90
|
+
"tool_name=<%s>, tool_path=<%s> | found function-based tool in path", attr_name, tool_path
|
|
91
|
+
)
|
|
92
|
+
# mypy has problems converting between DecoratedFunctionTool <-> AgentTool
|
|
93
|
+
return cast(AgentTool, attr)
|
|
94
|
+
|
|
95
|
+
# If no function-based tools found, fall back to traditional module-level tool
|
|
96
|
+
tool_spec = getattr(module, "TOOL_SPEC", None)
|
|
97
|
+
if not tool_spec:
|
|
98
|
+
raise AttributeError(
|
|
99
|
+
f"Tool {tool_name} missing TOOL_SPEC (neither at module level nor as a decorated function)"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Standard local tool loading
|
|
103
|
+
tool_func_name = tool_name
|
|
104
|
+
if not hasattr(module, tool_func_name):
|
|
105
|
+
raise AttributeError(f"Tool {tool_name} missing function {tool_func_name}")
|
|
106
|
+
|
|
107
|
+
tool_func = getattr(module, tool_func_name)
|
|
108
|
+
if not callable(tool_func):
|
|
109
|
+
raise TypeError(f"Tool {tool_name} function is not callable")
|
|
110
|
+
|
|
111
|
+
return PythonAgentTool(tool_name, tool_spec, tool_func)
|
|
112
|
+
|
|
113
|
+
except Exception:
|
|
114
|
+
logger.exception("tool_name=<%s>, sys_path=<%s> | failed to load python tool", tool_name, sys.path)
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def load_tool(cls, tool_path: str, tool_name: str) -> AgentTool:
|
|
119
|
+
"""Load a tool based on its file extension.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
tool_path: Path to the tool file.
|
|
123
|
+
tool_name: Name of the tool.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Tool instance.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
FileNotFoundError: If the tool file does not exist.
|
|
130
|
+
ValueError: If the tool file has an unsupported extension.
|
|
131
|
+
Exception: For other errors during tool loading.
|
|
132
|
+
"""
|
|
133
|
+
ext = Path(tool_path).suffix.lower()
|
|
134
|
+
abs_path = str(Path(tool_path).resolve())
|
|
135
|
+
|
|
136
|
+
if not os.path.exists(abs_path):
|
|
137
|
+
raise FileNotFoundError(f"Tool file not found: {abs_path}")
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
if ext == ".py":
|
|
141
|
+
return cls.load_python_tool(abs_path, tool_name)
|
|
142
|
+
else:
|
|
143
|
+
raise ValueError(f"Unsupported tool file type: {ext}")
|
|
144
|
+
except Exception:
|
|
145
|
+
logger.exception(
|
|
146
|
+
"tool_name=<%s>, tool_path=<%s>, tool_ext=<%s>, cwd=<%s> | failed to load tool",
|
|
147
|
+
tool_name,
|
|
148
|
+
abs_path,
|
|
149
|
+
ext,
|
|
150
|
+
os.getcwd(),
|
|
151
|
+
)
|
|
152
|
+
raise
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Model Context Protocol (MCP) integration.
|
|
2
|
+
|
|
3
|
+
This package provides integration with the Model Context Protocol (MCP), allowing agents to use tools provided by MCP
|
|
4
|
+
servers.
|
|
5
|
+
|
|
6
|
+
- Docs: https://www.anthropic.com/news/model-context-protocol
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .mcp_agent_tool import MCPAgentTool
|
|
10
|
+
from .mcp_client import MCPClient
|
|
11
|
+
from .mcp_types import MCPTransport
|
|
12
|
+
|
|
13
|
+
__all__ = ["MCPAgentTool", "MCPClient", "MCPTransport"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""MCP Agent Tool module for adapting Model Context Protocol tools to the agent framework.
|
|
2
|
+
|
|
3
|
+
This module provides the MCPAgentTool class which serves as an adapter between
|
|
4
|
+
MCP (Model Context Protocol) tools and the agent framework's tool interface.
|
|
5
|
+
It allows MCP tools to be seamlessly integrated and used within the agent ecosystem.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING, Any
|
|
10
|
+
|
|
11
|
+
from mcp.types import Tool as MCPTool
|
|
12
|
+
from typing_extensions import override
|
|
13
|
+
|
|
14
|
+
from ...types.tools import AgentTool, ToolGenerator, ToolSpec, ToolUse
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from .mcp_client import MCPClient
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MCPAgentTool(AgentTool):
|
|
23
|
+
"""Adapter class that wraps an MCP tool and exposes it as an AgentTool.
|
|
24
|
+
|
|
25
|
+
This class bridges the gap between the MCP protocol's tool representation
|
|
26
|
+
and the agent framework's tool interface, allowing MCP tools to be used
|
|
27
|
+
seamlessly within the agent framework.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, mcp_tool: MCPTool, mcp_client: "MCPClient") -> None:
|
|
31
|
+
"""Initialize a new MCPAgentTool instance.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
mcp_tool: The MCP tool to adapt
|
|
35
|
+
mcp_client: The MCP server connection to use for tool invocation
|
|
36
|
+
"""
|
|
37
|
+
super().__init__()
|
|
38
|
+
logger.debug("tool_name=<%s> | creating mcp agent tool", mcp_tool.name)
|
|
39
|
+
self.mcp_tool = mcp_tool
|
|
40
|
+
self.mcp_client = mcp_client
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def tool_name(self) -> str:
|
|
44
|
+
"""Get the name of the tool.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: The name of the MCP tool
|
|
48
|
+
"""
|
|
49
|
+
return self.mcp_tool.name
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def tool_spec(self) -> ToolSpec:
|
|
53
|
+
"""Get the specification of the tool.
|
|
54
|
+
|
|
55
|
+
This method converts the MCP tool specification to the agent framework's
|
|
56
|
+
ToolSpec format, including the input schema and description.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
ToolSpec: The tool specification in the agent framework format
|
|
60
|
+
"""
|
|
61
|
+
description: str = self.mcp_tool.description or f"Tool which performs {self.mcp_tool.name}"
|
|
62
|
+
return {
|
|
63
|
+
"inputSchema": {"json": self.mcp_tool.inputSchema},
|
|
64
|
+
"name": self.mcp_tool.name,
|
|
65
|
+
"description": description,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def tool_type(self) -> str:
|
|
70
|
+
"""Get the type of the tool.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
str: The type of the tool, always "python" for MCP tools
|
|
74
|
+
"""
|
|
75
|
+
return "python"
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
async def stream(self, tool_use: ToolUse, invocation_state: dict[str, Any], **kwargs: Any) -> ToolGenerator:
|
|
79
|
+
"""Stream the MCP tool.
|
|
80
|
+
|
|
81
|
+
This method delegates the tool stream to the MCP server connection, passing the tool use ID, tool name, and
|
|
82
|
+
input arguments.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
tool_use: The tool use request containing tool ID and parameters.
|
|
86
|
+
invocation_state: Context for the tool invocation, including agent state.
|
|
87
|
+
**kwargs: Additional keyword arguments for future extensibility.
|
|
88
|
+
|
|
89
|
+
Yields:
|
|
90
|
+
Tool events with the last being the tool result.
|
|
91
|
+
"""
|
|
92
|
+
logger.debug("tool_name=<%s>, tool_use_id=<%s> | streaming", self.tool_name, tool_use["toolUseId"])
|
|
93
|
+
|
|
94
|
+
result = await self.mcp_client.call_tool_async(
|
|
95
|
+
tool_use_id=tool_use["toolUseId"],
|
|
96
|
+
name=self.tool_name,
|
|
97
|
+
arguments=tool_use["input"],
|
|
98
|
+
)
|
|
99
|
+
yield result
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""Model Context Protocol (MCP) server connection management module.
|
|
2
|
+
|
|
3
|
+
This module provides the MCPClient class which handles connections to MCP servers.
|
|
4
|
+
It manages the lifecycle of MCP connections, including initialization, tool discovery,
|
|
5
|
+
tool invocation, and proper cleanup of resources. The connection runs in a background
|
|
6
|
+
thread to avoid blocking the main application thread while maintaining communication
|
|
7
|
+
with the MCP service.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import base64
|
|
12
|
+
import logging
|
|
13
|
+
import threading
|
|
14
|
+
import uuid
|
|
15
|
+
from asyncio import AbstractEventLoop
|
|
16
|
+
from concurrent import futures
|
|
17
|
+
from datetime import timedelta
|
|
18
|
+
from types import TracebackType
|
|
19
|
+
from typing import Any, Callable, Coroutine, Dict, Optional, TypeVar, Union
|
|
20
|
+
|
|
21
|
+
from mcp import ClientSession, ListToolsResult
|
|
22
|
+
from mcp.types import CallToolResult as MCPCallToolResult
|
|
23
|
+
from mcp.types import GetPromptResult, ListPromptsResult
|
|
24
|
+
from mcp.types import ImageContent as MCPImageContent
|
|
25
|
+
from mcp.types import TextContent as MCPTextContent
|
|
26
|
+
|
|
27
|
+
from ...types import PaginatedList
|
|
28
|
+
from ...types.exceptions import MCPClientInitializationError
|
|
29
|
+
from ...types.media import ImageFormat
|
|
30
|
+
from ...types.tools import ToolResultContent, ToolResultStatus
|
|
31
|
+
from .mcp_agent_tool import MCPAgentTool
|
|
32
|
+
from .mcp_instrumentation import mcp_instrumentation
|
|
33
|
+
from .mcp_types import MCPToolResult, MCPTransport
|
|
34
|
+
|
|
35
|
+
logger = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
T = TypeVar("T")
|
|
38
|
+
|
|
39
|
+
MIME_TO_FORMAT: Dict[str, ImageFormat] = {
|
|
40
|
+
"image/jpeg": "jpeg",
|
|
41
|
+
"image/jpg": "jpeg",
|
|
42
|
+
"image/png": "png",
|
|
43
|
+
"image/gif": "gif",
|
|
44
|
+
"image/webp": "webp",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE = (
|
|
48
|
+
"the client session is not running. Ensure the agent is used within "
|
|
49
|
+
"the MCP client context manager. For more information see: "
|
|
50
|
+
"https://strandsagents.com/latest/user-guide/concepts/tools/mcp-tools/#mcpclientinitializationerror"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class MCPClient:
|
|
55
|
+
"""Represents a connection to a Model Context Protocol (MCP) server.
|
|
56
|
+
|
|
57
|
+
This class implements a context manager pattern for efficient connection management,
|
|
58
|
+
allowing reuse of the same connection for multiple tool calls to reduce latency.
|
|
59
|
+
It handles the creation, initialization, and cleanup of MCP connections.
|
|
60
|
+
|
|
61
|
+
The connection runs in a background thread to avoid blocking the main application thread
|
|
62
|
+
while maintaining communication with the MCP service. When structured content is available
|
|
63
|
+
from MCP tools, it will be returned as the last item in the content array of the ToolResult.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, transport_callable: Callable[[], MCPTransport]):
|
|
67
|
+
"""Initialize a new MCP Server connection.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
transport_callable: A callable that returns an MCPTransport (read_stream, write_stream) tuple
|
|
71
|
+
"""
|
|
72
|
+
mcp_instrumentation()
|
|
73
|
+
self._session_id = uuid.uuid4()
|
|
74
|
+
self._log_debug_with_thread("initializing MCPClient connection")
|
|
75
|
+
self._init_future: futures.Future[None] = futures.Future() # Main thread blocks until future completes
|
|
76
|
+
self._close_event = asyncio.Event() # Do not want to block other threads while close event is false
|
|
77
|
+
self._transport_callable = transport_callable
|
|
78
|
+
|
|
79
|
+
self._background_thread: threading.Thread | None = None
|
|
80
|
+
self._background_thread_session: ClientSession
|
|
81
|
+
self._background_thread_event_loop: AbstractEventLoop
|
|
82
|
+
|
|
83
|
+
def __enter__(self) -> "MCPClient":
|
|
84
|
+
"""Context manager entry point which initializes the MCP server connection."""
|
|
85
|
+
return self.start()
|
|
86
|
+
|
|
87
|
+
def __exit__(self, exc_type: BaseException, exc_val: BaseException, exc_tb: TracebackType) -> None:
|
|
88
|
+
"""Context manager exit point that cleans up resources."""
|
|
89
|
+
self.stop(exc_type, exc_val, exc_tb)
|
|
90
|
+
|
|
91
|
+
def start(self) -> "MCPClient":
|
|
92
|
+
"""Starts the background thread and waits for initialization.
|
|
93
|
+
|
|
94
|
+
This method starts the background thread that manages the MCP connection
|
|
95
|
+
and blocks until the connection is ready or times out.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
self: The MCPClient instance
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
Exception: If the MCP connection fails to initialize within the timeout period
|
|
102
|
+
"""
|
|
103
|
+
if self._is_session_active():
|
|
104
|
+
raise MCPClientInitializationError("the client session is currently running")
|
|
105
|
+
|
|
106
|
+
self._log_debug_with_thread("entering MCPClient context")
|
|
107
|
+
self._background_thread = threading.Thread(target=self._background_task, args=[], daemon=True)
|
|
108
|
+
self._background_thread.start()
|
|
109
|
+
self._log_debug_with_thread("background thread started, waiting for ready event")
|
|
110
|
+
try:
|
|
111
|
+
# Blocking main thread until session is initialized in other thread or if the thread stops
|
|
112
|
+
self._init_future.result(timeout=30)
|
|
113
|
+
self._log_debug_with_thread("the client initialization was successful")
|
|
114
|
+
except futures.TimeoutError as e:
|
|
115
|
+
raise MCPClientInitializationError("background thread did not start in 30 seconds") from e
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.exception("client failed to initialize")
|
|
118
|
+
raise MCPClientInitializationError("the client initialization failed") from e
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def stop(
|
|
122
|
+
self, exc_type: Optional[BaseException], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType]
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Signals the background thread to stop and waits for it to complete, ensuring proper cleanup of all resources.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
exc_type: Exception type if an exception was raised in the context
|
|
128
|
+
exc_val: Exception value if an exception was raised in the context
|
|
129
|
+
exc_tb: Exception traceback if an exception was raised in the context
|
|
130
|
+
"""
|
|
131
|
+
self._log_debug_with_thread("exiting MCPClient context")
|
|
132
|
+
|
|
133
|
+
async def _set_close_event() -> None:
|
|
134
|
+
self._close_event.set()
|
|
135
|
+
|
|
136
|
+
self._invoke_on_background_thread(_set_close_event()).result()
|
|
137
|
+
self._log_debug_with_thread("waiting for background thread to join")
|
|
138
|
+
if self._background_thread is not None:
|
|
139
|
+
self._background_thread.join()
|
|
140
|
+
self._log_debug_with_thread("background thread joined, MCPClient context exited")
|
|
141
|
+
|
|
142
|
+
# Reset fields to allow instance reuse
|
|
143
|
+
self._init_future = futures.Future()
|
|
144
|
+
self._close_event = asyncio.Event()
|
|
145
|
+
self._background_thread = None
|
|
146
|
+
self._session_id = uuid.uuid4()
|
|
147
|
+
|
|
148
|
+
def list_tools_sync(self, pagination_token: Optional[str] = None) -> PaginatedList[MCPAgentTool]:
|
|
149
|
+
"""Synchronously retrieves the list of available tools from the MCP server.
|
|
150
|
+
|
|
151
|
+
This method calls the asynchronous list_tools method on the MCP session
|
|
152
|
+
and adapts the returned tools to the AgentTool interface.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
List[AgentTool]: A list of available tools adapted to the AgentTool interface
|
|
156
|
+
"""
|
|
157
|
+
self._log_debug_with_thread("listing MCP tools synchronously")
|
|
158
|
+
if not self._is_session_active():
|
|
159
|
+
raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE)
|
|
160
|
+
|
|
161
|
+
async def _list_tools_async() -> ListToolsResult:
|
|
162
|
+
return await self._background_thread_session.list_tools(cursor=pagination_token)
|
|
163
|
+
|
|
164
|
+
list_tools_response: ListToolsResult = self._invoke_on_background_thread(_list_tools_async()).result()
|
|
165
|
+
self._log_debug_with_thread("received %d tools from MCP server", len(list_tools_response.tools))
|
|
166
|
+
|
|
167
|
+
mcp_tools = [MCPAgentTool(tool, self) for tool in list_tools_response.tools]
|
|
168
|
+
self._log_debug_with_thread("successfully adapted %d MCP tools", len(mcp_tools))
|
|
169
|
+
return PaginatedList[MCPAgentTool](mcp_tools, token=list_tools_response.nextCursor)
|
|
170
|
+
|
|
171
|
+
def list_prompts_sync(self, pagination_token: Optional[str] = None) -> ListPromptsResult:
|
|
172
|
+
"""Synchronously retrieves the list of available prompts from the MCP server.
|
|
173
|
+
|
|
174
|
+
This method calls the asynchronous list_prompts method on the MCP session
|
|
175
|
+
and returns the raw ListPromptsResult with pagination support.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
pagination_token: Optional token for pagination
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
ListPromptsResult: The raw MCP response containing prompts and pagination info
|
|
182
|
+
"""
|
|
183
|
+
self._log_debug_with_thread("listing MCP prompts synchronously")
|
|
184
|
+
if not self._is_session_active():
|
|
185
|
+
raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE)
|
|
186
|
+
|
|
187
|
+
async def _list_prompts_async() -> ListPromptsResult:
|
|
188
|
+
return await self._background_thread_session.list_prompts(cursor=pagination_token)
|
|
189
|
+
|
|
190
|
+
list_prompts_result: ListPromptsResult = self._invoke_on_background_thread(_list_prompts_async()).result()
|
|
191
|
+
self._log_debug_with_thread("received %d prompts from MCP server", len(list_prompts_result.prompts))
|
|
192
|
+
for prompt in list_prompts_result.prompts:
|
|
193
|
+
self._log_debug_with_thread(prompt.name)
|
|
194
|
+
|
|
195
|
+
return list_prompts_result
|
|
196
|
+
|
|
197
|
+
def get_prompt_sync(self, prompt_id: str, args: dict[str, Any]) -> GetPromptResult:
|
|
198
|
+
"""Synchronously retrieves a prompt from the MCP server.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
prompt_id: The ID of the prompt to retrieve
|
|
202
|
+
args: Optional arguments to pass to the prompt
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
GetPromptResult: The prompt response from the MCP server
|
|
206
|
+
"""
|
|
207
|
+
self._log_debug_with_thread("getting MCP prompt synchronously")
|
|
208
|
+
if not self._is_session_active():
|
|
209
|
+
raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE)
|
|
210
|
+
|
|
211
|
+
async def _get_prompt_async() -> GetPromptResult:
|
|
212
|
+
return await self._background_thread_session.get_prompt(prompt_id, arguments=args)
|
|
213
|
+
|
|
214
|
+
get_prompt_result: GetPromptResult = self._invoke_on_background_thread(_get_prompt_async()).result()
|
|
215
|
+
self._log_debug_with_thread("received prompt from MCP server")
|
|
216
|
+
|
|
217
|
+
return get_prompt_result
|
|
218
|
+
|
|
219
|
+
def call_tool_sync(
|
|
220
|
+
self,
|
|
221
|
+
tool_use_id: str,
|
|
222
|
+
name: str,
|
|
223
|
+
arguments: dict[str, Any] | None = None,
|
|
224
|
+
read_timeout_seconds: timedelta | None = None,
|
|
225
|
+
) -> MCPToolResult:
|
|
226
|
+
"""Synchronously calls a tool on the MCP server.
|
|
227
|
+
|
|
228
|
+
This method calls the asynchronous call_tool method on the MCP session
|
|
229
|
+
and converts the result to the ToolResult format. If the MCP tool returns
|
|
230
|
+
structured content, it will be included as the last item in the content array
|
|
231
|
+
of the returned ToolResult.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
tool_use_id: Unique identifier for this tool use
|
|
235
|
+
name: Name of the tool to call
|
|
236
|
+
arguments: Optional arguments to pass to the tool
|
|
237
|
+
read_timeout_seconds: Optional timeout for the tool call
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
MCPToolResult: The result of the tool call
|
|
241
|
+
"""
|
|
242
|
+
self._log_debug_with_thread("calling MCP tool '%s' synchronously with tool_use_id=%s", name, tool_use_id)
|
|
243
|
+
if not self._is_session_active():
|
|
244
|
+
raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE)
|
|
245
|
+
|
|
246
|
+
async def _call_tool_async() -> MCPCallToolResult:
|
|
247
|
+
return await self._background_thread_session.call_tool(name, arguments, read_timeout_seconds)
|
|
248
|
+
|
|
249
|
+
try:
|
|
250
|
+
call_tool_result: MCPCallToolResult = self._invoke_on_background_thread(_call_tool_async()).result()
|
|
251
|
+
return self._handle_tool_result(tool_use_id, call_tool_result)
|
|
252
|
+
except Exception as e:
|
|
253
|
+
logger.exception("tool execution failed")
|
|
254
|
+
return self._handle_tool_execution_error(tool_use_id, e)
|
|
255
|
+
|
|
256
|
+
async def call_tool_async(
|
|
257
|
+
self,
|
|
258
|
+
tool_use_id: str,
|
|
259
|
+
name: str,
|
|
260
|
+
arguments: dict[str, Any] | None = None,
|
|
261
|
+
read_timeout_seconds: timedelta | None = None,
|
|
262
|
+
) -> MCPToolResult:
|
|
263
|
+
"""Asynchronously calls a tool on the MCP server.
|
|
264
|
+
|
|
265
|
+
This method calls the asynchronous call_tool method on the MCP session
|
|
266
|
+
and converts the result to the MCPToolResult format.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
tool_use_id: Unique identifier for this tool use
|
|
270
|
+
name: Name of the tool to call
|
|
271
|
+
arguments: Optional arguments to pass to the tool
|
|
272
|
+
read_timeout_seconds: Optional timeout for the tool call
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
MCPToolResult: The result of the tool call
|
|
276
|
+
"""
|
|
277
|
+
self._log_debug_with_thread("calling MCP tool '%s' asynchronously with tool_use_id=%s", name, tool_use_id)
|
|
278
|
+
if not self._is_session_active():
|
|
279
|
+
raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE)
|
|
280
|
+
|
|
281
|
+
async def _call_tool_async() -> MCPCallToolResult:
|
|
282
|
+
return await self._background_thread_session.call_tool(name, arguments, read_timeout_seconds)
|
|
283
|
+
|
|
284
|
+
try:
|
|
285
|
+
future = self._invoke_on_background_thread(_call_tool_async())
|
|
286
|
+
call_tool_result: MCPCallToolResult = await asyncio.wrap_future(future)
|
|
287
|
+
return self._handle_tool_result(tool_use_id, call_tool_result)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
logger.exception("tool execution failed")
|
|
290
|
+
return self._handle_tool_execution_error(tool_use_id, e)
|
|
291
|
+
|
|
292
|
+
def _handle_tool_execution_error(self, tool_use_id: str, exception: Exception) -> MCPToolResult:
|
|
293
|
+
"""Create error ToolResult with consistent logging."""
|
|
294
|
+
return MCPToolResult(
|
|
295
|
+
status="error",
|
|
296
|
+
toolUseId=tool_use_id,
|
|
297
|
+
content=[{"text": f"Tool execution failed: {str(exception)}"}],
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
def _handle_tool_result(self, tool_use_id: str, call_tool_result: MCPCallToolResult) -> MCPToolResult:
|
|
301
|
+
"""Maps MCP tool result to the agent's MCPToolResult format.
|
|
302
|
+
|
|
303
|
+
This method processes the content from the MCP tool call result and converts it to the format
|
|
304
|
+
expected by the framework.
|
|
305
|
+
|
|
306
|
+
Args:
|
|
307
|
+
tool_use_id: Unique identifier for this tool use
|
|
308
|
+
call_tool_result: The result from the MCP tool call
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
MCPToolResult: The converted tool result
|
|
312
|
+
"""
|
|
313
|
+
self._log_debug_with_thread("received tool result with %d content items", len(call_tool_result.content))
|
|
314
|
+
|
|
315
|
+
mapped_content = [
|
|
316
|
+
mapped_content
|
|
317
|
+
for content in call_tool_result.content
|
|
318
|
+
if (mapped_content := self._map_mcp_content_to_tool_result_content(content)) is not None
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
status: ToolResultStatus = "error" if call_tool_result.isError else "success"
|
|
322
|
+
self._log_debug_with_thread("tool execution completed with status: %s", status)
|
|
323
|
+
result = MCPToolResult(
|
|
324
|
+
status=status,
|
|
325
|
+
toolUseId=tool_use_id,
|
|
326
|
+
content=mapped_content,
|
|
327
|
+
)
|
|
328
|
+
if call_tool_result.structuredContent:
|
|
329
|
+
result["structuredContent"] = call_tool_result.structuredContent
|
|
330
|
+
|
|
331
|
+
return result
|
|
332
|
+
|
|
333
|
+
async def _async_background_thread(self) -> None:
|
|
334
|
+
"""Asynchronous method that runs in the background thread to manage the MCP connection.
|
|
335
|
+
|
|
336
|
+
This method establishes the transport connection, creates and initializes the MCP session,
|
|
337
|
+
signals readiness to the main thread, and waits for a close signal.
|
|
338
|
+
"""
|
|
339
|
+
self._log_debug_with_thread("starting async background thread for MCP connection")
|
|
340
|
+
try:
|
|
341
|
+
async with self._transport_callable() as (read_stream, write_stream, *_):
|
|
342
|
+
self._log_debug_with_thread("transport connection established")
|
|
343
|
+
async with ClientSession(read_stream, write_stream) as session:
|
|
344
|
+
self._log_debug_with_thread("initializing MCP session")
|
|
345
|
+
await session.initialize()
|
|
346
|
+
|
|
347
|
+
self._log_debug_with_thread("session initialized successfully")
|
|
348
|
+
# Store the session for use while we await the close event
|
|
349
|
+
self._background_thread_session = session
|
|
350
|
+
self._init_future.set_result(None) # Signal that the session has been created and is ready for use
|
|
351
|
+
|
|
352
|
+
self._log_debug_with_thread("waiting for close signal")
|
|
353
|
+
# Keep background thread running until signaled to close.
|
|
354
|
+
# Thread is not blocked as this is an asyncio.Event not a threading.Event
|
|
355
|
+
await self._close_event.wait()
|
|
356
|
+
self._log_debug_with_thread("close signal received")
|
|
357
|
+
except Exception as e:
|
|
358
|
+
# If we encounter an exception and the future is still running,
|
|
359
|
+
# it means it was encountered during the initialization phase.
|
|
360
|
+
if not self._init_future.done():
|
|
361
|
+
self._init_future.set_exception(e)
|
|
362
|
+
else:
|
|
363
|
+
self._log_debug_with_thread(
|
|
364
|
+
"encountered exception on background thread after initialization %s", str(e)
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def _background_task(self) -> None:
|
|
368
|
+
"""Sets up and runs the event loop in the background thread.
|
|
369
|
+
|
|
370
|
+
This method creates a new event loop for the background thread,
|
|
371
|
+
sets it as the current event loop, and runs the async_background_thread
|
|
372
|
+
coroutine until completion. In this case "until completion" means until the _close_event is set.
|
|
373
|
+
This allows for a long-running event loop.
|
|
374
|
+
"""
|
|
375
|
+
self._log_debug_with_thread("setting up background task event loop")
|
|
376
|
+
self._background_thread_event_loop = asyncio.new_event_loop()
|
|
377
|
+
asyncio.set_event_loop(self._background_thread_event_loop)
|
|
378
|
+
self._background_thread_event_loop.run_until_complete(self._async_background_thread())
|
|
379
|
+
|
|
380
|
+
def _map_mcp_content_to_tool_result_content(
|
|
381
|
+
self,
|
|
382
|
+
content: MCPTextContent | MCPImageContent | Any,
|
|
383
|
+
) -> Union[ToolResultContent, None]:
|
|
384
|
+
"""Maps MCP content types to tool result content types.
|
|
385
|
+
|
|
386
|
+
This method converts MCP-specific content types to the generic
|
|
387
|
+
ToolResultContent format used by the agent framework.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
content: The MCP content to convert
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
ToolResultContent or None: The converted content, or None if the content type is not supported
|
|
394
|
+
"""
|
|
395
|
+
if isinstance(content, MCPTextContent):
|
|
396
|
+
self._log_debug_with_thread("mapping MCP text content")
|
|
397
|
+
return {"text": content.text}
|
|
398
|
+
elif isinstance(content, MCPImageContent):
|
|
399
|
+
self._log_debug_with_thread("mapping MCP image content with mime type: %s", content.mimeType)
|
|
400
|
+
return {
|
|
401
|
+
"image": {
|
|
402
|
+
"format": MIME_TO_FORMAT[content.mimeType],
|
|
403
|
+
"source": {"bytes": base64.b64decode(content.data)},
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
else:
|
|
407
|
+
self._log_debug_with_thread("unhandled content type: %s - dropping content", content.__class__.__name__)
|
|
408
|
+
return None
|
|
409
|
+
|
|
410
|
+
def _log_debug_with_thread(self, msg: str, *args: Any, **kwargs: Any) -> None:
|
|
411
|
+
"""Logger helper to help differentiate logs coming from MCPClient background thread."""
|
|
412
|
+
formatted_msg = msg % args if args else msg
|
|
413
|
+
logger.debug(
|
|
414
|
+
"[Thread: %s, Session: %s] %s", threading.current_thread().name, self._session_id, formatted_msg, **kwargs
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
def _invoke_on_background_thread(self, coro: Coroutine[Any, Any, T]) -> futures.Future[T]:
|
|
418
|
+
if self._background_thread_session is None or self._background_thread_event_loop is None:
|
|
419
|
+
raise MCPClientInitializationError("the client session was not initialized")
|
|
420
|
+
return asyncio.run_coroutine_threadsafe(coro=coro, loop=self._background_thread_event_loop)
|
|
421
|
+
|
|
422
|
+
def _is_session_active(self) -> bool:
|
|
423
|
+
return self._background_thread is not None and self._background_thread.is_alive()
|