From 59cf1644789d9056d5bc0b237013a791f0adcd40 Mon Sep 17 00:00:00 2001 From: ketan-clairyon Date: Sat, 31 May 2025 23:21:03 -0400 Subject: [PATCH 1/7] added list_prompts, get_prompt methods --- src/strands/tools/mcp/mcp_client.py | 46 +++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/src/strands/tools/mcp/mcp_client.py b/src/strands/tools/mcp/mcp_client.py index 4cf4e1f85..96029e80e 100644 --- a/src/strands/tools/mcp/mcp_client.py +++ b/src/strands/tools/mcp/mcp_client.py @@ -163,6 +163,52 @@ async def _list_tools_async() -> ListToolsResult: mcp_tools = [MCPAgentTool(tool, self) for tool in list_tools_response.tools] self._log_debug_with_thread("successfully adapted %d MCP tools", len(mcp_tools)) return PaginatedList[MCPAgentTool](mcp_tools, token=list_tools_response.nextCursor) + + def list_prompts_sync(self) -> ListPromptsResult: + """Synchronously retrieves the list of available prompts from the MCP server. + + This method calls the asynchronous list_prompts method on the MCP session + and adapts the returned prompts to the AgentTool interface. + + Returns: + ListPromptsResult: A list of available prompts + """ + self._log_debug_with_thread("listing MCP prompts synchronously") + if not self._is_session_active(): + raise MCPClientInitializationError("the client session is not running") + + async def _list_prompts_async() -> ListPromptsResult: + return await self._background_thread_session.list_prompts() + + list_prompts_response: ListPromptsResult = self._invoke_on_background_thread(_list_prompts_async()) + self._log_debug_with_thread("received %d prompts from MCP server:") + for prompt in list_prompts_response.prompts: + self._log_debug_with_thread(prompt.name) + + return list_prompts_response + + def get_prompt_sync(self, prompt_id: str, args: dict[Any, Any]) -> GetPromptResult: + """Synchronously retrieves a prompt from the MCP server. + + Args: + prompt_id: The ID of the prompt to retrieve + args: Optional arguments to pass to the prompt + + Returns: + GetPromptResult: The prompt response from the MCP server + """ + self._log_debug_with_thread("getting MCP prompt synchronously") + if not self._is_session_active(): + raise MCPClientInitializationError("the client session is not running") + + async def _get_prompt_async(): + return await self._background_thread_session.get_prompt( + prompt_id, arguments=args + ) + get_prompt_response: GetPromptResult = self._invoke_on_background_thread(_get_prompt_async()) + self._log_debug_with_thread("received prompt from MCP server:", get_prompt_response) + + return get_prompt_response def call_tool_sync( self, From db17d4e491ede5b9924bb79dadeaf8dae872296c Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Thu, 24 Jul 2025 10:57:26 -0400 Subject: [PATCH 2/7] feat(mcp): add mcp support for prompts and resources --- src/strands/tools/mcp/mcp_client.py | 97 ++++++++++++ tests/strands/tools/mcp/test_mcp_client.py | 173 +++++++++++++++++++++ tests_integ/test_mcp_client.py | 140 +++++++++++++++-- 3 files changed, 396 insertions(+), 14 deletions(-) diff --git a/src/strands/tools/mcp/mcp_client.py b/src/strands/tools/mcp/mcp_client.py index 96029e80e..12acf1b6f 100644 --- a/src/strands/tools/mcp/mcp_client.py +++ b/src/strands/tools/mcp/mcp_client.py @@ -20,6 +20,7 @@ from mcp import ClientSession, ListToolsResult from mcp.types import CallToolResult as MCPCallToolResult +from mcp.types import GetPromptResult, ListPromptsResult from mcp.types import ImageContent as MCPImageContent from mcp.types import TextContent as MCPTextContent @@ -210,6 +211,54 @@ async def _get_prompt_async(): return get_prompt_response + def list_prompts_sync(self, pagination_token: Optional[str] = None) -> ListPromptsResult: + """Synchronously retrieves the list of available prompts from the MCP server. + + This method calls the asynchronous list_prompts method on the MCP session + and returns the raw ListPromptsResult with pagination support. + + Args: + pagination_token: Optional token for pagination + + Returns: + ListPromptsResult: The raw MCP response containing prompts and pagination info + """ + self._log_debug_with_thread("listing MCP prompts synchronously") + if not self._is_session_active(): + raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) + + async def _list_prompts_async() -> ListPromptsResult: + return await self._background_thread_session.list_prompts(cursor=pagination_token) + + list_prompts_response: ListPromptsResult = self._invoke_on_background_thread(_list_prompts_async()).result() + self._log_debug_with_thread("received %d prompts from MCP server", len(list_prompts_response.prompts)) + for prompt in list_prompts_response.prompts: + self._log_debug_with_thread(prompt.name) + + return list_prompts_response + + def get_prompt_sync(self, prompt_id: str, args: dict[str, Any]) -> GetPromptResult: + """Synchronously retrieves a prompt from the MCP server. + + Args: + prompt_id: The ID of the prompt to retrieve + args: Optional arguments to pass to the prompt + + Returns: + GetPromptResult: The prompt response from the MCP server + """ + self._log_debug_with_thread("getting MCP prompt synchronously") + if not self._is_session_active(): + raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) + + async def _get_prompt_async() -> GetPromptResult: + return await self._background_thread_session.get_prompt(prompt_id, arguments=args) + + get_prompt_response: GetPromptResult = self._invoke_on_background_thread(_get_prompt_async()).result() + self._log_debug_with_thread("received prompt from MCP server") + + return get_prompt_response + def call_tool_sync( self, tool_use_id: str, @@ -302,6 +351,54 @@ def _handle_tool_result(self, tool_use_id: str, call_tool_result: MCPCallToolRes self._log_debug_with_thread("tool execution completed with status: %s", status) return ToolResult(status=status, toolUseId=tool_use_id, content=mapped_content) + async def list_prompts_async(self, pagination_token: Optional[str] = None) -> ListPromptsResult: + """Asynchronously retrieves the list of available prompts from the MCP server. + + This method calls the asynchronous list_prompts method on the MCP session + and returns the raw ListPromptsResult with pagination support. + + Args: + pagination_token: Optional token for pagination + + Returns: + ListPromptsResult: The raw MCP response containing prompts and pagination info + """ + self._log_debug_with_thread("listing MCP prompts asynchronously") + if not self._is_session_active(): + raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) + + async def _list_prompts_async() -> ListPromptsResult: + return await self._background_thread_session.list_prompts(cursor=pagination_token) + + future = self._invoke_on_background_thread(_list_prompts_async()) + list_prompts_response: ListPromptsResult = await asyncio.wrap_future(future) + self._log_debug_with_thread("received %d prompts from MCP server", len(list_prompts_response.prompts)) + + return list_prompts_response + + async def get_prompt_async(self, prompt_id: str, args: dict[str, Any]) -> GetPromptResult: + """Asynchronously retrieves a prompt from the MCP server. + + Args: + prompt_id: The ID of the prompt to retrieve + args: Optional arguments to pass to the prompt + + Returns: + GetPromptResult: The prompt response from the MCP server + """ + self._log_debug_with_thread("getting MCP prompt asynchronously") + if not self._is_session_active(): + raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) + + async def _get_prompt_async() -> GetPromptResult: + return await self._background_thread_session.get_prompt(prompt_id, arguments=args) + + future = self._invoke_on_background_thread(_get_prompt_async()) + get_prompt_response: GetPromptResult = await asyncio.wrap_future(future) + self._log_debug_with_thread("received prompt from MCP server") + + return get_prompt_response + async def _async_background_thread(self) -> None: """Asynchronous method that runs in the background thread to manage the MCP connection. diff --git a/tests/strands/tools/mcp/test_mcp_client.py b/tests/strands/tools/mcp/test_mcp_client.py index 6a2fdd00c..29e969581 100644 --- a/tests/strands/tools/mcp/test_mcp_client.py +++ b/tests/strands/tools/mcp/test_mcp_client.py @@ -4,6 +4,7 @@ import pytest from mcp import ListToolsResult from mcp.types import CallToolResult as MCPCallToolResult +from mcp.types import GetPromptResult, ListPromptsResult, Prompt, PromptMessage from mcp.types import TextContent as MCPTextContent from mcp.types import Tool as MCPTool @@ -337,3 +338,175 @@ def test_exception_when_future_not_running(): # Verify that set_exception was not called since the future was not running mock_future.set_exception.assert_not_called() + + +# Prompt Tests - Sync Methods + + +def test_list_prompts_sync(mock_transport, mock_session): + """Test that list_prompts_sync correctly retrieves prompts.""" + mock_prompt = Prompt(name="test_prompt", description="A test prompt", id="prompt_1") + mock_session.list_prompts.return_value = ListPromptsResult(prompts=[mock_prompt]) + + with MCPClient(mock_transport["transport_callable"]) as client: + result = client.list_prompts_sync() + + mock_session.list_prompts.assert_called_once_with(cursor=None) + assert len(result.prompts) == 1 + assert result.prompts[0].name == "test_prompt" + assert result.nextCursor is None + + +def test_list_prompts_sync_with_pagination_token(mock_transport, mock_session): + """Test that list_prompts_sync correctly passes pagination token and returns next cursor.""" + mock_prompt = Prompt(name="test_prompt", description="A test prompt", id="prompt_1") + mock_session.list_prompts.return_value = ListPromptsResult(prompts=[mock_prompt], nextCursor="next_page_token") + + with MCPClient(mock_transport["transport_callable"]) as client: + result = client.list_prompts_sync(pagination_token="current_page_token") + + mock_session.list_prompts.assert_called_once_with(cursor="current_page_token") + assert len(result.prompts) == 1 + assert result.prompts[0].name == "test_prompt" + assert result.nextCursor == "next_page_token" + + +def test_list_prompts_sync_session_not_active(): + """Test that list_prompts_sync raises an error when session is not active.""" + client = MCPClient(MagicMock()) + + with pytest.raises(MCPClientInitializationError, match="client session is not running"): + client.list_prompts_sync() + + +def test_get_prompt_sync(mock_transport, mock_session): + """Test that get_prompt_sync correctly retrieves a prompt.""" + mock_message = PromptMessage(role="user", content=MCPTextContent(type="text", text="This is a test prompt")) + mock_session.get_prompt.return_value = GetPromptResult(messages=[mock_message]) + + with MCPClient(mock_transport["transport_callable"]) as client: + result = client.get_prompt_sync("test_prompt_id", {"key": "value"}) + + mock_session.get_prompt.assert_called_once_with("test_prompt_id", arguments={"key": "value"}) + assert len(result.messages) == 1 + assert result.messages[0].role == "user" + assert result.messages[0].content.text == "This is a test prompt" + + +def test_get_prompt_sync_session_not_active(): + """Test that get_prompt_sync raises an error when session is not active.""" + client = MCPClient(MagicMock()) + + with pytest.raises(MCPClientInitializationError, match="client session is not running"): + client.get_prompt_sync("test_prompt_id", {}) + + +# Prompt Tests - Async Methods + + +@pytest.mark.asyncio +async def test_list_prompts_async(mock_transport, mock_session): + """Test that list_prompts_async correctly retrieves prompts.""" + mock_prompt = Prompt(name="test_prompt", description="A test prompt", id="prompt_1") + mock_result = ListPromptsResult(prompts=[mock_prompt]) + mock_session.list_prompts.return_value = mock_result + + with MCPClient(mock_transport["transport_callable"]) as client: + with ( + patch("asyncio.run_coroutine_threadsafe") as mock_run_coroutine_threadsafe, + patch("asyncio.wrap_future") as mock_wrap_future, + ): + mock_future = MagicMock() + mock_run_coroutine_threadsafe.return_value = mock_future + + async def mock_awaitable(): + return mock_result + + mock_wrap_future.return_value = mock_awaitable() + + result = await client.list_prompts_async() + + mock_run_coroutine_threadsafe.assert_called_once() + mock_wrap_future.assert_called_once_with(mock_future) + + assert len(result.prompts) == 1 + assert result.prompts[0].name == "test_prompt" + assert result.nextCursor is None + + +@pytest.mark.asyncio +async def test_list_prompts_async_with_pagination(mock_transport, mock_session): + """Test that list_prompts_async correctly handles pagination.""" + mock_prompt = Prompt(name="test_prompt", description="A test prompt", id="prompt_1") + mock_result = ListPromptsResult(prompts=[mock_prompt], nextCursor="next_token") + mock_session.list_prompts.return_value = mock_result + + with MCPClient(mock_transport["transport_callable"]) as client: + with ( + patch("asyncio.run_coroutine_threadsafe") as mock_run_coroutine_threadsafe, + patch("asyncio.wrap_future") as mock_wrap_future, + ): + mock_future = MagicMock() + mock_run_coroutine_threadsafe.return_value = mock_future + + async def mock_awaitable(): + return mock_result + + mock_wrap_future.return_value = mock_awaitable() + + result = await client.list_prompts_async(pagination_token="current_token") + + mock_run_coroutine_threadsafe.assert_called_once() + mock_wrap_future.assert_called_once_with(mock_future) + + assert len(result.prompts) == 1 + assert result.prompts[0].name == "test_prompt" + assert result.nextCursor == "next_token" + + +@pytest.mark.asyncio +async def test_list_prompts_async_session_not_active(): + """Test that list_prompts_async raises an error when session is not active.""" + client = MCPClient(MagicMock()) + + with pytest.raises(MCPClientInitializationError, match="client session is not running"): + await client.list_prompts_async() + + +@pytest.mark.asyncio +async def test_get_prompt_async(mock_transport, mock_session): + """Test that get_prompt_async correctly retrieves a prompt.""" + mock_message = PromptMessage(role="assistant", content=MCPTextContent(type="text", text="This is a test prompt")) + mock_result = GetPromptResult(messages=[mock_message]) + mock_session.get_prompt.return_value = mock_result + + with MCPClient(mock_transport["transport_callable"]) as client: + with ( + patch("asyncio.run_coroutine_threadsafe") as mock_run_coroutine_threadsafe, + patch("asyncio.wrap_future") as mock_wrap_future, + ): + mock_future = MagicMock() + mock_run_coroutine_threadsafe.return_value = mock_future + + async def mock_awaitable(): + return mock_result + + mock_wrap_future.return_value = mock_awaitable() + + result = await client.get_prompt_async("test_prompt_id", {"key": "value"}) + + mock_run_coroutine_threadsafe.assert_called_once() + mock_wrap_future.assert_called_once_with(mock_future) + + assert len(result.messages) == 1 + assert result.messages[0].role == "assistant" + assert result.messages[0].content.text == "This is a test prompt" + + +@pytest.mark.asyncio +async def test_get_prompt_async_session_not_active(): + """Test that get_prompt_async raises an error when session is not active.""" + client = MCPClient(MagicMock()) + + with pytest.raises(MCPClientInitializationError, match="client session is not running"): + await client.get_prompt_async("test_prompt_id", {}) diff --git a/tests_integ/test_mcp_client.py b/tests_integ/test_mcp_client.py index 9163f625d..a4271291b 100644 --- a/tests_integ/test_mcp_client.py +++ b/tests_integ/test_mcp_client.py @@ -17,19 +17,19 @@ from strands.types.tools import ToolUse -def start_calculator_server(transport: Literal["sse", "streamable-http"], port=int): +def start_comprehensive_mcp_server(transport: Literal["sse", "streamable-http"], port=int): """ - Initialize and start an MCP calculator server for integration testing. + Initialize and start a comprehensive MCP server for integration testing. - This function creates a FastMCP server instance that provides a simple - calculator tool for performing addition operations. The server uses - Server-Sent Events (SSE) transport for communication, making it accessible - over HTTP. + This function creates a FastMCP server instance that provides tools, prompts, + and resources all in one server for comprehensive testing. The server uses + Server-Sent Events (SSE) or streamable HTTP transport for communication. """ from mcp.server import FastMCP - mcp = FastMCP("Calculator Server", port=port) + mcp = FastMCP("Comprehensive MCP Server", port=port) + # Tools @mcp.tool(description="Calculator tool which performs calculations") def calculator(x: int, y: int) -> int: return x + y @@ -43,12 +43,28 @@ def generate_custom_image() -> MCPImageContent: except Exception as e: print("Error while generating custom image: {}".format(e)) + # Prompts + @mcp.prompt(description="A greeting prompt template") + def greeting_prompt(name: str = "World") -> str: + return f"Hello, {name}! How are you today?" + + @mcp.prompt(description="A math problem prompt template") + def math_prompt(operation: str = "addition", difficulty: str = "easy") -> str: + return f"Create a {difficulty} {operation} math problem and solve it step by step." + + # Resources (if supported by FastMCP - this is a placeholder for when resources are implemented) + # @mcp.resource(description="A sample text resource") + # def sample_resource() -> str: + # return "This is a sample resource content" + mcp.run(transport=transport) def test_mcp_client(): """ - Test should yield output similar to the following + Comprehensive test for MCP client functionality including tools, prompts, and resources. + + Test should yield output similar to the following for tools: {'role': 'user', 'content': [{'text': 'add 1 and 2, then echo the result back to me'}]} {'role': 'assistant', 'content': [{'text': "I'll help you add 1 and 2 and then echo the result back to you.\n\nFirst, I'll calculate 1 + 2:"}, {'toolUse': {'toolUseId': 'tooluse_17ptaKUxQB20ySZxwgiI_w', 'name': 'calculator', 'input': {'x': 1, 'y': 2}}}]} {'role': 'user', 'content': [{'toolResult': {'status': 'success', 'toolUseId': 'tooluse_17ptaKUxQB20ySZxwgiI_w', 'content': [{'text': '3'}]}}]} @@ -57,8 +73,9 @@ def test_mcp_client(): {'role': 'assistant', 'content': [{'text': '\n\nThe result of adding 1 and 2 is 3.'}]} """ # noqa: E501 + # Start comprehensive server with tools, prompts, and resources server_thread = threading.Thread( - target=start_calculator_server, kwargs={"transport": "sse", "port": 8000}, daemon=True + target=start_comprehensive_mcp_server, kwargs={"transport": "sse", "port": 8000}, daemon=True ) server_thread.start() time.sleep(2) # wait for server to startup completely @@ -67,14 +84,21 @@ def test_mcp_client(): stdio_mcp_client = MCPClient( lambda: stdio_client(StdioServerParameters(command="python", args=["tests_integ/echo_server.py"])) ) + with sse_mcp_client, stdio_mcp_client: - agent = Agent(tools=sse_mcp_client.list_tools_sync() + stdio_mcp_client.list_tools_sync()) + # Test Tools functionality + sse_tools = sse_mcp_client.list_tools_sync() + stdio_tools = stdio_mcp_client.list_tools_sync() + all_tools = sse_tools + stdio_tools + + agent = Agent(tools=all_tools) agent("add 1 and 2, then echo the result back to me") tool_use_content_blocks = _messages_to_content_blocks(agent.messages) assert any([block["name"] == "echo" for block in tool_use_content_blocks]) assert any([block["name"] == "calculator" for block in tool_use_content_blocks]) + # Test image generation tool image_prompt = """ Generate a custom image, then tell me if the image is red, blue, yellow, pink, orange, or green. RESPOND ONLY WITH THE COLOR @@ -87,6 +111,43 @@ def test_mcp_client(): ] ) + # Test Prompts functionality + prompts_result = sse_mcp_client.list_prompts_sync() + assert len(prompts_result.prompts) >= 2 # We expect at least greeting and math prompts + + prompt_names = [prompt.name for prompt in prompts_result.prompts] + assert "greeting_prompt" in prompt_names + assert "math_prompt" in prompt_names + + # Test get_prompt_sync with greeting prompt + greeting_result = sse_mcp_client.get_prompt_sync("greeting_prompt", {"name": "Alice"}) + assert len(greeting_result.messages) > 0 + prompt_text = greeting_result.messages[0].content.text + assert "Hello, Alice!" in prompt_text + assert "How are you today?" in prompt_text + + # Test get_prompt_sync with math prompt + math_result = sse_mcp_client.get_prompt_sync( + "math_prompt", {"operation": "multiplication", "difficulty": "medium"} + ) + assert len(math_result.messages) > 0 + math_text = math_result.messages[0].content.text + assert "multiplication" in math_text + assert "medium" in math_text + assert "step by step" in math_text + + # Test pagination support for prompts + prompts_with_token = sse_mcp_client.list_prompts_sync(pagination_token=None) + assert len(prompts_with_token.prompts) >= 0 + + # Test pagination support for tools (existing functionality) + tools_with_token = sse_mcp_client.list_tools_sync(pagination_token=None) + assert len(tools_with_token) >= 0 + + # TODO: Add resources testing when resources are implemented + # resources_result = sse_mcp_client.list_resources_sync() + # assert len(resources_result.resources) >= 0 + def test_can_reuse_mcp_client(): stdio_mcp_client = MCPClient( @@ -108,8 +169,9 @@ def test_can_reuse_mcp_client(): reason="streamable transport is failing in GitHub actions, debugging if linux compatibility issue", ) def test_streamable_http_mcp_client(): + """Test comprehensive MCP client with streamable HTTP transport.""" server_thread = threading.Thread( - target=start_calculator_server, kwargs={"transport": "streamable-http", "port": 8001}, daemon=True + target=start_comprehensive_mcp_server, kwargs={"transport": "streamable-http", "port": 8001}, daemon=True ) server_thread.start() time.sleep(2) # wait for server to startup completely @@ -117,14 +179,64 @@ def test_streamable_http_mcp_client(): def transport_callback() -> MCPTransport: return streamablehttp_client(url="http://127.0.0.1:8001/mcp") - streamable_http_client = MCPClient(transport_callback) - with streamable_http_client: - agent = Agent(tools=streamable_http_client.list_tools_sync()) + sse_mcp_client = MCPClient(transport_callback) + with sse_mcp_client: + # Test tools + agent = Agent(tools=sse_mcp_client.list_tools_sync()) agent("add 1 and 2 using a calculator") tool_use_content_blocks = _messages_to_content_blocks(agent.messages) assert any([block["name"] == "calculator" for block in tool_use_content_blocks]) + # Test prompts + prompts_result = sse_mcp_client.list_prompts_sync() + assert len(prompts_result.prompts) >= 2 + + greeting_result = sse_mcp_client.get_prompt_sync("greeting_prompt", {"name": "Charlie"}) + assert len(greeting_result.messages) > 0 + prompt_text = greeting_result.messages[0].content.text + assert "Hello, Charlie!" in prompt_text + def _messages_to_content_blocks(messages: List[Message]) -> List[ToolUse]: return [block["toolUse"] for message in messages for block in message["content"] if "toolUse" in block] + + +@pytest.mark.asyncio +async def test_mcp_client_async(): + """Test MCP client async functionality with comprehensive server (tools, prompts, resources).""" + server_thread = threading.Thread( + target=start_comprehensive_mcp_server, kwargs={"transport": "sse", "port": 8005}, daemon=True + ) + server_thread.start() + time.sleep(2) # wait for server to startup completely + + sse_mcp_client = MCPClient(lambda: sse_client("http://127.0.0.1:8005/sse")) + + with sse_mcp_client: + # Test async prompts functionality + prompts_result = await sse_mcp_client.list_prompts_async() + assert len(prompts_result.prompts) >= 2 + + prompt_names = [prompt.name for prompt in prompts_result.prompts] + assert "greeting_prompt" in prompt_names + assert "math_prompt" in prompt_names + + # Test get_prompt_async + greeting_result = await sse_mcp_client.get_prompt_async("greeting_prompt", {"name": "Bob"}) + assert len(greeting_result.messages) > 0 + prompt_text = greeting_result.messages[0].content.text + assert "Hello, Bob!" in prompt_text + assert "How are you today?" in prompt_text + + # Test async pagination for prompts + prompts_with_pagination = await sse_mcp_client.list_prompts_async(pagination_token="test_token") + assert len(prompts_with_pagination.prompts) >= 0 + + # Test async tools functionality (existing) + tools_result = sse_mcp_client.list_tools_sync() + assert len(tools_result) >= 2 # calculator and generate_custom_image + + tool_names = [tool.tool_name for tool in tools_result] + assert "calculator" in tool_names + assert "generate_custom_image" in tool_names From a3c18341584fc8a3853e45a1a805ac8d9d71582b Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Thu, 24 Jul 2025 15:56:25 -0400 Subject: [PATCH 3/7] feat: rebase mcp_client --- src/strands/tools/mcp/mcp_client.py | 122 +++++++++------------------- 1 file changed, 38 insertions(+), 84 deletions(-) diff --git a/src/strands/tools/mcp/mcp_client.py b/src/strands/tools/mcp/mcp_client.py index 12acf1b6f..898faa1cb 100644 --- a/src/strands/tools/mcp/mcp_client.py +++ b/src/strands/tools/mcp/mcp_client.py @@ -165,54 +165,34 @@ async def _list_tools_async() -> ListToolsResult: self._log_debug_with_thread("successfully adapted %d MCP tools", len(mcp_tools)) return PaginatedList[MCPAgentTool](mcp_tools, token=list_tools_response.nextCursor) - def list_prompts_sync(self) -> ListPromptsResult: + def list_prompts_sync(self, pagination_token: Optional[str] = None) -> ListPromptsResult: """Synchronously retrieves the list of available prompts from the MCP server. This method calls the asynchronous list_prompts method on the MCP session - and adapts the returned prompts to the AgentTool interface. + and returns the raw ListPromptsResult with pagination support. + + Args: + pagination_token: Optional token for pagination Returns: - ListPromptsResult: A list of available prompts + ListPromptsResult: The raw MCP response containing prompts and pagination info """ self._log_debug_with_thread("listing MCP prompts synchronously") if not self._is_session_active(): - raise MCPClientInitializationError("the client session is not running") + raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) async def _list_prompts_async() -> ListPromptsResult: - return await self._background_thread_session.list_prompts() + return await self._background_thread_session.list_prompts(cursor=pagination_token) - list_prompts_response: ListPromptsResult = self._invoke_on_background_thread(_list_prompts_async()) - self._log_debug_with_thread("received %d prompts from MCP server:") + list_prompts_response: ListPromptsResult = self._invoke_on_background_thread(_list_prompts_async()).result() + self._log_debug_with_thread("received %d prompts from MCP server", len(list_prompts_response.prompts)) for prompt in list_prompts_response.prompts: self._log_debug_with_thread(prompt.name) return list_prompts_response - - def get_prompt_sync(self, prompt_id: str, args: dict[Any, Any]) -> GetPromptResult: - """Synchronously retrieves a prompt from the MCP server. - Args: - prompt_id: The ID of the prompt to retrieve - args: Optional arguments to pass to the prompt - - Returns: - GetPromptResult: The prompt response from the MCP server - """ - self._log_debug_with_thread("getting MCP prompt synchronously") - if not self._is_session_active(): - raise MCPClientInitializationError("the client session is not running") - - async def _get_prompt_async(): - return await self._background_thread_session.get_prompt( - prompt_id, arguments=args - ) - get_prompt_response: GetPromptResult = self._invoke_on_background_thread(_get_prompt_async()) - self._log_debug_with_thread("received prompt from MCP server:", get_prompt_response) - - return get_prompt_response - - def list_prompts_sync(self, pagination_token: Optional[str] = None) -> ListPromptsResult: - """Synchronously retrieves the list of available prompts from the MCP server. + async def list_prompts_async(self, pagination_token: Optional[str] = None) -> ListPromptsResult: + """Asynchronously retrieves the list of available prompts from the MCP server. This method calls the asynchronous list_prompts method on the MCP session and returns the raw ListPromptsResult with pagination support. @@ -223,17 +203,16 @@ def list_prompts_sync(self, pagination_token: Optional[str] = None) -> ListPromp Returns: ListPromptsResult: The raw MCP response containing prompts and pagination info """ - self._log_debug_with_thread("listing MCP prompts synchronously") + self._log_debug_with_thread("listing MCP prompts asynchronously") if not self._is_session_active(): raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) async def _list_prompts_async() -> ListPromptsResult: return await self._background_thread_session.list_prompts(cursor=pagination_token) - list_prompts_response: ListPromptsResult = self._invoke_on_background_thread(_list_prompts_async()).result() + future = self._invoke_on_background_thread(_list_prompts_async()) + list_prompts_response: ListPromptsResult = await asyncio.wrap_future(future) self._log_debug_with_thread("received %d prompts from MCP server", len(list_prompts_response.prompts)) - for prompt in list_prompts_response.prompts: - self._log_debug_with_thread(prompt.name) return list_prompts_response @@ -259,6 +238,29 @@ async def _get_prompt_async() -> GetPromptResult: return get_prompt_response + async def get_prompt_async(self, prompt_id: str, args: dict[str, Any]) -> GetPromptResult: + """Asynchronously retrieves a prompt from the MCP server. + + Args: + prompt_id: The ID of the prompt to retrieve + args: Optional arguments to pass to the prompt + + Returns: + GetPromptResult: The prompt response from the MCP server + """ + self._log_debug_with_thread("getting MCP prompt asynchronously") + if not self._is_session_active(): + raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) + + async def _get_prompt_async() -> GetPromptResult: + return await self._background_thread_session.get_prompt(prompt_id, arguments=args) + + future = self._invoke_on_background_thread(_get_prompt_async()) + get_prompt_response: GetPromptResult = await asyncio.wrap_future(future) + self._log_debug_with_thread("received prompt from MCP server") + + return get_prompt_response + def call_tool_sync( self, tool_use_id: str, @@ -351,54 +353,6 @@ def _handle_tool_result(self, tool_use_id: str, call_tool_result: MCPCallToolRes self._log_debug_with_thread("tool execution completed with status: %s", status) return ToolResult(status=status, toolUseId=tool_use_id, content=mapped_content) - async def list_prompts_async(self, pagination_token: Optional[str] = None) -> ListPromptsResult: - """Asynchronously retrieves the list of available prompts from the MCP server. - - This method calls the asynchronous list_prompts method on the MCP session - and returns the raw ListPromptsResult with pagination support. - - Args: - pagination_token: Optional token for pagination - - Returns: - ListPromptsResult: The raw MCP response containing prompts and pagination info - """ - self._log_debug_with_thread("listing MCP prompts asynchronously") - if not self._is_session_active(): - raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) - - async def _list_prompts_async() -> ListPromptsResult: - return await self._background_thread_session.list_prompts(cursor=pagination_token) - - future = self._invoke_on_background_thread(_list_prompts_async()) - list_prompts_response: ListPromptsResult = await asyncio.wrap_future(future) - self._log_debug_with_thread("received %d prompts from MCP server", len(list_prompts_response.prompts)) - - return list_prompts_response - - async def get_prompt_async(self, prompt_id: str, args: dict[str, Any]) -> GetPromptResult: - """Asynchronously retrieves a prompt from the MCP server. - - Args: - prompt_id: The ID of the prompt to retrieve - args: Optional arguments to pass to the prompt - - Returns: - GetPromptResult: The prompt response from the MCP server - """ - self._log_debug_with_thread("getting MCP prompt asynchronously") - if not self._is_session_active(): - raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) - - async def _get_prompt_async() -> GetPromptResult: - return await self._background_thread_session.get_prompt(prompt_id, arguments=args) - - future = self._invoke_on_background_thread(_get_prompt_async()) - get_prompt_response: GetPromptResult = await asyncio.wrap_future(future) - self._log_debug_with_thread("received prompt from MCP server") - - return get_prompt_response - async def _async_background_thread(self) -> None: """Asynchronous method that runs in the background thread to manage the MCP connection. From bcf1ddb96035a3e800293d34d2edf24cd729f57c Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Thu, 24 Jul 2025 16:51:05 -0400 Subject: [PATCH 4/7] revert: remove additional async method and just keep sync --- src/strands/tools/mcp/mcp_client.py | 48 --------- tests/strands/tools/mcp/test_mcp_client.py | 108 --------------------- tests_integ/test_mcp_client.py | 12 +-- 3 files changed, 6 insertions(+), 162 deletions(-) diff --git a/src/strands/tools/mcp/mcp_client.py b/src/strands/tools/mcp/mcp_client.py index 898faa1cb..34013d8ba 100644 --- a/src/strands/tools/mcp/mcp_client.py +++ b/src/strands/tools/mcp/mcp_client.py @@ -191,31 +191,6 @@ async def _list_prompts_async() -> ListPromptsResult: return list_prompts_response - async def list_prompts_async(self, pagination_token: Optional[str] = None) -> ListPromptsResult: - """Asynchronously retrieves the list of available prompts from the MCP server. - - This method calls the asynchronous list_prompts method on the MCP session - and returns the raw ListPromptsResult with pagination support. - - Args: - pagination_token: Optional token for pagination - - Returns: - ListPromptsResult: The raw MCP response containing prompts and pagination info - """ - self._log_debug_with_thread("listing MCP prompts asynchronously") - if not self._is_session_active(): - raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) - - async def _list_prompts_async() -> ListPromptsResult: - return await self._background_thread_session.list_prompts(cursor=pagination_token) - - future = self._invoke_on_background_thread(_list_prompts_async()) - list_prompts_response: ListPromptsResult = await asyncio.wrap_future(future) - self._log_debug_with_thread("received %d prompts from MCP server", len(list_prompts_response.prompts)) - - return list_prompts_response - def get_prompt_sync(self, prompt_id: str, args: dict[str, Any]) -> GetPromptResult: """Synchronously retrieves a prompt from the MCP server. @@ -238,29 +213,6 @@ async def _get_prompt_async() -> GetPromptResult: return get_prompt_response - async def get_prompt_async(self, prompt_id: str, args: dict[str, Any]) -> GetPromptResult: - """Asynchronously retrieves a prompt from the MCP server. - - Args: - prompt_id: The ID of the prompt to retrieve - args: Optional arguments to pass to the prompt - - Returns: - GetPromptResult: The prompt response from the MCP server - """ - self._log_debug_with_thread("getting MCP prompt asynchronously") - if not self._is_session_active(): - raise MCPClientInitializationError(CLIENT_SESSION_NOT_RUNNING_ERROR_MESSAGE) - - async def _get_prompt_async() -> GetPromptResult: - return await self._background_thread_session.get_prompt(prompt_id, arguments=args) - - future = self._invoke_on_background_thread(_get_prompt_async()) - get_prompt_response: GetPromptResult = await asyncio.wrap_future(future) - self._log_debug_with_thread("received prompt from MCP server") - - return get_prompt_response - def call_tool_sync( self, tool_use_id: str, diff --git a/tests/strands/tools/mcp/test_mcp_client.py b/tests/strands/tools/mcp/test_mcp_client.py index 29e969581..fb77f3d4e 100644 --- a/tests/strands/tools/mcp/test_mcp_client.py +++ b/tests/strands/tools/mcp/test_mcp_client.py @@ -401,112 +401,4 @@ def test_get_prompt_sync_session_not_active(): client.get_prompt_sync("test_prompt_id", {}) -# Prompt Tests - Async Methods - -@pytest.mark.asyncio -async def test_list_prompts_async(mock_transport, mock_session): - """Test that list_prompts_async correctly retrieves prompts.""" - mock_prompt = Prompt(name="test_prompt", description="A test prompt", id="prompt_1") - mock_result = ListPromptsResult(prompts=[mock_prompt]) - mock_session.list_prompts.return_value = mock_result - - with MCPClient(mock_transport["transport_callable"]) as client: - with ( - patch("asyncio.run_coroutine_threadsafe") as mock_run_coroutine_threadsafe, - patch("asyncio.wrap_future") as mock_wrap_future, - ): - mock_future = MagicMock() - mock_run_coroutine_threadsafe.return_value = mock_future - - async def mock_awaitable(): - return mock_result - - mock_wrap_future.return_value = mock_awaitable() - - result = await client.list_prompts_async() - - mock_run_coroutine_threadsafe.assert_called_once() - mock_wrap_future.assert_called_once_with(mock_future) - - assert len(result.prompts) == 1 - assert result.prompts[0].name == "test_prompt" - assert result.nextCursor is None - - -@pytest.mark.asyncio -async def test_list_prompts_async_with_pagination(mock_transport, mock_session): - """Test that list_prompts_async correctly handles pagination.""" - mock_prompt = Prompt(name="test_prompt", description="A test prompt", id="prompt_1") - mock_result = ListPromptsResult(prompts=[mock_prompt], nextCursor="next_token") - mock_session.list_prompts.return_value = mock_result - - with MCPClient(mock_transport["transport_callable"]) as client: - with ( - patch("asyncio.run_coroutine_threadsafe") as mock_run_coroutine_threadsafe, - patch("asyncio.wrap_future") as mock_wrap_future, - ): - mock_future = MagicMock() - mock_run_coroutine_threadsafe.return_value = mock_future - - async def mock_awaitable(): - return mock_result - - mock_wrap_future.return_value = mock_awaitable() - - result = await client.list_prompts_async(pagination_token="current_token") - - mock_run_coroutine_threadsafe.assert_called_once() - mock_wrap_future.assert_called_once_with(mock_future) - - assert len(result.prompts) == 1 - assert result.prompts[0].name == "test_prompt" - assert result.nextCursor == "next_token" - - -@pytest.mark.asyncio -async def test_list_prompts_async_session_not_active(): - """Test that list_prompts_async raises an error when session is not active.""" - client = MCPClient(MagicMock()) - - with pytest.raises(MCPClientInitializationError, match="client session is not running"): - await client.list_prompts_async() - - -@pytest.mark.asyncio -async def test_get_prompt_async(mock_transport, mock_session): - """Test that get_prompt_async correctly retrieves a prompt.""" - mock_message = PromptMessage(role="assistant", content=MCPTextContent(type="text", text="This is a test prompt")) - mock_result = GetPromptResult(messages=[mock_message]) - mock_session.get_prompt.return_value = mock_result - - with MCPClient(mock_transport["transport_callable"]) as client: - with ( - patch("asyncio.run_coroutine_threadsafe") as mock_run_coroutine_threadsafe, - patch("asyncio.wrap_future") as mock_wrap_future, - ): - mock_future = MagicMock() - mock_run_coroutine_threadsafe.return_value = mock_future - - async def mock_awaitable(): - return mock_result - - mock_wrap_future.return_value = mock_awaitable() - - result = await client.get_prompt_async("test_prompt_id", {"key": "value"}) - - mock_run_coroutine_threadsafe.assert_called_once() - mock_wrap_future.assert_called_once_with(mock_future) - - assert len(result.messages) == 1 - assert result.messages[0].role == "assistant" - assert result.messages[0].content.text == "This is a test prompt" - - -@pytest.mark.asyncio -async def test_get_prompt_async_session_not_active(): - """Test that get_prompt_async raises an error when session is not active.""" - client = MCPClient(MagicMock()) - - with pytest.raises(MCPClientInitializationError, match="client session is not running"): - await client.get_prompt_async("test_prompt_id", {}) diff --git a/tests_integ/test_mcp_client.py b/tests_integ/test_mcp_client.py index a4271291b..0ff8f303a 100644 --- a/tests_integ/test_mcp_client.py +++ b/tests_integ/test_mcp_client.py @@ -214,23 +214,23 @@ async def test_mcp_client_async(): sse_mcp_client = MCPClient(lambda: sse_client("http://127.0.0.1:8005/sse")) with sse_mcp_client: - # Test async prompts functionality - prompts_result = await sse_mcp_client.list_prompts_async() + # Test sync prompts functionality + prompts_result = sse_mcp_client.list_prompts_sync() assert len(prompts_result.prompts) >= 2 prompt_names = [prompt.name for prompt in prompts_result.prompts] assert "greeting_prompt" in prompt_names assert "math_prompt" in prompt_names - # Test get_prompt_async - greeting_result = await sse_mcp_client.get_prompt_async("greeting_prompt", {"name": "Bob"}) + # Test get_prompt_sync + greeting_result = sse_mcp_client.get_prompt_sync("greeting_prompt", {"name": "Bob"}) assert len(greeting_result.messages) > 0 prompt_text = greeting_result.messages[0].content.text assert "Hello, Bob!" in prompt_text assert "How are you today?" in prompt_text - # Test async pagination for prompts - prompts_with_pagination = await sse_mcp_client.list_prompts_async(pagination_token="test_token") + # Test sync pagination for prompts + prompts_with_pagination = sse_mcp_client.list_prompts_sync(pagination_token="test_token") assert len(prompts_with_pagination.prompts) >= 0 # Test async tools functionality (existing) From 628766599f9ba6785cedfee8e69ad6157be1e863 Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Thu, 24 Jul 2025 17:22:48 -0400 Subject: [PATCH 5/7] fix: cleanup --- tests_integ/test_mcp_client.py | 61 ++++------------------------------ 1 file changed, 6 insertions(+), 55 deletions(-) diff --git a/tests_integ/test_mcp_client.py b/tests_integ/test_mcp_client.py index 0ff8f303a..6919659f9 100644 --- a/tests_integ/test_mcp_client.py +++ b/tests_integ/test_mcp_client.py @@ -29,7 +29,6 @@ def start_comprehensive_mcp_server(transport: Literal["sse", "streamable-http"], mcp = FastMCP("Comprehensive MCP Server", port=port) - # Tools @mcp.tool(description="Calculator tool which performs calculations") def calculator(x: int, y: int) -> int: return x + y @@ -52,19 +51,12 @@ def greeting_prompt(name: str = "World") -> str: def math_prompt(operation: str = "addition", difficulty: str = "easy") -> str: return f"Create a {difficulty} {operation} math problem and solve it step by step." - # Resources (if supported by FastMCP - this is a placeholder for when resources are implemented) - # @mcp.resource(description="A sample text resource") - # def sample_resource() -> str: - # return "This is a sample resource content" - mcp.run(transport=transport) def test_mcp_client(): """ - Comprehensive test for MCP client functionality including tools, prompts, and resources. - - Test should yield output similar to the following for tools: + Test should yield output similar to the following {'role': 'user', 'content': [{'text': 'add 1 and 2, then echo the result back to me'}]} {'role': 'assistant', 'content': [{'text': "I'll help you add 1 and 2 and then echo the result back to you.\n\nFirst, I'll calculate 1 + 2:"}, {'toolUse': {'toolUseId': 'tooluse_17ptaKUxQB20ySZxwgiI_w', 'name': 'calculator', 'input': {'x': 1, 'y': 2}}}]} {'role': 'user', 'content': [{'toolResult': {'status': 'success', 'toolUseId': 'tooluse_17ptaKUxQB20ySZxwgiI_w', 'content': [{'text': '3'}]}}]} @@ -98,7 +90,6 @@ def test_mcp_client(): assert any([block["name"] == "echo" for block in tool_use_content_blocks]) assert any([block["name"] == "calculator" for block in tool_use_content_blocks]) - # Test image generation tool image_prompt = """ Generate a custom image, then tell me if the image is red, blue, yellow, pink, orange, or green. RESPOND ONLY WITH THE COLOR @@ -179,20 +170,20 @@ def test_streamable_http_mcp_client(): def transport_callback() -> MCPTransport: return streamablehttp_client(url="http://127.0.0.1:8001/mcp") - sse_mcp_client = MCPClient(transport_callback) - with sse_mcp_client: + streamable_http_client = MCPClient(transport_callback) + with streamable_http_client: # Test tools - agent = Agent(tools=sse_mcp_client.list_tools_sync()) + agent = Agent(tools=streamable_http_client.list_tools_sync()) agent("add 1 and 2 using a calculator") tool_use_content_blocks = _messages_to_content_blocks(agent.messages) assert any([block["name"] == "calculator" for block in tool_use_content_blocks]) # Test prompts - prompts_result = sse_mcp_client.list_prompts_sync() + prompts_result = streamable_http_client.list_prompts_sync() assert len(prompts_result.prompts) >= 2 - greeting_result = sse_mcp_client.get_prompt_sync("greeting_prompt", {"name": "Charlie"}) + greeting_result = streamable_http_client.get_prompt_sync("greeting_prompt", {"name": "Charlie"}) assert len(greeting_result.messages) > 0 prompt_text = greeting_result.messages[0].content.text assert "Hello, Charlie!" in prompt_text @@ -200,43 +191,3 @@ def transport_callback() -> MCPTransport: def _messages_to_content_blocks(messages: List[Message]) -> List[ToolUse]: return [block["toolUse"] for message in messages for block in message["content"] if "toolUse" in block] - - -@pytest.mark.asyncio -async def test_mcp_client_async(): - """Test MCP client async functionality with comprehensive server (tools, prompts, resources).""" - server_thread = threading.Thread( - target=start_comprehensive_mcp_server, kwargs={"transport": "sse", "port": 8005}, daemon=True - ) - server_thread.start() - time.sleep(2) # wait for server to startup completely - - sse_mcp_client = MCPClient(lambda: sse_client("http://127.0.0.1:8005/sse")) - - with sse_mcp_client: - # Test sync prompts functionality - prompts_result = sse_mcp_client.list_prompts_sync() - assert len(prompts_result.prompts) >= 2 - - prompt_names = [prompt.name for prompt in prompts_result.prompts] - assert "greeting_prompt" in prompt_names - assert "math_prompt" in prompt_names - - # Test get_prompt_sync - greeting_result = sse_mcp_client.get_prompt_sync("greeting_prompt", {"name": "Bob"}) - assert len(greeting_result.messages) > 0 - prompt_text = greeting_result.messages[0].content.text - assert "Hello, Bob!" in prompt_text - assert "How are you today?" in prompt_text - - # Test sync pagination for prompts - prompts_with_pagination = sse_mcp_client.list_prompts_sync(pagination_token="test_token") - assert len(prompts_with_pagination.prompts) >= 0 - - # Test async tools functionality (existing) - tools_result = sse_mcp_client.list_tools_sync() - assert len(tools_result) >= 2 # calculator and generate_custom_image - - tool_names = [tool.tool_name for tool in tools_result] - assert "calculator" in tool_names - assert "generate_custom_image" in tool_names From 94aa41bea986538ffa93a08dfe3c5f6f55257bea Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Thu, 24 Jul 2025 17:24:17 -0400 Subject: [PATCH 6/7] fix: linting --- src/strands/tools/mcp/mcp_client.py | 2 +- tests/strands/tools/mcp/test_mcp_client.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/strands/tools/mcp/mcp_client.py b/src/strands/tools/mcp/mcp_client.py index 34013d8ba..72891bdae 100644 --- a/src/strands/tools/mcp/mcp_client.py +++ b/src/strands/tools/mcp/mcp_client.py @@ -164,7 +164,7 @@ async def _list_tools_async() -> ListToolsResult: mcp_tools = [MCPAgentTool(tool, self) for tool in list_tools_response.tools] self._log_debug_with_thread("successfully adapted %d MCP tools", len(mcp_tools)) return PaginatedList[MCPAgentTool](mcp_tools, token=list_tools_response.nextCursor) - + def list_prompts_sync(self, pagination_token: Optional[str] = None) -> ListPromptsResult: """Synchronously retrieves the list of available prompts from the MCP server. diff --git a/tests/strands/tools/mcp/test_mcp_client.py b/tests/strands/tools/mcp/test_mcp_client.py index fb77f3d4e..512cd4a38 100644 --- a/tests/strands/tools/mcp/test_mcp_client.py +++ b/tests/strands/tools/mcp/test_mcp_client.py @@ -399,6 +399,3 @@ def test_get_prompt_sync_session_not_active(): with pytest.raises(MCPClientInitializationError, match="client session is not running"): client.get_prompt_sync("test_prompt_id", {}) - - - From a9c5d3d3f33d51acd885da2df3f6738a9b7a7d3d Mon Sep 17 00:00:00 2001 From: Dean Schmigelski Date: Fri, 25 Jul 2025 15:03:46 -0400 Subject: [PATCH 7/7] nit: update variable names --- src/strands/tools/mcp/mcp_client.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/strands/tools/mcp/mcp_client.py b/src/strands/tools/mcp/mcp_client.py index 72891bdae..064887030 100644 --- a/src/strands/tools/mcp/mcp_client.py +++ b/src/strands/tools/mcp/mcp_client.py @@ -184,12 +184,12 @@ def list_prompts_sync(self, pagination_token: Optional[str] = None) -> ListPromp async def _list_prompts_async() -> ListPromptsResult: return await self._background_thread_session.list_prompts(cursor=pagination_token) - list_prompts_response: ListPromptsResult = self._invoke_on_background_thread(_list_prompts_async()).result() - self._log_debug_with_thread("received %d prompts from MCP server", len(list_prompts_response.prompts)) - for prompt in list_prompts_response.prompts: + list_prompts_result: ListPromptsResult = self._invoke_on_background_thread(_list_prompts_async()).result() + self._log_debug_with_thread("received %d prompts from MCP server", len(list_prompts_result.prompts)) + for prompt in list_prompts_result.prompts: self._log_debug_with_thread(prompt.name) - return list_prompts_response + return list_prompts_result def get_prompt_sync(self, prompt_id: str, args: dict[str, Any]) -> GetPromptResult: """Synchronously retrieves a prompt from the MCP server. @@ -208,10 +208,10 @@ def get_prompt_sync(self, prompt_id: str, args: dict[str, Any]) -> GetPromptResu async def _get_prompt_async() -> GetPromptResult: return await self._background_thread_session.get_prompt(prompt_id, arguments=args) - get_prompt_response: GetPromptResult = self._invoke_on_background_thread(_get_prompt_async()).result() + get_prompt_result: GetPromptResult = self._invoke_on_background_thread(_get_prompt_async()).result() self._log_debug_with_thread("received prompt from MCP server") - return get_prompt_response + return get_prompt_result def call_tool_sync( self,