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.

Files changed (115) hide show
  1. agentrun_operation_sdk/cli/__init__.py +1 -0
  2. agentrun_operation_sdk/cli/cli.py +19 -0
  3. agentrun_operation_sdk/cli/common.py +21 -0
  4. agentrun_operation_sdk/cli/runtime/__init__.py +1 -0
  5. agentrun_operation_sdk/cli/runtime/commands.py +203 -0
  6. agentrun_operation_sdk/client/client.py +75 -0
  7. agentrun_operation_sdk/operations/runtime/__init__.py +8 -0
  8. agentrun_operation_sdk/operations/runtime/configure.py +101 -0
  9. agentrun_operation_sdk/operations/runtime/launch.py +82 -0
  10. agentrun_operation_sdk/operations/runtime/models.py +31 -0
  11. agentrun_operation_sdk/services/runtime.py +152 -0
  12. agentrun_operation_sdk/utils/logging_config.py +72 -0
  13. agentrun_operation_sdk/utils/runtime/config.py +94 -0
  14. agentrun_operation_sdk/utils/runtime/container.py +280 -0
  15. agentrun_operation_sdk/utils/runtime/entrypoint.py +203 -0
  16. agentrun_operation_sdk/utils/runtime/schema.py +56 -0
  17. agentrun_sdk/__init__.py +7 -0
  18. agentrun_sdk/agent/__init__.py +25 -0
  19. agentrun_sdk/agent/agent.py +696 -0
  20. agentrun_sdk/agent/agent_result.py +46 -0
  21. agentrun_sdk/agent/conversation_manager/__init__.py +26 -0
  22. agentrun_sdk/agent/conversation_manager/conversation_manager.py +88 -0
  23. agentrun_sdk/agent/conversation_manager/null_conversation_manager.py +46 -0
  24. agentrun_sdk/agent/conversation_manager/sliding_window_conversation_manager.py +179 -0
  25. agentrun_sdk/agent/conversation_manager/summarizing_conversation_manager.py +252 -0
  26. agentrun_sdk/agent/state.py +97 -0
  27. agentrun_sdk/event_loop/__init__.py +9 -0
  28. agentrun_sdk/event_loop/event_loop.py +499 -0
  29. agentrun_sdk/event_loop/streaming.py +319 -0
  30. agentrun_sdk/experimental/__init__.py +4 -0
  31. agentrun_sdk/experimental/hooks/__init__.py +15 -0
  32. agentrun_sdk/experimental/hooks/events.py +123 -0
  33. agentrun_sdk/handlers/__init__.py +10 -0
  34. agentrun_sdk/handlers/callback_handler.py +70 -0
  35. agentrun_sdk/hooks/__init__.py +49 -0
  36. agentrun_sdk/hooks/events.py +80 -0
  37. agentrun_sdk/hooks/registry.py +247 -0
  38. agentrun_sdk/models/__init__.py +10 -0
  39. agentrun_sdk/models/anthropic.py +432 -0
  40. agentrun_sdk/models/bedrock.py +649 -0
  41. agentrun_sdk/models/litellm.py +225 -0
  42. agentrun_sdk/models/llamaapi.py +438 -0
  43. agentrun_sdk/models/mistral.py +539 -0
  44. agentrun_sdk/models/model.py +95 -0
  45. agentrun_sdk/models/ollama.py +357 -0
  46. agentrun_sdk/models/openai.py +436 -0
  47. agentrun_sdk/models/sagemaker.py +598 -0
  48. agentrun_sdk/models/writer.py +449 -0
  49. agentrun_sdk/multiagent/__init__.py +22 -0
  50. agentrun_sdk/multiagent/a2a/__init__.py +15 -0
  51. agentrun_sdk/multiagent/a2a/executor.py +148 -0
  52. agentrun_sdk/multiagent/a2a/server.py +252 -0
  53. agentrun_sdk/multiagent/base.py +92 -0
  54. agentrun_sdk/multiagent/graph.py +555 -0
  55. agentrun_sdk/multiagent/swarm.py +656 -0
  56. agentrun_sdk/py.typed +1 -0
  57. agentrun_sdk/session/__init__.py +18 -0
  58. agentrun_sdk/session/file_session_manager.py +216 -0
  59. agentrun_sdk/session/repository_session_manager.py +152 -0
  60. agentrun_sdk/session/s3_session_manager.py +272 -0
  61. agentrun_sdk/session/session_manager.py +73 -0
  62. agentrun_sdk/session/session_repository.py +51 -0
  63. agentrun_sdk/telemetry/__init__.py +21 -0
  64. agentrun_sdk/telemetry/config.py +194 -0
  65. agentrun_sdk/telemetry/metrics.py +476 -0
  66. agentrun_sdk/telemetry/metrics_constants.py +15 -0
  67. agentrun_sdk/telemetry/tracer.py +563 -0
  68. agentrun_sdk/tools/__init__.py +17 -0
  69. agentrun_sdk/tools/decorator.py +569 -0
  70. agentrun_sdk/tools/executor.py +137 -0
  71. agentrun_sdk/tools/loader.py +152 -0
  72. agentrun_sdk/tools/mcp/__init__.py +13 -0
  73. agentrun_sdk/tools/mcp/mcp_agent_tool.py +99 -0
  74. agentrun_sdk/tools/mcp/mcp_client.py +423 -0
  75. agentrun_sdk/tools/mcp/mcp_instrumentation.py +322 -0
  76. agentrun_sdk/tools/mcp/mcp_types.py +63 -0
  77. agentrun_sdk/tools/registry.py +607 -0
  78. agentrun_sdk/tools/structured_output.py +421 -0
  79. agentrun_sdk/tools/tools.py +217 -0
  80. agentrun_sdk/tools/watcher.py +136 -0
  81. agentrun_sdk/types/__init__.py +5 -0
  82. agentrun_sdk/types/collections.py +23 -0
  83. agentrun_sdk/types/content.py +188 -0
  84. agentrun_sdk/types/event_loop.py +48 -0
  85. agentrun_sdk/types/exceptions.py +81 -0
  86. agentrun_sdk/types/guardrails.py +254 -0
  87. agentrun_sdk/types/media.py +89 -0
  88. agentrun_sdk/types/session.py +152 -0
  89. agentrun_sdk/types/streaming.py +201 -0
  90. agentrun_sdk/types/tools.py +258 -0
  91. agentrun_sdk/types/traces.py +5 -0
  92. agentrun_sdk-0.1.2.dist-info/METADATA +51 -0
  93. agentrun_sdk-0.1.2.dist-info/RECORD +115 -0
  94. agentrun_sdk-0.1.2.dist-info/WHEEL +5 -0
  95. agentrun_sdk-0.1.2.dist-info/entry_points.txt +2 -0
  96. agentrun_sdk-0.1.2.dist-info/top_level.txt +3 -0
  97. agentrun_wrapper/__init__.py +11 -0
  98. agentrun_wrapper/_utils/__init__.py +6 -0
  99. agentrun_wrapper/_utils/endpoints.py +16 -0
  100. agentrun_wrapper/identity/__init__.py +5 -0
  101. agentrun_wrapper/identity/auth.py +211 -0
  102. agentrun_wrapper/memory/__init__.py +6 -0
  103. agentrun_wrapper/memory/client.py +1697 -0
  104. agentrun_wrapper/memory/constants.py +103 -0
  105. agentrun_wrapper/memory/controlplane.py +626 -0
  106. agentrun_wrapper/py.typed +1 -0
  107. agentrun_wrapper/runtime/__init__.py +13 -0
  108. agentrun_wrapper/runtime/app.py +473 -0
  109. agentrun_wrapper/runtime/context.py +34 -0
  110. agentrun_wrapper/runtime/models.py +25 -0
  111. agentrun_wrapper/services/__init__.py +1 -0
  112. agentrun_wrapper/services/identity.py +192 -0
  113. agentrun_wrapper/tools/__init__.py +6 -0
  114. agentrun_wrapper/tools/browser_client.py +325 -0
  115. 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()