-
Notifications
You must be signed in to change notification settings - Fork 2.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add MCP support #3672
base: main
Are you sure you want to change the base?
Add MCP support #3672
Conversation
How to interpret the config in Here's what I know from Claude Desktop / Roo Cline:
|
For this, the config should be something like: mcp: true
mcp-servers:
- filesystem
mcp-server-command:
- "filesystem:node c:/Users/<user>/AppData/Roaming/npm/node_modules/@modelcontextprotocol/server-filesystem/dist/index.js D:/" But now you made me realize that I havent checked if it work on Windows... |
I checked your code, and I have to say - great job implementing this! While I’m not a Python developer and can’t comment on the threading aspect 😁, I do have some feedback. One suggestion: I’d avoid using the reflection message as the MCP tool’s response. If I’m not mistaken, only three reflections are allowed by default, which could limit the agent’s usability by restricting iterations. Changing From a UX perspective, I’d also reconsider how servers are defined in the config file. Something like this might be more intuitive:
|
I like the proposal by @wladimiiir for config formatting (no hypens). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General
Thank you for your PR and adding the ability to let aider use MCP :)
I think there is a potential problem with the queues/threading, but besides that (and your mentioned todos) your code looks very clean and nice to me.
Just some additional ideas:
- what about adding a new coder
/mcp
or/agent
to only add the tool prompt if I really want tools it in an ongoing conversation. There is always this overhead of adding the toolprompt with each request - is there a way to leverage native tool use via litellm and the OpenAI tool spec? i.e. LiteLLM plans to integrate a MCP Bridge which could maybe used: [Feature] MCP bridge support BerriAI/litellm#7934
aider/mcp/mcp_manager.py
Outdated
"""Sync call to the thread with queues.""" | ||
|
||
self.message_queue.put(CallArguments(server_name, function, args)) | ||
result = self.result_queue.get() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO I see a potential problem here.
All server threads read from the same message_queue
. Whichever thread calls get()
first, gets the message. If the server name does not match, that thread just calls task_done()
and discards it.
This means the correct server thread might never see that message—it’s already been consumed by the wrong thread.
aider/coders/base_coder.py
Outdated
@@ -1455,6 +1466,14 @@ def send_message(self, inp): | |||
self.reflected_message = add_rel_files_message | |||
return | |||
|
|||
tool_results = self.check_for_tool_calls(content) | |||
for tool_result in tool_results: | |||
if self.reflected_message: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As told already by others: max_reflections is the limit for consecutive tool calls.
while message:
self.reflected_message = None
list(self.send_message(message))
if not self.reflected_message:
break
if self.num_reflections >= self.max_reflections:
self.io.tool_warning(f"Only {self.max_reflections} reflections allowed, stopping.")
return
self.num_reflections += 1
message = self.reflected_message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What should be the correct way to implement that ? Since it behave almost exactly like a diff edit/a file inclusion,
should I implement a reflection limit / reflected_message only for the mcp tools ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say you could have another field self.mcp_tool_result_message = tool_result
instead of using self.reflected_message
. And then:
while message:
self.reflected_message = None
list(self.send_message(message))
if not self.reflected_message and not self.mcp_tool_result_message:
break
if self.num_reflections >= self.max_reflections:
self.io.tool_warning(f"Only {self.max_reflections} reflections allowed, stopping.")
return
if self.num_mcp_iterations >= self.max_mcp_iterations:
self.io.tool_warning(f"Only {self.max_mcp_iterations} MCP iterations allowed, stopping.")
return
if self.reflected_message:
self.num_reflections += 1
message = self.reflected_message
if self.mcp_tool_result_message:
self.num_mcp_iterations += 1
message = self.mcp_tool_result_message
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, it seem to work, maybe I'll all the max_mcp_iterations in the configuration too
aider/mcp/mcp_manager.py
Outdated
|
||
for server in self.get_enabled_servers(): | ||
if server.command: | ||
t = threading.Thread(target=self._server_loop, args=(server, loop)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You have one thread per MCP server, but because of the blocking behaviour of the queues the tool calls are made sync because the main thread is blocked. In addition there is the problem that all threads read from one queue (which leads to the problem only one thread can receive it atm).
Alternatives (provided by sonnet):
- Single AsyncIO Event Loop in a Background Thread
class AsyncioManager:
def __init__(self):
self.loop = asyncio.new_event_loop()
self.thread = threading.Thread(target=self._run_event_loop, daemon=True)
self.thread.start()
self.servers = {}
def _run_event_loop(self):
asyncio.set_event_loop(self.loop)
self.loop.run_forever()
def call_async(self, coro):
future = concurrent.futures.Future()
async def wrapper():
try:
result = await coro
future.set_result(result)
except Exception as e:
future.set_exception(e)
asyncio.run_coroutine_threadsafe(wrapper(), self.loop)
return future.result() # This blocks until result is available
- Asynchronous API with Callbacks
class AsyncToolManager:
def __init__(self):
self.loop = asyncio.new_event_loop()
self.thread = threading.Thread(target=self._run_event_loop, daemon=True)
self.thread.start()
def execute_tool(self, server_name, tool_name, arguments, callback):
"""Non-blocking tool execution with callback"""
async def run_tool():
result = await self._async_execute_tool(server_name, tool_name, arguments)
# Call the callback in the main thread
callback(result)
asyncio.run_coroutine_threadsafe(run_tool(), self.loop)
aider/coders/base_coder.py
Outdated
@@ -1106,6 +1108,11 @@ def fmt_system_prompt(self, prompt): | |||
) | |||
else: | |||
quad_backtick_reminder = "" | |||
|
|||
# Add MCP tools information if MCP is enabled |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The 2 sections could be combined to one, right?
prompt = prompt.format(
fence=self.fence,
quad_backtick_reminder=quad_backtick_reminder,
lazy_prompt=lazy_prompt,
platform=platform_text,
shell_cmd_prompt=shell_cmd_prompt,
shell_cmd_reminder=shell_cmd_reminder,
language=language,
)
if self.main_model.system_prompt_prefix:
prompt = self.main_model.system_prompt_prefix + prompt
# Add MCP tools information if MCP is enabled
if is_mcp_enabled():
mcp_tools_info = self.gpt_prompts.mcp_tools_prefix + "\n\n" + get_available_tools_prompt()
prompt += "\n\n" + mcp_tools_info
Remote task_done to avoid deadlock
hi @lutzleonhardt - I'm the litellm maintainer. We'd love to help aider support MCP. Here's our MCP bridge on the litellm python SDK: https://docs.litellm.ai/docs/mcp#litellm-python-sdk-mcp-bridge any thoughts on this ? how can we help you ? |
Add a mcp file configuration file Remove redundant enable in mcp server class Add num_mcp_iterations & max_mcp_iterations so it wont clash with max_reflections
Since the YAML configuration file cannot support a nested configuration structure, I added a mcp-configuration cli argument, that point to a YAML for MCP configuration specifically, eg: mcp-configuration: ~/.aider/mcp.yml In ~/.aider/mcp.yml servers:
- name: git-server
command: "python -m mcp_server_git"
env:
GIT_SERVER: server
permissions:
git_status: auto I removed redundant enable property in the MCP server class |
Can we have a fall back in the mcp config file names, like happens with a lot of other aider config files? That way there can be mcp config that's specific to a particular repo, but also config that is global. So the final mcp config would be ~/.aider/mcp.yml + ./.aider.mcp.yml |
Hi @ishaan-jaff |
…background, using non blocking message queue on the mcp side Fixed some problems with mcp tools permissions Use thread.join to gracefully stop thread
The new commit now aggregate all the session into one async while loop in a background thread. There is currently a problem I'm trying to debug where when we send a Ctrl-C (To cancel an llm call for example) it is sent to the mcp server too, and make it crash in a subsequent call. it cause issue with this one for example: https://github.com/modelcontextprotocol/servers/tree/main/src/git |
please do not invent a new format of configuring the MCP servers !!! |
no, please don't! again, the way to configure MCP servers is well established, and inventing a new configuration format will create endless headaches of incomplete translatability between classic and this new custom yaml way |
This is a rough implementation of MCP into Aider.
It currently support adding stdio MCP servers with this configuration in the ~/.aider.conf.yml
Todo: