Allow MCP servers list to partially initialize

This commit is contained in:
Quinlan Jager 2025-05-04 22:46:18 -07:00
parent 10ea9baad6
commit 282b349080
3 changed files with 117 additions and 12 deletions

View file

@ -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

View file

@ -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,
)

View file

@ -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."""