From 3194a35230156bbdc64724dfe6daaf568830684e Mon Sep 17 00:00:00 2001
From: Paul Gauthier
Date: Sun, 25 May 2025 16:00:07 -0700
Subject: [PATCH 01/51] feat: Add Gemini 2.5 Flash model and leaderboard data
---
aider/resources/model-settings.yml | 4 +++
aider/website/_data/polyglot_leaderboard.yml | 30 +++++++++++++++++++-
2 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/aider/resources/model-settings.yml b/aider/resources/model-settings.yml
index 2d004557be6..d8318962f5e 100644
--- a/aider/resources/model-settings.yml
+++ b/aider/resources/model-settings.yml
@@ -1747,3 +1747,7 @@
editor_edit_format: editor-diff
accepts_settings: ["thinking_tokens"]
+- name: vertex_ai/gemini-2.5-flash-preview-05-20
+ edit_format: diff
+ use_repo_map: true
+ accepts_settings: ["reasoning_effort", "thinking_tokens"]
\ No newline at end of file
diff --git a/aider/website/_data/polyglot_leaderboard.yml b/aider/website/_data/polyglot_leaderboard.yml
index 13ef32e368a..a9a30de3ff0 100644
--- a/aider/website/_data/polyglot_leaderboard.yml
+++ b/aider/website/_data/polyglot_leaderboard.yml
@@ -1419,4 +1419,32 @@
date: 2025-05-25
versions: 0.83.3.dev
seconds_per_case: 44.1
- total_cost: 65.7484
\ No newline at end of file
+ total_cost: 65.7484
+
+- dirname: 2025-05-25-22-42-53--flash25-05-20
+ test_cases: 225
+ model: gemini-2.5-flash-preview-05-20
+ edit_format: diff
+ commit_hash: a8568c3
+ pass_rate_1: 23.6
+ pass_rate_2: 55.1
+ pass_num_1: 53
+ pass_num_2: 124
+ percent_cases_well_formed: 96.4
+ error_outputs: 8
+ num_malformed_responses: 8
+ num_with_malformed_responses: 8
+ user_asks: 96
+ lazy_comments: 0
+ syntax_errors: 0
+ indentation_errors: 0
+ exhausted_context_windows: 0
+ prompt_tokens: 4512205
+ completion_tokens: 2991967
+ test_timeouts: 2
+ total_tests: 225
+ command: aider --model gemini/gemini-2.5-flash-preview-05-20
+ date: 2025-05-25
+ versions: 0.83.3.dev
+ seconds_per_case: 50.4
+ total_cost: 7.6091
\ No newline at end of file
From 214b811ef9226bd6a93f496fedf02d82755df163 Mon Sep 17 00:00:00 2001
From: Paul Gauthier
Date: Mon, 26 May 2025 08:56:01 -0700
Subject: [PATCH 02/51] chore: Add new polyglot benchmark results
---
aider/website/_data/polyglot_leaderboard.yml | 31 +++++++++++++++++++-
1 file changed, 30 insertions(+), 1 deletion(-)
diff --git a/aider/website/_data/polyglot_leaderboard.yml b/aider/website/_data/polyglot_leaderboard.yml
index a9a30de3ff0..2d6915d61ae 100644
--- a/aider/website/_data/polyglot_leaderboard.yml
+++ b/aider/website/_data/polyglot_leaderboard.yml
@@ -1447,4 +1447,33 @@
date: 2025-05-25
versions: 0.83.3.dev
seconds_per_case: 50.4
- total_cost: 7.6091
\ No newline at end of file
+ total_cost: 7.6091
+
+- dirname: 2025-05-25-22-58-44--flash25-05-20-24k-think
+ test_cases: 225
+ model: gemini/gemini-2.5-flash-preview-05-20
+ edit_format: diff
+ commit_hash: a8568c3-dirty
+ thinking_tokens: 24576
+ pass_rate_1: 26.2
+ pass_rate_2: 55.1
+ pass_num_1: 59
+ pass_num_2: 124
+ percent_cases_well_formed: 95.6
+ error_outputs: 15
+ num_malformed_responses: 15
+ num_with_malformed_responses: 10
+ user_asks: 101
+ lazy_comments: 0
+ syntax_errors: 0
+ indentation_errors: 0
+ exhausted_context_windows: 0
+ prompt_tokens: 3666792
+ completion_tokens: 2703162
+ test_timeouts: 4
+ total_tests: 225
+ command: aider --model gemini/gemini-2.5-flash-preview-05-20
+ date: 2025-05-25
+ versions: 0.83.3.dev
+ seconds_per_case: 53.9
+ total_cost: 8.5625
\ No newline at end of file
From acebc112376f8f7042aeb87d6043fb0dec5f6b20 Mon Sep 17 00:00:00 2001
From: Paul Gauthier
Date: Mon, 26 May 2025 08:56:35 -0700
Subject: [PATCH 03/51] chore: Update model names in polyglot leaderboard
---
aider/website/_data/polyglot_leaderboard.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/aider/website/_data/polyglot_leaderboard.yml b/aider/website/_data/polyglot_leaderboard.yml
index 2d6915d61ae..6bd5896af50 100644
--- a/aider/website/_data/polyglot_leaderboard.yml
+++ b/aider/website/_data/polyglot_leaderboard.yml
@@ -1423,7 +1423,7 @@
- dirname: 2025-05-25-22-42-53--flash25-05-20
test_cases: 225
- model: gemini-2.5-flash-preview-05-20
+ model: gemini-2.5-flash-preview-05-20 (no think)
edit_format: diff
commit_hash: a8568c3
pass_rate_1: 23.6
@@ -1451,7 +1451,7 @@
- dirname: 2025-05-25-22-58-44--flash25-05-20-24k-think
test_cases: 225
- model: gemini/gemini-2.5-flash-preview-05-20
+ model: gemini-2.5-flash-preview-05-20 (24k think)
edit_format: diff
commit_hash: a8568c3-dirty
thinking_tokens: 24576
From 9c9eedd9c53a85daa7ca2228ee384c5066fd6244 Mon Sep 17 00:00:00 2001
From: Paul Gauthier
Date: Mon, 26 May 2025 12:18:22 -0700
Subject: [PATCH 04/51] chore: Update polyglot leaderboard data for
gemini-2.5-flash
---
aider/website/_data/polyglot_leaderboard.yml | 37 ++++++++++----------
1 file changed, 19 insertions(+), 18 deletions(-)
diff --git a/aider/website/_data/polyglot_leaderboard.yml b/aider/website/_data/polyglot_leaderboard.yml
index 6bd5896af50..51676fc729b 100644
--- a/aider/website/_data/polyglot_leaderboard.yml
+++ b/aider/website/_data/polyglot_leaderboard.yml
@@ -1421,34 +1421,35 @@
seconds_per_case: 44.1
total_cost: 65.7484
-- dirname: 2025-05-25-22-42-53--flash25-05-20
+- dirname: 2025-05-26-15-56-31--flash25-05-20-24k-think # dirname is misleading
test_cases: 225
model: gemini-2.5-flash-preview-05-20 (no think)
edit_format: diff
- commit_hash: a8568c3
- pass_rate_1: 23.6
- pass_rate_2: 55.1
- pass_num_1: 53
- pass_num_2: 124
- percent_cases_well_formed: 96.4
- error_outputs: 8
- num_malformed_responses: 8
- num_with_malformed_responses: 8
- user_asks: 96
+ commit_hash: 214b811-dirty
+ thinking_tokens: 0 # <-- no thinking
+ pass_rate_1: 20.9
+ pass_rate_2: 44.0
+ pass_num_1: 47
+ pass_num_2: 99
+ percent_cases_well_formed: 93.8
+ error_outputs: 16
+ num_malformed_responses: 16
+ num_with_malformed_responses: 14
+ user_asks: 79
lazy_comments: 0
syntax_errors: 0
indentation_errors: 0
exhausted_context_windows: 0
- prompt_tokens: 4512205
- completion_tokens: 2991967
- test_timeouts: 2
+ prompt_tokens: 5512458
+ completion_tokens: 514145
+ test_timeouts: 4
total_tests: 225
command: aider --model gemini/gemini-2.5-flash-preview-05-20
- date: 2025-05-25
+ date: 2025-05-26
versions: 0.83.3.dev
- seconds_per_case: 50.4
- total_cost: 7.6091
-
+ seconds_per_case: 12.2
+ total_cost: 1.1354
+
- dirname: 2025-05-25-22-58-44--flash25-05-20-24k-think
test_cases: 225
model: gemini-2.5-flash-preview-05-20 (24k think)
From b79a77793643623adcbc6f885a7ce7c3b66031d9 Mon Sep 17 00:00:00 2001
From: Paul Gauthier
Date: Mon, 26 May 2025 16:29:10 -0700
Subject: [PATCH 05/51] copy
---
README.md | 2 +-
aider/website/assets/sample-analytics.jsonl | 536 +++++++++---------
.../website/docs/config/adv-model-settings.md | 317 +++++++++++
aider/website/docs/config/model-aliases.md | 4 +-
aider/website/docs/faq.md | 11 +-
aider/website/docs/leaderboards/index.md | 2 +-
aider/website/index.html | 4 +-
7 files changed, 600 insertions(+), 276 deletions(-)
diff --git a/README.md b/README.md
index 4a5ad7e8b87..7b3395da41d 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@ cog.out(text)
+src="https://img.shields.io/badge/📦%20Installs-2.4M-2ecc71?style=flat-square&labelColor=555555"/>
## Priority
diff --git a/aider/website/docs/faq.md b/aider/website/docs/faq.md
index 07132ce6ff7..6e8baecbb14 100644
--- a/aider/website/docs/faq.md
+++ b/aider/website/docs/faq.md
@@ -264,10 +264,17 @@ tr:hover { background-color: #f5f5f5; }
| Model Name | Total Tokens | Percent |
-| gemini/gemini-2.5-pro-exp-03-25 | 1,216,051 | 67.6% |
-| o3 | 542,669 | 30.2% |
+| gemini/gemini-2.5-pro-exp-03-25 | 1,109,768 | 61.9% |
+| o3 | 542,669 | 30.3% |
+| anthropic/claude-sonnet-4-20250514 | 92,508 | 5.2% |
| gemini/gemini-2.5-pro-preview-05-06 | 40,256 | 2.2% |
+| gemini/gemini-2.5-flash-preview-05-20 | 7,638 | 0.4% |
+| gemini/REDACTED | 643 | 0.0% |
+
+{: .note :}
+Some models show as REDACTED, because they are new or unpopular models.
+Aider's analytics only records the names of "well known" LLMs.
## How are the "aider wrote xx% of code" stats computed?
diff --git a/aider/website/docs/leaderboards/index.md b/aider/website/docs/leaderboards/index.md
index 1485ce5327b..18aac4a2499 100644
--- a/aider/website/docs/leaderboards/index.md
+++ b/aider/website/docs/leaderboards/index.md
@@ -285,6 +285,6 @@ mod_dates = [get_last_modified_date(file) for file in files]
latest_mod_date = max(mod_dates)
cog.out(f"{latest_mod_date.strftime('%B %d, %Y.')}")
]]]-->
-May 09, 2025.
+May 26, 2025.
diff --git a/aider/website/index.html b/aider/website/index.html
index 87e19b86272..9bcaa13a767 100644
--- a/aider/website/index.html
+++ b/aider/website/index.html
@@ -69,11 +69,11 @@ AI pair programming in your terminal
]]]-->
⭐ GitHub Stars
- 33K
+ 34K
📦 Installs
- 2.3M
+ 2.4M
📈 Tokens/week
From 8304029b92e5c20850467ed22c503921d4738796 Mon Sep 17 00:00:00 2001
From: Paul Gauthier
Date: Mon, 26 May 2025 16:29:28 -0700
Subject: [PATCH 06/51] lint
---
aider/models.py | 20 ++++++++++++--------
1 file changed, 12 insertions(+), 8 deletions(-)
diff --git a/aider/models.py b/aider/models.py
index 6289babca70..8c4a0c3eaae 100644
--- a/aider/models.py
+++ b/aider/models.py
@@ -878,20 +878,24 @@ def is_ollama(self):
def github_copilot_token_to_open_ai_key(self):
# check to see if there's an openai api key
# If so, check to see if it's expire
- openai_api_key = 'OPENAI_API_KEY'
+ openai_api_key = "OPENAI_API_KEY"
if openai_api_key not in os.environ or (
- int(dict(x.split("=") for x in os.environ[openai_api_key].split(";"))['exp']) < int(datetime.now().timestamp())
+ int(dict(x.split("=") for x in os.environ[openai_api_key].split(";"))["exp"])
+ < int(datetime.now().timestamp())
):
import requests
+
headers = {
- 'Authorization': f"Bearer {os.environ['GITHUB_COPILOT_TOKEN']}",
- 'Editor-Version': self.extra_params['extra_headers']['Editor-Version'],
- 'Copilot-Integration-Id': self.extra_params['extra_headers']['Copilot-Integration-Id'],
- 'Content-Type': 'application/json',
+ "Authorization": f"Bearer {os.environ['GITHUB_COPILOT_TOKEN']}",
+ "Editor-Version": self.extra_params["extra_headers"]["Editor-Version"],
+ "Copilot-Integration-Id": self.extra_params["extra_headers"][
+ "Copilot-Integration-Id"
+ ],
+ "Content-Type": "application/json",
}
res = requests.get("https://api.github.com/copilot_internal/v2/token", headers=headers)
- os.environ[openai_api_key] = res.json()['token']
+ os.environ[openai_api_key] = res.json()["token"]
def send_completion(self, messages, functions, stream, temperature=None):
if os.environ.get("AIDER_SANITY_CHECK_TURNS"):
@@ -935,7 +939,7 @@ def send_completion(self, messages, functions, stream, temperature=None):
kwargs["messages"] = messages
# Are we using github copilot?
- if 'GITHUB_COPILOT_TOKEN' in os.environ:
+ if "GITHUB_COPILOT_TOKEN" in os.environ:
self.github_copilot_token_to_open_ai_key()
res = litellm.completion(**kwargs)
From a0282dc143219231eed4cc518e99828a28f57bb7 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Tue, 27 May 2025 21:57:59 +0200
Subject: [PATCH 07/51] Fix merge conflit from mcp PR
---
aider/models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/aider/models.py b/aider/models.py
index 215312bfebf..cf95adad1a3 100644
--- a/aider/models.py
+++ b/aider/models.py
@@ -898,7 +898,7 @@ def github_copilot_token_to_open_ai_key(self):
res = requests.get("https://api.github.com/copilot_internal/v2/token", headers=headers)
os.environ[openai_api_key] = res.json()["token"]
- 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)
From ece40beee584d74ee5a1ea525696051f907f44bc Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Tue, 27 May 2025 22:40:14 +0200
Subject: [PATCH 08/51] feat: Add MCP profile data structure and manager
---
aider/mcp_profile_manager.py | 88 ++++++++++++++++++++++++++++++++++++
1 file changed, 88 insertions(+)
create mode 100644 aider/mcp_profile_manager.py
diff --git a/aider/mcp_profile_manager.py b/aider/mcp_profile_manager.py
new file mode 100644
index 00000000000..73170dfeaa3
--- /dev/null
+++ b/aider/mcp_profile_manager.py
@@ -0,0 +1,88 @@
+from pathlib import Path
+from typing import List, Dict, Optional, Tuple, Any
+from dataclasses import dataclass, asdict
+import yaml
+
+MCP_PROFILES_YAML_PATH = Path.home() / ".aider.mcp.profiles.yml"
+
+@dataclass
+class MCPProfile:
+ name: str
+ server_names: List[str]
+
+def load_mcp_profiles() -> Dict[str, MCPProfile]:
+ try:
+ with open(MCP_PROFILES_YAML_PATH, 'r') as f:
+ profiles_data = yaml.safe_load(f)
+ if isinstance(profiles_data, list):
+ profiles = {}
+ for profile_dict in profiles_data:
+ try:
+ profile = MCPProfile(**profile_dict)
+ profiles[profile.name] = profile
+ except TypeError as e:
+ # Handle cases where a dict in YAML doesn't match MCPProfile fields
+ # Potentially log this, for now, we'll skip malformed entries
+ print(f"Warning: Skipping malformed profile entry: {profile_dict} due to {e}")
+ return profiles
+ elif profiles_data is None: # File is empty
+ return {}
+ else: # File content is not a list as expected
+ # Potentially log this
+ print(f"Warning: MCP profiles YAML content is not a list: {MCP_PROFILES_YAML_PATH}")
+ return {}
+ except FileNotFoundError:
+ return {}
+ except yaml.YAMLError as e:
+ # Potentially log this, e.g., using io if available
+ print(f"Warning: Error parsing MCP profiles YAML: {e}")
+ return {}
+
+def save_mcp_profiles(profiles: Dict[str, MCPProfile]):
+ try:
+ profiles_list = [asdict(profile) for profile in profiles.values()]
+ with open(MCP_PROFILES_YAML_PATH, 'w') as f:
+ yaml.dump(profiles_list, f, sort_keys=False)
+ except (IOError, yaml.YAMLError) as e:
+ # Potentially log this, e.g., using io if available
+ print(f"Warning: Error saving MCP profiles YAML: {e}")
+
+def get_known_mcp_server_names(settings_mcpservers: Optional[List[Dict]]) -> List[str]:
+ if not settings_mcpservers:
+ return []
+
+ server_names = []
+ for server_config in settings_mcpservers:
+ if isinstance(server_config, dict) and 'name' in server_config:
+ server_names.append(server_config['name'])
+ return server_names
+
+class MCPProfileManager:
+ def __init__(self, io, settings):
+ self.io = io
+ self.settings = settings
+ self.profiles: Dict[str, MCPProfile] = {}
+ self.active_profile_name: Optional[str] = None
+ self.active_mcp_client_pool: Optional[Any] = None # Replace Any with MCPClientPool later
+
+ def load_or_initialize_profiles(self):
+ self.profiles = load_mcp_profiles()
+
+ if "all" not in self.profiles:
+ known_server_names = get_known_mcp_server_names(
+ self.settings.mcpservers if hasattr(self.settings, 'mcpservers') else None
+ )
+ all_profile = MCPProfile(name="all", server_names=known_server_names)
+ self.profiles["all"] = all_profile
+ save_mcp_profiles(self.profiles)
+ if self.io:
+ self.io.tool_output("Initialized default MCP profile 'all'.")
+
+ def get_profile(self, name: str) -> Optional[MCPProfile]:
+ return self.profiles.get(name)
+
+ def list_profile_names(self) -> List[str]:
+ return list(self.profiles.keys())
+
+ def list_profiles_details(self) -> List[Tuple[str, List[str]]]:
+ return [(profile.name, profile.server_names) for profile in self.profiles.values()]
From 6981e0cf4efde2e36dac9a498de908cae40258b5 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Tue, 27 May 2025 22:42:52 +0200
Subject: [PATCH 09/51] chore: Instantiate and pass MCP profile manager to
Coder/Commands
---
aider/coders/base_coder.py | 3 +++
aider/commands.py | 4 ++++
aider/main.py | 7 +++++++
3 files changed, 14 insertions(+)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index 0669b2597fb..3525f86a9e9 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -126,6 +126,7 @@ class Coder:
file_watcher = None
mcp_servers = None
mcp_tools = None
+ mcp_profile_manager = None
@classmethod
def create(
@@ -343,9 +344,11 @@ def __init__(
auto_copy_context=False,
auto_accept_architect=True,
mcp_servers=None,
+ mcp_profile_manager=None,
):
# Fill in a dummy Analytics if needed, but it is never .enable()'d
self.analytics = analytics if analytics is not None else Analytics()
+ self.mcp_profile_manager = mcp_profile_manager
self.event = self.analytics.event
self.chat_language = chat_language
diff --git a/aider/commands.py b/aider/commands.py
index aaf6d7ddd9a..22cb1261e83 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -36,6 +36,7 @@ def __init__(self, placeholder=None, **kwargs):
class Commands:
voice = None
scraper = None
+ mcp_profile_manager = None
def clone(self):
return Commands(
@@ -48,6 +49,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__(
@@ -63,10 +65,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
diff --git a/aider/main.py b/aider/main.py
index 9358f6b6bd8..b0b8bfc0afd 100644
--- a/aider/main.py
+++ b/aider/main.py
@@ -31,6 +31,7 @@
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_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
@@ -738,6 +739,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)
@@ -939,6 +944,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(
@@ -1004,6 +1010,7 @@ def get_io(pretty):
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))
From acb4580a73cdbd64cd281e80490755fcfdc3fdd3 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Tue, 27 May 2025 22:45:58 +0200
Subject: [PATCH 10/51] feat: Add /mcp command to list profiles and update
completion
fix: Include mcpservers-file in known server names
fix: Ensure 'all' MCP profile reflects current server configs
feat: Support dict format for MCP server config
fix: Fix 'all' profile update logic on startup
Fix keys for mcp config
refactor: Extract MCP server names from 'mcpServers' key
---
aider/commands.py | 51 ++++++++++
aider/io.py | 64 +++++++++++--
aider/mcp_profile_manager.py | 175 ++++++++++++++++++++++++++++++-----
3 files changed, 260 insertions(+), 30 deletions(-)
diff --git a/aider/commands.py b/aider/commands.py
index 22cb1261e83..35efde01f8b 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -1592,6 +1592,57 @@ 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, servers in profiles_details:
+ self.io.tool_output(
+ f" - {name}: {', '.join(servers) if servers 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 enable - Enable an MCP profile")
+ self.io.tool_output(" /mcp disable - Disable the active MCP profile")
+ # Add more subcommands as they are implemented
+ # self.io.tool_output(" /mcp create [server_name ...]")
+ # self.io.tool_output(" /mcp delete ")
+ # self.io.tool_output(" /mcp add ")
+ # self.io.tool_output(" /mcp remove ")
+ return
+
+ sub_command = args[0]
+ if sub_command == "enable":
+ if len(args) > 1:
+ profile_name = args[1]
+ # Placeholder for enable logic
+ self.io.tool_error(f"Subcommand 'enable {profile_name}' not yet implemented.")
+ else:
+ self.io.tool_error("Usage: /mcp enable ")
+ elif sub_command == "disable":
+ # Placeholder for disable logic
+ self.io.tool_error("Subcommand 'disable' not yet implemented.")
+ else:
+ self.io.tool_error(f"Unknown /mcp subcommand: {sub_command}")
+ self.io.tool_output("Valid subcommands are: enable, disable")
+
+
def cmd_copy_context(self, args=None):
"""Copy the current chat context as markdown, suitable to paste into a web UI"""
diff --git a/aider/io.py b/aider/io.py
index f28a1c86dfb..f02ed2b167f 100644
--- a/aider/io.py
+++ b/aider/io.py
@@ -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 "
+ # 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:
diff --git a/aider/mcp_profile_manager.py b/aider/mcp_profile_manager.py
index 73170dfeaa3..1228d5e017b 100644
--- a/aider/mcp_profile_manager.py
+++ b/aider/mcp_profile_manager.py
@@ -17,13 +17,26 @@ def load_mcp_profiles() -> Dict[str, MCPProfile]:
if isinstance(profiles_data, list):
profiles = {}
for profile_dict in profiles_data:
+ if not isinstance(profile_dict, dict):
+ print(f"Warning: Skipping non-dictionary profile entry: {profile_dict}")
+ continue
try:
+ # Ensure server_names is a list.
+ loaded_server_names = profile_dict.get('server_names')
+ if not isinstance(loaded_server_names, list):
+ profile_name = profile_dict.get('name', 'UnknownProfile')
+ if loaded_server_names is not None: # Log if it was present but wrong type
+ print(
+ f"Warning: 'server_names' for profile '{profile_name}' is not a list"
+ f" (type: {type(loaded_server_names)}). Initializing as empty list."
+ )
+ profile_dict['server_names'] = [] # Default to empty list
+
profile = MCPProfile(**profile_dict)
profiles[profile.name] = profile
- except TypeError as e:
- # Handle cases where a dict in YAML doesn't match MCPProfile fields
- # Potentially log this, for now, we'll skip malformed entries
- print(f"Warning: Skipping malformed profile entry: {profile_dict} due to {e}")
+ except TypeError as e: # Catches issues like missing 'name' or other fields
+ profile_name_for_error = profile_dict.get('name', str(profile_dict))
+ print(f"Warning: Skipping malformed profile entry for '{profile_name_for_error}': {e}")
return profiles
elif profiles_data is None: # File is empty
return {}
@@ -47,15 +60,107 @@ def save_mcp_profiles(profiles: Dict[str, MCPProfile]):
# Potentially log this, e.g., using io if available
print(f"Warning: Error saving MCP profiles YAML: {e}")
-def get_known_mcp_server_names(settings_mcpservers: Optional[List[Dict]]) -> List[str]:
- if not settings_mcpservers:
- return []
-
- server_names = []
- for server_config in settings_mcpservers:
- if isinstance(server_config, dict) and 'name' in server_config:
- server_names.append(server_config['name'])
- return server_names
+def get_known_mcp_server_names(
+ mcpservers_arg: Optional[Any], # This is args.mcpservers
+ mcpservers_file_arg: Optional[str], # This is args.mcpservers_file
+ io: Optional[Any]
+) -> List[str]:
+ server_names_set = set()
+ file_encoding = 'utf-8' # Default encoding
+
+ if io and hasattr(io, 'encoding') and io.encoding:
+ file_encoding = io.encoding
+
+ # Helper to process data loaded from YAML/JSON
+ def _extract_server_names_from_data(data: Any, source_description: str):
+
+ if not isinstance(data, dict):
+ if data is not None and io and hasattr(io, 'tool_warning'):
+ io.tool_warning(
+ f"MCP servers config from {source_description} is not a dictionary (type:"
+ f" {type(data)}), skipping. Expected a top-level dictionary."
+ )
+ return
+
+ mcp_servers_object = data.get("mcpServers")
+
+ if not isinstance(mcp_servers_object, dict):
+ if mcp_servers_object is not None and io and hasattr(io, 'tool_warning'):
+ io.tool_warning(
+ f"MCP servers config from {source_description}: 'mcpServers' key does not point to a dictionary (type:"
+ f" {type(mcp_servers_object)}), skipping."
+ )
+ elif "mcpServers" not in data and io and hasattr(io, 'tool_warning'):
+ io.tool_warning(
+ f"MCP servers config from {source_description}: Missing 'mcpServers' top-level key, skipping."
+ )
+ return
+
+ extracted_names = list(mcp_servers_object.keys())
+ for server_name in extracted_names:
+ server_names_set.add(server_name)
+
+ # Helper to process a file path
+ def _process_file_path(file_path_str: str, source_description: str):
+ try:
+ path_obj = Path(file_path_str)
+ # Check if path_obj is None or empty string before calling expanduser
+ if not path_obj or not str(path_obj).strip():
+ if io and hasattr(io, 'tool_warning'):
+ io.tool_warning(f"Empty file path provided for {source_description}, skipping.")
+ return
+
+ file_path = path_obj.expanduser()
+ if file_path.exists():
+ with open(file_path, 'r', encoding=file_encoding) as f:
+ data = yaml.safe_load(f)
+ _extract_server_names_from_data(data, f"file {file_path_str}")
+ # Silently ignore if file not found
+ except yaml.YAMLError as e:
+ if io and hasattr(io, 'tool_warning'):
+ io.tool_warning(f"Error parsing MCP servers {source_description} file {file_path_str}: {e}")
+ except IOError as e:
+ if io and hasattr(io, 'tool_warning'):
+ io.tool_warning(f"Error reading MCP servers {source_description} file {file_path_str}: {e}")
+ except Exception as e:
+ if io and hasattr(io, 'tool_warning'):
+ io.tool_warning(f"Unexpected error processing MCP {source_description} file {file_path_str}: {e}")
+
+ # 1. Process mcpservers_arg (args.mcpservers)
+ if mcpservers_arg:
+ if isinstance(mcpservers_arg, dict): # Already parsed as a dict (e.g. from aider.conf.yml)
+ _extract_server_names_from_data(mcpservers_arg, "direct config (dict)")
+ elif isinstance(mcpservers_arg, str):
+ parsed_as_yaml_dict = False
+ try:
+ data = yaml.safe_load(mcpservers_arg)
+ if isinstance(data, dict): # Parsed as a dict
+ _extract_server_names_from_data(data, "direct config (YAML string)")
+ parsed_as_yaml_dict = True
+ elif data is not None and io and hasattr(io, 'tool_warning'): # Parsed as something else
+ io.tool_warning(
+ f"MCP servers config from direct YAML string was not a dictionary (type: {type(data)}), skipping."
+ )
+ except yaml.YAMLError:
+ # Not valid YAML, so it's likely a path. Fall through.
+ pass
+
+ if not parsed_as_yaml_dict:
+ # If it wasn't parsed as a YAML dict treat the original string as a path.
+ _process_file_path(mcpservers_arg, "direct config (path)")
+ elif isinstance(mcpservers_arg, list) and io and hasattr(io, 'tool_warning'):
+ io.tool_warning(
+ f"MCP servers config from direct config was a list, skipping. Expected a dictionary."
+ )
+ # Else: other types are ignored or handled by _extract_server_names_from_data if it's a dict.
+
+ # 2. Process mcpservers_file_arg (args.mcpservers_file)
+ if mcpservers_file_arg:
+ _process_file_path(mcpservers_file_arg, "file arg")
+
+ final_names = list(server_names_set)
+
+ return final_names
class MCPProfileManager:
def __init__(self, io, settings):
@@ -67,16 +172,42 @@ def __init__(self, io, settings):
def load_or_initialize_profiles(self):
self.profiles = load_mcp_profiles()
-
- if "all" not in self.profiles:
- known_server_names = get_known_mcp_server_names(
- self.settings.mcpservers if hasattr(self.settings, 'mcpservers') else None
- )
- all_profile = MCPProfile(name="all", server_names=known_server_names)
- self.profiles["all"] = all_profile
- save_mcp_profiles(self.profiles)
- if self.io:
+
+ mcpservers_arg_val = getattr(self.settings, 'mcp_servers', None)
+ mcpservers_file_arg_val = getattr(self.settings, 'mcp_servers_file', None)
+
+ current_known_server_names = sorted(list(set(get_known_mcp_server_names(
+ mcpservers_arg_val,
+ mcpservers_file_arg_val,
+ self.io
+ ))))
+
+ needs_save = False
+
+ old_all_profile_server_names_sorted = None
+ if "all" in self.profiles:
+ # self.profiles["all"].server_names is guaranteed to be a list by robust load_mcp_profiles
+ old_all_profile_server_names_sorted = sorted(self.profiles["all"].server_names)
+
+ # Create/Update the 'all' profile in the in-memory dictionary to match current reality.
+ # This ensures self.profiles["all"] is always up-to-date after this step.
+ self.profiles["all"] = MCPProfile(name="all", server_names=current_known_server_names)
+
+ if old_all_profile_server_names_sorted is None:
+ # The 'all' profile was not in the loaded file, so we created it.
+ needs_save = True
+ if self.io and hasattr(self.io, 'tool_output'):
self.io.tool_output("Initialized default MCP profile 'all'.")
+ elif old_all_profile_server_names_sorted != current_known_server_names:
+ # The 'all' profile was in the loaded file, but its servers differed from current reality.
+ needs_save = True
+ if self.io and hasattr(self.io, 'tool_output'):
+ self.io.tool_output("Updated MCP profile 'all' with current server configurations.")
+ # If old_all_profile_server_names_sorted == current_known_server_names, then the loaded 'all' profile
+ # was already correct. No message, needs_save remains False.
+
+ if needs_save:
+ save_mcp_profiles(self.profiles)
def get_profile(self, name: str) -> Optional[MCPProfile]:
return self.profiles.get(name)
From 314a74f9aa36e1cad7239459bb3a3d28bb643d51 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Wed, 28 May 2025 00:12:47 +0200
Subject: [PATCH 11/51] Moved McpProfileManager to mcp module
---
aider/main.py | 2 +-
aider/{ => mcp}/mcp_profile_manager.py | 0
2 files changed, 1 insertion(+), 1 deletion(-)
rename aider/{ => mcp}/mcp_profile_manager.py (100%)
diff --git a/aider/main.py b/aider/main.py
index b0b8bfc0afd..d7d4296eb10 100644
--- a/aider/main.py
+++ b/aider/main.py
@@ -31,7 +31,7 @@
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_profile_manager import MCPProfileManager
+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
diff --git a/aider/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
similarity index 100%
rename from aider/mcp_profile_manager.py
rename to aider/mcp/mcp_profile_manager.py
From 6fd6502c074a4f28a1c15bed9d1eacb656596761 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 00:21:46 +0200
Subject: [PATCH 12/51] refactor: Use load_mcp_servers in profile manager
---
aider/mcp/mcp_profile_manager.py | 141 +++++++++++--------------------
1 file changed, 48 insertions(+), 93 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 1228d5e017b..09da23bdc63 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -2,6 +2,8 @@
from typing import List, Dict, Optional, Tuple, Any
from dataclasses import dataclass, asdict
import yaml
+import json # Added
+from aider.mcp import load_mcp_servers # Added
MCP_PROFILES_YAML_PATH = Path.home() / ".aider.mcp.profiles.yml"
@@ -65,102 +67,55 @@ def get_known_mcp_server_names(
mcpservers_file_arg: Optional[str], # This is args.mcpservers_file
io: Optional[Any]
) -> List[str]:
- server_names_set = set()
- file_encoding = 'utf-8' # Default encoding
-
- if io and hasattr(io, 'encoding') and io.encoding:
- file_encoding = io.encoding
-
- # Helper to process data loaded from YAML/JSON
- def _extract_server_names_from_data(data: Any, source_description: str):
-
- if not isinstance(data, dict):
- if data is not None and io and hasattr(io, 'tool_warning'):
- io.tool_warning(
- f"MCP servers config from {source_description} is not a dictionary (type:"
- f" {type(data)}), skipping. Expected a top-level dictionary."
- )
- return
+ loaded_mcp_objects = []
- mcp_servers_object = data.get("mcpServers")
-
- if not isinstance(mcp_servers_object, dict):
- if mcp_servers_object is not None and io and hasattr(io, 'tool_warning'):
- io.tool_warning(
- f"MCP servers config from {source_description}: 'mcpServers' key does not point to a dictionary (type:"
- f" {type(mcp_servers_object)}), skipping."
- )
- elif "mcpServers" not in data and io and hasattr(io, 'tool_warning'):
- io.tool_warning(
- f"MCP servers config from {source_description}: Missing 'mcpServers' top-level key, skipping."
- )
- return
-
- extracted_names = list(mcp_servers_object.keys())
- for server_name in extracted_names:
- server_names_set.add(server_name)
-
- # Helper to process a file path
- def _process_file_path(file_path_str: str, source_description: str):
- try:
- path_obj = Path(file_path_str)
- # Check if path_obj is None or empty string before calling expanduser
- if not path_obj or not str(path_obj).strip():
- if io and hasattr(io, 'tool_warning'):
- io.tool_warning(f"Empty file path provided for {source_description}, skipping.")
- return
-
- file_path = path_obj.expanduser()
- if file_path.exists():
- with open(file_path, 'r', encoding=file_encoding) as f:
- data = yaml.safe_load(f)
- _extract_server_names_from_data(data, f"file {file_path_str}")
- # Silently ignore if file not found
- except yaml.YAMLError as e:
- if io and hasattr(io, 'tool_warning'):
- io.tool_warning(f"Error parsing MCP servers {source_description} file {file_path_str}: {e}")
- except IOError as e:
- if io and hasattr(io, 'tool_warning'):
- io.tool_warning(f"Error reading MCP servers {source_description} file {file_path_str}: {e}")
- except Exception as e:
- if io and hasattr(io, 'tool_warning'):
- io.tool_warning(f"Unexpected error processing MCP {source_description} file {file_path_str}: {e}")
-
- # 1. Process mcpservers_arg (args.mcpservers)
+ # 1. Process mcpservers_arg
if mcpservers_arg:
- if isinstance(mcpservers_arg, dict): # Already parsed as a dict (e.g. from aider.conf.yml)
- _extract_server_names_from_data(mcpservers_arg, "direct config (dict)")
- elif isinstance(mcpservers_arg, str):
- parsed_as_yaml_dict = False
+ temp_servers_from_arg = []
+ if isinstance(mcpservers_arg, dict):
try:
- data = yaml.safe_load(mcpservers_arg)
- if isinstance(data, dict): # Parsed as a dict
- _extract_server_names_from_data(data, "direct config (YAML string)")
- parsed_as_yaml_dict = True
- elif data is not None and io and hasattr(io, 'tool_warning'): # Parsed as something else
- io.tool_warning(
- f"MCP servers config from direct YAML string was not a dictionary (type: {type(data)}), skipping."
- )
- except yaml.YAMLError:
- # Not valid YAML, so it's likely a path. Fall through.
- pass
-
- if not parsed_as_yaml_dict:
- # If it wasn't parsed as a YAML dict treat the original string as a path.
- _process_file_path(mcpservers_arg, "direct config (path)")
- elif isinstance(mcpservers_arg, list) and io and hasattr(io, 'tool_warning'):
- io.tool_warning(
- f"MCP servers config from direct config was a list, skipping. Expected a dictionary."
- )
- # Else: other types are ignored or handled by _extract_server_names_from_data if it's a dict.
-
- # 2. Process mcpservers_file_arg (args.mcpservers_file)
- if mcpservers_file_arg:
- _process_file_path(mcpservers_file_arg, "file arg")
+ # Convert dict to JSON string to pass to load_mcp_servers
+ mcp_json_string = json.dumps(mcpservers_arg)
+ # Call load_mcp_servers with the JSON string
+ temp_servers_from_arg = load_mcp_servers(mcp_servers=mcp_json_string, mcp_servers_file=None, io=io, verbose=False)
+ except TypeError as e: # json.dumps might fail
+ if io and hasattr(io, 'tool_warning'):
+ io.tool_warning(f"Could not serialize MCP servers dict to JSON: {e}")
+ elif isinstance(mcpservers_arg, str):
+ # load_mcp_servers will first try mcp_servers as a JSON string.
+ # If that doesn't yield results, we then try mcpservers_arg as a file path.
+ servers_from_json_str = load_mcp_servers(mcp_servers=mcpservers_arg, mcp_servers_file=None, io=io, verbose=False)
+ if servers_from_json_str:
+ temp_servers_from_arg = servers_from_json_str
+ else:
+ # If not a valid JSON string or it was empty, try as a file path
+ temp_servers_from_arg = load_mcp_servers(mcp_servers=None, mcp_servers_file=mcpservers_arg, io=io, verbose=False)
+ elif isinstance(mcpservers_arg, list):
+ if io and hasattr(io, 'tool_warning'):
+ io.tool_warning(
+ f"MCP servers config from direct config was a list, skipping. Expected a dictionary or JSON string."
+ )
+ # Other types for mcpservers_arg will be implicitly ignored or handled by load_mcp_servers
- final_names = list(server_names_set)
+ if temp_servers_from_arg:
+ loaded_mcp_objects.extend(temp_servers_from_arg)
- return final_names
+ # 2. Process mcpservers_file_arg
+ if mcpservers_file_arg:
+ # If mcpservers_arg was a path and is the same as mcpservers_file_arg,
+ # load_mcp_servers might process it again.
+ # The set operation for names at the end handles duplicates.
+ servers_from_file_arg = load_mcp_servers(mcp_servers=None, mcp_servers_file=mcpservers_file_arg, io=io, verbose=False)
+ if servers_from_file_arg:
+ loaded_mcp_objects.extend(servers_from_file_arg)
+
+ # Extract unique names from the McpServer objects
+ server_names_set = set()
+ for server_obj in loaded_mcp_objects:
+ if hasattr(server_obj, 'name') and isinstance(server_obj.name, str) and server_obj.name:
+ server_names_set.add(server_obj.name)
+
+ return sorted(list(server_names_set))
class MCPProfileManager:
def __init__(self, io, settings):
@@ -176,11 +131,11 @@ def load_or_initialize_profiles(self):
mcpservers_arg_val = getattr(self.settings, 'mcp_servers', None)
mcpservers_file_arg_val = getattr(self.settings, 'mcp_servers_file', None)
- current_known_server_names = sorted(list(set(get_known_mcp_server_names(
+ current_known_server_names = get_known_mcp_server_names(
mcpservers_arg_val,
mcpservers_file_arg_val,
self.io
- ))))
+ )
needs_save = False
From f4e21b15767e31b516346c2bc3a3ddd33461a881 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 00:25:38 +0200
Subject: [PATCH 13/51] feat: Add logic to enable/disable MCP profiles and pool
---
aider/commands.py | 9 +++---
aider/mcp/mcp_profile_manager.py | 51 +++++++++++++++++++++++++++++++-
2 files changed, 55 insertions(+), 5 deletions(-)
diff --git a/aider/commands.py b/aider/commands.py
index 35efde01f8b..13711ee5599 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -1631,13 +1631,14 @@ def cmd_mcp(self, args_str: str):
if sub_command == "enable":
if len(args) > 1:
profile_name = args[1]
- # Placeholder for enable logic
- self.io.tool_error(f"Subcommand 'enable {profile_name}' not yet implemented.")
+ 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 ")
elif sub_command == "disable":
- # Placeholder for disable logic
- self.io.tool_error("Subcommand 'disable' not yet implemented.")
+ if len(args) == 1:
+ self.mcp_profile_manager.disable_profile()
+ else:
+ self.io.tool_error("Usage: /mcp disable")
else:
self.io.tool_error(f"Unknown /mcp subcommand: {sub_command}")
self.io.tool_output("Valid subcommands are: enable, disable")
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 09da23bdc63..057baa5f810 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -4,6 +4,7 @@
import yaml
import json # Added
from aider.mcp import load_mcp_servers # Added
+from aider.mcp.mcp_client_pool import MCPClientPool
MCP_PROFILES_YAML_PATH = Path.home() / ".aider.mcp.profiles.yml"
@@ -123,7 +124,7 @@ def __init__(self, io, settings):
self.settings = settings
self.profiles: Dict[str, MCPProfile] = {}
self.active_profile_name: Optional[str] = None
- self.active_mcp_client_pool: Optional[Any] = None # Replace Any with MCPClientPool later
+ self.active_mcp_client_pool: Optional[MCPClientPool] = None
def load_or_initialize_profiles(self):
self.profiles = load_mcp_profiles()
@@ -172,3 +173,51 @@ def list_profile_names(self) -> List[str]:
def list_profiles_details(self) -> List[Tuple[str, List[str]]]:
return [(profile.name, profile.server_names) for profile in self.profiles.values()]
+
+ def enable_profile(self, profile_name: str, main_model, main_edit_format):
+ if self.active_profile_name == profile_name and self.active_mcp_client_pool:
+ self.io.tool_output(f"MCP profile '{profile_name}' is already active.")
+ return
+
+ if self.active_profile_name is not None:
+ self.disable_profile()
+
+ profile_to_enable = self.get_profile(profile_name)
+
+ if not profile_to_enable:
+ self.io.tool_error(f"MCP profile '{profile_name}' not found.")
+ return
+
+ if not profile_to_enable.server_names:
+ self.io.tool_warning(f"MCP profile '{profile_name}' has no servers configured. Profile enabled but no connections made.")
+ self.active_profile_name = profile_name
+ self.active_mcp_client_pool = None # Ensure it's cleared
+ return
+
+ all_server_configs = getattr(self.settings, 'mcpservers', [])
+ if not isinstance(all_server_configs, list): # Ensure it's a list if it was some other type
+ all_server_configs = []
+
+ matched_server_configs = [s_conf for s_conf in all_server_configs if isinstance(s_conf, dict) and s_conf.get('name') in profile_to_enable.server_names]
+
+ if not matched_server_configs:
+ self.io.tool_error(f"No configured MCP servers found for profile '{profile_name}'. Check server names and .aider.conf.yml.")
+ return
+
+ self.active_mcp_client_pool = MCPClientPool(self.io, main_model, main_edit_format, matched_server_configs)
+ self.active_profile_name = profile_name
+ self.io.tool_output(f"MCP profile '{profile_name}' enabled with {len(matched_server_configs)} server(s).")
+
+ def disable_profile(self):
+ if self.active_mcp_client_pool:
+ # Assuming MCPClientPool has a shutdown method.
+ # If not, this will need to be added to MCPClientPool.
+ if hasattr(self.active_mcp_client_pool, 'shutdown') and callable(getattr(self.active_mcp_client_pool, 'shutdown')):
+ self.active_mcp_client_pool.shutdown()
+ self.active_mcp_client_pool = None
+
+ if self.active_profile_name:
+ self.io.tool_output(f"MCP profile '{self.active_profile_name}' disabled.")
+ self.active_profile_name = None
+ else:
+ self.io.tool_output("No MCP profile was active.")
From a8ba33ee81b330b2c68f53612edfc91c54ca000a Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 00:26:46 +0200
Subject: [PATCH 14/51] feat: Create MCPClientPool for managing servers
---
aider/mcp/mcp_client_pool.py | 148 +++++++++++++++++++++++++++++++++++
1 file changed, 148 insertions(+)
create mode 100644 aider/mcp/mcp_client_pool.py
diff --git a/aider/mcp/mcp_client_pool.py b/aider/mcp/mcp_client_pool.py
new file mode 100644
index 00000000000..655e53404ae
--- /dev/null
+++ b/aider/mcp/mcp_client_pool.py
@@ -0,0 +1,148 @@
+import asyncio
+import logging
+from typing import List, Dict, Any, Optional
+
+from aider.mcp.server import McpServer # Assuming McpServer is in this path
+
+
+class MCPClientPool:
+ """
+ Manages a pool of MCPClient instances, one for each configured MCP server.
+ Provides a way to get available tools from all connected servers and
+ to shut down all connections gracefully.
+ """
+
+ def __init__(self, io: Any, model: Any, edit_format: str, server_configs: List[Dict[str, Any]]):
+ """
+ Initializes the MCPClientPool.
+
+ Args:
+ io: The input/output object for logging and user interaction.
+ model: The main language model being used.
+ edit_format: The edit format being used.
+ server_configs: A list of configurations for each MCP server.
+ """
+ self.io = io
+ self.model = model
+ self.edit_format = edit_format
+ self.server_configs = server_configs
+ self.clients: Dict[str, McpServer] = {}
+ self._init_clients()
+
+ def _init_clients(self):
+ """
+ Initializes McpServer instances based on the server configurations.
+ """
+ for config in self.server_configs:
+ server_name = config.get("name")
+ if not server_name:
+ logging.warning("MCP server config missing 'name', skipping.")
+ if self.io and hasattr(self.io, 'tool_warning'):
+ self.io.tool_warning("An MCP server configuration was missing a 'name' and has been skipped.")
+ continue
+
+ if server_name in self.clients:
+ logging.warning(f"Duplicate MCP server name '{server_name}' found in configuration, skipping.")
+ if self.io and hasattr(self.io, 'tool_warning'):
+ self.io.tool_warning(f"Duplicate MCP server name '{server_name}' found. Only the first configuration will be used.")
+ continue
+
+ try:
+ self.clients[server_name] = McpServer(server_config=config)
+ # TODO: Potentially connect here or connect on-demand when tools are requested.
+ # For now, just initializing. Connection will happen when tools are fetched.
+ logging.info(f"Initialized McpServer for '{server_name}'.")
+ except Exception as e:
+ logging.error(f"Failed to initialize McpServer for '{server_name}': {e}")
+ if self.io and hasattr(self.io, 'tool_error'):
+ self.io.tool_error(f"Failed to initialize MCP server '{server_name}': {e}")
+
+ async def get_all_tools(self, context: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
+ """
+ Connects to all MCP servers and retrieves their available tools.
+
+ Args:
+ context: Optional context to pass to the MCP servers.
+
+ Returns:
+ A list of tool definitions from all connected MCP servers.
+ """
+ all_tools = []
+ # TODO: Implement tool fetching logic. This will involve:
+ # 1. Iterating through self.clients.
+ # 2. For each client, ensuring it's connected (await client.connect()).
+ # 3. Calling a method on the client/session to get tools (e.g., await client.session.experimental_get_tools(context=context, format="openai_function"))
+ # 4. Aggregating the tools.
+ # 5. Handling potential errors during connection or tool retrieval for individual servers.
+
+ # Placeholder implementation:
+ if self.io and hasattr(self.io, 'tool_output'):
+ self.io.tool_output("MCPClientPool.get_all_tools() is not fully implemented yet.")
+
+ # Example of how one might connect and get tools (needs actual mcp.py library methods)
+ # for server_name, client in self.clients.items():
+ # try:
+ # session = await client.connect()
+ # # Assuming a method like `experimental_get_tools` exists on the session
+ # # The actual method name and parameters might differ based on mcp.py
+ # server_tools = await session.experimental_get_tools(format="openai_function", context=context)
+ # if server_tools:
+ # all_tools.extend(server_tools)
+ # logging.info(f"Retrieved {len(server_tools)} tools from {server_name}")
+ # except Exception as e:
+ # logging.error(f"Error getting tools from MCP server {server_name}: {e}")
+ # if self.io and hasattr(self.io, 'tool_error'):
+ # self.io.tool_error(f"Could not retrieve tools from MCP server '{server_name}': {e}")
+ return all_tools
+
+ def shutdown(self):
+ """
+ Shuts down all MCP client connections.
+ This method is synchronous but calls an asynchronous helper.
+ """
+ logging.info("Shutting down MCPClientPool...")
+ try:
+ asyncio.run(self._shutdown_async())
+ except RuntimeError as e:
+ if "cannot be called from a running event loop" in str(e):
+ # If we're already in an event loop, schedule _shutdown_async
+ # This might happen if shutdown is called from an async context
+ loop = asyncio.get_event_loop()
+ if loop.is_running():
+ loop.create_task(self._shutdown_async())
+ logging.info("Scheduled MCPClientPool shutdown in existing event loop.")
+ else: # Should not happen if RuntimeError is for running loop
+ logging.error(f"MCPClientPool shutdown error: {e}")
+ else:
+ logging.error(f"MCPClientPool shutdown error: {e}")
+ except Exception as e: # Catch any other unexpected errors
+ logging.error(f"Unexpected error during MCPClientPool shutdown: {e}")
+
+
+ async def _shutdown_async(self):
+ """
+ Asynchronously disconnects all McpServer instances.
+ """
+ logging.info("Starting asynchronous shutdown of MCP clients.")
+ shutdown_tasks = []
+ for server_name, client in self.clients.items():
+ logging.info(f"Initiating shutdown for MCP server: {server_name}")
+ shutdown_tasks.append(client.disconnect())
+
+ results = await asyncio.gather(*shutdown_tasks, return_exceptions=True)
+
+ for server_name, result in zip(self.clients.keys(), results):
+ if isinstance(result, Exception):
+ logging.error(f"Error disconnecting from MCP server {server_name}: {result}")
+ if self.io and hasattr(self.io, 'tool_warning'):
+ self.io.tool_warning(f"Error during disconnect from MCP server '{server_name}': {result}")
+ else:
+ logging.info(f"Successfully disconnected from MCP server: {server_name}")
+
+ self.clients.clear()
+ logging.info("All MCP clients have been processed for shutdown.")
+
+ # TODO: Add methods for coder to interact with tools, e.g.:
+ # async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
+ # pass
+ # This would require routing the call to the correct McpServer instance.
From 4ee5085411240330f6dd2e46747127712d65618a Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 00:47:22 +0200
Subject: [PATCH 15/51] refactor: Move MCP tool init from coder to profile
manager
---
aider/coders/base_coder.py | 186 ++++++++++++++-----------------
aider/mcp/mcp_client_pool.py | 96 +++++++++++-----
aider/mcp/mcp_profile_manager.py | 21 +++-
3 files changed, 173 insertions(+), 130 deletions(-)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index 3525f86a9e9..9e7bdd57b7d 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -343,7 +343,7 @@ def __init__(
file_watcher=None,
auto_copy_context=False,
auto_accept_architect=True,
- mcp_servers=None,
+ # mcp_servers is no longer used directly by BaseCoder for initialization
mcp_profile_manager=None,
):
# Fill in a dummy Analytics if needed, but it is never .enable()'d
@@ -372,7 +372,7 @@ def __init__(
self.detect_urls = detect_urls
self.num_cache_warming_pings = num_cache_warming_pings
- self.mcp_servers = mcp_servers
+ # self.mcp_servers = mcp_servers # No longer stored directly for initialization
if not fnames:
fnames = []
@@ -537,9 +537,9 @@ def __init__(
self.auto_test = auto_test
self.test_cmd = test_cmd
- # Instantiate MCP tools
- if self.mcp_servers:
- self.initialize_mcp_tools()
+ # MCP tools are now initialized via MCPProfileManager when a profile is enabled.
+ # BaseCoder will get tools from the active pool.
+
# validate the functions jsonschema
if self.functions:
from jsonschema import Draft7Validator
@@ -1652,12 +1652,16 @@ 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
+ llm_tool_calls = tool_call_response.choices[0].message.tool_calls
+ if not llm_tool_calls:
+ return False
+
# Collect all tool calls grouped by server
- server_tool_calls = self._gather_server_tool_calls(tool_calls)
+ # The keys of server_tool_calls are McpServer instances
+ server_tool_calls_map = self._gather_server_tool_calls(llm_tool_calls)
- if server_tool_calls and self.num_tool_calls < self.max_tool_calls:
- self._print_tool_call_info(server_tool_calls)
+ if server_tool_calls_map and self.num_tool_calls < self.max_tool_calls:
+ self._print_tool_call_info(server_tool_calls_map)
if self.io.confirm_ask("Run tools?"):
tool_responses = self._execute_tool_calls(server_tool_calls)
@@ -1691,124 +1695,102 @@ def _print_tool_call_info(self, server_tool_calls):
self.io.tool_output("\n")
- def _gather_server_tool_calls(self, tool_calls):
- """Collect all tool calls grouped by server.
+ def _gather_server_tool_calls(self, llm_tool_calls: List[Any]) -> Dict[McpServer, List[Any]]:
+ """Collect all tool calls grouped by McpServer instance.
Args:
- tool_calls: List of tool calls from the LLM response
+ llm_tool_calls: List of tool_call objects from the LLM response.
Returns:
- dict: Dictionary mapping servers to their respective tool calls
+ dict: Dictionary mapping McpServer instances to a list of their respective tool_call objects.
+ """
+ gathered_calls: Dict[McpServer, List[Any]] = {}
+
+ if not self.mcp_profile_manager or not self.mcp_profile_manager.active_mcp_client_pool:
+ return gathered_calls
+
+ active_pool = self.mcp_profile_manager.active_mcp_client_pool
+ # tools_by_server is Dict[server_name, List[tool_definition_dict]]
+ tools_by_server = active_pool.get_cached_tools_by_server()
+ # mcp_instances is Dict[server_name, McpServer_instance]
+ mcp_instances = active_pool.clients
+
+ if not tools_by_server or not mcp_instances:
+ return gathered_calls
+
+ for llm_tc in llm_tool_calls:
+ found_tool_for_llm_tc = False
+ for server_name, mcp_tool_definitions in tools_by_server.items():
+ for mcp_tool_def in mcp_tool_definitions:
+ if mcp_tool_def.get("function", {}).get("name") == llm_tc.function.name:
+ target_server_instance = mcp_instances.get(server_name)
+ if target_server_instance:
+ if target_server_instance not in gathered_calls:
+ gathered_calls[target_server_instance] = []
+ gathered_calls[target_server_instance].append(llm_tc)
+ found_tool_for_llm_tc = True
+ break # Found the definition for this llm_tc, move to next llm_tc
+ if found_tool_for_llm_tc:
+ break # Moved to next llm_tc
+
+ return gathered_calls
+
+ def _execute_tool_calls(self, server_tool_calls_map: Dict[McpServer, List[Any]]):
+ """Process tool calls from the response and execute them.
+ Args:
+ server_tool_calls_map: Dict mapping McpServer instances to their list of tool_calls.
+ Returns:
+ A list of tool response messages.
"""
- 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 = []
# Define the coroutine to execute all tool calls for a single server
- async def _exec_server_tools(server, tool_calls_list):
- tool_responses = []
+ async def _exec_server_tools(mcp_server_instance: McpServer, tool_calls_for_server: List[Any]):
+ # Renamed 'server' to 'mcp_server_instance' for clarity
+ responses_for_server = []
try:
# Connect to the server once
- session = await server.connect()
+ session = await mcp_server_instance.connect()
# Execute all tool calls for this server
- for tool_call in tool_calls_list:
+ for tool_call_obj in tool_calls_for_server:
call_result = await experimental_mcp_client.call_openai_tool(
session=session,
- openai_tool=tool_call,
+ openai_tool=tool_call_obj, # This is the tool_call object from LLM
)
result_text = str(call_result.content[0].text)
- tool_responses.append(
- {"role": "tool", "tool_call_id": tool_call.id, "content": result_text}
+ responses_for_server.append(
+ {"role": "tool", "tool_call_id": tool_call_obj.id, "content": result_text}
)
finally:
- await server.disconnect()
- return tool_responses
+ await mcp_server_instance.disconnect()
+ return responses_for_server
# Execute all tool calls concurrently
- async def _execute_all_tool_calls():
+ async def _execute_all_tool_calls_async():
tasks = []
- for server, tool_calls_list in tool_calls.items():
- tasks.append(_exec_server_tools(server, tool_calls_list))
+ for mcp_server_instance, tool_calls_for_server in server_tool_calls_map.items():
+ tasks.append(_exec_server_tools(mcp_server_instance, tool_calls_for_server))
# Wait for all tasks to complete
- results = await asyncio.gather(*tasks)
- return results
+ results_list_of_lists = await asyncio.gather(*tasks, return_exceptions=True)
+ return results_list_of_lists
# Run the async execution and collect results
- if tool_calls:
- all_results = asyncio.run(_execute_all_tool_calls())
+ if server_tool_calls_map:
+ all_results_nested = asyncio.run(_execute_all_tool_calls_async())
# Flatten the results from all servers
- for server_results in all_results:
- tool_responses.extend(server_results)
-
+ for server_batch_result in all_results_nested:
+ if isinstance(server_batch_result, Exception):
+ self.io.tool_error(f"Error executing batch of tool calls: {server_batch_result}")
+ # Potentially add a generic error tool_response for all tools in this batch
+ elif isinstance(server_batch_result, list):
+ tool_responses.extend(server_batch_result)
+
return tool_responses
- def initialize_mcp_tools(self):
- """
- 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):
- try:
- session = await server.connect()
- server_tools = await experimental_mcp_client.load_mcp_tools(
- 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 [result for result in results if result is not None]
-
- if self.mcp_servers:
- tools = asyncio.run(get_all_server_tools())
-
- 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}")
-
- 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 get_tool_list(self) -> List[Dict[str, Any]]:
+ """Get a flattened list of all MCP tools from the active profile."""
+ if self.mcp_profile_manager and self.mcp_profile_manager.active_mcp_client_pool:
+ return self.mcp_profile_manager.active_mcp_client_pool.get_cached_tools_flat_list()
+ return []
def reply_completed(self):
pass
diff --git a/aider/mcp/mcp_client_pool.py b/aider/mcp/mcp_client_pool.py
index 655e53404ae..0a696491a37 100644
--- a/aider/mcp/mcp_client_pool.py
+++ b/aider/mcp/mcp_client_pool.py
@@ -27,6 +27,7 @@ def __init__(self, io: Any, model: Any, edit_format: str, server_configs: List[D
self.edit_format = edit_format
self.server_configs = server_configs
self.clients: Dict[str, McpServer] = {}
+ self.cached_tools_by_server: Optional[Dict[str, List[Dict[str, Any]]]] = None
self._init_clients()
def _init_clients(self):
@@ -57,44 +58,85 @@ def _init_clients(self):
if self.io and hasattr(self.io, 'tool_error'):
self.io.tool_error(f"Failed to initialize MCP server '{server_name}': {e}")
- async def get_all_tools(self, context: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
+ async def _fetch_tools_for_server(self, server_name: str, client: McpServer, context: Optional[Dict[str, Any]] = None) -> Optional[List[Dict[str, Any]]]:
+ """Helper to fetch tools for a single server."""
+ from litellm import experimental_mcp_client # Local import to avoid circular dependency if mcp.py imports aider stuff
+ try:
+ session = await client.connect()
+ # The context parameter for load_mcp_tools is not standard, removing for now.
+ # If needed, it would be passed to experimental_get_tools if that was the target.
+ server_tools = await experimental_mcp_client.load_mcp_tools(session=session, format="openai")
+ logging.info(f"Retrieved {len(server_tools)} tools from {server_name}")
+ return server_tools
+ except Exception as e:
+ logging.error(f"Error getting tools from MCP server {server_name}: {e}")
+ if self.io and hasattr(self.io, 'tool_error'):
+ self.io.tool_error(f"Could not retrieve tools from MCP server '{server_name}': {e}")
+ return None
+ finally:
+ # Ensure client is disconnected even if tool fetching fails
+ await client.disconnect()
+
+
+ async def _fetch_tools_for_all_servers(self, context: Optional[Dict[str, Any]] = None) -> Dict[str, List[Dict[str, Any]]]:
"""
Connects to all MCP servers and retrieves their available tools.
Args:
- context: Optional context to pass to the MCP servers.
+ context: Optional context to pass to the MCP servers (currently unused by load_mcp_tools).
Returns:
- A list of tool definitions from all connected MCP servers.
+ A dictionary mapping server names to their list of tool definitions.
"""
- all_tools = []
- # TODO: Implement tool fetching logic. This will involve:
- # 1. Iterating through self.clients.
- # 2. For each client, ensuring it's connected (await client.connect()).
- # 3. Calling a method on the client/session to get tools (e.g., await client.session.experimental_get_tools(context=context, format="openai_function"))
- # 4. Aggregating the tools.
- # 5. Handling potential errors during connection or tool retrieval for individual servers.
-
- # Placeholder implementation:
+ tools_by_server: Dict[str, List[Dict[str, Any]]] = {}
+
+ tasks = []
+ for server_name, client in self.clients.items():
+ tasks.append(self._fetch_tools_for_server(server_name, client, context))
+
+ results = await asyncio.gather(*tasks, return_exceptions=True)
+
+ for i, server_name in enumerate(self.clients.keys()):
+ result = results[i]
+ if isinstance(result, Exception):
+ logging.error(f"Exception fetching tools from {server_name}: {result}")
+ # Optionally store empty list or skip this server
+ elif result is not None:
+ tools_by_server[server_name] = result
+
+ return tools_by_server
+
+ async def fetch_and_cache_tools(self, context: Optional[Dict[str, Any]] = None):
+ """
+ Fetches tools from all configured servers and caches them.
+ """
+ if self.io and hasattr(self.io, 'tool_output'):
+ self.io.tool_output("Fetching tools from enabled MCP profile servers...")
+ self.cached_tools_by_server = await self._fetch_tools_for_all_servers(context)
if self.io and hasattr(self.io, 'tool_output'):
- self.io.tool_output("MCPClientPool.get_all_tools() is not fully implemented yet.")
+ num_tools = sum(len(tools) for tools in self.cached_tools_by_server.values())
+ num_servers = len(self.cached_tools_by_server)
+ self.io.tool_output(f"Fetched {num_tools} tool(s) from {num_servers} server(s).")
+
+
+ def get_cached_tools_flat_list(self) -> List[Dict[str, Any]]:
+ """
+ Returns a flat list of all cached tool definitions.
+ """
+ if not self.cached_tools_by_server:
+ return []
- # Example of how one might connect and get tools (needs actual mcp.py library methods)
- # for server_name, client in self.clients.items():
- # try:
- # session = await client.connect()
- # # Assuming a method like `experimental_get_tools` exists on the session
- # # The actual method name and parameters might differ based on mcp.py
- # server_tools = await session.experimental_get_tools(format="openai_function", context=context)
- # if server_tools:
- # all_tools.extend(server_tools)
- # logging.info(f"Retrieved {len(server_tools)} tools from {server_name}")
- # except Exception as e:
- # logging.error(f"Error getting tools from MCP server {server_name}: {e}")
- # if self.io and hasattr(self.io, 'tool_error'):
- # self.io.tool_error(f"Could not retrieve tools from MCP server '{server_name}': {e}")
+ all_tools = []
+ for server_tools in self.cached_tools_by_server.values():
+ all_tools.extend(server_tools)
return all_tools
+ def get_cached_tools_by_server(self) -> Optional[Dict[str, List[Dict[str, Any]]]]:
+ """
+ Returns the cached tools, structured as a dictionary mapping server name to its tools.
+ """
+ return self.cached_tools_by_server
+
def shutdown(self):
"""
Shuts down all MCP client connections.
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 057baa5f810..09f45acf347 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -204,7 +204,26 @@ def enable_profile(self, profile_name: str, main_model, main_edit_format):
self.io.tool_error(f"No configured MCP servers found for profile '{profile_name}'. Check server names and .aider.conf.yml.")
return
- self.active_mcp_client_pool = MCPClientPool(self.io, main_model, main_edit_format, matched_server_configs)
+ pool = MCPClientPool(self.io, main_model, main_edit_format, matched_server_configs)
+ try:
+ # Run the async method to fetch tools in a blocking way
+ asyncio.run(pool.fetch_and_cache_tools())
+ except RuntimeError as e:
+ if "cannot be called from a running event loop" in str(e):
+ loop = asyncio.get_event_loop()
+ if loop.is_running():
+ # This case should ideally be avoided by design.
+ # If enable_profile is called from an async context, it should be awaited.
+ # For now, log a warning and proceed without tools if this rare case occurs.
+ self.io.tool_warning("Warning: Could not fetch MCP tools due to running asyncio event loop during profile enabling.")
+ else:
+ self.io.tool_error(f"Error fetching MCP tools: {e}")
+ return # Stop if asyncio.run fails not due to existing loop
+ else:
+ self.io.tool_error(f"Error fetching MCP tools: {e}")
+ return # Stop on other runtime errors
+
+ self.active_mcp_client_pool = pool
self.active_profile_name = profile_name
self.io.tool_output(f"MCP profile '{profile_name}' enabled with {len(matched_server_configs)} server(s).")
From 3141845bfe196c9e3b7cbea20389af55c0bf240a Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 00:47:43 +0200
Subject: [PATCH 16/51] fix: Import missing modules and fix tool call typo
---
aider/coders/base_coder.py | 7 +++++--
aider/mcp/mcp_profile_manager.py | 1 +
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index 9e7bdd57b7d..96e9f5f6218 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -1,5 +1,7 @@
#!/usr/bin/env python
+import asyncio
+import base64
import asyncio
import base64
import hashlib
@@ -25,7 +27,7 @@
Locale = None
from json.decoder import JSONDecodeError
from pathlib import Path
-from typing import List
+from typing import List, Dict, Any
from litellm import experimental_mcp_client
from rich.console import Console
@@ -39,6 +41,7 @@
from aider.linter import Linter
from aider.llm import litellm
from aider.models import RETRY_TIMEOUT
+from aider.mcp.server import McpServer
from aider.reasoning_tags import (
REASONING_TAG,
format_reasoning_content,
@@ -1664,7 +1667,7 @@ def process_tool_calls(self, tool_call_response):
self._print_tool_call_info(server_tool_calls_map)
if self.io.confirm_ask("Run tools?"):
- tool_responses = self._execute_tool_calls(server_tool_calls)
+ tool_responses = self._execute_tool_calls(server_tool_calls_map)
# Add the assistant message with tool calls
# Converting to a dict so it can be safely dumped to json
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 09f45acf347..6e57493d87a 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -1,3 +1,4 @@
+import asyncio
from pathlib import Path
from typing import List, Dict, Optional, Tuple, Any
from dataclasses import dataclass, asdict
From d6b3c88e90f1a973e6497617b36a059e228f1a88 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 00:48:49 +0200
Subject: [PATCH 17/51] fix: Remove mcp_servers from kwargs in Coder.create
---
aider/coders/base_coder.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index 96e9f5f6218..1883774d0e3 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -197,6 +197,11 @@ def create(
kwargs = use_kwargs
from_coder.ok_to_warm_cache = False
+ # Ensure 'mcp_servers' is not passed as it's no longer a valid argument for Coder.__init__.
+ # This applies whether kwargs come from from_coder or directly to Coder.create.
+ if 'mcp_servers' in kwargs:
+ del kwargs['mcp_servers']
+
for coder in coders.__all__:
if hasattr(coder, "edit_format") and coder.edit_format == edit_format:
res = coder(main_model, io, **kwargs)
From 5b0bcb4f52eb89edc0da74e0c9b44b299fee70cd Mon Sep 17 00:00:00 2001
From: Your Name
Date: Wed, 28 May 2025 01:01:59 +0200
Subject: [PATCH 18/51] fix: Use load_mcp_servers to get configs for profile
activation
---
aider/mcp/mcp_profile_manager.py | 26 +++++++++++++++++++-------
1 file changed, 19 insertions(+), 7 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 6e57493d87a..aa996e3e1f9 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -110,13 +110,13 @@ def get_known_mcp_server_names(
servers_from_file_arg = load_mcp_servers(mcp_servers=None, mcp_servers_file=mcpservers_file_arg, io=io, verbose=False)
if servers_from_file_arg:
loaded_mcp_objects.extend(servers_from_file_arg)
-
+
# Extract unique names from the McpServer objects
server_names_set = set()
for server_obj in loaded_mcp_objects:
if hasattr(server_obj, 'name') and isinstance(server_obj.name, str) and server_obj.name:
server_names_set.add(server_obj.name)
-
+
return sorted(list(server_names_set))
class MCPProfileManager:
@@ -195,11 +195,23 @@ def enable_profile(self, profile_name: str, main_model, main_edit_format):
self.active_mcp_client_pool = None # Ensure it's cleared
return
- all_server_configs = getattr(self.settings, 'mcpservers', [])
- if not isinstance(all_server_configs, list): # Ensure it's a list if it was some other type
- all_server_configs = []
-
- matched_server_configs = [s_conf for s_conf in all_server_configs if isinstance(s_conf, dict) and s_conf.get('name') in profile_to_enable.server_names]
+ # Retrieve server configurations using the centralized load_mcp_servers function
+ mcpservers_arg_val = getattr(self.settings, 'mcp_servers', None)
+ mcpservers_file_arg_val = getattr(self.settings, 'mcp_servers_file', None)
+ verbose_setting = getattr(self.settings, 'verbose', False)
+
+ all_mcp_server_objects = load_mcp_servers(
+ mcp_servers=mcpservers_arg_val,
+ mcp_servers_file=mcpservers_file_arg_val,
+ io=self.io,
+ verbose=verbose_setting
+ )
+
+ # Filter these McpServer objects by name and get their config dictionaries
+ matched_server_configs = [
+ s_obj.config for s_obj in all_mcp_server_objects
+ if s_obj.name in profile_to_enable.server_names
+ ]
if not matched_server_configs:
self.io.tool_error(f"No configured MCP servers found for profile '{profile_name}'. Check server names and .aider.conf.yml.")
From 59da4ffd571cb626053f6c3d7410a582f1ea56ac Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 03:18:30 +0200
Subject: [PATCH 19/51] feat: Reuse MCP profile manager on coder switch
---
aider/coders/base_coder.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index 1883774d0e3..e860c537a7d 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -190,6 +190,7 @@ def create(
total_tokens_sent=from_coder.total_tokens_sent,
total_tokens_received=from_coder.total_tokens_received,
file_watcher=from_coder.file_watcher,
+ mcp_profile_manager=from_coder.mcp_profile_manager,
)
use_kwargs.update(update) # override to complete the switch
use_kwargs.update(kwargs) # override passed kwargs
From 675698eb471e7efcb1ac2e6587ab32837e4d21b0 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Wed, 28 May 2025 09:27:26 +0200
Subject: [PATCH 20/51] Cleanup
---
aider/coders/base_coder.py | 16 ++++------------
aider/main.py | 8 --------
2 files changed, 4 insertions(+), 20 deletions(-)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index e860c537a7d..556059e7c43 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -127,7 +127,6 @@ class Coder:
ignore_mentions = None
chat_language = None
file_watcher = None
- mcp_servers = None
mcp_tools = None
mcp_profile_manager = None
@@ -198,11 +197,6 @@ def create(
kwargs = use_kwargs
from_coder.ok_to_warm_cache = False
- # Ensure 'mcp_servers' is not passed as it's no longer a valid argument for Coder.__init__.
- # This applies whether kwargs come from from_coder or directly to Coder.create.
- if 'mcp_servers' in kwargs:
- del kwargs['mcp_servers']
-
for coder in coders.__all__:
if hasattr(coder, "edit_format") and coder.edit_format == edit_format:
res = coder(main_model, io, **kwargs)
@@ -352,7 +346,6 @@ def __init__(
file_watcher=None,
auto_copy_context=False,
auto_accept_architect=True,
- # mcp_servers is no longer used directly by BaseCoder for initialization
mcp_profile_manager=None,
):
# Fill in a dummy Analytics if needed, but it is never .enable()'d
@@ -381,7 +374,6 @@ def __init__(
self.detect_urls = detect_urls
self.num_cache_warming_pings = num_cache_warming_pings
- # self.mcp_servers = mcp_servers # No longer stored directly for initialization
if not fnames:
fnames = []
@@ -1664,7 +1656,7 @@ def process_tool_calls(self, tool_call_response):
llm_tool_calls = tool_call_response.choices[0].message.tool_calls
if not llm_tool_calls:
return False
-
+
# Collect all tool calls grouped by server
# The keys of server_tool_calls are McpServer instances
server_tool_calls_map = self._gather_server_tool_calls(llm_tool_calls)
@@ -1716,7 +1708,7 @@ def _gather_server_tool_calls(self, llm_tool_calls: List[Any]) -> Dict[McpServer
if not self.mcp_profile_manager or not self.mcp_profile_manager.active_mcp_client_pool:
return gathered_calls
-
+
active_pool = self.mcp_profile_manager.active_mcp_client_pool
# tools_by_server is Dict[server_name, List[tool_definition_dict]]
tools_by_server = active_pool.get_cached_tools_by_server()
@@ -1740,7 +1732,7 @@ def _gather_server_tool_calls(self, llm_tool_calls: List[Any]) -> Dict[McpServer
break # Found the definition for this llm_tc, move to next llm_tc
if found_tool_for_llm_tc:
break # Moved to next llm_tc
-
+
return gathered_calls
def _execute_tool_calls(self, server_tool_calls_map: Dict[McpServer, List[Any]]):
@@ -1792,7 +1784,7 @@ async def _execute_all_tool_calls_async():
# Potentially add a generic error tool_response for all tools in this batch
elif isinstance(server_batch_result, list):
tool_responses.extend(server_batch_result)
-
+
return tool_responses
def get_tool_list(self) -> List[Dict[str, Any]]:
diff --git a/aider/main.py b/aider/main.py
index d7d4296eb10..161a2bf6f01 100644
--- a/aider/main.py
+++ b/aider/main.py
@@ -30,7 +30,6 @@
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
@@ -971,12 +970,6 @@ def get_io(pretty):
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 = []
-
coder = Coder.create(
main_model=main_model,
edit_format=args.edit_format,
@@ -1009,7 +1002,6 @@ 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:
From a2a5beb96fa3a6e24005a7794c3213697d4d956b Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 13:58:37 +0200
Subject: [PATCH 21/51] refactor: Remove unused mcp_tools and use profile
manager
---
aider/coders/base_coder.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index 556059e7c43..d67149b4912 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -127,7 +127,6 @@ class Coder:
ignore_mentions = None
chat_language = None
file_watcher = None
- mcp_tools = None
mcp_profile_manager = None
@classmethod
@@ -1222,7 +1221,7 @@ def fmt_system_prompt(self, prompt):
else:
quad_backtick_reminder = ""
- if self.mcp_tools and len(self.mcp_tools) > 0:
+ if self.mcp_profile_manager and self.mcp_profile_manager.active_profile_name:
final_reminders.append(self.gpt_prompts.tool_prompt)
final_reminders = "\n\n".join(final_reminders)
From 7a57a160bfc469b5b3d34bbb5402a0151238a2ce Mon Sep 17 00:00:00 2001
From: Your Name
Date: Wed, 28 May 2025 14:51:41 +0200
Subject: [PATCH 22/51] Use McpProfileManager to provide tool prompt
---
aider/coders/base_coder.py | 6 ++++--
aider/mcp/mcp_profile_manager.py | 11 +++++++++++
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index d67149b4912..58032116462 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -1221,8 +1221,10 @@ def fmt_system_prompt(self, prompt):
else:
quad_backtick_reminder = ""
- if self.mcp_profile_manager and self.mcp_profile_manager.active_profile_name:
- final_reminders.append(self.gpt_prompts.tool_prompt)
+ if self.mcp_profile_manager:
+ tool_prompt_from_mcp = self.mcp_profile_manager.get_tool_prompt_if_active()
+ if tool_prompt_from_mcp:
+ final_reminders.append(tool_prompt_from_mcp)
final_reminders = "\n\n".join(final_reminders)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index aa996e3e1f9..592e6b4b82c 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -6,6 +6,8 @@
import json # Added
from aider.mcp import load_mcp_servers # Added
from aider.mcp.mcp_client_pool import MCPClientPool
+from aider.coders.base_prompts import CoderPrompts
+
MCP_PROFILES_YAML_PATH = Path.home() / ".aider.mcp.profiles.yml"
@@ -175,6 +177,15 @@ def list_profile_names(self) -> List[str]:
def list_profiles_details(self) -> List[Tuple[str, List[str]]]:
return [(profile.name, profile.server_names) for profile in self.profiles.values()]
+ def get_tool_prompt_if_active(self) -> Optional[str]:
+ """
+ Returns the standard tool prompt string if an MCP profile is active,
+ otherwise None.
+ """
+ if self.active_profile_name:
+ return CoderPrompts.tool_prompt
+ return None
+
def enable_profile(self, profile_name: str, main_model, main_edit_format):
if self.active_profile_name == profile_name and self.active_mcp_client_pool:
self.io.tool_output(f"MCP profile '{profile_name}' is already active.")
From 965be2340474245e5b65f96869be5e615b0c1995 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Wed, 28 May 2025 14:52:09 +0200
Subject: [PATCH 23/51] Update tool prompt
---
aider/coders/base_prompts.py | 11 +++++------
1 file changed, 5 insertions(+), 6 deletions(-)
diff --git a/aider/coders/base_prompts.py b/aider/coders/base_prompts.py
index cd93b9ef807..a3211669352 100644
--- a/aider/coders/base_prompts.py
+++ b/aider/coders/base_prompts.py
@@ -57,17 +57,16 @@ class CoderPrompts:
no_shell_cmd_reminder = ""
tool_prompt = """
-
-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.
-
"""
rename_with_shell = ""
go_ahead_tip = ""
+
From 67d609b76ea4b2aab775904db771280034611e90 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 15:15:56 +0200
Subject: [PATCH 24/51] refactor: Print MCP tool calls on single line
refactor: Remove redundant MCP tool preparation message
colors
---
aider/coders/base_coder.py | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index 58032116462..ea6dfb7a27e 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -1681,21 +1681,21 @@ def process_tool_calls(self, tool_call_response):
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."""
- self.io.tool_output("Preparing to run MCP tools", bold=True)
-
- for server, tool_calls in server_tool_calls.items():
+ def _print_tool_call_info(self, server_tool_calls_map):
+ """Print information about an MCP tool call on a single line."""
+ for server, tool_calls in server_tool_calls_map.items():
for tool_call in tool_calls:
- 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}")
+ output_str = (
+ f"[reverse][bold]MCP[/bold][/reverse] [yellow]{server.name}:"
+ f" {tool_call.function.name}[/yellow] ({tool_call.function.arguments})"
+ )
+ self.io.tool_output(output_str)
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")
+ self.io.tool_output(
+ f" (Verbose: ID={tool_call.id}, Type={tool_call.type})"
+ )
+ self.io.tool_output("") # Add a newline after all tool call infos
def _gather_server_tool_calls(self, llm_tool_calls: List[Any]) -> Dict[McpServer, List[Any]]:
"""Collect all tool calls grouped by McpServer instance.
From e5c431c579bbb08e3fda4ee6e390684247cccf6b Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 15:29:38 +0200
Subject: [PATCH 25/51] feat: Handle Text objects in tool_output method
fix: Parse Rich markup in tool output strings
---
aider/io.py | 33 ++++++++++++++++++++++++++-------
1 file changed, 26 insertions(+), 7 deletions(-)
diff --git a/aider/io.py b/aider/io.py
index f02ed2b167f..8b3e4400990 100644
--- a/aider/io.py
+++ b/aider/io.py
@@ -1031,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_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
- style = RichStyle(**style)
- self.console.print(*messages, style=style)
+ 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)
From e7686494c2b4aad3bd60fcbb7f957f13af98f794 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 17:25:13 +0200
Subject: [PATCH 26/51] refactor: Refactor MCP profiles to use 'servers' list
with 'no_confirm' option
refactor: Create constants for MCP profile YAML keys
fix: Update cmd_mcp to display server names from list of dicts
---
aider/commands.py | 5 +-
aider/mcp/mcp_profile_manager.py | 95 ++++++++++++++++++++++++--------
2 files changed, 74 insertions(+), 26 deletions(-)
diff --git a/aider/commands.py b/aider/commands.py
index 13711ee5599..d790025cb78 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -1604,9 +1604,10 @@ def cmd_mcp(self, args_str: str):
profiles_details = self.mcp_profile_manager.list_profiles_details()
self.io.tool_output("Available MCP profiles:")
if profiles_details:
- for name, servers in 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(servers) if servers else ' (no servers)'}"
+ f" - {name}: {', '.join(server_names_list) if server_names_list else ' (no servers)'}"
)
else:
self.io.tool_output(" (No profiles defined)")
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 592e6b4b82c..91d3ba89ac6 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -11,10 +11,19 @@
MCP_PROFILES_YAML_PATH = Path.home() / ".aider.mcp.profiles.yml"
+# Constants for YAML keys
+PROFILE_NAME_KEY = "name"
+PROFILE_SERVERS_KEY = "servers"
+SERVER_NAME_KEY = "name"
+SERVER_NO_CONFIRM_KEY = "no_confirm"
+# Old key, for removal during load if present
+OLD_PROFILE_SERVER_NAMES_KEY = "server_names"
+
+
@dataclass
class MCPProfile:
name: str
- server_names: List[str]
+ servers: List[Dict[str, Any]] # Each dict: {'name': str, 'no_confirm': bool}
def load_mcp_profiles() -> Dict[str, MCPProfile]:
try:
@@ -27,22 +36,57 @@ def load_mcp_profiles() -> Dict[str, MCPProfile]:
print(f"Warning: Skipping non-dictionary profile entry: {profile_dict}")
continue
try:
- # Ensure server_names is a list.
- loaded_server_names = profile_dict.get('server_names')
- if not isinstance(loaded_server_names, list):
- profile_name = profile_dict.get('name', 'UnknownProfile')
- if loaded_server_names is not None: # Log if it was present but wrong type
- print(
- f"Warning: 'server_names' for profile '{profile_name}' is not a list"
- f" (type: {type(loaded_server_names)}). Initializing as empty list."
- )
- profile_dict['server_names'] = [] # Default to empty list
-
- profile = MCPProfile(**profile_dict)
+ # Ensure servers is a list of dicts with name and no_confirm.
+ loaded_servers_data = profile_dict.get(PROFILE_SERVERS_KEY)
+ parsed_servers_list = []
+ profile_name_for_error = profile_dict.get(PROFILE_NAME_KEY, 'UnknownProfile')
+
+ if isinstance(loaded_servers_data, list):
+ for server_entry in loaded_servers_data:
+ if isinstance(server_entry, str):
+ parsed_servers_list.append({SERVER_NAME_KEY: server_entry, SERVER_NO_CONFIRM_KEY: False})
+ elif isinstance(server_entry, dict):
+ s_name = server_entry.get(SERVER_NAME_KEY)
+ if not isinstance(s_name, str) or not s_name:
+ print(f"Warning: Server entry in profile '{profile_name_for_error}' is missing a valid '{SERVER_NAME_KEY}': {server_entry}. Skipping this server.")
+ continue
+ s_no_confirm = server_entry.get(SERVER_NO_CONFIRM_KEY, False)
+ if not isinstance(s_no_confirm, bool):
+ print(f"Warning: '{SERVER_NO_CONFIRM_KEY}' for server '{s_name}' in profile '{profile_name_for_error}' is not a boolean (type: {type(s_no_confirm)}). Defaulting to False.")
+ s_no_confirm = False
+ parsed_servers_list.append({SERVER_NAME_KEY: s_name, SERVER_NO_CONFIRM_KEY: s_no_confirm})
+ else:
+ print(f"Warning: Invalid server entry type in profile '{profile_name_for_error}': {server_entry} (type: {type(server_entry)}). Skipping this server.")
+ continue
+ elif loaded_servers_data is not None: # It was present but not a list
+ print(
+ f"Warning: '{PROFILE_SERVERS_KEY}' for profile '{profile_name_for_error}' is not a list"
+ f" (type: {type(loaded_servers_data)}). Initializing as empty list."
+ )
+ # parsed_servers_list remains empty
+
+ profile_dict[PROFILE_SERVERS_KEY] = parsed_servers_list # Store the parsed list
+ if OLD_PROFILE_SERVER_NAMES_KEY in profile_dict: # Remove old key if present for clean object creation
+ del profile_dict[OLD_PROFILE_SERVER_NAMES_KEY]
+
+ # Ensure 'name' key is present for MCPProfile dataclass
+ if PROFILE_NAME_KEY not in profile_dict:
+ print(f"Warning: Profile entry is missing '{PROFILE_NAME_KEY}': {profile_dict}. Skipping this profile.")
+ continue
+
+
+ # Rename 'servers' to 'servers' for MCPProfile dataclass if needed (already done by key const)
+ # profile_dict_for_dataclass = {
+ # 'name': profile_dict[PROFILE_NAME_KEY],
+ # 'servers': profile_dict[PROFILE_SERVERS_KEY]
+ # }
+
+
+ profile = MCPProfile(**profile_dict)
profiles[profile.name] = profile
- except TypeError as e: # Catches issues like missing 'name' or other fields
- profile_name_for_error = profile_dict.get('name', str(profile_dict))
- print(f"Warning: Skipping malformed profile entry for '{profile_name_for_error}': {e}")
+ except TypeError as e: # Catches issues like missing 'name' or other fields for MCPProfile
+ profile_name_display = profile_dict.get(PROFILE_NAME_KEY, str(profile_dict))
+ print(f"Warning: Skipping malformed profile entry for '{profile_name_display}': {e}")
return profiles
elif profiles_data is None: # File is empty
return {}
@@ -145,19 +189,20 @@ def load_or_initialize_profiles(self):
old_all_profile_server_names_sorted = None
if "all" in self.profiles:
- # self.profiles["all"].server_names is guaranteed to be a list by robust load_mcp_profiles
- old_all_profile_server_names_sorted = sorted(self.profiles["all"].server_names)
+ # self.profiles["all"].servers is List[Dict[str, Any]]
+ old_all_profile_server_names_sorted = sorted([s[SERVER_NAME_KEY] for s in self.profiles["all"].servers])
# Create/Update the 'all' profile in the in-memory dictionary to match current reality.
# This ensures self.profiles["all"] is always up-to-date after this step.
- self.profiles["all"] = MCPProfile(name="all", server_names=current_known_server_names)
+ current_servers_as_dicts = [{SERVER_NAME_KEY: s_name, SERVER_NO_CONFIRM_KEY: False} for s_name in current_known_server_names]
+ self.profiles["all"] = MCPProfile(name="all", servers=current_servers_as_dicts)
if old_all_profile_server_names_sorted is None:
# The 'all' profile was not in the loaded file, so we created it.
needs_save = True
if self.io and hasattr(self.io, 'tool_output'):
self.io.tool_output("Initialized default MCP profile 'all'.")
- elif old_all_profile_server_names_sorted != current_known_server_names:
+ elif old_all_profile_server_names_sorted != current_known_server_names: # Compare with sorted list of names
# The 'all' profile was in the loaded file, but its servers differed from current reality.
needs_save = True
if self.io and hasattr(self.io, 'tool_output'):
@@ -174,8 +219,8 @@ def get_profile(self, name: str) -> Optional[MCPProfile]:
def list_profile_names(self) -> List[str]:
return list(self.profiles.keys())
- def list_profiles_details(self) -> List[Tuple[str, List[str]]]:
- return [(profile.name, profile.server_names) for profile in self.profiles.values()]
+ def list_profiles_details(self) -> List[Tuple[str, List[Dict[str, Any]]]]:
+ return [(profile.name, profile.servers) for profile in self.profiles.values()]
def get_tool_prompt_if_active(self) -> Optional[str]:
"""
@@ -200,7 +245,8 @@ def enable_profile(self, profile_name: str, main_model, main_edit_format):
self.io.tool_error(f"MCP profile '{profile_name}' not found.")
return
- if not profile_to_enable.server_names:
+ server_configs_in_profile = profile_to_enable.servers
+ if not server_configs_in_profile:
self.io.tool_warning(f"MCP profile '{profile_name}' has no servers configured. Profile enabled but no connections made.")
self.active_profile_name = profile_name
self.active_mcp_client_pool = None # Ensure it's cleared
@@ -219,9 +265,10 @@ def enable_profile(self, profile_name: str, main_model, main_edit_format):
)
# Filter these McpServer objects by name and get their config dictionaries
+ profile_server_names = [s_conf[SERVER_NAME_KEY] for s_conf in server_configs_in_profile]
matched_server_configs = [
s_obj.config for s_obj in all_mcp_server_objects
- if s_obj.name in profile_to_enable.server_names
+ if s_obj.name in profile_server_names
]
if not matched_server_configs:
From e8979e5fc71804b4aab5ae2d04466bfa29e9e2c3 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 17:30:33 +0200
Subject: [PATCH 27/51] refactor: Use per-server tool confirmation based on
profile
refactor: Encapsulate server no_confirm check in profile manager
---
aider/coders/base_coder.py | 46 +++++++++++++++++++++++++++-----
aider/mcp/mcp_profile_manager.py | 19 +++++++++++++
2 files changed, 59 insertions(+), 6 deletions(-)
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index ea6dfb7a27e..72ecb6346c8 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -1665,18 +1665,52 @@ def process_tool_calls(self, tool_call_response):
if server_tool_calls_map and self.num_tool_calls < self.max_tool_calls:
self._print_tool_call_info(server_tool_calls_map)
- if self.io.confirm_ask("Run tools?"):
- tool_responses = self._execute_tool_calls(server_tool_calls_map)
+ approved_server_tool_calls_map: Dict[McpServer, List[Any]] = {}
+ any_tool_cancelled = False
- # Add the assistant message with tool calls
- # Converting to a dict so it can be safely dumped to json
+ for mcp_server_instance, tool_calls_for_server in server_tool_calls_map.items():
+ server_name = mcp_server_instance.name
+ server_no_confirm = False # Default to needing confirmation
+
+ if self.mcp_profile_manager:
+ server_no_confirm = self.mcp_profile_manager.is_server_no_confirm(server_name)
+
+ proceed_for_this_server = False
+ if server_no_confirm:
+ proceed_for_this_server = True
+ else:
+ if self.io.confirm_ask(f"Run tools for server '{server_name}'?"):
+ proceed_for_this_server = True
+ else:
+ self.io.tool_output(f"Tool execution for server '{server_name}' cancelled by user.")
+ any_tool_cancelled = True
+
+ if proceed_for_this_server:
+ approved_server_tool_calls_map[mcp_server_instance] = tool_calls_for_server
+
+ if approved_server_tool_calls_map:
+ tool_responses = self._execute_tool_calls(approved_server_tool_calls_map)
+
+ # Add the assistant message with ALL original tool calls
+ # (even if some were cancelled, the LLM requested them)
self.cur_messages.append(tool_call_response.choices[0].message.to_dict())
- # Add all tool responses
+ # Add responses only for the tools that were actually executed
for tool_response in tool_responses:
self.cur_messages.append(tool_response)
+
+ return True # Indicates that some tools were processed or attempted
+ elif any_tool_cancelled:
+ # All tools were cancelled by the user, but we should still record the LLM's attempt
+ self.cur_messages.append(tool_call_response.choices[0].message.to_dict())
+ # Add a generic response indicating cancellation if needed, or just return False
+ # For now, returning False implies no *successful* tool execution path was taken.
+ return False
+ else:
+ # No tools to execute (e.g., map was empty initially, or all were filtered out for other reasons)
+ return False
+
- 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
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 91d3ba89ac6..9a76f44e4c4 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -222,6 +222,25 @@ def list_profile_names(self) -> List[str]:
def list_profiles_details(self) -> List[Tuple[str, List[Dict[str, Any]]]]:
return [(profile.name, profile.servers) for profile in self.profiles.values()]
+ def is_server_no_confirm(self, server_name_to_check: str) -> bool:
+ """
+ Checks if a specific server in the currently active profile is set to 'no_confirm'.
+ Defaults to False (meaning, confirmation is required) if the profile is not active,
+ the server is not found in the profile, or 'no_confirm' is not set/False.
+ """
+ if not self.active_profile_name:
+ return False
+
+ active_profile = self.get_profile(self.active_profile_name)
+ if not active_profile:
+ return False
+
+ for server_config in active_profile.servers:
+ if server_config.get(SERVER_NAME_KEY) == server_name_to_check:
+ return server_config.get(SERVER_NO_CONFIRM_KEY, False)
+
+ return False # Server not found in profile or no_confirm not set
+
def get_tool_prompt_if_active(self) -> Optional[str]:
"""
Returns the standard tool prompt string if an MCP profile is active,
From c57b79196b9ba372bd24f5ab984773713a871f59 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 17:44:56 +0200
Subject: [PATCH 28/51] fix: Preserve no_confirm in 'all' MCP profile on
refresh
---
aider/mcp/mcp_profile_manager.py | 48 ++++++++++++++++++++------------
1 file changed, 30 insertions(+), 18 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 9a76f44e4c4..cb6f090c919 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -186,30 +186,42 @@ def load_or_initialize_profiles(self):
)
needs_save = False
+ old_all_profile_data = self.profiles.get("all") # This is an MCPProfile object or None
+
+ # Construct the new 'all' profile's server list, preserving no_confirm where possible
+ new_all_profile_servers_list = []
+ for s_name in current_known_server_names:
+ no_confirm_val = False # Default
+ if old_all_profile_data:
+ # Check if this server was in the old 'all' profile
+ existing_server_config = next(
+ (s_conf for s_conf in old_all_profile_data.servers if s_conf.get(SERVER_NAME_KEY) == s_name),
+ None
+ )
+ if existing_server_config:
+ no_confirm_val = existing_server_config.get(SERVER_NO_CONFIRM_KEY, False)
+ new_all_profile_servers_list.append({SERVER_NAME_KEY: s_name, SERVER_NO_CONFIRM_KEY: no_confirm_val})
+
+ # Sort for consistent comparison later
+ new_all_profile_servers_list_sorted = sorted(new_all_profile_servers_list, key=lambda x: x[SERVER_NAME_KEY])
- old_all_profile_server_names_sorted = None
- if "all" in self.profiles:
- # self.profiles["all"].servers is List[Dict[str, Any]]
- old_all_profile_server_names_sorted = sorted([s[SERVER_NAME_KEY] for s in self.profiles["all"].servers])
-
- # Create/Update the 'all' profile in the in-memory dictionary to match current reality.
- # This ensures self.profiles["all"] is always up-to-date after this step.
- current_servers_as_dicts = [{SERVER_NAME_KEY: s_name, SERVER_NO_CONFIRM_KEY: False} for s_name in current_known_server_names]
- self.profiles["all"] = MCPProfile(name="all", servers=current_servers_as_dicts)
+ # Create/Update the 'all' profile in the in-memory dictionary
+ self.profiles["all"] = MCPProfile(name="all", servers=new_all_profile_servers_list_sorted)
- if old_all_profile_server_names_sorted is None:
+ if old_all_profile_data is None:
# The 'all' profile was not in the loaded file, so we created it.
needs_save = True
if self.io and hasattr(self.io, 'tool_output'):
self.io.tool_output("Initialized default MCP profile 'all'.")
- elif old_all_profile_server_names_sorted != current_known_server_names: # Compare with sorted list of names
- # The 'all' profile was in the loaded file, but its servers differed from current reality.
- needs_save = True
- if self.io and hasattr(self.io, 'tool_output'):
- self.io.tool_output("Updated MCP profile 'all' with current server configurations.")
- # If old_all_profile_server_names_sorted == current_known_server_names, then the loaded 'all' profile
- # was already correct. No message, needs_save remains False.
-
+ else:
+ # The 'all' profile was in the loaded file. Check if it changed.
+ # Sort old server data for consistent comparison
+ old_all_profile_servers_list_sorted = sorted(old_all_profile_data.servers, key=lambda x: x[SERVER_NAME_KEY])
+ if old_all_profile_servers_list_sorted != new_all_profile_servers_list_sorted:
+ needs_save = True
+ if self.io and hasattr(self.io, 'tool_output'):
+ self.io.tool_output("Updated MCP profile 'all' with current server configurations (names or no_confirm values changed).")
+
if needs_save:
save_mcp_profiles(self.profiles)
From 4b6f3c9aba8ce8f59779a41898a2d7bd7f8a4d73 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 18:00:10 +0200
Subject: [PATCH 29/51] feat: add /mcp new command to create profiles
---
aider/commands.py | 35 +++++++++++++++++++++++++++++---
aider/mcp/mcp_profile_manager.py | 25 +++++++++++++++++++++++
2 files changed, 57 insertions(+), 3 deletions(-)
diff --git a/aider/commands.py b/aider/commands.py
index d790025cb78..8989d8fc28e 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -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
@@ -1619,17 +1620,45 @@ def cmd_mcp(self, args_str: str):
self.io.tool_output("\nNo MCP profile is currently active.")
self.io.tool_output("\nCommands:")
+ self.io.tool_output(" /mcp new - Create a new MCP profile")
self.io.tool_output(" /mcp enable - Enable an MCP profile")
self.io.tool_output(" /mcp disable - Disable the active MCP profile")
# Add more subcommands as they are implemented
- # self.io.tool_output(" /mcp create [server_name ...]")
# self.io.tool_output(" /mcp delete ")
# self.io.tool_output(" /mcp add ")
# self.io.tool_output(" /mcp remove ")
return
sub_command = args[0]
- if sub_command == "enable":
+ 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
+
+ dialog_choices = [(name, name) for name in known_server_names]
+
+ selected_servers = checkboxlist_dialog(
+ title=f"Create MCP Profile: {profile_name}",
+ text="Select servers to include in this profile:",
+ values=dialog_choices,
+ ).run()
+
+ if selected_servers:
+ self.mcp_profile_manager.create_new_profile(profile_name, selected_servers)
+ self.io.tool_output(f"MCP profile '{profile_name}' created with {len(selected_servers)} server(s).")
+ else:
+ self.io.tool_output("No servers selected. Profile not created.")
+ else:
+ self.io.tool_error("Usage: /mcp new ")
+
+ 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)
@@ -1642,7 +1671,7 @@ def cmd_mcp(self, args_str: str):
self.io.tool_error("Usage: /mcp disable")
else:
self.io.tool_error(f"Unknown /mcp subcommand: {sub_command}")
- self.io.tool_output("Valid subcommands are: enable, disable")
+ self.io.tool_output("Valid subcommands are: new, enable, disable")
def cmd_copy_context(self, args=None):
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index cb6f090c919..e66819f2bb6 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -234,6 +234,31 @@ def list_profile_names(self) -> List[str]:
def list_profiles_details(self) -> List[Tuple[str, List[Dict[str, Any]]]]:
return [(profile.name, profile.servers) for profile in self.profiles.values()]
+ def get_known_server_names(self) -> List[str]:
+ """Helper to get current known server names based on settings."""
+ mcpservers_arg_val = getattr(self.settings, 'mcp_servers', None)
+ mcpservers_file_arg_val = getattr(self.settings, 'mcp_servers_file', None)
+ return get_known_mcp_server_names(
+ mcpservers_arg_val,
+ mcpservers_file_arg_val,
+ self.io
+ )
+
+ def create_new_profile(self, profile_name: str, selected_server_names: List[str]):
+ if profile_name in self.profiles:
+ self.io.tool_error(f"Profile '{profile_name}' already exists. Cannot create.")
+ return
+
+ servers_config_list = []
+ for server_name in selected_server_names:
+ servers_config_list.append({SERVER_NAME_KEY: server_name, SERVER_NO_CONFIRM_KEY: False})
+
+ new_profile = MCPProfile(name=profile_name, servers=servers_config_list)
+ self.profiles[profile_name] = new_profile
+ save_mcp_profiles(self.profiles)
+ self.io.tool_output(f"MCP Profile '{profile_name}' created and saved.")
+
+
def is_server_no_confirm(self, server_name_to_check: str) -> bool:
"""
Checks if a specific server in the currently active profile is set to 'no_confirm'.
From 857c8a09e2992917e10f37d4a8fa1be3ec72e38a Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 18:13:08 +0200
Subject: [PATCH 30/51] feat: Add two-step dialog for /mcp new command
---
aider/commands.py | 82 ++++++++++++++++++++++++++++----
aider/mcp/mcp_profile_manager.py | 9 ++--
2 files changed, 77 insertions(+), 14 deletions(-)
diff --git a/aider/commands.py b/aider/commands.py
index 8989d8fc28e..eba2e1e2d98 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -1642,19 +1642,83 @@ def cmd_mcp(self, args_str: str):
self.io.tool_error("No MCP servers found. Configure servers first (e.g., in .aider.conf.yml).")
return
- dialog_choices = [(name, name) for name in known_server_names]
-
- selected_servers = checkboxlist_dialog(
- title=f"Create MCP Profile: {profile_name}",
+ # 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=dialog_choices,
+ values=server_selection_choices,
).run()
- if selected_servers:
- self.mcp_profile_manager.create_new_profile(profile_name, selected_servers)
- self.io.tool_output(f"MCP profile '{profile_name}' created with {len(selected_servers)} server(s).")
+ 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_checkboxes = [Checkbox() for _ in selected_server_names]
+ dialog_body_elements = []
+ for i, server_name in enumerate(selected_server_names):
+ dialog_body_elements.append(
+ HSplit(
+ [
+ Label(text=f"{server_name:<30}"), # Adjust width as needed
+ Label(text="No Confirm:"),
+ no_confirm_checkboxes[i],
+ ]
+ )
+ )
+
+ dialog_content = VSplit(dialog_body_elements, padding=1)
+
+ def ok_handler_no_confirm():
+ servers_with_no_confirm_config = []
+ for i, server_name in enumerate(selected_server_names):
+ servers_with_no_confirm_config.append(
+ {
+ "name": server_name,
+ "no_confirm": no_confirm_checkboxes[i].checked,
+ }
+ )
+ return servers_with_no_confirm_config
+
+ def final_ok_handler_no_confirm():
+ selected_config = ok_handler_no_confirm()
+ get_app().exit(result=selected_config)
+
+ def final_cancel_handler_no_confirm():
+ get_app().exit(result=None)
+
+ dialog_container_no_confirm = Dialog(
+ title=f"Create MCP Profile: {profile_name} (Step 2/2)",
+ body=dialog_content,
+ buttons=[
+ Button(text="OK", handler=final_ok_handler_no_confirm),
+ Button(text="Cancel", handler=final_cancel_handler_no_confirm),
+ ],
+ width=D(preferred=80), # Adjust width as needed
+ )
+
+ kb_no_confirm = KeyBindings()
+ @kb_no_confirm.add("escape", eager=True)
+ @kb_no_confirm.add("c-c", eager=True)
+ @kb_no_confirm.add("c-q", eager=True)
+ def _(event):
+ event.app.exit(result=None)
+
+ application_no_confirm = Application(
+ layout=Layout(dialog_container_no_confirm),
+ key_bindings=kb_no_confirm,
+ mouse_support=True,
+ full_screen=True,
+ )
+
+ final_selected_servers_config = application_no_confirm.run()
+
+ if final_selected_servers_config:
+ 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).")
else:
- self.io.tool_output("No servers selected. Profile not created.")
+ self.io.tool_output("Profile creation cancelled during no_confirm configuration.")
else:
self.io.tool_error("Usage: /mcp new ")
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index e66819f2bb6..8840c7bcad4 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -244,16 +244,15 @@ def get_known_server_names(self) -> List[str]:
self.io
)
- def create_new_profile(self, profile_name: str, selected_server_names: List[str]):
+ def create_new_profile(self, profile_name: str, selected_servers_config: List[Dict[str, Any]]):
if profile_name in self.profiles:
self.io.tool_error(f"Profile '{profile_name}' already exists. Cannot create.")
return
- servers_config_list = []
- for server_name in selected_server_names:
- servers_config_list.append({SERVER_NAME_KEY: server_name, SERVER_NO_CONFIRM_KEY: False})
+ # selected_servers_config is already in the format: [{'name': str, 'no_confirm': bool}, ...]
+ # So, we can use it directly.
- new_profile = MCPProfile(name=profile_name, servers=servers_config_list)
+ new_profile = MCPProfile(name=profile_name, servers=selected_servers_config)
self.profiles[profile_name] = new_profile
save_mcp_profiles(self.profiles)
self.io.tool_output(f"MCP Profile '{profile_name}' created and saved.")
From 0ed6545ecc8e57c6e932fd14c8079bf1d73b42b4 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 18:13:36 +0200
Subject: [PATCH 31/51] fix: Import prompt_toolkit components for mcp command
dialogs
---
aider/commands.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/aider/commands.py b/aider/commands.py
index eba2e1e2d98..0f5b16b96f1 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -13,6 +13,13 @@
from prompt_toolkit.completion import Completion, PathCompleter
from prompt_toolkit.document import Document
from prompt_toolkit.shortcuts import checkboxlist_dialog
+from prompt_toolkit.shortcuts.dialogs import Dialog, Button
+from prompt_toolkit.layout.containers import VSplit, HSplit, Window
+from prompt_toolkit.widgets import Checkbox, Label
+from prompt_toolkit.layout.dimension import D
+from prompt_toolkit.application import Application, get_app
+from prompt_toolkit.layout.layout import Layout
+from prompt_toolkit.key_binding import KeyBindings
from aider import models, prompts, voice
from aider.editor import pipe_editor
From 0bc906baaa2e1b0a34f97e410941b7359437600f Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 18:15:04 +0200
Subject: [PATCH 32/51] refactor: Reuse checkboxlist_dialog for /mcp new step 2
---
aider/commands.py | 87 ++++++++++++-----------------------------------
1 file changed, 21 insertions(+), 66 deletions(-)
diff --git a/aider/commands.py b/aider/commands.py
index 0f5b16b96f1..bf349378733 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -13,13 +13,6 @@
from prompt_toolkit.completion import Completion, PathCompleter
from prompt_toolkit.document import Document
from prompt_toolkit.shortcuts import checkboxlist_dialog
-from prompt_toolkit.shortcuts.dialogs import Dialog, Button
-from prompt_toolkit.layout.containers import VSplit, HSplit, Window
-from prompt_toolkit.widgets import Checkbox, Label
-from prompt_toolkit.layout.dimension import D
-from prompt_toolkit.application import Application, get_app
-from prompt_toolkit.layout.layout import Layout
-from prompt_toolkit.key_binding import KeyBindings
from aider import models, prompts, voice
from aider.editor import pipe_editor
@@ -1662,70 +1655,32 @@ def cmd_mcp(self, args_str: str):
return
# Second dialog: Configure no_confirm for selected servers
- no_confirm_checkboxes = [Checkbox() for _ in selected_server_names]
- dialog_body_elements = []
- for i, server_name in enumerate(selected_server_names):
- dialog_body_elements.append(
- HSplit(
- [
- Label(text=f"{server_name:<30}"), # Adjust width as needed
- Label(text="No Confirm:"),
- no_confirm_checkboxes[i],
- ]
- )
- )
+ no_confirm_choices = [(name, name) for name in selected_server_names]
- dialog_content = VSplit(dialog_body_elements, padding=1)
-
- def ok_handler_no_confirm():
- servers_with_no_confirm_config = []
- for i, server_name in enumerate(selected_server_names):
- servers_with_no_confirm_config.append(
- {
- "name": server_name,
- "no_confirm": no_confirm_checkboxes[i].checked,
- }
- )
- return servers_with_no_confirm_config
-
- def final_ok_handler_no_confirm():
- selected_config = ok_handler_no_confirm()
- get_app().exit(result=selected_config)
-
- def final_cancel_handler_no_confirm():
- get_app().exit(result=None)
-
- dialog_container_no_confirm = Dialog(
+ # checkboxlist_dialog returns None if Cancel is chosen
+ servers_for_no_confirm = checkboxlist_dialog(
title=f"Create MCP Profile: {profile_name} (Step 2/2)",
- body=dialog_content,
- buttons=[
- Button(text="OK", handler=final_ok_handler_no_confirm),
- Button(text="Cancel", handler=final_cancel_handler_no_confirm),
- ],
- width=D(preferred=80), # Adjust width as needed
- )
+ 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
- kb_no_confirm = KeyBindings()
- @kb_no_confirm.add("escape", eager=True)
- @kb_no_confirm.add("c-c", eager=True)
- @kb_no_confirm.add("c-q", eager=True)
- def _(event):
- event.app.exit(result=None)
-
- application_no_confirm = Application(
- layout=Layout(dialog_container_no_confirm),
- key_bindings=kb_no_confirm,
- mouse_support=True,
- full_screen=True,
- )
+ 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,
+ }
+ )
- final_selected_servers_config = application_no_confirm.run()
+ 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).")
- if final_selected_servers_config:
- 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).")
- else:
- self.io.tool_output("Profile creation cancelled during no_confirm configuration.")
else:
self.io.tool_error("Usage: /mcp new ")
From 779b43e06a81a5006b933035399b3329f34bc758 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Wed, 28 May 2025 18:20:47 +0200
Subject: [PATCH 33/51] feat: Add /mcp rm command to delete profiles
---
aider/commands.py | 10 ++++++++--
aider/mcp/mcp_profile_manager.py | 15 +++++++++++++++
2 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/aider/commands.py b/aider/commands.py
index bf349378733..930d3448a3f 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -1623,8 +1623,8 @@ def cmd_mcp(self, args_str: str):
self.io.tool_output(" /mcp new - Create a new MCP profile")
self.io.tool_output(" /mcp enable - Enable an MCP profile")
self.io.tool_output(" /mcp disable - Disable the active MCP profile")
+ self.io.tool_output(" /mcp rm - Delete an MCP profile")
# Add more subcommands as they are implemented
- # self.io.tool_output(" /mcp delete ")
# self.io.tool_output(" /mcp add ")
# self.io.tool_output(" /mcp remove ")
return
@@ -1695,9 +1695,15 @@ def cmd_mcp(self, args_str: str):
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 ")
else:
self.io.tool_error(f"Unknown /mcp subcommand: {sub_command}")
- self.io.tool_output("Valid subcommands are: new, enable, disable")
+ self.io.tool_output("Valid subcommands are: new, enable, disable, rm")
def cmd_copy_context(self, args=None):
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 8840c7bcad4..9ea0d46a9a5 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -257,6 +257,21 @@ def create_new_profile(self, profile_name: str, selected_servers_config: List[Di
save_mcp_profiles(self.profiles)
self.io.tool_output(f"MCP Profile '{profile_name}' created and saved.")
+ def delete_profile(self, profile_name: str):
+ if profile_name == "all":
+ self.io.tool_error("The 'all' profile cannot be deleted.")
+ return
+
+ if profile_name not in self.profiles:
+ self.io.tool_error(f"Profile '{profile_name}' not found.")
+ return
+
+ if self.active_profile_name == profile_name:
+ self.disable_profile() # Ensure it's disabled if it was active
+
+ del self.profiles[profile_name]
+ save_mcp_profiles(self.profiles)
+ self.io.tool_output(f"MCP profile '{profile_name}' deleted.")
def is_server_no_confirm(self, server_name_to_check: str) -> bool:
"""
From 72bfd95136c5d55c49439a16dd22ea7ef7ed6657 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 14:58:37 +0200
Subject: [PATCH 34/51] feat: Persist active MCP profile name in config
---
aider/mcp/mcp_profile_manager.py | 75 ++++++++++++++++++++++----------
1 file changed, 52 insertions(+), 23 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 9ea0d46a9a5..b95fa77868a 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -18,6 +18,9 @@
SERVER_NO_CONFIRM_KEY = "no_confirm"
# Old key, for removal during load if present
OLD_PROFILE_SERVER_NAMES_KEY = "server_names"
+# New top-level keys for the YAML file structure
+YAML_PROFILES_KEY = "profiles"
+YAML_ACTIVE_PROFILE_KEY = "active_profile_name"
@dataclass
@@ -25,14 +28,34 @@ class MCPProfile:
name: str
servers: List[Dict[str, Any]] # Each dict: {'name': str, 'no_confirm': bool}
-def load_mcp_profiles() -> Dict[str, MCPProfile]:
+def load_mcp_profiles() -> Tuple[Dict[str, MCPProfile], Optional[str]]:
+ profiles: Dict[str, MCPProfile] = {}
+ persisted_active_profile_name: Optional[str] = None
try:
with open(MCP_PROFILES_YAML_PATH, 'r') as f:
- profiles_data = yaml.safe_load(f)
- if isinstance(profiles_data, list):
- profiles = {}
- for profile_dict in profiles_data:
- if not isinstance(profile_dict, dict):
+ raw_data = yaml.safe_load(f)
+
+ profiles_list_from_yaml = []
+
+ if isinstance(raw_data, dict):
+ persisted_active_profile_name = raw_data.get(YAML_ACTIVE_PROFILE_KEY)
+ profiles_list_from_yaml = raw_data.get(YAML_PROFILES_KEY, [])
+ if not isinstance(profiles_list_from_yaml, list):
+ print(f"Warning: '{YAML_PROFILES_KEY}' key in MCP profiles YAML at {MCP_PROFILES_YAML_PATH} is not a list. Profiles section will be ignored.")
+ profiles_list_from_yaml = []
+ elif isinstance(raw_data, list): # Old format compatibility
+ profiles_list_from_yaml = raw_data
+ # persisted_active_profile_name remains None
+ elif raw_data is None: # File is empty
+ # profiles_list_from_yaml remains [], persisted_active_profile_name remains None
+ pass
+ else: # File content is not a list or dict as expected
+ print(f"Warning: MCP profiles YAML content at {MCP_PROFILES_YAML_PATH} is not a list or dictionary. File will be treated as empty.")
+ # profiles_list_from_yaml remains [], persisted_active_profile_name remains None
+
+ # Process the extracted list of profiles
+ for profile_dict in profiles_list_from_yaml:
+ if not isinstance(profile_dict, dict):
print(f"Warning: Skipping non-dictionary profile entry: {profile_dict}")
continue
try:
@@ -87,25 +110,23 @@ def load_mcp_profiles() -> Dict[str, MCPProfile]:
except TypeError as e: # Catches issues like missing 'name' or other fields for MCPProfile
profile_name_display = profile_dict.get(PROFILE_NAME_KEY, str(profile_dict))
print(f"Warning: Skipping malformed profile entry for '{profile_name_display}': {e}")
- return profiles
- elif profiles_data is None: # File is empty
- return {}
- else: # File content is not a list as expected
- # Potentially log this
- print(f"Warning: MCP profiles YAML content is not a list: {MCP_PROFILES_YAML_PATH}")
- return {}
+ return profiles, persisted_active_profile_name
except FileNotFoundError:
- return {}
+ return {}, None
except yaml.YAMLError as e:
# Potentially log this, e.g., using io if available
print(f"Warning: Error parsing MCP profiles YAML: {e}")
- return {}
+ return {}, None
-def save_mcp_profiles(profiles: Dict[str, MCPProfile]):
+def save_mcp_profiles(profiles: Dict[str, MCPProfile], active_profile_name: Optional[str]):
try:
profiles_list = [asdict(profile) for profile in profiles.values()]
+ data_to_save = {YAML_PROFILES_KEY: profiles_list}
+ if active_profile_name is not None:
+ data_to_save[YAML_ACTIVE_PROFILE_KEY] = active_profile_name
+
with open(MCP_PROFILES_YAML_PATH, 'w') as f:
- yaml.dump(profiles_list, f, sort_keys=False)
+ yaml.dump(data_to_save, f, sort_keys=False)
except (IOError, yaml.YAMLError) as e:
# Potentially log this, e.g., using io if available
print(f"Warning: Error saving MCP profiles YAML: {e}")
@@ -171,10 +192,11 @@ def __init__(self, io, settings):
self.settings = settings
self.profiles: Dict[str, MCPProfile] = {}
self.active_profile_name: Optional[str] = None
+ self.persisted_active_profile_name: Optional[str] = None # For storing loaded active profile name
self.active_mcp_client_pool: Optional[MCPClientPool] = None
def load_or_initialize_profiles(self):
- self.profiles = load_mcp_profiles()
+ self.profiles, self.persisted_active_profile_name = load_mcp_profiles()
mcpservers_arg_val = getattr(self.settings, 'mcp_servers', None)
mcpservers_file_arg_val = getattr(self.settings, 'mcp_servers_file', None)
@@ -223,7 +245,9 @@ def load_or_initialize_profiles(self):
self.io.tool_output("Updated MCP profile 'all' with current server configurations (names or no_confirm values changed).")
if needs_save:
- save_mcp_profiles(self.profiles)
+ # self.active_profile_name is None at this point, so this will either
+ # not write the active_profile_name key or write it as null if it was newly created.
+ save_mcp_profiles(self.profiles, self.active_profile_name)
def get_profile(self, name: str) -> Optional[MCPProfile]:
return self.profiles.get(name)
@@ -254,7 +278,7 @@ def create_new_profile(self, profile_name: str, selected_servers_config: List[Di
new_profile = MCPProfile(name=profile_name, servers=selected_servers_config)
self.profiles[profile_name] = new_profile
- save_mcp_profiles(self.profiles)
+ save_mcp_profiles(self.profiles, self.active_profile_name)
self.io.tool_output(f"MCP Profile '{profile_name}' created and saved.")
def delete_profile(self, profile_name: str):
@@ -267,10 +291,12 @@ def delete_profile(self, profile_name: str):
return
if self.active_profile_name == profile_name:
- self.disable_profile() # Ensure it's disabled if it was active
+ self.disable_profile() # This will also save the state with active_profile_name as None
del self.profiles[profile_name]
- save_mcp_profiles(self.profiles)
+ # Save the deletion of the profile, preserving the current active_profile_name
+ # (which would be None if the deleted profile was the active one).
+ save_mcp_profiles(self.profiles, self.active_profile_name)
self.io.tool_output(f"MCP profile '{profile_name}' deleted.")
def is_server_no_confirm(self, server_name_to_check: str) -> bool:
@@ -366,6 +392,7 @@ def enable_profile(self, profile_name: str, main_model, main_edit_format):
self.active_mcp_client_pool = pool
self.active_profile_name = profile_name
+ save_mcp_profiles(self.profiles, self.active_profile_name)
self.io.tool_output(f"MCP profile '{profile_name}' enabled with {len(matched_server_configs)} server(s).")
def disable_profile(self):
@@ -376,8 +403,10 @@ def disable_profile(self):
self.active_mcp_client_pool.shutdown()
self.active_mcp_client_pool = None
- if self.active_profile_name:
+ was_active = self.active_profile_name is not None
+ if was_active:
self.io.tool_output(f"MCP profile '{self.active_profile_name}' disabled.")
self.active_profile_name = None
+ save_mcp_profiles(self.profiles, self.active_profile_name)
else:
self.io.tool_output("No MCP profile was active.")
From 4b61383bb599217d2e41f29001f788f022a932ab Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 14:59:11 +0200
Subject: [PATCH 35/51] fix: Fix indentation in profile loading
---
aider/mcp/mcp_profile_manager.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index b95fa77868a..b0491dc3574 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -56,10 +56,10 @@ def load_mcp_profiles() -> Tuple[Dict[str, MCPProfile], Optional[str]]:
# Process the extracted list of profiles
for profile_dict in profiles_list_from_yaml:
if not isinstance(profile_dict, dict):
- print(f"Warning: Skipping non-dictionary profile entry: {profile_dict}")
- continue
- try:
- # Ensure servers is a list of dicts with name and no_confirm.
+ print(f"Warning: Skipping non-dictionary profile entry: {profile_dict}")
+ continue
+ try:
+ # Ensure servers is a list of dicts with name and no_confirm.
loaded_servers_data = profile_dict.get(PROFILE_SERVERS_KEY)
parsed_servers_list = []
profile_name_for_error = profile_dict.get(PROFILE_NAME_KEY, 'UnknownProfile')
From d34cc55c9d61d83bdbad6a50316f515654113949 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 14:59:23 +0200
Subject: [PATCH 36/51] fix: Adjust indentation for profile loading error
handling
---
aider/mcp/mcp_profile_manager.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index b0491dc3574..3eb1242bffc 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -107,9 +107,9 @@ def load_mcp_profiles() -> Tuple[Dict[str, MCPProfile], Optional[str]]:
profile = MCPProfile(**profile_dict)
profiles[profile.name] = profile
- except TypeError as e: # Catches issues like missing 'name' or other fields for MCPProfile
- profile_name_display = profile_dict.get(PROFILE_NAME_KEY, str(profile_dict))
- print(f"Warning: Skipping malformed profile entry for '{profile_name_display}': {e}")
+ except TypeError as e: # Catches issues like missing 'name' or other fields for MCPProfile
+ profile_name_display = profile_dict.get(PROFILE_NAME_KEY, str(profile_dict))
+ print(f"Warning: Skipping malformed profile entry for '{profile_name_display}': {e}")
return profiles, persisted_active_profile_name
except FileNotFoundError:
return {}, None
From 97a1673c48011bdf1dae2ae94ef9942e4295f2df Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 15:02:41 +0200
Subject: [PATCH 37/51] feat: Add /mcp persist command
---
aider/commands.py | 8 +++++++-
aider/mcp/mcp_profile_manager.py | 8 ++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/aider/commands.py b/aider/commands.py
index 930d3448a3f..14d7ec67519 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -1624,6 +1624,7 @@ def cmd_mcp(self, args_str: str):
self.io.tool_output(" /mcp enable - Enable an MCP profile")
self.io.tool_output(" /mcp disable - Disable the active MCP profile")
self.io.tool_output(" /mcp rm - Delete an MCP profile")
+ self.io.tool_output(" /mcp persist - Save the active MCP profile as the default for next launch")
# Add more subcommands as they are implemented
# self.io.tool_output(" /mcp add ")
# self.io.tool_output(" /mcp remove ")
@@ -1701,9 +1702,14 @@ def cmd_mcp(self, args_str: str):
self.mcp_profile_manager.delete_profile(profile_name)
else:
self.io.tool_error("Usage: /mcp rm ")
+ elif sub_command == "persist":
+ if len(args) == 1:
+ self.mcp_profile_manager.persist_active_profile()
+ else:
+ self.io.tool_error("Usage: /mcp persist")
else:
self.io.tool_error(f"Unknown /mcp subcommand: {sub_command}")
- self.io.tool_output("Valid subcommands are: new, enable, disable, rm")
+ self.io.tool_output("Valid subcommands are: new, enable, disable, rm, persist")
def cmd_copy_context(self, args=None):
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 3eb1242bffc..31c1e7a0187 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -327,6 +327,14 @@ def get_tool_prompt_if_active(self) -> Optional[str]:
return CoderPrompts.tool_prompt
return None
+ def persist_active_profile(self):
+ """Saves the currently active MCP profile name to the configuration file."""
+ if self.active_profile_name:
+ save_mcp_profiles(self.profiles, self.active_profile_name)
+ self.io.tool_output(f"MCP profile '{self.active_profile_name}' persisted as active.")
+ else:
+ self.io.tool_output("No active MCP profile to persist.")
+
def enable_profile(self, profile_name: str, main_model, main_edit_format):
if self.active_profile_name == profile_name and self.active_mcp_client_pool:
self.io.tool_output(f"MCP profile '{profile_name}' is already active.")
From 0b518058aa59257228258332fa719b409fa1d0ef Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 15:03:27 +0200
Subject: [PATCH 38/51] fix: Remove duplicated lines in
MCPProfileManager.__init__
---
aider/mcp/mcp_profile_manager.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 31c1e7a0187..5ad486f6cb4 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -194,6 +194,11 @@ def __init__(self, io, settings):
self.active_profile_name: Optional[str] = None
self.persisted_active_profile_name: Optional[str] = None # For storing loaded active profile name
self.active_mcp_client_pool: Optional[MCPClientPool] = None
+ self.settings = settings
+ self.profiles: Dict[str, MCPProfile] = {}
+ self.active_profile_name: Optional[str] = None
+ self.persisted_active_profile_name: Optional[str] = None # For storing loaded active profile name
+ self.active_mcp_client_pool: Optional[MCPClientPool] = None
def load_or_initialize_profiles(self):
self.profiles, self.persisted_active_profile_name = load_mcp_profiles()
From e8d33eaeb9990d5201222c00c3458e4e9330274c Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 15:19:49 +0200
Subject: [PATCH 39/51] refactor: Rename YAML_ACTIVE_PROFILE_KEY to
YAML_DEFAULT_PROFILE_KEY
---
aider/mcp/mcp_profile_manager.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 5ad486f6cb4..f63ca35e85c 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -20,7 +20,7 @@
OLD_PROFILE_SERVER_NAMES_KEY = "server_names"
# New top-level keys for the YAML file structure
YAML_PROFILES_KEY = "profiles"
-YAML_ACTIVE_PROFILE_KEY = "active_profile_name"
+YAML_DEFAULT_PROFILE_KEY = "default_profile_name"
@dataclass
@@ -38,7 +38,7 @@ def load_mcp_profiles() -> Tuple[Dict[str, MCPProfile], Optional[str]]:
profiles_list_from_yaml = []
if isinstance(raw_data, dict):
- persisted_active_profile_name = raw_data.get(YAML_ACTIVE_PROFILE_KEY)
+ persisted_active_profile_name = raw_data.get(YAML_DEFAULT_PROFILE_KEY)
profiles_list_from_yaml = raw_data.get(YAML_PROFILES_KEY, [])
if not isinstance(profiles_list_from_yaml, list):
print(f"Warning: '{YAML_PROFILES_KEY}' key in MCP profiles YAML at {MCP_PROFILES_YAML_PATH} is not a list. Profiles section will be ignored.")
@@ -123,7 +123,7 @@ def save_mcp_profiles(profiles: Dict[str, MCPProfile], active_profile_name: Opti
profiles_list = [asdict(profile) for profile in profiles.values()]
data_to_save = {YAML_PROFILES_KEY: profiles_list}
if active_profile_name is not None:
- data_to_save[YAML_ACTIVE_PROFILE_KEY] = active_profile_name
+ data_to_save[YAML_DEFAULT_PROFILE_KEY] = active_profile_name
with open(MCP_PROFILES_YAML_PATH, 'w') as f:
yaml.dump(data_to_save, f, sort_keys=False)
From 7946b468cafbb5ae5331ec911c7d027950fc0448 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 15:24:40 +0200
Subject: [PATCH 40/51] feat: Enable default MCP profile on startup
---
aider/main.py | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/aider/main.py b/aider/main.py
index 161a2bf6f01..3cc0a3d68a5 100644
--- a/aider/main.py
+++ b/aider/main.py
@@ -969,6 +969,19 @@ def get_io(pretty):
# Track auto-commits configuration
analytics.event("auto_commits", enabled=bool(args.auto_commits))
+ # 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,
From 33be77f64dfc4c3c592b6bcaf8e53f2b9eb00fa1 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 15:27:37 +0200
Subject: [PATCH 41/51] feat: add /mcp persist clear command
---
aider/commands.py | 7 +++++--
aider/mcp/mcp_profile_manager.py | 15 ++++++++++++++-
2 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/aider/commands.py b/aider/commands.py
index 14d7ec67519..9d496544707 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -1625,6 +1625,7 @@ def cmd_mcp(self, args_str: str):
self.io.tool_output(" /mcp disable - Disable the active MCP profile")
self.io.tool_output(" /mcp rm - 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")
# Add more subcommands as they are implemented
# self.io.tool_output(" /mcp add ")
# self.io.tool_output(" /mcp remove ")
@@ -1703,10 +1704,12 @@ def cmd_mcp(self, args_str: str):
else:
self.io.tool_error("Usage: /mcp rm ")
elif sub_command == "persist":
- if len(args) == 1:
+ 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")
+ 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, enable, disable, rm, persist")
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index f63ca35e85c..99dcb820af0 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -336,10 +336,23 @@ def persist_active_profile(self):
"""Saves the currently active MCP profile name to the configuration file."""
if self.active_profile_name:
save_mcp_profiles(self.profiles, self.active_profile_name)
- self.io.tool_output(f"MCP profile '{self.active_profile_name}' persisted as active.")
+ self.persisted_active_profile_name = self.active_profile_name
+ self.io.tool_output(f"MCP profile '{self.active_profile_name}' persisted as default.")
else:
self.io.tool_output("No active MCP profile to persist.")
+ def clear_persisted_default_profile(self):
+ """Clears the persisted default MCP profile name from the configuration file."""
+ if self.persisted_active_profile_name is None:
+ self.io.tool_output("No default MCP profile is currently persisted.")
+ return
+
+ cleared_profile_name = self.persisted_active_profile_name
+ self.persisted_active_profile_name = None
+ save_mcp_profiles(self.profiles, None) # Pass None to remove the key
+ self.io.tool_output(f"Cleared persisted default MCP profile '{cleared_profile_name}'.")
+
+
def enable_profile(self, profile_name: str, main_model, main_edit_format):
if self.active_profile_name == profile_name and self.active_mcp_client_pool:
self.io.tool_output(f"MCP profile '{profile_name}' is already active.")
From 12dee185c10bd109f9a796e387dc10fdc0de9feb Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 16:43:39 +0200
Subject: [PATCH 42/51] feat: Add support for enabling specific tools per
server in profiles
---
aider/mcp/mcp_profile_manager.py | 76 +++++++++++++++++++++++++++++---
1 file changed, 69 insertions(+), 7 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 99dcb820af0..2342886801b 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -16,6 +16,7 @@
PROFILE_SERVERS_KEY = "servers"
SERVER_NAME_KEY = "name"
SERVER_NO_CONFIRM_KEY = "no_confirm"
+PROFILE_SERVER_ENABLED_TOOLS_KEY = "enabled_tools" # New key for server entry in profile
# Old key, for removal during load if present
OLD_PROFILE_SERVER_NAMES_KEY = "server_names"
# New top-level keys for the YAML file structure
@@ -26,7 +27,7 @@
@dataclass
class MCPProfile:
name: str
- servers: List[Dict[str, Any]] # Each dict: {'name': str, 'no_confirm': bool}
+ servers: List[Dict[str, Any]] # Each dict: {'name': str, 'no_confirm': bool, 'enabled_tools': Optional[List[str]]}
def load_mcp_profiles() -> Tuple[Dict[str, MCPProfile], Optional[str]]:
profiles: Dict[str, MCPProfile] = {}
@@ -66,8 +67,13 @@ def load_mcp_profiles() -> Tuple[Dict[str, MCPProfile], Optional[str]]:
if isinstance(loaded_servers_data, list):
for server_entry in loaded_servers_data:
- if isinstance(server_entry, str):
- parsed_servers_list.append({SERVER_NAME_KEY: server_entry, SERVER_NO_CONFIRM_KEY: False})
+ current_server_dict = {}
+ if isinstance(server_entry, str): # server_entry is just a name
+ current_server_dict = {
+ SERVER_NAME_KEY: server_entry,
+ SERVER_NO_CONFIRM_KEY: False,
+ PROFILE_SERVER_ENABLED_TOOLS_KEY: None # Default for string-only entry
+ }
elif isinstance(server_entry, dict):
s_name = server_entry.get(SERVER_NAME_KEY)
if not isinstance(s_name, str) or not s_name:
@@ -77,10 +83,25 @@ def load_mcp_profiles() -> Tuple[Dict[str, MCPProfile], Optional[str]]:
if not isinstance(s_no_confirm, bool):
print(f"Warning: '{SERVER_NO_CONFIRM_KEY}' for server '{s_name}' in profile '{profile_name_for_error}' is not a boolean (type: {type(s_no_confirm)}). Defaulting to False.")
s_no_confirm = False
- parsed_servers_list.append({SERVER_NAME_KEY: s_name, SERVER_NO_CONFIRM_KEY: s_no_confirm})
+
+ current_server_dict = {
+ SERVER_NAME_KEY: s_name,
+ SERVER_NO_CONFIRM_KEY: s_no_confirm
+ }
+
+ enabled_tools_val = server_entry.get(PROFILE_SERVER_ENABLED_TOOLS_KEY)
+ if enabled_tools_val is not None: # Key is present
+ if isinstance(enabled_tools_val, list) and all(isinstance(tool_name, str) for tool_name in enabled_tools_val):
+ current_server_dict[PROFILE_SERVER_ENABLED_TOOLS_KEY] = enabled_tools_val
+ else:
+ print(f"Warning: '{PROFILE_SERVER_ENABLED_TOOLS_KEY}' for server '{s_name}' in profile '{profile_name_for_error}' is invalid (must be a list of strings). Defaulting to all tools enabled for this server in this profile.")
+ current_server_dict[PROFILE_SERVER_ENABLED_TOOLS_KEY] = None
+ else: # Key is not present
+ current_server_dict[PROFILE_SERVER_ENABLED_TOOLS_KEY] = None
else:
print(f"Warning: Invalid server entry type in profile '{profile_name_for_error}': {server_entry} (type: {type(server_entry)}). Skipping this server.")
continue
+ parsed_servers_list.append(current_server_dict)
elif loaded_servers_data is not None: # It was present but not a list
print(
f"Warning: '{PROFILE_SERVERS_KEY}' for profile '{profile_name_for_error}' is not a list"
@@ -120,8 +141,23 @@ def load_mcp_profiles() -> Tuple[Dict[str, MCPProfile], Optional[str]]:
def save_mcp_profiles(profiles: Dict[str, MCPProfile], active_profile_name: Optional[str]):
try:
- profiles_list = [asdict(profile) for profile in profiles.values()]
- data_to_save = {YAML_PROFILES_KEY: profiles_list}
+ profiles_list_to_save = []
+ for profile_obj in profiles.values():
+ profile_dict = asdict(profile_obj) # Converts MCPProfile to dict
+
+ # Ensure 'servers' key exists and is a list before iterating
+ if PROFILE_SERVERS_KEY in profile_dict and isinstance(profile_dict[PROFILE_SERVERS_KEY], list):
+ processed_servers = []
+ for server_conf_dict in profile_dict[PROFILE_SERVERS_KEY]:
+ # server_conf_dict is a new dict created by asdict
+ if PROFILE_SERVER_ENABLED_TOOLS_KEY in server_conf_dict and \
+ server_conf_dict[PROFILE_SERVER_ENABLED_TOOLS_KEY] is None:
+ del server_conf_dict[PROFILE_SERVER_ENABLED_TOOLS_KEY]
+ processed_servers.append(server_conf_dict)
+ profile_dict[PROFILE_SERVERS_KEY] = processed_servers
+ profiles_list_to_save.append(profile_dict)
+
+ data_to_save = {YAML_PROFILES_KEY: profiles_list_to_save}
if active_profile_name is not None:
data_to_save[YAML_DEFAULT_PROFILE_KEY] = active_profile_name
@@ -227,7 +263,11 @@ def load_or_initialize_profiles(self):
)
if existing_server_config:
no_confirm_val = existing_server_config.get(SERVER_NO_CONFIRM_KEY, False)
- new_all_profile_servers_list.append({SERVER_NAME_KEY: s_name, SERVER_NO_CONFIRM_KEY: no_confirm_val})
+ new_all_profile_servers_list.append({
+ SERVER_NAME_KEY: s_name,
+ SERVER_NO_CONFIRM_KEY: no_confirm_val,
+ PROFILE_SERVER_ENABLED_TOOLS_KEY: None # 'all' profile enables all tools by default
+ })
# Sort for consistent comparison later
new_all_profile_servers_list_sorted = sorted(new_all_profile_servers_list, key=lambda x: x[SERVER_NAME_KEY])
@@ -323,6 +363,28 @@ def is_server_no_confirm(self, server_name_to_check: str) -> bool:
return False # Server not found in profile or no_confirm not set
+ def get_enabled_tools_for_server(self, server_name_to_check: str) -> Optional[List[str]]:
+ """
+ Returns the list of enabled tool names for a specific server in the
+ currently active profile. Returns None if all tools are implicitly enabled
+ (i.e., 'enabled_tools' was not specified or was null for this server in the profile).
+ Returns an empty list if the server is found but explicitly has no tools enabled (e.g. enabled_tools: []).
+ Returns None if the profile is not active or the server is not found in the active profile.
+ """
+ if not self.active_profile_name:
+ return None
+
+ active_profile = self.get_profile(self.active_profile_name)
+ if not active_profile:
+ return None
+
+ for server_config in active_profile.servers:
+ if server_config.get(SERVER_NAME_KEY) == server_name_to_check:
+ # .get will return None if PROFILE_SERVER_ENABLED_TOOLS_KEY is missing
+ return server_config.get(PROFILE_SERVER_ENABLED_TOOLS_KEY)
+
+ return None # Server not found in profile
+
def get_tool_prompt_if_active(self) -> Optional[str]:
"""
Returns the standard tool prompt string if an MCP profile is active,
From 0275fc66ac32f51c86f793ac1e34ab0399e417d3 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 19:01:51 +0200
Subject: [PATCH 43/51] feat: Add /mcp tools command to configure server tools
---
aider/commands.py | 15 ++++++++++-----
aider/mcp/mcp_profile_manager.py | 1 +
2 files changed, 11 insertions(+), 5 deletions(-)
diff --git a/aider/commands.py b/aider/commands.py
index 9d496544707..c1c65fc895f 100644
--- a/aider/commands.py
+++ b/aider/commands.py
@@ -1621,14 +1621,12 @@ def cmd_mcp(self, args_str: str):
self.io.tool_output("\nCommands:")
self.io.tool_output(" /mcp new - Create a new MCP profile")
+ self.io.tool_output(" /mcp tools - Configure enabled tools for a server in the active profile")
self.io.tool_output(" /mcp enable - Enable an MCP profile")
self.io.tool_output(" /mcp disable - Disable the active MCP profile")
self.io.tool_output(" /mcp rm - 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")
- # Add more subcommands as they are implemented
- # self.io.tool_output(" /mcp add ")
- # self.io.tool_output(" /mcp remove ")
return
sub_command = args[0]
@@ -1681,10 +1679,17 @@ def cmd_mcp(self, args_str: str):
)
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).")
+ # 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 ")
+
+ 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 ")
elif sub_command == "enable":
if len(args) > 1:
@@ -1712,7 +1717,7 @@ def cmd_mcp(self, args_str: str):
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, enable, disable, rm, persist")
+ self.io.tool_output("Valid subcommands are: new, tools, enable, disable, rm, persist")
def cmd_copy_context(self, args=None):
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 2342886801b..8eab37e828f 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -4,6 +4,7 @@
from dataclasses import dataclass, asdict
import yaml
import json # Added
+from prompt_toolkit.shortcuts import checkboxlist_dialog # Added
from aider.mcp import load_mcp_servers # Added
from aider.mcp.mcp_client_pool import MCPClientPool
from aider.coders.base_prompts import CoderPrompts
From 7be704922554ccea97a4284ee9623772eeeee500 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 19:02:10 +0200
Subject: [PATCH 44/51] feat: Add command to configure MCP server tools via
dialog
---
aider/mcp/mcp_profile_manager.py | 92 ++++++++++++++++++++++++++++++++
1 file changed, 92 insertions(+)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 8eab37e828f..52fcbbf8406 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -327,6 +327,98 @@ def create_new_profile(self, profile_name: str, selected_servers_config: List[Di
save_mcp_profiles(self.profiles, self.active_profile_name)
self.io.tool_output(f"MCP Profile '{profile_name}' created and saved.")
+ def configure_server_tools(self, target_server_name: str):
+ if not self.active_profile_name:
+ self.io.tool_error("No MCP profile is currently active. Enable a profile first with `/mcp enable `.")
+ return
+
+ active_profile = self.get_profile(self.active_profile_name)
+ if not active_profile:
+ # This case should ideally not happen if active_profile_name is set
+ self.io.tool_error(f"Internal error: Active profile '{self.active_profile_name}' data not found.")
+ return
+
+ server_config_in_profile = None
+ server_config_index = -1
+ for i, s_conf in enumerate(active_profile.servers):
+ if s_conf.get(SERVER_NAME_KEY) == target_server_name:
+ server_config_in_profile = s_conf
+ server_config_index = i
+ break
+
+ if not server_config_in_profile:
+ self.io.tool_error(f"Server '{target_server_name}' not found in active profile '{self.active_profile_name}'.")
+ return
+
+ if not self.active_mcp_client_pool:
+ self.io.tool_error(f"MCP client pool not initialized for active profile '{self.active_profile_name}'. Cannot fetch tool list.")
+ return
+
+ mcp_client = self.active_mcp_client_pool.get_client_by_name(target_server_name)
+ if not mcp_client:
+ self.io.tool_error(f"MCP client for server '{target_server_name}' not found in the active pool. It might have failed to connect.")
+ return
+
+ # Assuming mcp_client.tools_available is a list of tool definition dicts
+ # Each dict: {"type": "function", "function": {"name": "tool_name", ...}}
+ all_tool_names = []
+ if hasattr(mcp_client, 'tools_available') and mcp_client.tools_available:
+ for tool_def in mcp_client.tools_available:
+ if isinstance(tool_def, dict) and \
+ tool_def.get('type') == 'function' and \
+ isinstance(tool_def.get('function'), dict) and \
+ isinstance(tool_def['function'].get('name'), str):
+ all_tool_names.append(tool_def['function']['name'])
+
+ if not all_tool_names:
+ self.io.tool_output(f"No tools reported by server '{target_server_name}'. Nothing to configure.")
+ # Ensure enabled_tools is empty or None if no tools are available
+ if server_config_in_profile.get(PROFILE_SERVER_ENABLED_TOOLS_KEY) is not None:
+ server_config_in_profile[PROFILE_SERVER_ENABLED_TOOLS_KEY] = None # Or [] if explicit empty is preferred
+ save_mcp_profiles(self.profiles, self.active_profile_name)
+ self.io.tool_output(f"Cleared enabled tools for server '{target_server_name}' as it reports no available tools.")
+ return
+
+ all_tool_names.sort()
+
+ current_enabled_tools_in_profile = server_config_in_profile.get(PROFILE_SERVER_ENABLED_TOOLS_KEY)
+
+ pre_selected_tool_names = []
+ if current_enabled_tools_in_profile is None: # None means all tools are implicitly enabled
+ pre_selected_tool_names = list(all_tool_names)
+ else: # It's a list of explicitly enabled tools
+ pre_selected_tool_names = [
+ tool_name for tool_name in all_tool_names if tool_name in current_enabled_tools_in_profile
+ ]
+
+ choices = [(name, name) for name in all_tool_names]
+
+ # checkboxlist_dialog expects default_values to be a list of the *values* of the selected choices
+ selected_tools_from_dialog = checkboxlist_dialog(
+ title=f"Configure Tools for Server: {target_server_name}",
+ text=f"Select tools to enable for server '{target_server_name}' in profile '{self.active_profile_name}':\n(Space to toggle, Enter to confirm)",
+ values=choices,
+ default_values=pre_selected_tool_names # Pass the names that should be pre-checked
+ ).run()
+
+ if selected_tools_from_dialog is None: # User cancelled the dialog
+ self.io.tool_output("Tool configuration cancelled.")
+ return
+
+ # Update the profile
+ # If all tools are selected, store None to signify "all enabled by default"
+ # Otherwise, store the explicit list.
+ if set(selected_tools_from_dialog) == set(all_tool_names):
+ new_enabled_tools_for_profile = None
+ else:
+ new_enabled_tools_for_profile = selected_tools_from_dialog
+
+ active_profile.servers[server_config_index][PROFILE_SERVER_ENABLED_TOOLS_KEY] = new_enabled_tools_for_profile
+
+ save_mcp_profiles(self.profiles, self.active_profile_name)
+ self.io.tool_output(f"Tool configuration updated for server '{target_server_name}' in profile '{self.active_profile_name}'.")
+
+
def delete_profile(self, profile_name: str):
if profile_name == "all":
self.io.tool_error("The 'all' profile cannot be deleted.")
From 3de2f0d786be836b1a38f3870f4637e1bac808ae Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 19:11:52 +0200
Subject: [PATCH 45/51] fix: Find MCP client by iterating pool clients
---
aider/mcp/mcp_profile_manager.py | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 52fcbbf8406..ca55cf89137 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -354,9 +354,17 @@ def configure_server_tools(self, target_server_name: str):
self.io.tool_error(f"MCP client pool not initialized for active profile '{self.active_profile_name}'. Cannot fetch tool list.")
return
- mcp_client = self.active_mcp_client_pool.get_client_by_name(target_server_name)
+ mcp_client = None
+ # Assuming active_mcp_client_pool has a list of client objects (e.g., in an attribute like 'clients'),
+ # and each client object has a 'name' attribute.
+ if hasattr(self.active_mcp_client_pool, 'clients') and isinstance(self.active_mcp_client_pool.clients, list):
+ for client_in_pool in self.active_mcp_client_pool.clients:
+ if hasattr(client_in_pool, 'name') and client_in_pool.name == target_server_name:
+ mcp_client = client_in_pool
+ break
+
if not mcp_client:
- self.io.tool_error(f"MCP client for server '{target_server_name}' not found in the active pool. It might have failed to connect.")
+ self.io.tool_error(f"MCP client for server '{target_server_name}' not found in the active pool. It might have failed to connect, or the pool's client list attribute is not named 'clients'.")
return
# Assuming mcp_client.tools_available is a list of tool definition dicts
From e0b4495ed06324003ffbbc9343d687df8cd57d18 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 19:15:10 +0200
Subject: [PATCH 46/51] fix: Only persist default profile via /mcp persist
command
---
aider/mcp/mcp_profile_manager.py | 43 +++++++++++++++++++++-----------
1 file changed, 29 insertions(+), 14 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index ca55cf89137..3262d0e8e0c 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -291,9 +291,9 @@ def load_or_initialize_profiles(self):
self.io.tool_output("Updated MCP profile 'all' with current server configurations (names or no_confirm values changed).")
if needs_save:
- # self.active_profile_name is None at this point, so this will either
- # not write the active_profile_name key or write it as null if it was newly created.
- save_mcp_profiles(self.profiles, self.active_profile_name)
+ # self.active_profile_name is None at this point.
+ # We pass self.persisted_active_profile_name to preserve the loaded default.
+ save_mcp_profiles(self.profiles, self.persisted_active_profile_name)
def get_profile(self, name: str) -> Optional[MCPProfile]:
return self.profiles.get(name)
@@ -324,7 +324,7 @@ def create_new_profile(self, profile_name: str, selected_servers_config: List[Di
new_profile = MCPProfile(name=profile_name, servers=selected_servers_config)
self.profiles[profile_name] = new_profile
- save_mcp_profiles(self.profiles, self.active_profile_name)
+ save_mcp_profiles(self.profiles, self.persisted_active_profile_name)
self.io.tool_output(f"MCP Profile '{profile_name}' created and saved.")
def configure_server_tools(self, target_server_name: str):
@@ -383,7 +383,7 @@ def configure_server_tools(self, target_server_name: str):
# Ensure enabled_tools is empty or None if no tools are available
if server_config_in_profile.get(PROFILE_SERVER_ENABLED_TOOLS_KEY) is not None:
server_config_in_profile[PROFILE_SERVER_ENABLED_TOOLS_KEY] = None # Or [] if explicit empty is preferred
- save_mcp_profiles(self.profiles, self.active_profile_name)
+ save_mcp_profiles(self.profiles, self.persisted_active_profile_name)
self.io.tool_output(f"Cleared enabled tools for server '{target_server_name}' as it reports no available tools.")
return
@@ -423,7 +423,7 @@ def configure_server_tools(self, target_server_name: str):
active_profile.servers[server_config_index][PROFILE_SERVER_ENABLED_TOOLS_KEY] = new_enabled_tools_for_profile
- save_mcp_profiles(self.profiles, self.active_profile_name)
+ save_mcp_profiles(self.profiles, self.persisted_active_profile_name)
self.io.tool_output(f"Tool configuration updated for server '{target_server_name}' in profile '{self.active_profile_name}'.")
@@ -437,12 +437,20 @@ def delete_profile(self, profile_name: str):
return
if self.active_profile_name == profile_name:
- self.disable_profile() # This will also save the state with active_profile_name as None
+ # This will set self.active_profile_name = None in memory.
+ # The save call within disable_profile will use self.persisted_active_profile_name.
+ self.disable_profile()
+
+ profile_was_persisted_default = (self.persisted_active_profile_name == profile_name)
+
+ del self.profiles[profile_name] # Delete from in-memory dict
- del self.profiles[profile_name]
- # Save the deletion of the profile, preserving the current active_profile_name
- # (which would be None if the deleted profile was the active one).
- save_mcp_profiles(self.profiles, self.active_profile_name)
+ if profile_was_persisted_default:
+ self.persisted_active_profile_name = None # Update in-memory persisted name
+ self.io.tool_output(f"MCP profile '{profile_name}' was the persisted default and has been cleared as default.")
+
+ # Save the updated profiles list and the current (possibly updated) persisted_active_profile_name
+ save_mcp_profiles(self.profiles, self.persisted_active_profile_name)
self.io.tool_output(f"MCP profile '{profile_name}' deleted.")
def is_server_no_confirm(self, server_name_to_check: str) -> bool:
@@ -581,7 +589,10 @@ def enable_profile(self, profile_name: str, main_model, main_edit_format):
self.active_mcp_client_pool = pool
self.active_profile_name = profile_name
- save_mcp_profiles(self.profiles, self.active_profile_name)
+ # Enabling a profile does not change the persisted default profile.
+ # Only /mcp persist command does that.
+ # We still save the profiles list in case other changes (like 'all' profile update) occurred.
+ save_mcp_profiles(self.profiles, self.persisted_active_profile_name)
self.io.tool_output(f"MCP profile '{profile_name}' enabled with {len(matched_server_configs)} server(s).")
def disable_profile(self):
@@ -594,8 +605,12 @@ def disable_profile(self):
was_active = self.active_profile_name is not None
if was_active:
- self.io.tool_output(f"MCP profile '{self.active_profile_name}' disabled.")
+ disabled_profile_name = self.active_profile_name
self.active_profile_name = None
- save_mcp_profiles(self.profiles, self.active_profile_name)
+ # Disabling a profile does not change the persisted default profile.
+ # Only /mcp persist clear command does that.
+ # We still save the profiles list in case other changes occurred.
+ save_mcp_profiles(self.profiles, self.persisted_active_profile_name)
+ self.io.tool_output(f"MCP profile '{disabled_profile_name}' disabled.")
else:
self.io.tool_output("No MCP profile was active.")
From ca4bb576de6133f9455be6839457e2f3f27cfa69 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 19:20:42 +0200
Subject: [PATCH 47/51] fix: Check mcp_servers and clients attributes for MCP
pool
---
aider/mcp/mcp_profile_manager.py | 22 +++++++++++++++++-----
1 file changed, 17 insertions(+), 5 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 3262d0e8e0c..f4b320155e2 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -355,16 +355,28 @@ def configure_server_tools(self, target_server_name: str):
return
mcp_client = None
- # Assuming active_mcp_client_pool has a list of client objects (e.g., in an attribute like 'clients'),
- # and each client object has a 'name' attribute.
- if hasattr(self.active_mcp_client_pool, 'clients') and isinstance(self.active_mcp_client_pool.clients, list):
- for client_in_pool in self.active_mcp_client_pool.clients:
+ pool = self.active_mcp_client_pool
+
+ # Try iterating over 'mcp_servers' attribute if it's a list
+ if hasattr(pool, 'mcp_servers') and isinstance(pool.mcp_servers, list):
+ for client_in_pool in pool.mcp_servers:
if hasattr(client_in_pool, 'name') and client_in_pool.name == target_server_name:
mcp_client = client_in_pool
break
+ # Fallback: Try iterating over 'clients' attribute if it's a list (previous attempt)
+ if not mcp_client and hasattr(pool, 'clients') and isinstance(pool.clients, list):
+ for client_in_pool in pool.clients:
+ if hasattr(client_in_pool, 'name') and client_in_pool.name == target_server_name:
+ mcp_client = client_in_pool
+ break
+
if not mcp_client:
- self.io.tool_error(f"MCP client for server '{target_server_name}' not found in the active pool. It might have failed to connect, or the pool's client list attribute is not named 'clients'.")
+ self.io.tool_error(
+ f"MCP client for server '{target_server_name}' not found in the active pool. "
+ "It might have failed to connect, or the pool's internal client list attribute "
+ "is not recognized (tried 'mcp_servers', 'clients')."
+ )
return
# Assuming mcp_client.tools_available is a list of tool definition dicts
From 62fe67edf2acb6e67b9b75ebfce36538d94a93b6 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 19:22:52 +0200
Subject: [PATCH 48/51] fix: Access server tools from pool cache, not server
instance
---
aider/mcp/mcp_profile_manager.py | 45 ++++++++++++++++----------------
1 file changed, 22 insertions(+), 23 deletions(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index f4b320155e2..5e281b47a9d 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -354,36 +354,35 @@ def configure_server_tools(self, target_server_name: str):
self.io.tool_error(f"MCP client pool not initialized for active profile '{self.active_profile_name}'. Cannot fetch tool list.")
return
- mcp_client = None
pool = self.active_mcp_client_pool
+ # The existence of active_mcp_client_pool is already checked earlier in this function.
- # Try iterating over 'mcp_servers' attribute if it's a list
- if hasattr(pool, 'mcp_servers') and isinstance(pool.mcp_servers, list):
- for client_in_pool in pool.mcp_servers:
- if hasattr(client_in_pool, 'name') and client_in_pool.name == target_server_name:
- mcp_client = client_in_pool
- break
-
- # Fallback: Try iterating over 'clients' attribute if it's a list (previous attempt)
- if not mcp_client and hasattr(pool, 'clients') and isinstance(pool.clients, list):
- for client_in_pool in pool.clients:
- if hasattr(client_in_pool, 'name') and client_in_pool.name == target_server_name:
- mcp_client = client_in_pool
- break
-
- if not mcp_client:
+ if not hasattr(pool, 'cached_tools_by_server') or pool.cached_tools_by_server is None:
+ self.io.tool_error(
+ f"Tool information cache is not available for active profile '{self.active_profile_name}'. "
+ "Tools might not have been fetched successfully when the profile was enabled."
+ )
+ return
+
+ # Check if the target server was even part of the successfully initialized clients in the pool.
+ # pool.clients is Dict[str, McpServer], mapping server name to McpServer instance.
+ if not hasattr(pool, 'clients') or target_server_name not in pool.clients:
self.io.tool_error(
- f"MCP client for server '{target_server_name}' not found in the active pool. "
- "It might have failed to connect, or the pool's internal client list attribute "
- "is not recognized (tried 'mcp_servers', 'clients')."
+ f"Server '{target_server_name}' was not successfully initialized or found in the active MCP client pool. "
+ "Check server configuration or connection issues during profile enabling."
)
return
+
+ # Get tool definitions from the pool's cache for the specific server.
+ # This will be None if the server isn't in the cache (e.g., failed tool fetch, or reported no tools).
+ # It will be a list (possibly empty) if tools were fetched.
+ server_tool_definitions = pool.cached_tools_by_server.get(target_server_name)
- # Assuming mcp_client.tools_available is a list of tool definition dicts
- # Each dict: {"type": "function", "function": {"name": "tool_name", ...}}
all_tool_names = []
- if hasattr(mcp_client, 'tools_available') and mcp_client.tools_available:
- for tool_def in mcp_client.tools_available:
+ # Process server_tool_definitions if it's a non-empty list.
+ # The existing parsing logic for tool_def to populate all_tool_names follows.
+ if isinstance(server_tool_definitions, list) and server_tool_definitions:
+ for tool_def in server_tool_definitions:
if isinstance(tool_def, dict) and \
tool_def.get('type') == 'function' and \
isinstance(tool_def.get('function'), dict) and \
From 10bffa9a4e84a96983497b8f8115e8877de3442d Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 19:31:03 +0200
Subject: [PATCH 49/51] docs: Add comment explaining tool selection logic
---
aider/mcp/mcp_profile_manager.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 5e281b47a9d..9afa474483e 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -402,6 +402,10 @@ def configure_server_tools(self, target_server_name: str):
current_enabled_tools_in_profile = server_config_in_profile.get(PROFILE_SERVER_ENABLED_TOOLS_KEY)
+ # Determine which tools should be pre-selected in the dialog:
+ # - If current_enabled_tools_in_profile is None, all available tools are pre-selected.
+ # - Otherwise, only tools present in both all_tool_names (from server)
+ # and current_enabled_tools_in_profile (from saved profile) are pre-selected.
pre_selected_tool_names = []
if current_enabled_tools_in_profile is None: # None means all tools are implicitly enabled
pre_selected_tool_names = list(all_tool_names)
From 14b25266b5dbb67ef961697944adcf51135a468a Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 19:34:02 +0200
Subject: [PATCH 50/51] fix: Preserve enabled tools config for 'all' profile
---
aider/mcp/mcp_profile_manager.py | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/aider/mcp/mcp_profile_manager.py b/aider/mcp/mcp_profile_manager.py
index 9afa474483e..a5a3cd5de6e 100644
--- a/aider/mcp/mcp_profile_manager.py
+++ b/aider/mcp/mcp_profile_manager.py
@@ -264,10 +264,20 @@ def load_or_initialize_profiles(self):
)
if existing_server_config:
no_confirm_val = existing_server_config.get(SERVER_NO_CONFIRM_KEY, False)
+ # Preserve enabled_tools if it was set for this server in the old 'all' profile
+ # Defaults to None (all tools enabled) if not previously set or server is new
+ enabled_tools_val = existing_server_config.get(PROFILE_SERVER_ENABLED_TOOLS_KEY, None)
+ else:
+ # Server is new to the 'all' profile, or 'all' profile itself is new
+ enabled_tools_val = None
+ else:
+ # old_all_profile_data does not exist (first run or empty profiles file)
+ enabled_tools_val = None
+
new_all_profile_servers_list.append({
SERVER_NAME_KEY: s_name,
SERVER_NO_CONFIRM_KEY: no_confirm_val,
- PROFILE_SERVER_ENABLED_TOOLS_KEY: None # 'all' profile enables all tools by default
+ PROFILE_SERVER_ENABLED_TOOLS_KEY: enabled_tools_val
})
# Sort for consistent comparison later
From d849d786052bede728bc0bc1ad335f769b228688 Mon Sep 17 00:00:00 2001
From: "Your Name (aider)"
Date: Thu, 29 May 2025 20:43:01 +0200
Subject: [PATCH 51/51] feat: Filter MCP tools based on profile enabled
settings
fix: Fix TypeError in get_tool_list
---
aider/coders/base_coder.py | 2 +-
aider/mcp/mcp_client_pool.py | 109 ++++++++++++++++++++++++++++++++---
aider/mcp/tool_filter.py | 38 ++++++++++++
3 files changed, 139 insertions(+), 10 deletions(-)
create mode 100644 aider/mcp/tool_filter.py
diff --git a/aider/coders/base_coder.py b/aider/coders/base_coder.py
index 72ecb6346c8..b0742777455 100755
--- a/aider/coders/base_coder.py
+++ b/aider/coders/base_coder.py
@@ -1825,7 +1825,7 @@ async def _execute_all_tool_calls_async():
def get_tool_list(self) -> List[Dict[str, Any]]:
"""Get a flattened list of all MCP tools from the active profile."""
if self.mcp_profile_manager and self.mcp_profile_manager.active_mcp_client_pool:
- return self.mcp_profile_manager.active_mcp_client_pool.get_cached_tools_flat_list()
+ return self.mcp_profile_manager.active_mcp_client_pool.get_cached_tools_flat_list(self.mcp_profile_manager)
return []
def reply_completed(self):
diff --git a/aider/mcp/mcp_client_pool.py b/aider/mcp/mcp_client_pool.py
index 0a696491a37..d11d8f31684 100644
--- a/aider/mcp/mcp_client_pool.py
+++ b/aider/mcp/mcp_client_pool.py
@@ -1,8 +1,11 @@
import asyncio
import logging
-from typing import List, Dict, Any, Optional
+import asyncio
+import logging
+from typing import List, Dict, Any, Optional, Tuple
from aider.mcp.server import McpServer # Assuming McpServer is in this path
+from aider.mcp.tool_filter import filter_tools_for_server
class MCPClientPool:
@@ -119,17 +122,105 @@ async def fetch_and_cache_tools(self, context: Optional[Dict[str, Any]] = None):
self.io.tool_output(f"Fetched {num_tools} tool(s) from {num_servers} server(s).")
- def get_cached_tools_flat_list(self) -> List[Dict[str, Any]]:
+ def get_cached_tools_flat_list(self, mcp_profile_manager) -> List[Dict[str, Any]]:
+ """
+ Returns a flat list of tool definitions from all servers in the pool,
+ filtered by the enabled_tools setting in the active MCP profile for each server.
+ """
+ all_effective_tools_flat = []
+ if self.cached_tools_by_server and mcp_profile_manager and mcp_profile_manager.active_profile_name:
+ for server_name, tools_definitions_for_server in self.cached_tools_by_server.items():
+ if tools_definitions_for_server: # Ensure the list is not empty
+ # Get enabled tool names for this server from the active profile
+ enabled_tool_names = mcp_profile_manager.get_enabled_tools_for_server(server_name)
+
+ # Filter the tools for this server
+ filtered_server_tools = filter_tools_for_server(tools_definitions_for_server, enabled_tool_names)
+ all_effective_tools_flat.extend(filtered_server_tools)
+
+ # Basic deduplication by function name, preferring the first encountered.
+ # More sophisticated deduplication might be needed if tools can have same name but different defs.
+ seen_tool_names = set()
+ deduplicated_tools = []
+ for tool_def in all_effective_tools_flat:
+ try:
+ if isinstance(tool_def, dict) and isinstance(tool_def.get("function"), dict):
+ tool_name = tool_def["function"].get("name")
+ if tool_name and tool_name not in seen_tool_names:
+ seen_tool_names.add(tool_name)
+ deduplicated_tools.append(tool_def)
+ except (KeyError, TypeError):
+ continue # Skip malformed tool_def
+ return deduplicated_tools
+
+ def get_effective_tools_and_tool_choice(
+ self,
+ mcp_profile_manager, # MCPProfileManager instance
+ current_tool_choice: Optional[Any], # The tool_choice that would be used if no MCP filtering
+ io: Optional[Any] # For logging warnings
+ ) -> Tuple[List[Dict[str, Any]], Optional[Any]]:
"""
- Returns a flat list of all cached tool definitions.
+ Gets the list of MCP tool definitions, filtered by enabled status in the profile,
+ and logs a warning if current_tool_choice refers to a disabled MCP tool.
+
+ Args:
+ mcp_profile_manager: The MCPProfileManager to get enabled tool lists.
+ current_tool_choice: The tool_choice value that would be active.
+ io: InputOutput object for logging.
+
+ Returns:
+ A tuple containing:
+ - List of effective (filtered) MCP tool definitions.
+ - The original current_tool_choice (adjustment is for logging only here).
"""
- if not self.cached_tools_by_server:
- return []
+ effective_mcp_tools_flat_list = self.get_cached_tools_flat_list(mcp_profile_manager)
+
+ # Check if the current_tool_choice (if it's specific) refers to an MCP tool that got disabled.
+ if isinstance(current_tool_choice, dict) and current_tool_choice.get("type") == "function":
+ function_spec = current_tool_choice.get("function")
+ if isinstance(function_spec, dict):
+ chosen_tool_name = function_spec.get("name")
+ if chosen_tool_name:
+ # Check if this chosen tool was an MCP tool by seeing if it was defined by any MCP server
+ # (regardless of its current enabled status).
+ was_an_mcp_tool = False
+ if self.cached_tools_by_server:
+ for _, tools_definitions_for_server in self.cached_tools_by_server.items():
+ if tools_definitions_for_server:
+ for tool_def in tools_definitions_for_server:
+ try:
+ if isinstance(tool_def, dict) and \
+ isinstance(tool_def.get("function"), dict) and \
+ tool_def["function"].get("name") == chosen_tool_name:
+ was_an_mcp_tool = True
+ break
+ except (KeyError, TypeError):
+ continue
+ if was_an_mcp_tool:
+ break
+
+ if was_an_mcp_tool:
+ # Now check if this chosen MCP tool is in the *effective* (enabled) list
+ is_chosen_mcp_tool_enabled = False
+ for tool_def in effective_mcp_tools_flat_list:
+ try:
+ if isinstance(tool_def, dict) and \
+ isinstance(tool_def.get("function"), dict) and \
+ tool_def["function"].get("name") == chosen_tool_name:
+ is_chosen_mcp_tool_enabled = True
+ break
+ except (KeyError, TypeError):
+ continue
+
+ if not is_chosen_mcp_tool_enabled:
+ if io and hasattr(io, 'tool_warning'):
+ io.tool_warning(
+ f"Warning: Tool '{chosen_tool_name}' is specified by `tool_choice` but is not "
+ f"enabled for any server in the active MCP profile. "
+ f"The `tool_choice` may effectively be 'auto' or 'none' by the LLM."
+ )
- all_tools = []
- for server_tools in self.cached_tools_by_server.values():
- all_tools.extend(server_tools)
- return all_tools
+ return effective_mcp_tools_flat_list, current_tool_choice
def get_cached_tools_by_server(self) -> Optional[Dict[str, List[Dict[str, Any]]]]:
"""
diff --git a/aider/mcp/tool_filter.py b/aider/mcp/tool_filter.py
new file mode 100644
index 00000000000..eaa1ad53208
--- /dev/null
+++ b/aider/mcp/tool_filter.py
@@ -0,0 +1,38 @@
+from typing import List, Dict, Optional, Any
+
+def filter_tools_for_server(
+ original_tool_definitions: List[Dict[str, Any]],
+ enabled_tool_names: Optional[List[str]]
+) -> List[Dict[str, Any]]:
+ """
+ Filters a list of tool definitions based on a list of enabled tool names.
+
+ Args:
+ original_tool_definitions: The original list of tool definition dictionaries.
+ enabled_tool_names: An optional list of tool names that are enabled.
+ If None, all original_tool_definitions are considered enabled.
+
+ Returns:
+ A new list containing only the tool definitions that are enabled.
+ """
+ if enabled_tool_names is None:
+ # If enabled_tool_names is None, it means all tools are implicitly enabled for this server in the profile.
+ return list(original_tool_definitions) # Return a copy
+
+ filtered_tools = []
+ if original_tool_definitions: # Ensure there are tools to iterate over
+ for tool_def in original_tool_definitions:
+ try:
+ # Ensure tool_def is a dict and has the expected structure
+ if isinstance(tool_def, dict) and \
+ tool_def.get("type") == "function" and \
+ isinstance(tool_def.get("function"), dict) and \
+ isinstance(tool_def["function"].get("name"), str) and \
+ tool_def["function"]["name"] in enabled_tool_names:
+ filtered_tools.append(tool_def)
+ except (KeyError, TypeError):
+ # Handle cases where tool_def might not have the expected structure
+ # This might happen if tool definitions are malformed.
+ # Optionally, log a warning here if io is available.
+ continue
+ return filtered_tools