Skip to content

Add MCP support #3672

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions aider/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,62 @@ def get_parser(default_config_files, git_root):
default=False,
)

#########
group = parser.add_argument_group("Model Context Protocol (MCP)")
group.add_argument(
"--mcp",
action=argparse.BooleanOptionalAction,
default=False,
help="Enable/disable Model Context Protocol (MCP) support (default: False)",
)
group.add_argument(
"--mcp-servers",
action="append",
metavar="SERVER_NAME",
help="Specify MCP server names to enable (can be used multiple times)",
default=[],
)
group.add_argument(
"--mcp-server-command",
action="append",
metavar="SERVER_NAME:COMMAND",
help="Specify the command to execute for an MCP server (can be used multiple times)",
default=[],
)

group.add_argument(
"--mcp-server-env",
action="append",
metavar="SERVER_NAME:ENV_VAR=VALUE",
help="Specify environment variables for an MCP server (can be used multiple times)",
default=[],
)
group.add_argument(
"--mcp-tool-permission",
action="append",
metavar="SERVER_NAME:TOOL_NAME=PERMISSION",
help="Specify permission ('manual' or 'auto') for an MCP tool (can be used multiple times)",
default=[],
)
group.add_argument(
"--list-mcp-servers",
action="store_true",
help="List all configured MCP servers and exit",
default=False,
)
group.add_argument(
"--list-mcp-tools",
action="store_true",
help="List all available MCP tools and exit",
default=False,
)
group.add_argument(
"--mcp-wait-for-stdio",
action=argparse.BooleanOptionalAction,
default=False,
help="Wait for STDIO MCP clients to complete tool discovery (default: False)",
)

#########
group = parser.add_argument_group("Upgrading")
group.add_argument(
Expand Down
28 changes: 27 additions & 1 deletion aider/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
from datetime import datetime
from json.decoder import JSONDecodeError
from pathlib import Path
from typing import List
from typing import List, Optional, Tuple

from aider import __version__, models, prompts, urls, utils
from aider.mcp import is_mcp_enabled, get_available_tools_prompt, process_llm_tool_requests, stop_mcp_servers
from aider.analytics import Analytics
from aider.commands import Commands
from aider.exceptions import LiteLLMExceptions
Expand Down Expand Up @@ -957,6 +958,7 @@ def keyboard_interrupt(self):
if self.last_keyboard_interrupt and now - self.last_keyboard_interrupt < thresh:
self.io.tool_warning("\n\n^C KeyboardInterrupt")
self.event("exit", reason="Control-C")
stop_mcp_servers()
sys.exit()

self.io.tool_warning("\n\n^C again to exit")
Expand Down Expand Up @@ -1106,6 +1108,11 @@ def fmt_system_prompt(self, prompt):
)
else:
quad_backtick_reminder = ""

# Add MCP tools information if MCP is enabled

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 2 sections could be combined to one, right?

prompt = prompt.format(
    fence=self.fence,
    quad_backtick_reminder=quad_backtick_reminder,
    lazy_prompt=lazy_prompt,
    platform=platform_text,
    shell_cmd_prompt=shell_cmd_prompt,
    shell_cmd_reminder=shell_cmd_reminder,
    language=language,
)

if self.main_model.system_prompt_prefix:
    prompt = self.main_model.system_prompt_prefix + prompt

# Add MCP tools information if MCP is enabled
if is_mcp_enabled():
    mcp_tools_info = self.gpt_prompts.mcp_tools_prefix + "\n\n" + get_available_tools_prompt()
    prompt += "\n\n" + mcp_tools_info

mcp_tools_info = ""
if is_mcp_enabled():
mcp_tools_info = self.gpt_prompts.mcp_tools_prefix + "\n\n" + get_available_tools_prompt()

