mirror of
https://github.com/Aider-AI/aider.git
synced 2025-06-02 02:34:59 +00:00
Allow MCP servers list to partially initialize
This commit is contained in:
parent
10ea9baad6
commit
282b349080
3 changed files with 117 additions and 12 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue