Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
3194a35
feat: Add Gemini 2.5 Flash model and leaderboard data
paul-gauthier May 25, 2025
214b811
chore: Add new polyglot benchmark results
paul-gauthier May 26, 2025
acebc11
chore: Update model names in polyglot leaderboard
paul-gauthier May 26, 2025
9c9eedd
chore: Update polyglot leaderboard data for gemini-2.5-flash
paul-gauthier May 26, 2025
b79a777
copy
paul-gauthier May 26, 2025
ef2986a
Merge branch 'main' of github.com:Aider-AI/aider
paul-gauthier May 26, 2025
8304029
lint
paul-gauthier May 26, 2025
8849abb
Merge branch 'main' into mcp
May 27, 2025
a0282dc
Fix merge conflit from mcp PR
May 27, 2025
ece40be
feat: Add MCP profile data structure and manager
May 27, 2025
6981e0c
chore: Instantiate and pass MCP profile manager to Coder/Commands
May 27, 2025
acb4580
feat: Add /mcp command to list profiles and update completion
May 27, 2025
314a74f
Moved McpProfileManager to mcp module
May 27, 2025
6fd6502
refactor: Use load_mcp_servers in profile manager
May 27, 2025
f4e21b1
feat: Add logic to enable/disable MCP profiles and pool
May 27, 2025
a8ba33e
feat: Create MCPClientPool for managing servers
May 27, 2025
4ee5085
refactor: Move MCP tool init from coder to profile manager
May 27, 2025
3141845
fix: Import missing modules and fix tool call typo
May 27, 2025
d6b3c88
fix: Remove mcp_servers from kwargs in Coder.create
May 27, 2025
5b0bcb4
fix: Use load_mcp_servers to get configs for profile activation
May 27, 2025
59da4ff
feat: Reuse MCP profile manager on coder switch
May 28, 2025
675698e
Cleanup
May 28, 2025
a2a5beb
refactor: Remove unused mcp_tools and use profile manager
May 28, 2025
7a57a16
Use McpProfileManager to provide tool prompt
May 28, 2025
965be23
Update tool prompt
May 28, 2025
67d609b
refactor: Print MCP tool calls on single line
May 28, 2025
e5c431c
feat: Handle Text objects in tool_output method
May 28, 2025
e768649
refactor: Refactor MCP profiles to use 'servers' list with 'no_confir…
May 28, 2025
e8979e5
refactor: Use per-server tool confirmation based on profile
May 28, 2025
c57b791
fix: Preserve no_confirm in 'all' MCP profile on refresh
May 28, 2025
4b6f3c9
feat: add /mcp new command to create profiles
May 28, 2025
857c8a0
feat: Add two-step dialog for /mcp new command
May 28, 2025
0ed6545
fix: Import prompt_toolkit components for mcp command dialogs
May 28, 2025
0bc906b
refactor: Reuse checkboxlist_dialog for /mcp new step 2
May 28, 2025
779b43e
feat: Add /mcp rm command to delete profiles
May 28, 2025
72bfd95
feat: Persist active MCP profile name in config
May 29, 2025
4b61383
fix: Fix indentation in profile loading
May 29, 2025
d34cc55
fix: Adjust indentation for profile loading error handling
May 29, 2025
97a1673
feat: Add /mcp persist command
May 29, 2025
0b51805
fix: Remove duplicated lines in MCPProfileManager.__init__
May 29, 2025
e8d33ea
refactor: Rename YAML_ACTIVE_PROFILE_KEY to YAML_DEFAULT_PROFILE_KEY
May 29, 2025
7946b46
feat: Enable default MCP profile on startup
May 29, 2025
33be77f
feat: add /mcp persist clear command
May 29, 2025
12dee18
feat: Add support for enabling specific tools per server in profiles
May 29, 2025
0275fc6
feat: Add /mcp tools command to configure server tools
May 29, 2025
7be7049
feat: Add command to configure MCP server tools via dialog
May 29, 2025
3de2f0d
fix: Find MCP client by iterating pool clients
May 29, 2025
e0b4495
fix: Only persist default profile via /mcp persist command
May 29, 2025
ca4bb57
fix: Check mcp_servers and clients attributes for MCP pool
May 29, 2025
62fe67e
fix: Access server tools from pool cache, not server instance
May 29, 2025
10bffa9
docs: Add comment explaining tool selection logic
May 29, 2025
14b2526
fix: Preserve enabled tools config for 'all' profile
May 29, 2025
d849d78
feat: Filter MCP tools based on profile enabled settings
May 29, 2025
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ cog.out(text)
<a href="https://github.com/Aider-AI/aider/stargazers"><img alt="GitHub Stars" title="Total number of GitHub stars the Aider project has received"
src="https://img.shields.io/github/stars/Aider-AI/aider?style=flat-square&logo=github&color=f1c40f&labelColor=555555"/></a>
<a href="https://pypi.org/project/aider-chat/"><img alt="PyPI Downloads" title="Total number of installations via pip from PyPI"
src="https://img.shields.io/badge/📦%20Installs-2.3M-2ecc71?style=flat-square&labelColor=555555"/></a>
src="https://img.shields.io/badge/📦%20Installs-2.4M-2ecc71?style=flat-square&labelColor=555555"/></a>
<img alt="Tokens per week" title="Number of tokens processed weekly by Aider users"
src="https://img.shields.io/badge/📈%20Tokens%2Fweek-15B-3498db?style=flat-square&labelColor=555555"/>
<a href="https://openrouter.ai/#options-menu"><img alt="OpenRouter Ranking" title="Aider's ranking among applications on the OpenRouter platform"
Expand Down
269 changes: 145 additions & 124 deletions aider/coders/base_coder.py

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions aider/coders/base_prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,16 @@ class CoderPrompts:
no_shell_cmd_reminder = ""

tool_prompt = """
<tool_calling>
When solving problems, you have special tools available. Please follow these rules:
You have MCP (Model Context Protocol) tools available.
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.
3. Don't mention tool names unless explicitly requested. For example, if a code_analyzer tool is available, say "I'll check your code" before making the tool call instead of "I'll use the code_analyzer tool." unless explicitly requested.
4. Only use tools when necessary. If you know the answer, just respond directly unless explicitly requested.
5. Before using any tool, briefly explain why you need to use it.
</tool_calling>
"""

rename_with_shell = ""
go_ahead_tip = ""

132 changes: 132 additions & 0 deletions aider/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from PIL import Image, ImageGrab
from prompt_toolkit.completion import Completion, PathCompleter
from prompt_toolkit.document import Document
from prompt_toolkit.shortcuts import checkboxlist_dialog

from aider import models, prompts, voice
from aider.editor import pipe_editor
Expand All @@ -36,6 +37,7 @@ def __init__(self, placeholder=None, **kwargs):
class Commands:
voice = None
scraper = None
mcp_profile_manager = None

def clone(self):
return Commands(
Expand All @@ -48,6 +50,7 @@ def clone(self):
verbose=self.verbose,
editor=self.editor,
original_read_only_fnames=self.original_read_only_fnames,
mcp_profile_manager=self.mcp_profile_manager,
)