prompt = prompt.format(
fence=self.fence,
Expand All @@ -1119,6 +1126,10 @@ def fmt_system_prompt(self, prompt):

if self.main_model.system_prompt_prefix:
prompt = self.main_model.system_prompt_prefix + prompt

# Append MCP tools information to the end of the prompt
if mcp_tools_info:
prompt += "\n\n" + mcp_tools_info

return prompt

Expand Down Expand Up @@ -1455,6 +1466,14 @@ def send_message(self, inp):
self.reflected_message = add_rel_files_message
return

tool_results = self.check_for_tool_calls(content)
for tool_result in tool_results:
if self.reflected_message:
Copy link

@lutzleonhardt lutzleonhardt Mar 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As told already by others: max_reflections is the limit for consecutive tool calls.

        while message:
            self.reflected_message = None
            list(self.send_message(message))

            if not self.reflected_message:
                break

            if self.num_reflections >= self.max_reflections:
                self.io.tool_warning(f"Only {self.max_reflections} reflections allowed, stopping.")
                return

            self.num_reflections += 1
            message = self.reflected_message

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What should be the correct way to implement that ? Since it behave almost exactly like a diff edit/a file inclusion,
should I implement a reflection limit / reflected_message only for the mcp tools ?

Copy link

@wladimiiir wladimiiir Mar 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say you could have another field self.mcp_tool_result_message = tool_result instead of using self.reflected_message. And then:

while message:
    self.reflected_message = None
    list(self.send_message(message))

    if not self.reflected_message and not self.mcp_tool_result_message:
        break

    if self.num_reflections >= self.max_reflections:
        self.io.tool_warning(f"Only {self.max_reflections} reflections allowed, stopping.")
        return
    if self.num_mcp_iterations >= self.max_mcp_iterations:
        self.io.tool_warning(f"Only {self.max_mcp_iterations} MCP iterations allowed, stopping.")
        return

    if self.reflected_message:
        self.num_reflections += 1
        message = self.reflected_message
    if self.mcp_tool_result_message:
        self.num_mcp_iterations += 1
        message = self.mcp_tool_result_message

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, it seem to work, maybe I'll all the max_mcp_iterations in the configuration too

self.reflected_message += "\n\n" + tool_result
else:
self.reflected_message = tool_result
return

try:
if self.reply_completed():
return
Expand Down Expand Up @@ -1647,6 +1666,13 @@ def get_file_mentions(self, content, ignore_current=False):

return mentioned_rel_fnames

def check_for_tool_calls(self, content):
"""Process the LLM's response after it's completed."""
if is_mcp_enabled():
return process_llm_tool_requests(content, self.io)
else:
return []

def check_for_file_mentions(self, content):
mentioned_rel_fnames = self.get_file_mentions(content)

Expand Down
20 changes: 20 additions & 0 deletions aider/coders/base_prompts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
class CoderPrompts:
system_reminder = ""

mcp_tools_prefix = """# MCP Tools

You can use Model Context Protocol (MCP) tools to interact with external systems. To use a tool, include a tool execution request in your response using the following format:

```
<use_mcp_tool>
<server_name>server name here</server_name>
<tool_name>tool name here</tool_name>
<arguments>
{
"param1": "value1",
"param2": "value2"
}
</arguments>
</use_mcp_tool>
```

The available tools will be listed below if any are connected.
"""

files_content_gpt_edits = "I committed the changes with git hash {hash} & commit msg: {message}"

Expand Down
75 changes: 75 additions & 0 deletions aider/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import re
import subprocess
import json
import sys
import tempfile
from collections import OrderedDict
Expand All @@ -13,6 +14,7 @@
from prompt_toolkit.completion import Completion, PathCompleter
from prompt_toolkit.document import Document

from aider.mcp import list_mcp_servers, list_mcp_tools, execute_mcp_tool, stop_mcp_servers
from aider import models, prompts, voice
from aider.editor import pipe_editor
from aider.format_settings import format_settings
Expand Down Expand Up @@ -1009,6 +1011,7 @@ def cmd_run(self, args, add_on_nonzero_exit=False):

def cmd_exit(self, args):
"Exit the application"
stop_mcp_servers()
self.coder.event("exit", reason="/exit")
sys.exit()

Expand Down Expand Up @@ -1542,6 +1545,78 @@ def cmd_reasoning_effort(self, args):
announcements = "\n".join(self.coder.get_announcements())
self.io.tool_output(announcements)

def cmd_mcp_servers(self, args):
"List all configured MCP servers"
servers = list_mcp_servers()
if servers:
self.io.tool_output("Configured MCP servers:")
for server in servers:
status = "Enabled" if server.enabled else "Disabled"
self.io.tool_output(f" - {server.name} ({status})")
if server.command:
self.io.tool_output(f" Command: {server.command}")
if server.env_vars:
self.io.tool_output(" Environment variables:")
for var, value in server.env_vars.items():
masked_value = "*" * len(value) if value else "(empty)"
self.io.tool_output(f" {var}={masked_value}")
else:
self.io.tool_output("No MCP servers configured.")

def cmd_mcp_tools(self, args):
"List all available MCP tools, optionally filtered by server name"
server_name = args.strip()
tools_by_server = list_mcp_tools()

if not tools_by_server:
self.io.tool_output("No MCP tools available.")
return

if server_name:
if server_name in tools_by_server:
self.io.tool_output(f"Tools for MCP server '{server_name}':")
for tool in tools_by_server[server_name]:
self.io.tool_output(f" - {tool.name} (Permission: {tool.permission})")
if tool.description:
self.io.tool_output(f" Description: {tool.description}")
else:
self.io.tool_output(f"No MCP server found with name '{server_name}'.")
else:
self.io.tool_output("Available MCP tools:")
for server_name, tools in tools_by_server.items():
self.io.tool_output(f" Server: {server_name}")
for tool in tools:
self.io.tool_output(f" - {tool.name} (Permission: {tool.permission})")
if tool.description:
self.io.tool_output(f" Description: {tool.description}")

def cmd_mcp_execute(self, args):
"Execute an MCP tool with the given arguments"
# Parse the arguments
parts = args.strip().split(maxsplit=2)
if len(parts) < 2:
self.io.tool_error("Usage: /mcp-execute <server_name> <tool_name> [arguments_json]")
return

server_name, tool_name = parts[0], parts[1]
arguments = {}

if len(parts) > 2:
try:
arguments = json.loads(parts[2])
except json.JSONDecodeError:
self.io.tool_error(f"Invalid JSON arguments: {parts[2]}")
return

# Execute the tool
self.io.tool_output(f"Executing MCP tool '{tool_name}' from server '{server_name}'...")
try:
result = execute_mcp_tool(server_name, tool_name, arguments, self.io)
self.io.tool_output("Result:")
self.io.tool_output(result)
except Exception as e:
self.io.tool_error(f"Error executing MCP tool: {str(e)}")

def cmd_copy_context(self, args=None):
"""Copy the current chat context as markdown, suitable to paste into a web UI"""

Expand Down
52 changes: 51 additions & 1 deletion aider/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from dataclasses import fields
from pathlib import Path

from aider import mcp

try:
import git
except ImportError:
Expand Down Expand Up @@ -714,6 +716,53 @@ def get_io(pretty):
if args.check_update:
check_version(io, verbose=args.verbose)

if args.list_models:
models.print_matching_models(io, args.list_models)
analytics.event("exit", reason="Listed models")
return 0

# Configure MCP manager
mcp.configure_mcp(args)

# Initialize MCP servers and discover tools
if mcp.is_mcp_enabled():
mcp.initialize_mcp_servers(io)

# Handle MCP-related commands
if args.list_mcp_servers:
servers = mcp.list_mcp_servers()
if servers:
io.tool_output("Configured MCP servers:")
for server in servers:
status = "Enabled" if server.enabled else "Disabled"
io.tool_output(f" - {server.name} ({status})")
if server.command:
io.tool_output(f" Command: {server.command}")
if server.env_vars:
io.tool_output(" Environment variables:")
for var, value in server.env_vars.items():
masked_value = "*" * len(value) if value else "(empty)"
io.tool_output(f" {var}={masked_value}")
else:
io.tool_output("No MCP servers configured.")
analytics.event("exit", reason="Listed MCP servers")
return 0

if args.list_mcp_tools:
tools_by_server = mcp.list_mcp_tools()
if tools_by_server:
io.tool_output("Available MCP tools:")
for server_name, tools in tools_by_server.items():
io.tool_output(f" Server: {server_name}")
for tool in tools:
io.tool_output(f" - {tool.name} (Permission: {tool.permission})")
if tool.description:
io.tool_output(f" Description: {tool.description}")
else:
io.tool_output("No MCP tools available.")
analytics.event("exit", reason="Listed MCP tools")
return 0

if args.git:
git_root = setup_git(git_root, io)
if args.gitignore:
Expand Down Expand Up @@ -852,6 +901,7 @@ def get_io(pretty):
io.tool_output()
except KeyboardInterrupt:
analytics.event("exit", reason="Keyboard interrupt during model warnings")
mcp.stop_mcp_servers()
return 1

repo = None
Expand Down Expand Up @@ -1215,4 +1265,4 @@ def load_slow_imports(swallow=True):

if __name__ == "__main__":
status = main()
sys.exit(status)
sys.exit(status)
Loading