Skip to content

Commit 282b349

Browse files
committed
Allow MCP servers list to partially initialize
1 parent 10ea9ba commit 282b349

File tree

3 files changed

+117
-12
lines changed

3 files changed

+117
-12
lines changed

aider/coders/base_coder.py

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,7 +1626,10 @@ async def _execute_all_tool_calls():
16261626
return tool_responses
16271627

16281628
def initialize_mcp_tools(self):
1629-
"""Initialize tools from all configured MCP servers."""
1629+
"""
1630+
Initialize tools from all configured MCP servers. MCP Servers that fail to be
1631+
initialized will not be available to the Coder instance.
1632+
"""
16301633
tools = []
16311634

16321635
async def get_server_tools(server):
@@ -1636,26 +1639,30 @@ async def get_server_tools(server):
16361639
session=session, format="openai"
16371640
)
16381641
return (server.name, server_tools)
1642+
except Exception as e:
1643+
self.io.tool_warning(f"Error initializing MCP server {server.name}:\n{e}")
1644+
return None
16391645
finally:
16401646
await server.disconnect()
16411647

16421648
async def get_all_server_tools():
16431649
tasks = [get_server_tools(server) for server in self.mcp_servers]
16441650
results = await asyncio.gather(*tasks)
1645-
return results
1651+
return [result for result in results if result is not None]
16461652

16471653
if self.mcp_servers:
16481654
tools = asyncio.run(get_all_server_tools())
16491655

1650-
self.io.tool_output("MCP server configured:")
1651-
for server_name, server_tools in tools:
1652-
self.io.tool_output(f" - {server_name}")
1656+
if len(tools) > 0:
1657+
self.io.tool_output("MCP servers configured:")
1658+
for server_name, server_tools in tools:
1659+
self.io.tool_output(f" - {server_name}")
16531660

1654-
if self.verbose:
1655-
for tool in server_tools:
1656-
tool_name = tool.get("function", {}).get("name", "unknown")
1657-
tool_desc = tool.get("function", {}).get("description", "").split("\n")[0]
1658-
self.io.tool_output(f" - {tool_name}: {tool_desc}")
1661+
if self.verbose:
1662+
for tool in server_tools:
1663+
tool_name = tool.get("function", {}).get("name", "unknown")
1664+
tool_desc = tool.get("function", {}).get("description", "").split("\n")[0]
1665+
self.io.tool_output(f" - {tool_name}: {tool_desc}")
16591666

16601667
self.mcp_tools = tools
16611668

aider/mcp/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ async def connect(self):
4949
command = self.config["command"]
5050
server_params = StdioServerParameters(
5151
command=command,
52-
args=self.config["args"],
52+
args=self.config.get("args"),
5353
env={**os.environ, **self.config["env"]} if self.config.get("env") else None,
5454
)
5555

tests/basic/test_coder.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import tempfile
33
import unittest
44
from pathlib import Path
5-
from unittest.mock import MagicMock, patch
5+
from unittest.mock import AsyncMock, MagicMock, patch
66

77
import git
88

@@ -575,6 +575,7 @@ def test_new_file_edit_one_commit(self):
575575
fname = Path("file.txt")
576576

577577
io = InputOutput(yes=True)
578+
io.tool_warning = MagicMock()
578579
coder = Coder.create(self.GPT35, "diff", io=io, fnames=[str(fname)])
579580

580581
self.assertTrue(fname.exists())
@@ -1351,6 +1352,103 @@ def test_mcp_server_connection(self, mock_mcp_client):
13511352
self.assertEqual(tool_responses[0]["tool_call_id"], "test_id")
13521353
self.assertEqual(tool_responses[0]["content"], "Tool execution result")
13531354