def __init__(
Expand All @@ -63,10 +66,12 @@ def __init__(
verbose=False,
editor=None,
original_read_only_fnames=None,
mcp_profile_manager=None,
):
self.io = io
self.coder = coder
self.parser = parser
self.mcp_profile_manager = mcp_profile_manager
self.args = args
self.verbose = verbose

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

def cmd_mcp(self, args_str: str):
"Manage MCP (Model-Controller-Proxy) profiles"
args = args_str.strip().split()

if not self.mcp_profile_manager:
self.io.tool_error("MCP profile manager is not initialized.")
return

if not args:
profiles_details = self.mcp_profile_manager.list_profiles_details()
self.io.tool_output("Available MCP profiles:")
if profiles_details:
for name, server_configs in profiles_details:
server_names_list = [s_conf.get("name", "UnknownServer") for s_conf in server_configs]
self.io.tool_output(
f" - {name}: {', '.join(server_names_list) if server_names_list else ' (no servers)'}"
)
else:
self.io.tool_output(" (No profiles defined)")

active_name = self.mcp_profile_manager.active_profile_name
if active_name:
self.io.tool_output(f"\nCurrently active MCP profile: {active_name}")
else:
self.io.tool_output("\nNo MCP profile is currently active.")

self.io.tool_output("\nCommands:")
self.io.tool_output(" /mcp new <profile_name> - Create a new MCP profile")
self.io.tool_output(" /mcp tools <server_name> - Configure enabled tools for a server in the active profile")
self.io.tool_output(" /mcp enable <profile_name> - Enable an MCP profile")
self.io.tool_output(" /mcp disable - Disable the active MCP profile")
self.io.tool_output(" /mcp rm <profile_name> - Delete an MCP profile")
self.io.tool_output(" /mcp persist - Save the active MCP profile as the default for next launch")
self.io.tool_output(" /mcp persist clear - Clear the persisted default MCP profile")
return

sub_command = args[0]
if sub_command == "new":
if len(args) > 1:
profile_name = args[1]
if self.mcp_profile_manager.get_profile(profile_name):
self.io.tool_error(f"Profile '{profile_name}' already exists.")
return

known_server_names = self.mcp_profile_manager.get_known_server_names()
if not known_server_names:
self.io.tool_error("No MCP servers found. Configure servers first (e.g., in .aider.conf.yml).")
return

# First dialog: Select servers
server_selection_choices = [(name, name) for name in known_server_names]
selected_server_names = checkboxlist_dialog(
title=f"Create MCP Profile: {profile_name} (Step 1/2)",
text="Select servers to include in this profile:",
values=server_selection_choices,
).run()

if not selected_server_names:
self.io.tool_output("Profile creation cancelled or no servers selected.")
return

# Second dialog: Configure no_confirm for selected servers
no_confirm_choices = [(name, name) for name in selected_server_names]

# checkboxlist_dialog returns None if Cancel is chosen
servers_for_no_confirm = checkboxlist_dialog(
title=f"Create MCP Profile: {profile_name} (Step 2/2)",
text="Select servers that should NOT require confirmation for tool use:",
values=no_confirm_choices,
).run()

# If the second dialog was cancelled, treat it as cancelling the whole operation
if servers_for_no_confirm is None:
self.io.tool_output("Profile creation cancelled during no_confirm configuration.")
return

final_selected_servers_config = []
for server_name in selected_server_names:
final_selected_servers_config.append(
{
"name": server_name,
"no_confirm": server_name in servers_for_no_confirm,
}
)

self.mcp_profile_manager.create_new_profile(profile_name, final_selected_servers_config)
# self.io.tool_output(f"MCP profile '{profile_name}' created with {len(final_selected_servers_config)} server(s).") # Redundant with manager's output

else:
self.io.tool_error("Usage: /mcp new <profile_name>")

elif sub_command == "tools":
if len(args) > 1:
server_name = args[1]
self.mcp_profile_manager.configure_server_tools(server_name)
else:
self.io.tool_error("Usage: /mcp tools <server_name>")

elif sub_command == "enable":
if len(args) > 1:
profile_name = args[1]
self.mcp_profile_manager.enable_profile(profile_name, self.coder.main_model, self.coder.main_model.edit_format)
else:
self.io.tool_error("Usage: /mcp enable <profile_name>")
elif sub_command == "disable":
if len(args) == 1:
self.mcp_profile_manager.disable_profile()
else:
self.io.tool_error("Usage: /mcp disable")
elif sub_command == "rm":
if len(args) > 1:
profile_name = args[1]
self.mcp_profile_manager.delete_profile(profile_name)
else:
self.io.tool_error("Usage: /mcp rm <profile_name>")
elif sub_command == "persist":
if len(args) == 1: # /mcp persist
self.mcp_profile_manager.persist_active_profile()
elif len(args) == 2 and args[1] == "clear": # /mcp persist clear
self.mcp_profile_manager.clear_persisted_default_profile()
else:
self.io.tool_error("Usage: /mcp persist [clear]")
else:
self.io.tool_error(f"Unknown /mcp subcommand: {sub_command}")
self.io.tool_output("Valid subcommands are: new, tools, enable, disable, rm, persist")


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

Expand Down
99 changes: 83 additions & 16 deletions aider/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,18 +146,66 @@ def tokenize(self):
)

def get_command_completions(self, document, complete_event, text, words):
if len(words) == 1 and not text[-1].isspace():
partial = words[0].lower()
candidates = [cmd for cmd in self.command_names if cmd.startswith(partial)]
if len(words) == 1 and not text[-1].isspace(): # Completing the command itself
partial_cmd = words[0].lower()
# Ensure self.command_names is populated
if not hasattr(self, 'command_names') or not self.command_names:
if self.commands:
self.command_names = self.commands.get_commands()
else:
self.command_names = []

candidates = [cmd for cmd in self.command_names if cmd.startswith(partial_cmd)]
for candidate in sorted(candidates):
yield Completion(candidate, start_position=-len(words[-1]))
return

if len(words) <= 1 or text[-1].isspace():
yield Completion(candidate, start_position=-len(words[0]))
return

# If we are here, words[0] is a complete command.
# We are completing arguments for words[0].
cmd = words[0]
partial = words[-1].lower()

# If text ends with a space, or no partial word to complete
if text[-1].isspace() or len(words) == 1 :
partial_arg = ""
else: # We are completing the current (last) word as an argument
partial_arg = words[-1].lower()

