-
Notifications
You must be signed in to change notification settings - Fork 41
[gh-392] Enable and disable mcp servers #398
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -139,7 +139,6 @@ class Coder: | |
| commit_language = None | ||
| file_watcher = None | ||
| mcp_manager = None | ||
| mcp_tools = None | ||
| run_one_completed = True | ||
| compact_context_completed = True | ||
| suppress_announcements_for_next_prompt = False | ||
|
|
@@ -228,6 +227,7 @@ async 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_manager=from_coder.mcp_manager, | ||
| ) | ||
| use_kwargs.update(update) # override to complete the switch | ||
| use_kwargs.update(kwargs) # override passed kwargs | ||
|
|
@@ -251,7 +251,6 @@ async def create( | |
| if from_coder: | ||
| if from_coder.mcp_manager: | ||
| res.mcp_manager = from_coder.mcp_manager | ||
| res.mcp_tools = from_coder.mcp_tools | ||
|
|
||
| # Transfer TUI app weak reference | ||
| res.tui = from_coder.tui | ||
|
|
@@ -2747,69 +2746,20 @@ async def _execute_all_tool_calls(): | |
|
|
||
| async 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. | ||
| Any setup that needs to happen for MCP Servers so that coder can use it properly | ||
| """ | ||
| # TODO(@gopar): refactor here once we have fully moved over to use the mcp manager | ||
| tools = [] | ||
|
|
||
| async def get_server_tools(server): | ||
| # Check if we already have tools for this server in mcp_tools | ||
| if self.mcp_tools: | ||
| for server_name, server_tools in self.mcp_tools: | ||
| if server_name == server.name: | ||
| return (server.name, server_tools) | ||
|
|
||
| try: | ||
| did_connect = await self.mcp_manager.connect_server(server.name) | ||
| if not did_connect: | ||
| raise Exception("Failed to load tools") | ||
|
|
||
| server = self.mcp_manager.get_server(server.name) | ||
| server_tools = await experimental_mcp_client.load_mcp_tools( | ||
| session=server.session, format="openai" | ||
| ) | ||
| return (server.name, server_tools) | ||
| except Exception as e: | ||
| if server.name != "unnamed-server" and server.name != "Local": | ||
| self.io.tool_warning(f"Error initializing MCP server {server.name}: {e}") | ||
| return None | ||
|
|
||
| async def get_all_server_tools(): | ||
| tasks = [get_server_tools(server) for server in self.mcp_manager if server.is_enabled] | ||
| results = await asyncio.gather(*tasks) | ||
| return [result for result in results if result is not None] | ||
|
|
||
| if self.mcp_manager: | ||
| # Retry initialization in case of CancelledError | ||
| max_retries = 3 | ||
| for i in range(max_retries): | ||
| try: | ||
| tools = await get_all_server_tools() | ||
| break | ||
| except asyncio.exceptions.CancelledError: | ||
| if i < max_retries - 1: | ||
| await asyncio.sleep(0.1) # Brief pause before retrying | ||
| else: | ||
| self.io.tool_warning( | ||
| "MCP tool initialization failed after multiple retries due to" | ||
| " cancellation." | ||
| ) | ||
| tools = [] | ||
|
|
||
| if len(tools) > 0: | ||
| if self.verbose: | ||
| self.io.tool_output("MCP servers configured:") | ||
| pass | ||
|
|
||
| for server_name, server_tools in tools: | ||
| self.io.tool_output(f" - {server_name}") | ||
| @property | ||
| def mcp_tools(self): | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In order to reduce size of PR (and minimize breaking things), i'm creating a "wrapper" that keeps the original |
||
| if not self.mcp_manager: | ||
| return [] | ||
|
|
||
| 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}") | ||
| return list(self.mcp_manager.all_tools.items()) | ||
|
|
||
| self.mcp_tools = tools | ||
| @mcp_tools.setter | ||
| def mcp_tools(self, value): | ||
| raise AttributeError("mcp_tools is read only.") | ||
|
|
||
| def get_tool_list(self): | ||
| """Get a flattened list of all MCP tools.""" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| from typing import List | ||
|
|
||
| from cecli.commands.utils.base_command import BaseCommand | ||
| from cecli.commands.utils.helpers import format_command_result | ||
|
|
||
|
|
||
| class LoadMcpCommand(BaseCommand): | ||
| NORM_NAME = "load-mcp" | ||
| DESCRIPTION = "Load a MCP server by name" | ||
|
|
||
| @classmethod | ||
| async def execute(cls, io, coder, args, **kwargs): | ||
| """Execute the load-mcp command with given parameters.""" | ||
| if not args.strip(): | ||
| return format_command_result(io, cls.NORM_NAME, "Usage: /load-mcp <mcp-name>") | ||
|
|
||
| if not coder.mcp_manager or not coder.mcp_manager.servers: | ||
| return format_command_result( | ||
| io, cls.NORM_NAME, "No MCP servers found, nothing to load." | ||
| ) | ||
|
|
||
| server_name = args.strip() | ||
| server = coder.mcp_manager.get_server(server_name) | ||
| if server is None: | ||
| return format_command_result( | ||
| io, cls.NORM_NAME, "", f"MCP server {server_name} does not exist." | ||
| ) | ||
|
|
||
| did_connect = await coder.mcp_manager.connect_server(server.name) | ||
|
|
||
| if not did_connect: | ||
| return format_command_result(io, cls.NORM_NAME, f"Unable to load server: {server_name}") | ||
|
|
||
| try: | ||
| if did_connect: | ||
| return format_command_result(io, cls.NORM_NAME, f"Loaded server: {server_name}") | ||
| else: | ||
| return format_command_result( | ||
| io, cls.NORM_NAME, "", f"Unable to Load server: {server_name}" | ||
| ) | ||
| finally: | ||
| from . import SwitchCoderSignal | ||
|
|
||
| raise SwitchCoderSignal( | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like raising signals is the only way to make things persistent when changing internal states? Otherwise when i would remove/load an mcp, all my changes disappeared when switching do a different mode |
||
| edit_format=coder.edit_format, | ||
| summarize_from_coder=False, | ||
| from_coder=coder, | ||
| show_announcements=True, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def get_completions(cls, io, coder, args) -> List[str]: | ||
| """Get completion options for load-mcp command.""" | ||
| if not coder.mcp_manager or not coder.mcp_manager.servers: | ||
| return [] | ||
|
|
||
| try: | ||
| server_names = [ | ||
| server.name | ||
| for server in coder.mcp_manager | ||
| if server not in coder.mcp_manager.connected_servers | ||
| ] | ||
| return server_names | ||
| except Exception: | ||
| return [] | ||
|
|
||
| @classmethod | ||
| def get_help(cls) -> str: | ||
| """Get help text for the load-mcp command.""" | ||
| help_text = super().get_help() | ||
| help_text += "\nUsage:\n" | ||
| help_text += " /load-mcp <mcp-name> # Load a mcp by name\n" | ||
| help_text += "\nExamples:\n" | ||
| help_text += " /load-mcp context7 # Load the context7 mcp\n" | ||
| help_text += " /load-mcp github # Load the github mcp\n" | ||
| help_text += "\nThis command loads a MCP server by name.\n" | ||
| return help_text | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| from typing import List | ||
|
|
||
| from cecli.commands.utils.base_command import BaseCommand | ||
| from cecli.commands.utils.helpers import format_command_result | ||
|
|
||
|
|
||
| class RemoveMcpCommand(BaseCommand): | ||
| NORM_NAME = "remove-mcp" | ||
| DESCRIPTION = "Remove a MCP server by name" | ||
|
|
||
| @classmethod | ||
| async def execute(cls, io, coder, args, **kwargs): | ||
| """Execute the remove-mcp command with given parameters.""" | ||
| if not args.strip(): | ||
| return format_command_result(io, cls.NORM_NAME, "Usage: /remove-mcp <mcp-name>") | ||
|
|
||
| if not coder.mcp_manager or not coder.mcp_manager.servers: | ||
| return format_command_result( | ||
| io, cls.NORM_NAME, "No MCP servers connected, nothing to remove." | ||
| ) | ||
|
|
||
| server_name = args.strip() | ||
| was_disconnected = await coder.mcp_manager.disconnect_server(server_name) | ||
|
|
||
| try: | ||
| if was_disconnected: | ||
| return format_command_result(io, cls.NORM_NAME, f"Removed server: {server_name}") | ||
| else: | ||
| return format_command_result( | ||
| io, cls.NORM_NAME, "", f"Unable to remove server: {server_name}" | ||
| ) | ||
| finally: | ||
| from . import SwitchCoderSignal | ||
|
|
||
| raise SwitchCoderSignal( | ||
| edit_format=coder.edit_format, | ||
| summarize_from_coder=False, | ||
| from_coder=coder, | ||
| show_announcements=True, | ||
| mcp_manager=coder.mcp_manager, | ||
| ) | ||
|
|
||
| @classmethod | ||
| def get_completions(cls, io, coder, args) -> List[str]: | ||
| """Get completion options for remove-mcp command.""" | ||
| if not coder.mcp_manager or not coder.mcp_manager.servers: | ||
| return [] | ||
|
|
||
| try: | ||
| server_names = [server.name for server in coder.mcp_manager if server.is_connected] | ||
| return server_names | ||
| except Exception: | ||
| return [] | ||
|
|
||
| @classmethod | ||
| def get_help(cls) -> str: | ||
| """Get help text for the remove-mcp command.""" | ||
| help_text = super().get_help() | ||
| help_text += "\nUsage:\n" | ||
| help_text += " /remove-mcp <mcp-name> # Remove a mcp by name\n" | ||
| help_text += "\nExamples:\n" | ||
| help_text += " /remove-mcp context7 # Remove the context7 mcp\n" | ||
| help_text += " /remove-mcp github # Remove the github mcp\n" | ||
| help_text += "\nThis command removes a MCP server by name.\n" | ||
| return help_text |
Uh oh!
There was an error while loading. Please reload this page.