1355+
@patch("aider.coders.base_coder.experimental_mcp_client")
1356+
def test_coder_creation_with_partial_failed_mcp_server(self, mock_mcp_client):
1357+
"""Test that a coder can still be created even if an MCP server fails to initialize."""
1358+
with GitTemporaryDirectory():
1359+
io = InputOutput(yes=True)
1360+
io.tool_warning = MagicMock()
1361+
1362+
# Create mock MCP servers - one working, one failing
1363+
working_server = AsyncMock()
1364+
working_server.name = "working_server"
1365+
working_server.connect = AsyncMock()
1366+
working_server.disconnect = AsyncMock()
1367+
1368+
failing_server = AsyncMock()
1369+
failing_server.name = "failing_server"
1370+
failing_server.connect = AsyncMock()
1371+
failing_server.disconnect = AsyncMock()
1372+
1373+
# Mock load_mcp_tools to succeed for working_server and fail for failing_server
1374+
async def mock_load_mcp_tools(session, format):
1375+
if session == await working_server.connect():
1376+
return [{"function": {"name": "working_tool"}}]
1377+
else:
1378+
raise Exception("Failed to load tools")
1379+
1380+
mock_mcp_client.load_mcp_tools = AsyncMock(side_effect=mock_load_mcp_tools)
1381+
1382+
# Create coder with both servers
1383+
coder = Coder.create(
1384+
self.GPT35,
1385+
"diff",
1386+
io=io,
1387+
mcp_servers=[working_server, failing_server],
1388+
verbose=True,
1389+
)
1390+
1391+
# Verify that coder was created successfully
1392+
self.assertIsInstance(coder, Coder)
1393+
1394+
# Verify that only the working server's tools were added
1395+
self.assertIsNotNone(coder.mcp_tools)
1396+
self.assertEqual(len(coder.mcp_tools), 1)
1397+
self.assertEqual(coder.mcp_tools[0][0], "working_server")
1398+
1399+
# Verify that the tool list contains only working tools
1400+
tool_list = coder.get_tool_list()
1401+
self.assertEqual(len(tool_list), 1)
1402+
self.assertEqual(tool_list[0]["function"]["name"], "working_tool")
1403+
1404+
# Verify that the warning was logged for the failing server
1405+
io.tool_warning.assert_called_with(
1406+
"Error initializing MCP server failing_server:\nFailed to load tools"
1407+
)
1408+
1409+
@patch("aider.coders.base_coder.experimental_mcp_client")
1410+
def test_coder_creation_with_all_failed_mcp_server(self, mock_mcp_client):
1411+
"""Test that a coder can still be created even if an MCP server fails to initialize."""
1412+
with GitTemporaryDirectory():
1413+
io = InputOutput(yes=True)
1414+
io.tool_warning = MagicMock()
1415+
1416+
failing_server = AsyncMock()
1417+
failing_server.name = "failing_server"
1418+
failing_server.connect = AsyncMock()
1419+
failing_server.disconnect = AsyncMock()
1420+
1421+
# Mock load_mcp_tools to succeed for working_server and fail for failing_server
1422+
async def mock_load_mcp_tools(session, format):
1423+
raise Exception("Failed to load tools")
1424+
1425+
mock_mcp_client.load_mcp_tools = AsyncMock(side_effect=mock_load_mcp_tools)
1426+
1427+
# Create coder with both servers
1428+
coder = Coder.create(
1429+
self.GPT35,
1430+
"diff",
1431+
io=io,
1432+
mcp_servers=[failing_server],
1433+
verbose=True,
1434+
)
1435+
1436+
# Verify that coder was created successfully
1437+
self.assertIsInstance(coder, Coder)
1438+
1439+
# Verify that only the working server's tools were added
1440+
self.assertIsNotNone(coder.mcp_tools)
1441+
self.assertEqual(len(coder.mcp_tools), 0)
1442+
1443+
# Verify that the tool list contains only working tools
1444+
tool_list = coder.get_tool_list()
1445+
self.assertEqual(len(tool_list), 0)
1446+
1447+
# Verify that the warning was logged for the failing server
1448+
io.tool_warning.assert_called_with(
1449+
"Error initializing MCP server failing_server:\nFailed to load tools"
1450+
)
1451+
13541452
@patch("aider.coders.base_coder.experimental_mcp_client")
13551453
def test_initialize_mcp_tools(self, mock_mcp_client):
13561454
"""Test that the coder initializes MCP tools correctly."""

0 commit comments

Comments
 (0)