# Special handling for /mcp
if cmd == "/mcp":
if len(words) == 2 and not text[-1].isspace(): # Completing the subcommand (enable/disable)
subcommands = ["enable", "disable"]
for sub_cmd_candidate in subcommands:
if sub_cmd_candidate.startswith(partial_arg):
yield Completion(sub_cmd_candidate, start_position=-len(words[-1]))
return
elif (len(words) == 2 and text[-1].isspace()) or \
(len(words) == 1 and text[-1].isspace()): # Suggesting subcommand after "/mcp "
subcommands = ["enable", "disable"]
for sub_cmd_candidate in subcommands:
yield Completion(sub_cmd_candidate, start_position=0)
return
elif len(words) >= 2 and words[1] == "enable":
# Completing profile name for "/mcp enable <profile_partial>"
# Or suggesting profile names after "/mcp enable "
if self.commands and self.commands.mcp_profile_manager:
profile_names = self.commands.mcp_profile_manager.list_profile_names()
for profile_name in profile_names:
if profile_name.startswith(partial_arg):
start_pos = -len(words[-1]) if (len(words) > 2 and not text[-1].isspace()) else 0
yield Completion(profile_name, start_position=start_pos)
return
return # No mcp_profile_manager or no profiles

# Fallback to existing general command argument completion
if (len(words) <= 1 or text[-1].isspace()) and cmd != "/mcp": # Avoid this path if we handled /mcp above
return

# If we fell through /mcp logic, and we are not at a space, use original partial.
# Otherwise, partial_arg would have been set above.
if cmd != "/mcp" and not text[-1].isspace(): # Original logic for other commands
partial = words[-1].lower()
else: # Use the partial_arg determined for argument completion
partial = partial_arg

matches, _, _ = self.commands.matching_commands(cmd)
if len(matches) == 1:
Expand Down Expand Up @@ -983,22 +1031,41 @@ def tool_warning(self, message="", strip=True):

def tool_output(self, *messages, log_only=False, bold=False):
if messages:
hist = " ".join(messages)
hist_parts = []
for m in messages:
if isinstance(m, Text):
hist_parts.append(m.plain)
else:
hist_parts.append(str(m))
hist = " ".join(hist_parts)
hist = f"{hist.strip()}"
self.append_chat_history(hist, linebreak=True, blockquote=True)

if log_only:
return

messages = list(map(Text, messages))
style = dict()
processed_messages = []
for m in messages:
if not isinstance(m, Text):
# If m is a string, parse it for Rich markup
processed_messages.append(Text.from_markup(str(m)))
else:
# If m is already a Text object, use it as is
processed_messages.append(m)

style_args = {}
if self.pretty:
if self.tool_output_color:
style["color"] = ensure_hash_prefix(self.tool_output_color)
style["reverse"] = bold

style = RichStyle(**style)
self.console.print(*messages, style=style)
style_args["color"] = ensure_hash_prefix(self.tool_output_color)
if bold: # bold is a boolean, reverse is the RichStyle attribute
style_args["reverse"] = True

style = RichStyle(**style_args) if style_args else None

if style:
self.console.print(*processed_messages, style=style)
else:
self.console.print(*processed_messages)

def get_assistant_mdstream(self):
mdargs = dict(style=self.assistant_output_color, code_theme=self.code_theme)
Expand Down
28 changes: 20 additions & 8 deletions aider/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,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.mcp_profile_manager import MCPProfileManager
from aider.models import ModelSettings
from aider.onboarding import offer_openrouter_oauth, select_default_model
from aider.repo import ANY_GIT_ERROR, GitRepo
Expand Down Expand Up @@ -738,6 +738,10 @@ def get_io(pretty):
if args.gitignore:
check_gitignore(git_root, io)

# Instantiate MCPProfileManager
mcp_profile_manager = MCPProfileManager(io, args)
mcp_profile_manager.load_or_initialize_profiles()

if args.verbose:
show = format_settings(parser, args)
io.tool_output(show)
Expand Down Expand Up @@ -939,6 +943,7 @@ def get_io(pretty):
verbose=args.verbose,
editor=args.editor,
original_read_only_fnames=read_only_fnames,
mcp_profile_manager=mcp_profile_manager,
)

summarizer = ChatSummary(
Expand All @@ -964,13 +969,20 @@ def get_io(pretty):
# Track auto-commits configuration
analytics.event("auto_commits", enabled=bool(args.auto_commits))

try:
# 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 = []
# Enable persisted default MCP profile if one exists
if mcp_profile_manager.persisted_active_profile_name:
if args.verbose:
io.tool_output(
f"Attempting to enable persisted default MCP profile:"
f" {mcp_profile_manager.persisted_active_profile_name}"
)
mcp_profile_manager.enable_profile(
mcp_profile_manager.persisted_active_profile_name,
main_model,
main_model.edit_format # Use the main model's default edit format
)

try:
coder = Coder.create(
main_model=main_model,
edit_format=args.edit_format,
Expand Down Expand Up @@ -1003,7 +1015,7 @@ def get_io(pretty):
detect_urls=args.detect_urls,
auto_copy_context=args.copy_paste,
auto_accept_architect=args.auto_accept_architect,
mcp_servers=mcp_servers,
mcp_profile_manager=mcp_profile_manager,
)
except UnknownEditFormat as err:
io.tool_error(str(err))
Expand Down
Loading