From 3e51f2a7ebb5cf7ca0534889c4032cb513dcfa96 Mon Sep 17 00:00:00 2001 From: Chris Nestrud Date: Sat, 27 Dec 2025 12:03:01 -0600 Subject: [PATCH 1/2] Refactor MCP shims to avoid litellm import cycles Lazy-load litellm.experimental_mcp_client and move LocalServer helpers under aider.mcp_support so coders no longer import the heavy MCP stack at module import time. Without this change the branch repeatedly crashed pytest with circular ImportError whenever litellm shipped without the experimental MCP extras, blocking any tests that touch aider.coders. --- aider/coders/agent_coder.py | 22 ++++++++++++++++-- aider/coders/base_coder.py | 22 ++++++++++++++++-- aider/main.py | 2 +- aider/{mcp => mcp_support}/__init__.py | 31 ++++++++++++++++---------- aider/{mcp => mcp_support}/server.py | 0 5 files changed, 60 insertions(+), 17 deletions(-) rename aider/{mcp => mcp_support}/__init__.py (88%) rename aider/{mcp => mcp_support}/server.py (100%) diff --git a/aider/coders/agent_coder.py b/aider/coders/agent_coder.py index 7cb60a16eaf..c85f88466e1 100644 --- a/aider/coders/agent_coder.py +++ b/aider/coders/agent_coder.py @@ -14,10 +14,28 @@ from datetime import datetime from pathlib import Path -from litellm import experimental_mcp_client from aider import urls, utils + +class _ExperimentalMCPClientProxy: + """Lazy proxy to defer importing litellm.experimental_mcp_client.""" + + _client = None + + def _get_client(self): + if self._client is None: + from litellm import experimental_mcp_client as client + + self._client = client + return self._client + + def __getattr__(self, name): + return getattr(self._get_client(), name) + + +experimental_mcp_client = _ExperimentalMCPClientProxy() + # Import the change tracker from aider.change_tracker import ChangeTracker @@ -30,7 +48,7 @@ # Import skills helper for skills from aider.helpers.skills import SkillsManager -from aider.mcp.server import LocalServer +from aider.mcp_support.server import LocalServer from aider.repo import ANY_GIT_ERROR # Import tool modules for registry diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 62c64bd35ba..818ba50075a 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -29,7 +29,6 @@ from typing import List import httpx -from litellm import experimental_mcp_client from litellm.types.utils import ModelResponse from prompt_toolkit.patch_stdout import patch_stdout from rich.console import Console @@ -42,7 +41,7 @@ from aider.io import ConfirmGroup, InputOutput from aider.linter import Linter from aider.llm import litellm -from aider.mcp.server import LocalServer +from aider.mcp_support.server import LocalServer from aider.models import RETRY_TIMEOUT from aider.reasoning_tags import ( REASONING_TAG, @@ -57,6 +56,25 @@ from aider.tools.utils.output import print_tool_response from aider.utils import format_tokens, is_image_file + +class _ExperimentalMCPClientProxy: + """Lazy proxy to defer importing litellm.experimental_mcp_client.""" + + _client = None + + def _get_client(self): + if self._client is None: + from litellm import experimental_mcp_client as client + + self._client = client + return self._client + + def __getattr__(self, name): + return getattr(self._get_client(), name) + + +experimental_mcp_client = _ExperimentalMCPClientProxy() + from ..dump import dump # noqa: F401 from .chat_chunks import ChatChunks diff --git a/aider/main.py b/aider/main.py index a5b79e7137b..107e7a100f9 100644 --- a/aider/main.py +++ b/aider/main.py @@ -48,7 +48,7 @@ from aider.history import ChatSummary from aider.io import InputOutput from aider.llm import litellm # noqa: F401; properly init litellm on launch -from aider.mcp import load_mcp_servers +from aider.mcp_support import load_mcp_servers from aider.models import ModelSettings from aider.onboarding import offer_openrouter_oauth, select_default_model from aider.repo import ANY_GIT_ERROR, GitRepo diff --git a/aider/mcp/__init__.py b/aider/mcp_support/__init__.py similarity index 88% rename from aider/mcp/__init__.py rename to aider/mcp_support/__init__.py index eea87122003..26a6bd44b41 100644 --- a/aider/mcp/__init__.py +++ b/aider/mcp_support/__init__.py @@ -1,7 +1,17 @@ import json from pathlib import Path -from aider.mcp.server import HttpStreamingServer, McpServer, SseServer + +def _create_server_from_config(server_config, transport): + from .server import HttpStreamingServer, McpServer, SseServer + + if transport == "stdio": + return McpServer(server_config) + if transport == "http": + return HttpStreamingServer(server_config) + if transport == "sse": + return SseServer(server_config) + return None def _parse_mcp_servers_from_json_string(json_string, io, verbose=False, mcp_transport="stdio"): @@ -21,12 +31,9 @@ def _parse_mcp_servers_from_json_string(json_string, io, verbose=False, mcp_tran # Create a server config with name included server_config["name"] = name transport = server_config.get("transport", mcp_transport) - if transport == "stdio": - servers.append(McpServer(server_config)) - elif transport == "http": - servers.append(HttpStreamingServer(server_config)) - elif transport == "sse": - servers.append(SseServer(server_config)) + server = _create_server_from_config(server_config, transport) + if server: + servers.append(server) if verbose: io.tool_output(f"Loaded {len(servers)} MCP servers from JSON string") @@ -120,10 +127,9 @@ def _parse_mcp_servers_from_file(file_path, io, verbose=False, mcp_transport="st # Create a server config with name included server_config["name"] = name transport = server_config.get("transport", mcp_transport) - if transport == "stdio": - servers.append(McpServer(server_config)) - elif transport == "http": - servers.append(HttpStreamingServer(server_config)) + server = _create_server_from_config(server_config, transport) + if server: + servers.append(server) if verbose: io.tool_output(f"Loaded {len(servers)} MCP servers from {file_path}") @@ -169,6 +175,7 @@ def load_mcp_servers(mcp_servers, mcp_servers_file, io, verbose=False, mcp_trans # and maybe it is actually prompt_toolkit's fault # but this hack works swimmingly because ??? # so sure! why not - servers = [McpServer(json.loads('{"aider_default": {}}'))] + default_server = _create_server_from_config(json.loads('{"aider_default": {}}'), "stdio") + servers = [default_server] if default_server else [] return servers diff --git a/aider/mcp/server.py b/aider/mcp_support/server.py similarity index 100% rename from aider/mcp/server.py rename to aider/mcp_support/server.py From 8f652f3cec17d26c902b32c45a294fb844d8d49a Mon Sep 17 00:00:00 2001 From: Chris Nestrud Date: Sat, 27 Dec 2025 21:11:33 -0600 Subject: [PATCH 2/2] Satisfy pre-commit import ordering Running the documented pre-commit hooks failed with E402/isort warnings because the lazy MCP proxy lived above the project imports. Move the proxy definitions below the import blocks in agent_coder and base_coder so auto-formatting passes. --- aider/coders/agent_coder.py | 43 ++++++++++++++++--------------------- aider/coders/base_coder.py | 6 +++--- 2 files changed, 22 insertions(+), 27 deletions(-) diff --git a/aider/coders/agent_coder.py b/aider/coders/agent_coder.py index c85f88466e1..78e3d4a8373 100644 --- a/aider/coders/agent_coder.py +++ b/aider/coders/agent_coder.py @@ -14,32 +14,8 @@ from datetime import datetime from pathlib import Path - from aider import urls, utils - - -class _ExperimentalMCPClientProxy: - """Lazy proxy to defer importing litellm.experimental_mcp_client.""" - - _client = None - - def _get_client(self): - if self._client is None: - from litellm import experimental_mcp_client as client - - self._client = client - return self._client - - def __getattr__(self, name): - return getattr(self._get_client(), name) - - -experimental_mcp_client = _ExperimentalMCPClientProxy() - -# Import the change tracker from aider.change_tracker import ChangeTracker - -# Import similarity functions for tool usage analysis from aider.helpers.similarity import ( cosine_similarity, create_bigram_vector, @@ -95,6 +71,25 @@ def __getattr__(self, name): from .editblock_coder import do_replace, find_original_update_blocks, find_similar_lines +class _ExperimentalMCPClientProxy: + """Lazy proxy to defer importing litellm.experimental_mcp_client.""" + + _client = None + + def _get_client(self): + if self._client is None: + from litellm import experimental_mcp_client as client + + self._client = client + return self._client + + def __getattr__(self, name): + return getattr(self._get_client(), name) + + +experimental_mcp_client = _ExperimentalMCPClientProxy() + + class AgentCoder(Coder): """Mode where the LLM autonomously manages which files are in context.""" diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 818ba50075a..44eff7e3a3c 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -56,6 +56,9 @@ from aider.tools.utils.output import print_tool_response from aider.utils import format_tokens, is_image_file +from ..dump import dump # noqa: F401 +from .chat_chunks import ChatChunks + class _ExperimentalMCPClientProxy: """Lazy proxy to defer importing litellm.experimental_mcp_client.""" @@ -75,9 +78,6 @@ def __getattr__(self, name): experimental_mcp_client = _ExperimentalMCPClientProxy() -from ..dump import dump # noqa: F401 -from .chat_chunks import ChatChunks - class UnknownEditFormat(ValueError): def __init__(self, edit_format, valid_formats):