From c5414e2601739afe6a80bc09c4bc85af8bd71751 Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Wed, 30 Apr 2025 23:51:31 -0700 Subject: [PATCH 01/14] Add MCP Python SDK package dependency --- requirements.txt | 35 +++++++++++++++++++++++++++++ requirements/common-constraints.txt | 33 ++++++++++++++++++++++++--- requirements/requirements.in | 1 + 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index d8efa792e..ec08e337e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,7 +20,10 @@ anyio==4.9.0 # via # -c requirements/common-constraints.txt # httpx + # mcp # openai + # sse-starlette + # starlette # watchfiles attrs==25.3.0 # via @@ -60,6 +63,7 @@ click==8.1.8 # via # -c requirements/common-constraints.txt # litellm + # uvicorn configargparse==1.7 # via # -c requirements/common-constraints.txt @@ -154,6 +158,7 @@ h11==0.16.0 # via # -c requirements/common-constraints.txt # httpcore + # uvicorn httpcore==1.0.9 # via # -c requirements/common-constraints.txt @@ -167,7 +172,12 @@ httpx==0.28.1 # via # -c requirements/common-constraints.txt # litellm + # mcp # openai +httpx-sse==0.4.0 + # via + # -c requirements/common-constraints.txt + # mcp huggingface-hub==0.30.2 # via # -c requirements/common-constraints.txt @@ -225,6 +235,10 @@ mccabe==0.7.0 # via # -c requirements/common-constraints.txt # flake8 +mcp==1.6.0 + # via + # -c requirements/common-constraints.txt + # -r requirements/requirements.in mdurl==0.1.2 # via # -c requirements/common-constraints.txt @@ -334,11 +348,17 @@ pydantic==2.11.3 # -c requirements/common-constraints.txt # google-generativeai # litellm + # mcp # openai + # pydantic-settings pydantic-core==2.33.1 # via # -c requirements/common-constraints.txt # pydantic +pydantic-settings==2.9.1 + # via + # -c requirements/common-constraints.txt + # mcp pydub==0.25.1 # via # -c requirements/common-constraints.txt @@ -371,6 +391,7 @@ python-dotenv==1.1.0 # via # -c requirements/common-constraints.txt # litellm + # pydantic-settings pyyaml==6.0.2 # via # -c requirements/common-constraints.txt @@ -441,6 +462,15 @@ soupsieve==2.7 # via # -c requirements/common-constraints.txt # beautifulsoup4 +sse-starlette==2.3.3 + # via + # -c requirements/common-constraints.txt + # mcp +starlette==0.46.2 + # via + # -c requirements/common-constraints.txt + # mcp + # sse-starlette tiktoken==0.9.0 # via # -c requirements/common-constraints.txt @@ -490,6 +520,7 @@ typing-inspection==0.4.0 # via # -c requirements/common-constraints.txt # pydantic + # pydantic-settings uritemplate==4.1.1 # via # -c requirements/common-constraints.txt @@ -499,6 +530,10 @@ urllib3==2.4.0 # -c requirements/common-constraints.txt # mixpanel # requests +uvicorn==0.34.2 + # via + # -c requirements/common-constraints.txt + # mcp watchfiles==1.0.5 # via # -c requirements/common-constraints.txt diff --git a/requirements/common-constraints.txt b/requirements/common-constraints.txt index dcaa773a3..2814985b1 100644 --- a/requirements/common-constraints.txt +++ b/requirements/common-constraints.txt @@ -16,7 +16,10 @@ annotated-types==0.7.0 anyio==4.9.0 # via # httpx + # mcp # openai + # sse-starlette + # starlette # watchfiles attrs==25.3.0 # via @@ -59,6 +62,7 @@ click==8.1.8 # pip-tools # streamlit # typer + # uvicorn codespell==2.4.1 # via -r requirements/requirements-dev.in cogapp==3.4.1 @@ -171,7 +175,9 @@ grpcio==1.71.0 grpcio-status==1.71.0 # via google-api-core h11==0.16.0 - # via httpcore + # via + # httpcore + # uvicorn httpcore==1.0.9 # via httpx httplib2==0.22.0 @@ -182,7 +188,10 @@ httpx==0.28.1 # via # litellm # llama-index-core + # mcp # openai +httpx-sse==0.4.0 + # via mcp huggingface-hub[inference]==0.30.2 # via # llama-index-embeddings-huggingface @@ -251,6 +260,8 @@ matplotlib==3.10.1 # via -r requirements/requirements-dev.in mccabe==0.7.0 # via flake8 +mcp==1.6.0 + # via -r requirements/requirements.in mdurl==0.1.2 # via markdown-it-py mixpanel==4.10.1 @@ -389,9 +400,13 @@ pydantic==2.11.3 # google-generativeai # litellm # llama-index-core + # mcp # openai + # pydantic-settings pydantic-core==2.33.1 # via pydantic +pydantic-settings==2.9.1 + # via mcp pydeck==0.9.1 # via streamlit pydub==0.25.1 @@ -427,7 +442,9 @@ python-dateutil==2.9.0.post0 # pandas # posthog python-dotenv==1.1.0 - # via litellm + # via + # litellm + # pydantic-settings pytz==2025.2 # via pandas pyyaml==6.0.2 @@ -505,6 +522,12 @@ soupsieve==2.7 # via beautifulsoup4 sqlalchemy[asyncio]==2.0.40 # via llama-index-core +sse-starlette==2.3.3 + # via mcp +starlette==0.46.2 + # via + # mcp + # sse-starlette streamlit==1.44.1 # via -r requirements/requirements-browser.in sympy==1.14.0 @@ -579,7 +602,9 @@ typing-inspect==0.9.0 # dataclasses-json # llama-index-core typing-inspection==0.4.0 - # via pydantic + # via + # pydantic + # pydantic-settings tzdata==2025.2 # via pandas uritemplate==4.1.1 @@ -590,6 +615,8 @@ urllib3==2.4.0 # requests uv==0.6.17 # via -r requirements/requirements-dev.in +uvicorn==0.34.2 + # via mcp virtualenv==20.30.0 # via pre-commit watchfiles==1.0.5 diff --git a/requirements/requirements.in b/requirements/requirements.in index 56bd17132..a0b5545a7 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -29,6 +29,7 @@ socksio pip pillow google-generativeai +mcp>=1.0.0 # The proper dependency is networkx[default], but this brings # in matplotlib and a bunch of other deps From 162f49c98e16f7463816912ea173a9874eac5eda Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Wed, 30 Apr 2025 23:53:13 -0700 Subject: [PATCH 02/14] Introduce and call MCP servers --- aider/coders/base_coder.py | 34 +++++++++++++++++ aider/main.py | 6 +++ aider/mcp/__init__.py | 0 aider/mcp/server.py | 76 ++++++++++++++++++++++++++++++++++++++ aider/models.py | 7 +++- 5 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 aider/mcp/__init__.py create mode 100644 aider/mcp/server.py diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 675570c60..617c78d13 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1,5 +1,6 @@ #!/usr/bin/env python +import asyncio import base64 import hashlib import json @@ -111,6 +112,8 @@ class Coder: ignore_mentions = None chat_language = None file_watcher = None + mcp_servers = None + mcp_tools = None @classmethod def create( @@ -323,6 +326,7 @@ class Coder: file_watcher=None, auto_copy_context=False, auto_accept_architect=True, + mcp_servers=None, ): # Fill in a dummy Analytics if needed, but it is never .enable()'d self.analytics = analytics if analytics is not None else Analytics() @@ -349,6 +353,7 @@ class Coder: self.detect_urls = detect_urls self.num_cache_warming_pings = num_cache_warming_pings + self.mcp_servers = mcp_servers if not fnames: fnames = [] @@ -508,6 +513,30 @@ class Coder: self.auto_test = auto_test self.test_cmd = test_cmd + # Instantiate MCP tools + if self.mcp_servers: + from litellm import experimental_mcp_client + + tools = [] + print("GETTING SERVER TOOLS") + for server in self.mcp_servers: + print(f"Getting server tools: {server.name}") + + async def get_server_tools(all_tools): + try: + session = await server.connect() # Use connect() directly + server_tools = await experimental_mcp_client.load_mcp_tools( + session=session, format="openai" + ) + return all_tools + server_tools + finally: + await server.disconnect() + + tools = asyncio.run(get_server_tools(tools)) + + self.mcp_tools = tools + print("All TOOLS") + print(tools) # validate the functions jsonschema if self.functions: from jsonschema import Draft7Validator @@ -1461,6 +1490,8 @@ class Coder: self.reflected_message = add_rel_files_message return + print(content) + try: if self.reply_completed(): return @@ -1688,12 +1719,15 @@ class Coder: self.io.log_llm_history("TO LLM", format_messages(messages)) completion = None + try: hash_object, completion = model.send_completion( messages, functions, self.stream, self.temperature, + # This could include any tools, but for now it is just MCP tools + tools=self.mcp_tools, ) self.chat_completion_call_hashes.append(hash_object.hexdigest()) diff --git a/aider/main.py b/aider/main.py index 89286e1de..5648ac966 100644 --- a/aider/main.py +++ b/aider/main.py @@ -29,6 +29,7 @@ from aider.format_settings import format_settings, scrub_sensitive_info 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.server import McpServer from aider.models import ModelSettings from aider.onboarding import offer_openrouter_oauth, select_default_model from aider.repo import ANY_GIT_ERROR, GitRepo @@ -956,6 +957,10 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F analytics.event("auto_commits", enabled=bool(args.auto_commits)) try: + fetch_server = McpServer({"name": "fetch", "command": "uvx", "args": ["mcp-server-fetch"]}) + + git_server = McpServer({"name": "git", "command": "uvx", "args": ["mcp-server-git"]}) + coder = Coder.create( main_model=main_model, edit_format=args.edit_format, @@ -988,6 +993,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F detect_urls=args.detect_urls, auto_copy_context=args.copy_paste, auto_accept_architect=args.auto_accept_architect, + mcp_servers=[fetch_server, git_server], ) except UnknownEditFormat as err: io.tool_error(str(err)) diff --git a/aider/mcp/__init__.py b/aider/mcp/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/aider/mcp/server.py b/aider/mcp/server.py new file mode 100644 index 000000000..db0d56c58 --- /dev/null +++ b/aider/mcp/server.py @@ -0,0 +1,76 @@ +import asyncio +import logging +import os +from contextlib import AsyncExitStack + +from mcp import ClientSession, StdioServerParameters +from mcp.client.stdio import stdio_client + + +class McpServer: + """ + A client for MCP servers that provides tools to Aider coders. An McpServer class + is initialized per configured MCP Server + + Current usage: + + conn = await session.connect() # Use connect() directly + tools = await experimental_mcp_client.load_mcp_tools(session=s, format="openai") + await session.disconnect() + print(tools) + """ + + def __init__(self, server_config): + """Initialize the MCP tool provider. + + Args: + server_config: Configuration for the MCP server + """ + self.config = server_config + self.name = server_config.get("name", "unnamed-server") + self.session = None + self._cleanup_lock: asyncio.Lock = asyncio.Lock() + self.exit_stack = AsyncExitStack() + + async def connect(self): + """Connect to the MCP server and return the session. + + If a session is already active, returns the existing session. + Otherwise, establishes a new connection and initializes the session. + + Returns: + ClientSession: The active session + """ + if self.session is not None: + logging.info(f"Using existing session for MCP server: {self.name}") + return self.session + + logging.info(f"Establishing new connection to MCP server: {self.name}") + command = self.config["command"] + server_params = StdioServerParameters( + command=command, + args=self.config["args"], + env={**os.environ, **self.config["env"]} if self.config.get("env") else None, + ) + + try: + stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params)) + read, write = stdio_transport + session = await self.exit_stack.enter_async_context(ClientSession(read, write)) + await session.initialize() + self.session = session + return session + except Exception as e: + logging.error(f"Error initializing server {self.name}: {e}") + await self.disconnect() + raise + + async def disconnect(self): + """Disconnect from the MCP server and clean up resources.""" + async with self._cleanup_lock: + try: + await self.exit_stack.aclose() + self.session = None + self.stdio_context = None + except Exception as e: + logging.error(f"Error during cleanup of server {self.name}: {e}") diff --git a/aider/models.py b/aider/models.py index dd0abd452..72407d16e 100644 --- a/aider/models.py +++ b/aider/models.py @@ -767,7 +767,7 @@ class Model(ModelSettings): def is_ollama(self): return self.name.startswith("ollama/") or self.name.startswith("ollama_chat/") - def send_completion(self, messages, functions, stream, temperature=None): + def send_completion(self, messages, functions, stream, temperature=None, tools=None): if os.environ.get("AIDER_SANITY_CHECK_TURNS"): sanity_check_messages(messages) @@ -797,8 +797,11 @@ class Model(ModelSettings): if self.is_ollama() and "num_ctx" not in kwargs: num_ctx = int(self.token_count(messages) * 1.25) + 8192 kwargs["num_ctx"] = num_ctx - key = json.dumps(kwargs, sort_keys=True).encode() + if tools: + kwargs["tools"] = kwargs["tools"] + tools + + key = json.dumps(kwargs, sort_keys=True).encode() # dump(kwargs) hash_object = hashlib.sha1(key) From 2c24084cb0347ee4883b2ab5825076a47cef497b Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Thu, 1 May 2025 12:16:32 -0700 Subject: [PATCH 03/14] Models may use tools during completions --- aider/coders/base_coder.py | 173 ++++++++++++++++++++++++++++++++----- aider/main.py | 11 ++- aider/models.py | 5 +- tests/basic/test_coder.py | 114 ++++++++++++++++++++++++ 4 files changed, 272 insertions(+), 31 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 617c78d13..bef6a5c2c 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -20,6 +20,8 @@ from json.decoder import JSONDecodeError from pathlib import Path from typing import List +from litellm import experimental_mcp_client + from aider import __version__, models, prompts, urls, utils from aider.analytics import Analytics from aider.commands import Commands @@ -100,6 +102,7 @@ class Coder: test_outcome = None multi_response_content = "" partial_response_content = "" + partial_response_tool_call = [] commit_before_message = [] message_cost = 0.0 message_tokens_sent = 0 @@ -515,28 +518,7 @@ class Coder: # Instantiate MCP tools if self.mcp_servers: - from litellm import experimental_mcp_client - - tools = [] - print("GETTING SERVER TOOLS") - for server in self.mcp_servers: - print(f"Getting server tools: {server.name}") - - async def get_server_tools(all_tools): - try: - session = await server.connect() # Use connect() directly - server_tools = await experimental_mcp_client.load_mcp_tools( - session=session, format="openai" - ) - return all_tools + server_tools - finally: - await server.disconnect() - - tools = asyncio.run(get_server_tools(tools)) - - self.mcp_tools = tools - print("All TOOLS") - print(tools) + self.initialize_mcp_tools() # validate the functions jsonschema if self.functions: from jsonschema import Draft7Validator @@ -1360,6 +1342,7 @@ class Coder: chunks = self.format_messages() messages = chunks.all_messages() + if not self.check_tokens(messages): return self.warm_cache(chunks) @@ -1490,7 +1473,19 @@ class Coder: self.reflected_message = add_rel_files_message return - print(content) + tool_call_response = litellm.stream_chunk_builder(self.partial_response_tool_call) + + if tool_call_response: + tool_responses = self.execute_tool_calls(tool_call_response) + + # Add the assistant message with tool calls + self.cur_messages.append(tool_call_response.choices[0].message) + + # Add all tool responses + for tool_response in tool_responses: + self.cur_messages.append(tool_response) + + return self.run(with_message="Continue", preproc=False) try: if self.reply_completed(): @@ -1548,6 +1543,129 @@ class Coder: self.reflected_message = test_errors return + async def _exec_server_tool(self, server, tool_call): + """Execute a tool call on an MCP server.""" + try: + session = await server.connect() # Use connect() directly + call_result = await experimental_mcp_client.call_openai_tool( + session=session, + openai_tool=tool_call, + ) + return (str(call_result.content[0].text),) + finally: + await server.disconnect() + + def execute_tool_calls(self, tool_call_response): + """Process tool calls from the response and execute them if they match MCP tools. + Returns a list of tool response messages.""" + tool_responses = [] + tool_calls = tool_call_response.choices[0].message.tool_calls + + # First, collect all tool calls grouped by server + server_tool_calls = {} + + for tool_call in tool_calls: + # Check if this tool_call matches any MCP tool + if self.mcp_tools: + for server_name, server_tools in self.mcp_tools: + for tool in server_tools: + if tool.get("function", {}).get("name") == tool_call.function.name: + self.io.tool_output( + f"Found MCP tool: {tool_call.function.name} from server" + f" {server_name}" + ) + self.io.tool_output(f"Tool arguments: {tool_call.function.arguments}") + self.io.tool_output(f"Tool ID: {tool_call.id}") + self.io.tool_output(f"Tool type: {tool_call.type}") + + # Find the corresponding server + for server in self.mcp_servers: + if server.name == server_name: + if server not in server_tool_calls: + server_tool_calls[server] = [] + server_tool_calls[server].append(tool_call) + break + + # Define the coroutine to execute all tool calls for a single server + async def _exec_server_tools(server, tool_calls_list): + tool_responses = [] + try: + # Connect to the server once + session = await server.connect() + # Execute all tool calls for this server + for tool_call in tool_calls_list: + call_result = await experimental_mcp_client.call_openai_tool( + session=session, + openai_tool=tool_call, + ) + result_text = str(call_result.content[0].text) + tool_responses.append( + {"role": "tool", "tool_call_id": tool_call.id, "content": result_text} + ) + finally: + await server.disconnect() + return tool_responses + + # Execute all tool calls concurrently + async def _execute_all_tool_calls(): + tasks = [] + for server, tool_calls_list in server_tool_calls.items(): + tasks.append(_exec_server_tools(server, tool_calls_list)) + # Wait for all tasks to complete + results = await asyncio.gather(*tasks) + return results + + # Run the async execution and collect results + if server_tool_calls: + all_results = asyncio.run(_execute_all_tool_calls()) + # Flatten the results from all servers + for server_results in all_results: + tool_responses.extend(server_results) + + return tool_responses + + def initialize_mcp_tools(self): + """Initialize tools from all configured MCP servers.""" + tools = [] + + async def get_server_tools(server): + try: + session = await server.connect() + server_tools = await experimental_mcp_client.load_mcp_tools( + session=session, format="openai" + ) + return (server.name, server_tools) + 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 + + 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 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 + + def get_tool_list(self): + """Get a flattened list of all MCP tools.""" + tool_list = [] + if self.mcp_tools: + for _, server_tools in self.mcp_tools: + tool_list.extend(server_tools) + return tool_list + def reply_completed(self): pass @@ -1721,13 +1839,15 @@ class Coder: completion = None try: + tool_list = self.get_tool_list() + hash_object, completion = model.send_completion( messages, functions, self.stream, self.temperature, # This could include any tools, but for now it is just MCP tools - tools=self.mcp_tools, + tools=tool_list, ) self.chat_completion_call_hashes.append(hash_object.hexdigest()) @@ -1825,6 +1945,7 @@ class Coder: def show_send_output_stream(self, completion): received_content = False + self.partial_response_tool_call = [] for chunk in completion: if len(chunk.choices) == 0: @@ -1836,6 +1957,9 @@ class Coder: ): raise FinishReasonLength() + if chunk.choices[0].delta.tool_calls: + self.partial_response_tool_call.append(chunk) + try: func = chunk.choices[0].delta.function_call # dump(func) @@ -1844,6 +1968,7 @@ class Coder: self.partial_response_function_call[k] += v else: self.partial_response_function_call[k] = v + received_content = True except AttributeError: pass diff --git a/aider/main.py b/aider/main.py index 5648ac966..bac638f6e 100644 --- a/aider/main.py +++ b/aider/main.py @@ -957,9 +957,14 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F analytics.event("auto_commits", enabled=bool(args.auto_commits)) try: - fetch_server = McpServer({"name": "fetch", "command": "uvx", "args": ["mcp-server-fetch"]}) - git_server = McpServer({"name": "git", "command": "uvx", "args": ["mcp-server-git"]}) + context_seven_server = McpServer( + { + "name": "context7", + "command": "deno", + "args": ["run", "--allow-net", "npm:@upstash/context7-mcp"], + } + ) coder = Coder.create( main_model=main_model, @@ -993,7 +998,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F detect_urls=args.detect_urls, auto_copy_context=args.copy_paste, auto_accept_architect=args.auto_accept_architect, - mcp_servers=[fetch_server, git_server], + mcp_servers=[context_seven_server, git_server], ) except UnknownEditFormat as err: io.tool_error(str(err)) diff --git a/aider/models.py b/aider/models.py index 72407d16e..c16edae4a 100644 --- a/aider/models.py +++ b/aider/models.py @@ -774,10 +774,7 @@ class Model(ModelSettings): if self.is_deepseek_r1(): messages = ensure_alternating_roles(messages) - kwargs = dict( - model=self.name, - stream=stream, - ) + kwargs = dict(model=self.name, stream=stream, tools=[]) if self.use_temperature is not False: if temperature is None: diff --git a/tests/basic/test_coder.py b/tests/basic/test_coder.py index c58ade1b2..697a06652 100644 --- a/tests/basic/test_coder.py +++ b/tests/basic/test_coder.py @@ -1287,6 +1287,120 @@ This command will print 'Hello, World!' to the console.""" # (because user rejected the changes) mock_editor.run.assert_not_called() + @patch("aider.coders.base_coder.experimental_mcp_client") + def test_mcp_server_connection(self, mock_mcp_client): + """Test that the coder connects to MCP servers for tools.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + + # Create mock MCP server + mock_server = MagicMock() + mock_server.name = "test_server" + mock_server.connect = MagicMock() + mock_server.disconnect = MagicMock() + + # Setup mock for initialize_mcp_tools + mock_tools = [("test_server", [{"function": {"name": "test_tool"}}])] + + # Create coder with mock MCP server + with patch.object(Coder, "initialize_mcp_tools", return_value=mock_tools): + coder = Coder.create(self.GPT35, "diff", io=io, mcp_servers=[mock_server]) + + # Manually set mcp_tools since we're bypassing initialize_mcp_tools + coder.mcp_tools = mock_tools + + # Verify that mcp_tools contains the expected data + self.assertIsNotNone(coder.mcp_tools) + self.assertEqual(len(coder.mcp_tools), 1) + self.assertEqual(coder.mcp_tools[0][0], "test_server") + + # Test execute_tool_calls + tool_call = MagicMock() + tool_call.function.name = "test_tool" + tool_call.function.arguments = "{}" + tool_call.id = "test_id" + tool_call.type = "function" + + response = MagicMock() + response.choices = [MagicMock()] + response.choices[0].message.tool_calls = [tool_call] + + # Setup mock for call_openai_tool + mock_call_result = MagicMock() + mock_call_result.content = [MagicMock()] + mock_call_result.content[0].text = "Tool execution result" + mock_mcp_client.call_openai_tool.return_value = mock_call_result + + # Mock the async execution directly + with patch.object( + coder, + "execute_tool_calls", + return_value=[ + { + "role": "tool", + "tool_call_id": "test_id", + "content": "Tool execution result", + } + ], + ): + tool_responses = coder.execute_tool_calls(response) + + # Verify tool responses + self.assertEqual(len(tool_responses), 1) + self.assertEqual(tool_responses[0]["role"], "tool") + 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_initialize_mcp_tools(self, mock_mcp_client): + """Test that the coder initializes MCP tools correctly.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + + # Create mock MCP servers + mock_server1 = MagicMock() + mock_server1.name = "server1" + mock_server1.connect = MagicMock() + mock_server1.disconnect = MagicMock() + + mock_server2 = MagicMock() + mock_server2.name = "server2" + mock_server2.connect = MagicMock() + mock_server2.disconnect = MagicMock() + + # Setup mock return values + server1_tools = [{"function": {"name": "tool1", "description": "Tool 1 description"}}] + server2_tools = [{"function": {"name": "tool2", "description": "Tool 2 description"}}] + + # Mock the initialize_mcp_tools method + expected_tools = [("server1", server1_tools), ("server2", server2_tools)] + + # Create coder with mock MCP servers and patch initialize_mcp_tools + with patch.object(Coder, "initialize_mcp_tools"): + coder = Coder.create( + self.GPT35, + "diff", + io=io, + mcp_servers=[mock_server1, mock_server2], + verbose=True, + ) + + # Manually set mcp_tools to expected value + coder.mcp_tools = expected_tools + + # Verify that mcp_tools contains the expected tools + self.assertEqual(len(coder.mcp_tools), 2) + self.assertEqual(coder.mcp_tools[0][0], "server1") + self.assertEqual(coder.mcp_tools[0][1], server1_tools) + self.assertEqual(coder.mcp_tools[1][0], "server2") + self.assertEqual(coder.mcp_tools[1][1], server2_tools) + + # Test get_tool_list + tool_list = coder.get_tool_list() + self.assertEqual(len(tool_list), 2) + self.assertEqual(tool_list[0], server1_tools[0]) + self.assertEqual(tool_list[1], server2_tools[0]) + if __name__ == "__main__": unittest.main() From 16c4e622e3572ad101eb33ed46ee3bb8f85d19b6 Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Fri, 2 May 2025 17:14:26 -0700 Subject: [PATCH 04/14] Configurable mcp servers --- aider/args.py | 12 ++++ aider/gui.py | 1 + aider/main.py | 17 +++--- aider/mcp/__init__.py | 86 +++++++++++++++++++++++++++++ aider/website/docs/config/mcp.md | 94 ++++++++++++++++++++++++++++++++ tests/basic/test_main.py | 54 ++++++++++++++++++ 6 files changed, 254 insertions(+), 10 deletions(-) create mode 100644 aider/website/docs/config/mcp.md diff --git a/aider/args.py b/aider/args.py index 6df19778b..4dc3abd72 100644 --- a/aider/args.py +++ b/aider/args.py @@ -723,6 +723,18 @@ def get_parser(default_config_files, git_root): default="platform", help="Line endings to use when writing files (default: platform)", ) + group.add_argument( + "--mcp-servers", + metavar="MCP_CONFIG_JSON", + help="Specify MCP server configurations as a JSON string", + default=None, + ) + group.add_argument( + "--mcp-servers-file", + metavar="MCP_CONFIG_FILE", + help="Specify a file path with MCP server configurations", + default=None, + ) group.add_argument( "-c", "--config", diff --git a/aider/gui.py b/aider/gui.py index 7fa90bc38..23e59520d 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -198,6 +198,7 @@ class GUI: ) for fname in fnames: + print(fname) if fname not in self.coder.get_inchat_relative_files(): self.coder.add_rel_fname(fname) self.info(f"Added {fname} to the chat") diff --git a/aider/main.py b/aider/main.py index bac638f6e..ca234a652 100644 --- a/aider/main.py +++ b/aider/main.py @@ -29,7 +29,7 @@ from aider.format_settings import format_settings, scrub_sensitive_info 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.server import McpServer +from aider.mcp 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 @@ -957,14 +957,11 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F analytics.event("auto_commits", enabled=bool(args.auto_commits)) try: - git_server = McpServer({"name": "git", "command": "uvx", "args": ["mcp-server-git"]}) - context_seven_server = McpServer( - { - "name": "context7", - "command": "deno", - "args": ["run", "--allow-net", "npm:@upstash/context7-mcp"], - } - ) + # Load MCP servers from config string or file + mcp_servers = load_mcp_servers(args.mcp_servers, args.mcp_servers_file, io, args.verbose) + + if not mcp_servers: + mcp_servers = [] coder = Coder.create( main_model=main_model, @@ -998,7 +995,7 @@ def main(argv=None, input=None, output=None, force_git_root=None, return_coder=F detect_urls=args.detect_urls, auto_copy_context=args.copy_paste, auto_accept_architect=args.auto_accept_architect, - mcp_servers=[context_seven_server, git_server], + mcp_servers=mcp_servers, ) except UnknownEditFormat as err: io.tool_error(str(err)) diff --git a/aider/mcp/__init__.py b/aider/mcp/__init__.py index e69de29bb..636f8daf7 100644 --- a/aider/mcp/__init__.py +++ b/aider/mcp/__init__.py @@ -0,0 +1,86 @@ +import json + +from aider.mcp.server import McpServer + + +def _parse_mcp_servers_from_json_string(json_string, io, verbose=False): + """Parse MCP servers from a JSON string.""" + servers = [] + + try: + config = json.loads(json_string) + if verbose: + io.tool_output("Loading MCP servers from provided JSON string") + + if "mcpServers" in config: + for name, server_config in config["mcpServers"].items(): + if verbose: + io.tool_output(f"Loading MCP server: {name}") + + # Create a server config with name included + server_config["name"] = name + servers.append(McpServer(server_config)) + + if verbose: + io.tool_output(f"Loaded {len(servers)} MCP servers from JSON string") + return servers + else: + io.tool_warning("No 'mcpServers' key found in MCP config JSON string") + except json.JSONDecodeError: + io.tool_error("Invalid JSON in MCP config string") + except Exception as e: + io.tool_error(f"Error loading MCP config from string: {e}") + + return servers + + +def _parse_mcp_servers_from_file(file_path, io, verbose=False): + """Parse MCP servers from a JSON file.""" + servers = [] + + try: + with open(file_path, "r") as f: + config = json.load(f) + + if verbose: + io.tool_output(f"Loading MCP servers from file: {file_path}") + + if "mcpServers" in config: + for name, server_config in config["mcpServers"].items(): + if verbose: + io.tool_output(f"Loading MCP server: {name}") + + # Create a server config with name included + server_config["name"] = name + servers.append(McpServer(server_config)) + + if verbose: + io.tool_output(f"Loaded {len(servers)} MCP servers from {file_path}") + return servers + else: + io.tool_warning(f"No 'mcpServers' key found in MCP config file: {file_path}") + except FileNotFoundError: + io.tool_warning(f"MCP config file not found: {file_path}") + except json.JSONDecodeError: + io.tool_error(f"Invalid JSON in MCP config file: {file_path}") + except Exception as e: + io.tool_error(f"Error loading MCP config from file: {e}") + + return servers + + +def load_mcp_servers(mcp_servers, mcp_servers_file, io, verbose=False): + """Load MCP servers from a JSON string or file.""" + servers = [] + + # First try to load from the JSON string (preferred) + if mcp_servers: + servers = _parse_mcp_servers_from_json_string(mcp_servers, io, verbose) + if servers: + return servers + + # If JSON string failed or wasn't provided, try the file + if mcp_servers_file: + servers = _parse_mcp_servers_from_file(mcp_servers_file, io, verbose) + + return servers diff --git a/aider/website/docs/config/mcp.md b/aider/website/docs/config/mcp.md new file mode 100644 index 000000000..8916ddbab --- /dev/null +++ b/aider/website/docs/config/mcp.md @@ -0,0 +1,94 @@ +--- +parent: Configuration +nav_order: 120 +description: Configure Model Control Protocol (MCP) servers for enhanced AI capabilities. +--- + +# Model Control Protocol (MCP) + +Model Control Protocol (MCP) servers extend aider's capabilities by providing additional tools and functionality to the AI models. MCP servers can add features like git operations, context retrieval, and other specialized tools. + +## Configuring MCP Servers + +Aider supports configuring MCP servers using the MCP Server Configuration schema. Please +see the [Model Context Protocol documentation](https://modelcontextprotocol.io/introduction) +for more information. + +You have two ways of sharing your MCP server configuration with Aider. + +### 1. Using a JSON string + +You can specify MCP servers directly on the command line using the `--mcp-servers` option with a JSON string: + +```bash +aider --mcp-servers '{"mcpServers":{"git":{"command":"uvx","args":["mcp-server-git"]}}}' +``` + +### 2. Using a configuration file + +Alternatively, you can store your MCP server configurations in a JSON file and reference it with the `--mcp-servers-file` option: + +```bash +aider --mcp-servers-file mcp.json +``` + +Example `mcp.json` file: + +```json +{ + "mcpServers": { + "git": { + "command": "uvx", + "args": ["mcp-server-git"] + } + } +} +``` + +## YAML Configuration + +You can also configure MCP servers in your `.aider.conf.yml` file: + +```yaml +mcp-servers: | + { + "mcpServers": { + "git": { + "command": "uvx", + "args": ["mcp-server-git"] + } + } + } +``` + +Or specify a configuration file: + +```yaml +mcp-servers-file: /path/to/mcp.json +``` + +These options are configurable in any of Aider's config file formats. + +## Environment Variables + +You can also configure MCP servers using environment variables in your `.env` file: + +``` +AIDER_MCP_SERVERS={"mcpServers":{"git":{"command":"uvx","args":["mcp-server-git"]}}} +``` + +Or specify a configuration file: + +``` +AIDER_MCP_SERVERS_FILE=/path/to/mcp.json +``` + +## Troubleshooting + +If you encounter issues with MCP servers: + +1. Use the `--verbose` flag to see detailed information about MCP server loading +2. Check that the specified executables are installed and available in your PATH +3. Verify that your JSON configuration is valid + +For more information about specific MCP servers and their capabilities, refer to their respective documentation. diff --git a/tests/basic/test_main.py b/tests/basic/test_main.py index 0836c75d7..3ca3e02c8 100644 --- a/tests/basic/test_main.py +++ b/tests/basic/test_main.py @@ -1345,3 +1345,57 @@ class TestMain(TestCase): ) for call in mock_io_instance.tool_warning.call_args_list: self.assertNotIn("Cost estimates may be inaccurate", call[0][0]) + + @patch("aider.coders.Coder.create") + def test_mcp_servers_parsing(self, mock_coder_create): + # Setup mock coder + mock_coder_instance = MagicMock() + mock_coder_create.return_value = mock_coder_instance + + # Test with --mcp-servers option + with GitTemporaryDirectory(): + main( + [ + "--mcp-servers", + '{"mcpServers":{"git":{"command":"uvx","args":["mcp-server-git"]}}}', + "--exit", + "--yes", + ], + input=DummyInput(), + output=DummyOutput(), + ) + + # Verify that Coder.create was called with mcp_servers parameter + mock_coder_create.assert_called_once() + _, kwargs = mock_coder_create.call_args + self.assertIn("mcp_servers", kwargs) + self.assertIsNotNone(kwargs["mcp_servers"]) + # At least one server should be in the list + self.assertTrue(len(kwargs["mcp_servers"]) > 0) + # First server should have a name attribute + self.assertTrue(hasattr(kwargs["mcp_servers"][0], "name")) + + # Test with --mcp-servers-file option + mock_coder_create.reset_mock() + + with GitTemporaryDirectory(): + # Create a temporary MCP servers file + mcp_file = Path("mcp_servers.json") + mcp_content = {"mcpServers": {"git": {"command": "uvx", "args": ["mcp-server-git"]}}} + mcp_file.write_text(json.dumps(mcp_content)) + + main( + ["--mcp-servers-file", str(mcp_file), "--exit", "--yes"], + input=DummyInput(), + output=DummyOutput(), + ) + + # Verify that Coder.create was called with mcp_servers parameter + mock_coder_create.assert_called_once() + _, kwargs = mock_coder_create.call_args + self.assertIn("mcp_servers", kwargs) + self.assertIsNotNone(kwargs["mcp_servers"]) + # At least one server should be in the list + self.assertTrue(len(kwargs["mcp_servers"]) > 0) + # First server should have a name attribute + self.assertTrue(hasattr(kwargs["mcp_servers"][0], "name")) From 7eaa92b70f02b049b291d6e0f134775a00f2731f Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Fri, 2 May 2025 23:09:53 -0700 Subject: [PATCH 05/14] Fix json encoding bug --- aider/coders/base_coder.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index bef6a5c2c..6d0279eee 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1479,7 +1479,8 @@ class Coder: tool_responses = self.execute_tool_calls(tool_call_response) # Add the assistant message with tool calls - self.cur_messages.append(tool_call_response.choices[0].message) + # Converting to a dict so it can be safely dumped to json + self.cur_messages.append(tool_call_response.choices[0].message.to_dict()) # Add all tool responses for tool_response in tool_responses: @@ -1571,13 +1572,16 @@ class Coder: for tool in server_tools: if tool.get("function", {}).get("name") == tool_call.function.name: self.io.tool_output( - f"Found MCP tool: {tool_call.function.name} from server" + f"Running MCP tool: {tool_call.function.name} from server" f" {server_name}" ) self.io.tool_output(f"Tool arguments: {tool_call.function.arguments}") - self.io.tool_output(f"Tool ID: {tool_call.id}") - self.io.tool_output(f"Tool type: {tool_call.type}") + if self.verbose: + self.io.tool_output(f"Tool ID: {tool_call.id}") + self.io.tool_output(f"Tool type: {tool_call.type}") + + self.io.tool_output("\n") # Find the corresponding server for server in self.mcp_servers: if server.name == server_name: From 6a9b72e5b30acbda476aa6410ee823d258109fd9 Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Sat, 3 May 2025 00:15:35 -0700 Subject: [PATCH 06/14] Set a tool_call_limit of 25 --- aider/coders/base_coder.py | 8 ++++- aider/gui.py | 1 - aider/website/docs/config/mcp.md | 52 ++++++++++++++------------------ 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 6d0279eee..f926e42e8 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -92,6 +92,8 @@ class Coder: last_keyboard_interrupt = None num_reflections = 0 max_reflections = 3 + num_tool_calls = 0 + max_tool_calls = 25 edit_format = None yield_stream = False temperature = None @@ -1475,7 +1477,8 @@ class Coder: tool_call_response = litellm.stream_chunk_builder(self.partial_response_tool_call) - if tool_call_response: + if tool_call_response and self.num_tool_calls < self.max_tool_calls: + self.num_tool_calls += 1 tool_responses = self.execute_tool_calls(tool_call_response) # Add the assistant message with tool calls @@ -1487,7 +1490,10 @@ class Coder: self.cur_messages.append(tool_response) return self.run(with_message="Continue", preproc=False) + elif self.num_tool_calls >= self.max_tool_calls: + self.io.tool_warning(f"Only {self.max_tool_calls} tool calls allowed, stopping.") + self.tool_call_limit = 0 try: if self.reply_completed(): return diff --git a/aider/gui.py b/aider/gui.py index 23e59520d..7fa90bc38 100755 --- a/aider/gui.py +++ b/aider/gui.py @@ -198,7 +198,6 @@ class GUI: ) for fname in fnames: - print(fname) if fname not in self.coder.get_inchat_relative_files(): self.coder.add_rel_fname(fname) self.info(f"Added {fname} to the chat") diff --git a/aider/website/docs/config/mcp.md b/aider/website/docs/config/mcp.md index 8916ddbab..2d49227bb 100644 --- a/aider/website/docs/config/mcp.md +++ b/aider/website/docs/config/mcp.md @@ -16,36 +16,10 @@ for more information. You have two ways of sharing your MCP server configuration with Aider. -### 1. Using a JSON string +{: .note } +Today, Aider only supports connecting to MCP servers using the stdio transport -You can specify MCP servers directly on the command line using the `--mcp-servers` option with a JSON string: - -```bash -aider --mcp-servers '{"mcpServers":{"git":{"command":"uvx","args":["mcp-server-git"]}}}' -``` - -### 2. Using a configuration file - -Alternatively, you can store your MCP server configurations in a JSON file and reference it with the `--mcp-servers-file` option: - -```bash -aider --mcp-servers-file mcp.json -``` - -Example `mcp.json` file: - -```json -{ - "mcpServers": { - "git": { - "command": "uvx", - "args": ["mcp-server-git"] - } - } -} -``` - -## YAML Configuration +### Config Files You can also configure MCP servers in your `.aider.conf.yml` file: @@ -69,7 +43,25 @@ mcp-servers-file: /path/to/mcp.json These options are configurable in any of Aider's config file formats. -## Environment Variables +### Flags + +You can specify MCP servers directly on the command line using the `--mcp-servers` option with a JSON string: + +#### Using a JSON String + +```bash +aider --mcp-servers '{"mcpServers":{"git":{"command":"uvx","args":["mcp-server-git"]}}}' +``` + +#### Using a configuration file + +Alternatively, you can store your MCP server configurations in a JSON file and reference it with the `--mcp-servers-file` option: + +```bash +aider --mcp-servers-file mcp.json +``` + +### Environment Variables You can also configure MCP servers using environment variables in your `.env` file: From 99bec73f2ec261752e1f5cb5715fc898279c153f Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Sat, 3 May 2025 00:55:01 -0700 Subject: [PATCH 07/14] Remove unused private function --- aider/coders/base_coder.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index f926e42e8..102886dee 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1550,18 +1550,6 @@ class Coder: self.reflected_message = test_errors return - async def _exec_server_tool(self, server, tool_call): - """Execute a tool call on an MCP server.""" - try: - session = await server.connect() # Use connect() directly - call_result = await experimental_mcp_client.call_openai_tool( - session=session, - openai_tool=tool_call, - ) - return (str(call_result.content[0].text),) - finally: - await server.disconnect() - def execute_tool_calls(self, tool_call_response): """Process tool calls from the response and execute them if they match MCP tools. Returns a list of tool response messages.""" From 10ea9baad6729288dcf08ad303adf2dbd7b7b49f Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Sat, 3 May 2025 01:02:11 -0700 Subject: [PATCH 08/14] Account for None type content messages in tools --- aider/coders/base_coder.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 102886dee..1c754ac7a 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -654,7 +654,10 @@ class Coder: def get_cur_message_text(self): text = "" for msg in self.cur_messages: - text += msg["content"] + "\n" + # For some models the content is None if the message + # contains tool calls. + content = msg["content"] or "" + text += content + "\n" return text def get_ident_mentions(self, text): From 282b34908035fc66964f7220b79e5b15688dd57c Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Sun, 4 May 2025 22:46:18 -0700 Subject: [PATCH 09/14] Allow MCP servers list to partially initialize --- aider/coders/base_coder.py | 27 ++++++---- aider/mcp/server.py | 2 +- tests/basic/test_coder.py | 100 ++++++++++++++++++++++++++++++++++++- 3 files changed, 117 insertions(+), 12 deletions(-) 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.""" From 097620026f3c1544e7dd2c9dd4a77a6ffe3e29a7 Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Mon, 5 May 2025 22:59:31 -0700 Subject: [PATCH 10/14] Respect Aider confirmation settings --- aider/coders/base_coder.py | 124 +++++++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 47 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 1d172bbce..028d29280 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1478,25 +1478,14 @@ class Coder: self.reflected_message = add_rel_files_message return + # Process any tools using MCP servers tool_call_response = litellm.stream_chunk_builder(self.partial_response_tool_call) - - if tool_call_response and self.num_tool_calls < self.max_tool_calls: + if self.process_tool_calls(tool_call_response): self.num_tool_calls += 1 - tool_responses = self.execute_tool_calls(tool_call_response) - - # Add the assistant message with tool calls - # Converting to a dict so it can be safely dumped to json - self.cur_messages.append(tool_call_response.choices[0].message.to_dict()) - - # Add all tool responses - for tool_response in tool_responses: - self.cur_messages.append(tool_response) - return self.run(with_message="Continue", preproc=False) - elif self.num_tool_calls >= self.max_tool_calls: - self.io.tool_warning(f"Only {self.max_tool_calls} tool calls allowed, stopping.") - self.tool_call_limit = 0 + self.num_tool_calls = 0 + try: if self.reply_completed(): return @@ -1553,39 +1542,80 @@ class Coder: self.reflected_message = test_errors return - def execute_tool_calls(self, tool_call_response): + def process_tool_calls(self, tool_call_response): + if tool_call_response is None: + return False + + tool_calls = tool_call_response.choices[0].message.tool_calls + # Collect all tool calls grouped by server + server_tool_calls = self._gather_server_tool_calls(tool_calls) + + if server_tool_calls and self.num_tool_calls < self.max_tool_calls: + self._print_tool_call_info(server_tool_calls) + + if self.io.confirm_ask("Run tools?"): + tool_responses = self._execute_tool_calls(server_tool_calls) + + # Add the assistant message with tool calls + # Converting to a dict so it can be safely dumped to json + self.cur_messages.append(tool_call_response.choices[0].message.to_dict()) + + # Add all tool responses + for tool_response in tool_responses: + self.cur_messages.append(tool_response) + + return True + elif self.num_tool_calls >= self.max_tool_calls: + self.io.tool_warning(f"Only {self.max_tool_calls} tool calls allowed, stopping.") + return False + + def _print_tool_call_info(self, server_tool_calls): + """Print information about an MCP tool call.""" + + for server, tool_calls in server_tool_calls.items(): + for tool_call in tool_calls: + self.io.tool_output( + f"Running MCP tool: {tool_call.function.name} from server {server.name}" + ) + self.io.tool_output(f"Tool arguments: {tool_call.function.arguments}") + + if self.verbose: + self.io.tool_output(f"Tool ID: {tool_call.id}") + self.io.tool_output(f"Tool type: {tool_call.type}") + + self.io.tool_output("\n") + + def _gather_server_tool_calls(self, tool_calls): + """Collect all tool calls grouped by server. + Args: + tool_calls: List of tool calls from the LLM response + + Returns: + dict: Dictionary mapping servers to their respective tool calls + """ + if not self.mcp_tools or len(self.mcp_tools) == 0: + return None + + server_tool_calls = {} + for tool_call in tool_calls: + # Check if this tool_call matches any MCP tool + for server_name, server_tools in self.mcp_tools: + for tool in server_tools: + if tool.get("function", {}).get("name") == tool_call.function.name: + # Find the McpServer instance that will be used for communication + for server in self.mcp_servers: + if server.name == server_name: + if server not in server_tool_calls: + server_tool_calls[server] = [] + server_tool_calls[server].append(tool_call) + break + + return server_tool_calls + + def _execute_tool_calls(self, tool_calls): """Process tool calls from the response and execute them if they match MCP tools. Returns a list of tool response messages.""" tool_responses = [] - tool_calls = tool_call_response.choices[0].message.tool_calls - - # First, collect all tool calls grouped by server - server_tool_calls = {} - - for tool_call in tool_calls: - # Check if this tool_call matches any MCP tool - if self.mcp_tools: - for server_name, server_tools in self.mcp_tools: - for tool in server_tools: - if tool.get("function", {}).get("name") == tool_call.function.name: - self.io.tool_output( - f"Running MCP tool: {tool_call.function.name} from server" - f" {server_name}" - ) - self.io.tool_output(f"Tool arguments: {tool_call.function.arguments}") - - if self.verbose: - self.io.tool_output(f"Tool ID: {tool_call.id}") - self.io.tool_output(f"Tool type: {tool_call.type}") - - self.io.tool_output("\n") - # Find the corresponding server - for server in self.mcp_servers: - if server.name == server_name: - if server not in server_tool_calls: - server_tool_calls[server] = [] - server_tool_calls[server].append(tool_call) - break # Define the coroutine to execute all tool calls for a single server async def _exec_server_tools(server, tool_calls_list): @@ -1610,14 +1640,14 @@ class Coder: # Execute all tool calls concurrently async def _execute_all_tool_calls(): tasks = [] - for server, tool_calls_list in server_tool_calls.items(): + for server, tool_calls_list in tool_calls.items(): tasks.append(_exec_server_tools(server, tool_calls_list)) # Wait for all tasks to complete results = await asyncio.gather(*tasks) return results # Run the async execution and collect results - if server_tool_calls: + if tool_calls: all_results = asyncio.run(_execute_all_tool_calls()) # Flatten the results from all servers for server_results in all_results: From 5fd049b44b801b0205775256c95b51e10a83144d Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Mon, 5 May 2025 23:06:59 -0700 Subject: [PATCH 11/14] Only print empty response warning log if there are tools --- aider/coders/base_coder.py | 10 +- tests/basic/test_coder.py | 259 ++++++++++++++++++++++++++----------- 2 files changed, 188 insertions(+), 81 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 028d29280..af34a3fc4 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1571,13 +1571,13 @@ class Coder: def _print_tool_call_info(self, server_tool_calls): """Print information about an MCP tool call.""" + self.io.tool_output("Preparing to run MCP tools", bold=True) for server, tool_calls in server_tool_calls.items(): for tool_call in tool_calls: - self.io.tool_output( - f"Running MCP tool: {tool_call.function.name} from server {server.name}" - ) - self.io.tool_output(f"Tool arguments: {tool_call.function.arguments}") + self.io.tool_output(f"Tool Call: {tool_call.function.name}") + self.io.tool_output(f"Arguments: {tool_call.function.arguments}") + self.io.tool_output(f"MCP Server: {server.name}") if self.verbose: self.io.tool_output(f"Tool ID: {tool_call.id}") @@ -2058,7 +2058,7 @@ class Coder: sys.stdout.flush() yield text - if not received_content: + if not received_content and len(self.partial_response_tool_call) == 0: self.io.tool_warning("Empty response received from LLM. Check your provider account?") def live_incremental_response(self, final): diff --git a/tests/basic/test_coder.py b/tests/basic/test_coder.py index e99c48fa0..67b9e04c3 100644 --- a/tests/basic/test_coder.py +++ b/tests/basic/test_coder.py @@ -1315,43 +1315,6 @@ This command will print 'Hello, World!' to the console.""" self.assertEqual(len(coder.mcp_tools), 1) self.assertEqual(coder.mcp_tools[0][0], "test_server") - # Test execute_tool_calls - tool_call = MagicMock() - tool_call.function.name = "test_tool" - tool_call.function.arguments = "{}" - tool_call.id = "test_id" - tool_call.type = "function" - - response = MagicMock() - response.choices = [MagicMock()] - response.choices[0].message.tool_calls = [tool_call] - - # Setup mock for call_openai_tool - mock_call_result = MagicMock() - mock_call_result.content = [MagicMock()] - mock_call_result.content[0].text = "Tool execution result" - mock_mcp_client.call_openai_tool.return_value = mock_call_result - - # Mock the async execution directly - with patch.object( - coder, - "execute_tool_calls", - return_value=[ - { - "role": "tool", - "tool_call_id": "test_id", - "content": "Tool execution result", - } - ], - ): - tool_responses = coder.execute_tool_calls(response) - - # Verify tool responses - self.assertEqual(len(tool_responses), 1) - self.assertEqual(tool_responses[0]["role"], "tool") - 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.""" @@ -1449,55 +1412,199 @@ This command will print 'Hello, World!' to the console.""" "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.""" + def test_process_tool_calls_none_response(self): + """Test that process_tool_calls handles None response correctly.""" with GitTemporaryDirectory(): io = InputOutput(yes=True) + coder = Coder.create(self.GPT35, "diff", io=io) - # Create mock MCP servers - mock_server1 = MagicMock() - mock_server1.name = "server1" - mock_server1.connect = MagicMock() - mock_server1.disconnect = MagicMock() + # Test with None response + result = coder.process_tool_calls(None) + self.assertFalse(result) - mock_server2 = MagicMock() - mock_server2.name = "server2" - mock_server2.connect = MagicMock() - mock_server2.disconnect = MagicMock() + def test_process_tool_calls_no_tool_calls(self): + """Test that process_tool_calls handles response with no tool calls.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = Coder.create(self.GPT35, "diff", io=io) - # Setup mock return values - server1_tools = [{"function": {"name": "tool1", "description": "Tool 1 description"}}] - server2_tools = [{"function": {"name": "tool2", "description": "Tool 2 description"}}] + # Create a response with no tool calls + response = MagicMock() + response.choices = [MagicMock()] + response.choices[0].message = MagicMock() + response.choices[0].message.tool_calls = [] - # Mock the initialize_mcp_tools method - expected_tools = [("server1", server1_tools), ("server2", server2_tools)] + result = coder.process_tool_calls(response) + self.assertFalse(result) - # Create coder with mock MCP servers and patch initialize_mcp_tools - with patch.object(Coder, "initialize_mcp_tools"): - coder = Coder.create( - self.GPT35, - "diff", - io=io, - mcp_servers=[mock_server1, mock_server2], - verbose=True, - ) + @patch("aider.coders.base_coder.experimental_mcp_client") + @patch("asyncio.run") + def test_process_tool_calls_with_tools(self, mock_asyncio_run, mock_mcp_client): + """Test that process_tool_calls processes tool calls correctly.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + io.confirm_ask = MagicMock(return_value=True) - # Manually set mcp_tools to expected value - coder.mcp_tools = expected_tools + # Create mock MCP server + mock_server = MagicMock() + mock_server.name = "test_server" - # Verify that mcp_tools contains the expected tools - self.assertEqual(len(coder.mcp_tools), 2) - self.assertEqual(coder.mcp_tools[0][0], "server1") - self.assertEqual(coder.mcp_tools[0][1], server1_tools) - self.assertEqual(coder.mcp_tools[1][0], "server2") - self.assertEqual(coder.mcp_tools[1][1], server2_tools) + # Create a tool call + tool_call = MagicMock() + tool_call.id = "test_id" + tool_call.type = "function" + tool_call.function = MagicMock() + tool_call.function.name = "test_tool" + tool_call.function.arguments = '{"param": "value"}' - # Test get_tool_list - tool_list = coder.get_tool_list() - self.assertEqual(len(tool_list), 2) - self.assertEqual(tool_list[0], server1_tools[0]) - self.assertEqual(tool_list[1], server2_tools[0]) + # Create a response with tool calls + response = MagicMock() + response.choices = [MagicMock()] + response.choices[0].message = MagicMock() + response.choices[0].message.tool_calls = [tool_call] + response.choices[0].message.to_dict = MagicMock( + return_value={"role": "assistant", "tool_calls": [{"id": "test_id"}]} + ) + + # Create coder with mock MCP tools and servers + coder = Coder.create(self.GPT35, "diff", io=io) + coder.mcp_tools = [("test_server", [{"function": {"name": "test_tool"}}])] + coder.mcp_servers = [mock_server] + + # Mock asyncio.run to return tool responses + tool_responses = [ + [{"role": "tool", "tool_call_id": "test_id", "content": "Tool execution result"}] + ] + mock_asyncio_run.return_value = tool_responses + + # Test process_tool_calls + result = coder.process_tool_calls(response) + self.assertTrue(result) + + # Verify that asyncio.run was called + mock_asyncio_run.assert_called_once() + + # Verify that the messages were added + self.assertEqual(len(coder.cur_messages), 2) + self.assertEqual(coder.cur_messages[0]["role"], "assistant") + self.assertEqual(coder.cur_messages[1]["role"], "tool") + self.assertEqual(coder.cur_messages[1]["tool_call_id"], "test_id") + self.assertEqual(coder.cur_messages[1]["content"], "Tool execution result") + + def test_process_tool_calls_max_calls_exceeded(self): + """Test that process_tool_calls handles max tool calls exceeded.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + io.tool_warning = MagicMock() + + # Create a tool call + tool_call = MagicMock() + tool_call.id = "test_id" + tool_call.type = "function" + tool_call.function = MagicMock() + tool_call.function.name = "test_tool" + + # Create a response with tool calls + response = MagicMock() + response.choices = [MagicMock()] + response.choices[0].message = MagicMock() + response.choices[0].message.tool_calls = [tool_call] + + # Create mock MCP server + mock_server = MagicMock() + mock_server.name = "test_server" + + # Create coder with max tool calls exceeded + coder = Coder.create(self.GPT35, "diff", io=io) + coder.num_tool_calls = coder.max_tool_calls + coder.mcp_tools = [("test_server", [{"function": {"name": "test_tool"}}])] + coder.mcp_servers = [mock_server] + + # Test process_tool_calls + result = coder.process_tool_calls(response) + self.assertFalse(result) + + # Verify that warning was shown + io.tool_warning.assert_called_once_with( + f"Only {coder.max_tool_calls} tool calls allowed, stopping." + ) + + def test_process_tool_calls_user_rejects(self): + """Test that process_tool_calls handles user rejection.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + io.confirm_ask = MagicMock(return_value=False) + + # Create a tool call + tool_call = MagicMock() + tool_call.id = "test_id" + tool_call.type = "function" + tool_call.function = MagicMock() + tool_call.function.name = "test_tool" + + # Create a response with tool calls + response = MagicMock() + response.choices = [MagicMock()] + response.choices[0].message = MagicMock() + response.choices[0].message.tool_calls = [tool_call] + + # Create mock MCP server + mock_server = MagicMock() + mock_server.name = "test_server" + + # Create coder with mock MCP tools + coder = Coder.create(self.GPT35, "diff", io=io) + coder.mcp_tools = [("test_server", [{"function": {"name": "test_tool"}}])] + coder.mcp_servers = [mock_server] + + # Test process_tool_calls + result = coder.process_tool_calls(response) + self.assertFalse(result) + + # Verify that confirm_ask was called + io.confirm_ask.assert_called_once_with("Run tools?") + + # Verify that no messages were added + self.assertEqual(len(coder.cur_messages), 0) + + @patch("asyncio.run") + def test_execute_tool_calls(self, mock_asyncio_run): + """Test that _execute_tool_calls executes tool calls correctly.""" + with GitTemporaryDirectory(): + io = InputOutput(yes=True) + coder = Coder.create(self.GPT35, "diff", io=io) + + # Create mock server and tool call + mock_server = MagicMock() + mock_server.name = "test_server" + + tool_call = MagicMock() + tool_call.id = "test_id" + tool_call.type = "function" + tool_call.function = MagicMock() + tool_call.function.name = "test_tool" + tool_call.function.arguments = '{"param": "value"}' + + # Create server_tool_calls + server_tool_calls = {mock_server: [tool_call]} + + # Mock asyncio.run to return tool responses + tool_responses = [ + [{"role": "tool", "tool_call_id": "test_id", "content": "Tool execution result"}] + ] + mock_asyncio_run.return_value = tool_responses + + # Test _execute_tool_calls directly + result = coder._execute_tool_calls(server_tool_calls) + + # Verify that asyncio.run was called + mock_asyncio_run.assert_called_once() + + # Verify that the correct tool responses were returned + self.assertEqual(len(result), 1) + self.assertEqual(result[0]["role"], "tool") + self.assertEqual(result[0]["tool_call_id"], "test_id") + self.assertEqual(result[0]["content"], "Tool execution result") if __name__ == "__main__": From de35fb6a2c076e851269b28e588f9d8288396cfc Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Tue, 6 May 2025 11:40:28 -0700 Subject: [PATCH 12/14] Add MCP tool prompt support to system prompts - Add tool_prompt to CoderPrompts class - Modify fmt_system_prompt to include tool prompt when MCP tools are available - This enables better handling of tool-based interactions when using MCP servers --- aider/coders/base_coder.py | 3 +++ aider/coders/base_prompts.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 24f320bcd..ac1fc3485 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1155,6 +1155,9 @@ class Coder: def fmt_system_prompt(self, prompt): final_reminders = [] + if self.mcp_tools and len(self.mcp_tools) > 0: + final_reminders.append(self.gpt_prompts.tool_prompt) + if self.main_model.lazy: final_reminders.append(self.gpt_prompts.lazy_prompt) if self.main_model.overeager: diff --git a/aider/coders/base_prompts.py b/aider/coders/base_prompts.py index 36f991f1e..b5b252a0b 100644 --- a/aider/coders/base_prompts.py +++ b/aider/coders/base_prompts.py @@ -56,5 +56,16 @@ Do not edit these files! no_shell_cmd_prompt = "" no_shell_cmd_reminder = "" + tool_prompt = """Answer the user's request using the relevant tool(s), if they are available. +Check that all the required parameters for each tool call are provided or can reasonably be +inferred from context. IF there are no relevant tools or there are missing values for required +parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the +user provides a specific value for a parameter (for example provided in quotes), make sure to +use that value EXACTLY. +DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in +the request as they may indicate required parameter values that should be included even if not +explicitly quoted. +""" + rename_with_shell = "" go_ahead_tip = "" From 0fe93b797906b3129b88db222838be3145b12a8b Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Tue, 6 May 2025 12:10:56 -0700 Subject: [PATCH 13/14] Fix typo in tool_prompt: add missing space before 'available' --- aider/coders/base_coder.py | 2 +- aider/coders/base_prompts.py | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index ac1fc3485..b0ba1165d 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1549,7 +1549,7 @@ class Coder: tool_call_response = litellm.stream_chunk_builder(self.partial_response_tool_call) if self.process_tool_calls(tool_call_response): self.num_tool_calls += 1 - return self.run(with_message="Continue", preproc=False) + return self.run(with_message="Continue with tool call response", preproc=False) self.num_tool_calls = 0 diff --git a/aider/coders/base_prompts.py b/aider/coders/base_prompts.py index b5b252a0b..e7fb79d3f 100644 --- a/aider/coders/base_prompts.py +++ b/aider/coders/base_prompts.py @@ -56,15 +56,13 @@ Do not edit these files! no_shell_cmd_prompt = "" no_shell_cmd_reminder = "" - tool_prompt = """Answer the user's request using the relevant tool(s), if they are available. -Check that all the required parameters for each tool call are provided or can reasonably be -inferred from context. IF there are no relevant tools or there are missing values for required -parameters, ask the user to supply these values; otherwise proceed with the tool calls. If the -user provides a specific value for a parameter (for example provided in quotes), make sure to -use that value EXACTLY. -DO NOT make up values for or ask about optional parameters. Carefully analyze descriptive terms in -the request as they may indicate required parameter values that should be included even if not -explicitly quoted. + tool_prompt = """ALWAYS Answer the user's request using the relevant tool(s), if they are +available. +Do your best to infer required parameters from your context. If the user provides a +specific value for a parameter (for example provided in quotes), make sure to use that +value EXACTLY. +Carefully analyze descriptive terms in the request as they may indicate required parameter values +that should be included even if not explicitly quoted. """ rename_with_shell = "" From 943fabe4d690d0ce947913709476705dc563f4b5 Mon Sep 17 00:00:00 2001 From: Quinlan Jager Date: Thu, 8 May 2025 10:21:42 -0700 Subject: [PATCH 14/14] Update tool prompting to be more direct --- aider/coders/base_coder.py | 5 +++-- aider/coders/base_prompts.py | 18 +++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py index 8e4d900dc..8c7f16490 100755 --- a/aider/coders/base_coder.py +++ b/aider/coders/base_coder.py @@ -1161,8 +1161,6 @@ class Coder: def fmt_system_prompt(self, prompt): final_reminders = [] - if self.mcp_tools and len(self.mcp_tools) > 0: - final_reminders.append(self.gpt_prompts.tool_prompt) if self.main_model.lazy: final_reminders.append(self.gpt_prompts.lazy_prompt) @@ -1198,6 +1196,9 @@ class Coder: else: quad_backtick_reminder = "" + if self.mcp_tools and len(self.mcp_tools) > 0: + final_reminders.append(self.gpt_prompts.tool_prompt) + final_reminders = "\n\n".join(final_reminders) prompt = prompt.format( diff --git a/aider/coders/base_prompts.py b/aider/coders/base_prompts.py index e7fb79d3f..cd93b9ef8 100644 --- a/aider/coders/base_prompts.py +++ b/aider/coders/base_prompts.py @@ -56,13 +56,17 @@ Do not edit these files! no_shell_cmd_prompt = "" no_shell_cmd_reminder = "" - tool_prompt = """ALWAYS Answer the user's request using the relevant tool(s), if they are -available. -Do your best to infer required parameters from your context. If the user provides a -specific value for a parameter (for example provided in quotes), make sure to use that -value EXACTLY. -Carefully analyze descriptive terms in the request as they may indicate required parameter values -that should be included even if not explicitly quoted. + tool_prompt = """ + +When solving problems, you have special tools available. Please follow these rules: + +1. Always use the exact format required for each tool and include all needed information. +2. Only use tools that are currently available in this conversation. +3. Don't mention tool names when talking to people. Say "I'll check your code" instead + of "I'll use the code_analyzer tool." +4. Only use tools when necessary. If you know the answer, just respond directly. +5. Before using any tool, briefly explain why you need to use it. + """ rename_with_shell = ""