diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 1c754ac7a..1d172bbce 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1626,7 +1626,10 @@ class Coder: return tool_responses def initialize_mcp_tools(self): - """Initialize tools from all configured MCP servers.""" + """ + Initialize tools from all configured MCP servers. MCP Servers that fail to be + initialized will not be available to the Coder instance. + """ tools = [] async def get_server_tools(server): @@ -1636,26 +1639,30 @@ class Coder: session=session, format="openai" ) return (server.name, server_tools) + except Exception as e: + self.io.tool_warning(f"Error initializing MCP server {server.name}:\n{e}") + return None finally: await server.disconnect() async def get_all_server_tools(): tasks = [get_server_tools(server) for server in self.mcp_servers] results = await asyncio.gather(*tasks) - return results + return [result for result in results if result is not None] if self.mcp_servers: tools = asyncio.run(get_all_server_tools()) - self.io.tool_output("MCP server configured:") - for server_name, server_tools in tools: - self.io.tool_output(f" - {server_name}") + if len(tools) > 0: + self.io.tool_output("MCP servers configured:") + for server_name, server_tools in tools: + self.io.tool_output(f" - {server_name}") - if self.verbose: - for tool in server_tools: - tool_name = tool.get("function", {}).get("name", "unknown") - tool_desc = tool.get("function", {}).get("description", "").split("\n")[0] - self.io.tool_output(f" - {tool_name}: {tool_desc}") + if self.verbose: + for tool in server_tools: + tool_name = tool.get("function", {}).get("name", "unknown") + tool_desc = tool.get("function", {}).get("description", "").split("\n")[0] + self.io.tool_output(f" - {tool_name}: {tool_desc}") self.mcp_tools = tools diff --git a/aider/mcp/server.py b/aider/mcp/server.py index db0d56c58..9f21453f2 100644 --- a/aider/mcp/server.py +++ b/aider/mcp/server.py @@ -49,7 +49,7 @@ class McpServer: command = self.config["command"] server_params = StdioServerParameters( command=command, - args=self.config["args"], + args=self.config.get("args"), env={**os.environ, **self.config["env"]} if self.config.get("env") else None, ) diff --git a/tests/basic/test_coder.py b/tests/basic/test_coder.py index 697a06652..e99c48fa0 100644 --- a/tests/basic/test_coder.py +++ b/tests/basic/test_coder.py @@ -2,7 +2,7 @@ import os import tempfile import unittest from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch import git @@ -575,6 +575,7 @@ Once I have these, I can show you precisely how to do the thing. fname = Path("file.txt") io = InputOutput(yes=True) + io.tool_warning = MagicMock() coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)]) self.assertTrue(fname.exists()) @@ -1351,6 +1352,103 @@ This command will print 'Hello, World!' to the console.""" self.assertEqual(tool_responses[0]["tool_call_id"], "test_id") self.assertEqual(tool_responses[0]["content"], "Tool execution result") + @patch("aider.coders.base_coder.experimental_mcp_client") + def test_coder_creation_with_partial_failed_mcp_server(self, mock_mcp_client): + """Test that a coder can still be created even if an MCP server fails to initialize.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + io.tool_warning = MagicMock() + + # Create mock MCP servers - one working, one failing + working_server = AsyncMock() + working_server.name = "working_server" + working_server.connect = AsyncMock() + working_server.disconnect = AsyncMock() + + failing_server = AsyncMock() + failing_server.name = "failing_server" + failing_server.connect = AsyncMock() + failing_server.disconnect = AsyncMock() + + # Mock load_mcp_tools to succeed for working_server and fail for failing_server + async def mock_load_mcp_tools(session, format): + if session == await working_server.connect(): + return [{"function": {"name": "working_tool"}}] + else: + raise Exception("Failed to load tools") + + mock_mcp_client.load_mcp_tools = AsyncMock(side_effect=mock_load_mcp_tools) + + # Create coder with both servers + coder = Coder.create( + self.GPT35, + "diff", + io=io, + mcp_servers=[working_server, failing_server], + verbose=True, + ) + + # Verify that coder was created successfully + self.assertIsInstance(coder, Coder) + + # Verify that only the working server's tools were added + self.assertIsNotNone(coder.mcp_tools) + self.assertEqual(len(coder.mcp_tools), 1) + self.assertEqual(coder.mcp_tools[0][0], "working_server") + + # Verify that the tool list contains only working tools + tool_list = coder.get_tool_list() + self.assertEqual(len(tool_list), 1) + self.assertEqual(tool_list[0]["function"]["name"], "working_tool") + + # Verify that the warning was logged for the failing server + io.tool_warning.assert_called_with( + "Error initializing MCP server failing_server:\nFailed to load tools" + ) + + @patch("aider.coders.base_coder.experimental_mcp_client") + def test_coder_creation_with_all_failed_mcp_server(self, mock_mcp_client): + """Test that a coder can still be created even if an MCP server fails to initialize.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + io.tool_warning = MagicMock() + + failing_server = AsyncMock() + failing_server.name = "failing_server" + failing_server.connect = AsyncMock() + failing_server.disconnect = AsyncMock() + + # Mock load_mcp_tools to succeed for working_server and fail for failing_server + async def mock_load_mcp_tools(session, format): + raise Exception("Failed to load tools") + + mock_mcp_client.load_mcp_tools = AsyncMock(side_effect=mock_load_mcp_tools) + + # Create coder with both servers + coder = Coder.create( + self.GPT35, + "diff", + io=io, + mcp_servers=[failing_server], + verbose=True, + ) + + # Verify that coder was created successfully + self.assertIsInstance(coder, Coder) + + # Verify that only the working server's tools were added + self.assertIsNotNone(coder.mcp_tools) + self.assertEqual(len(coder.mcp_tools), 0) + + # Verify that the tool list contains only working tools + tool_list = coder.get_tool_list() + self.assertEqual(len(tool_list), 0) + + # Verify that the warning was logged for the failing server + io.tool_warning.assert_called_with( + "Error initializing MCP server failing_server:\nFailed to load tools" + ) + @patch("aider.coders.base_coder.experimental_mcp_client") def test_initialize_mcp_tools(self, mock_mcp_client): """Test that the coder initializes MCP tools correctly."""