diff --git a/src/serena/cli.py b/src/serena/cli.py index 8e1b3093..90ea178b 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -19,6 +19,7 @@ from tqdm import tqdm from serena import serena_version from serena.agent import SerenaAgent +from serena.config.client_setup import client_setup_handlers from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode from serena.config.serena_config import ( LanguageBackend, @@ -176,6 +177,32 @@ class TopLevelCommands(AutoRegisteringGroup): click.echo(f"Language backend: {language_backend}") click.echo("\nSerena has been initialised successfully.\n") + @staticmethod + @click.command( + "setup", + help="Set up Serena for use with a specific client by registering it as an MCP server.", + context_settings={"max_content_width": _MAX_CONTENT_WIDTH}, + ) + @click.argument( + "client", + type=click.Choice([h.name for h in client_setup_handlers]), + ) + def setup(client: str) -> None: + # find the matching handler + handler = next(h for h in client_setup_handlers if h.name == client) + + # check applicability + if not handler.is_applicable(): + click.echo(f"\nCannot apply setup for client '{client}' (not found or not functional).\n") + raise SystemExit(1) + + # apply the setup + if handler.apply(): + click.echo(f"\nSerena has been successfully set up for {client}.\n") + else: + click.echo(f"\nFailed to set up Serena for {client}.\n") + raise SystemExit(1) + @staticmethod @click.command("start-mcp-server", help="Starts the Serena MCP server.", context_settings={"max_content_width": _MAX_CONTENT_WIDTH}) @click.option("--project", "project", type=PROJECT_TYPE, default=None, help="Path or name of project to activate at startup.") diff --git a/src/serena/config/client_setup.py b/src/serena/config/client_setup.py new file mode 100644 index 00000000..5d125257 --- /dev/null +++ b/src/serena/config/client_setup.py @@ -0,0 +1,46 @@ +from abc import ABC, abstractmethod + +from serena.util.shell import execute_shell_command + + +class ClientSetupHandler(ABC): + def __init__(self, name: str) -> None: + self.name = name + + @abstractmethod + def is_applicable(self) -> bool: + """ + :return: whether the client setup can applied (respective client is available) + """ + + @abstractmethod + def get_mcp_server_options(self) -> list[str]: + pass + + def get_mcp_server_command(self) -> str: + return f"serena start-mcp-server {' '.join(self.get_mcp_server_options())}" + + @abstractmethod + def apply(self) -> bool: + """ + Applies the client setup + """ + + +class ClientSetupHandlerClaudeCode(ClientSetupHandler): + def __init__(self) -> None: + super().__init__("ClaudeCode") + + def is_applicable(self) -> bool: + result = execute_shell_command("claude --version") + return result.return_code == 0 and "Claude" in result.stdout + + def get_mcp_server_options(self) -> list[str]: + return ["--context=claude-code", "--project-from-cwd"] + + def apply(self) -> bool: + result = execute_shell_command(f"claude mcp add --scope user serena -- {self.get_mcp_server_command()}") + return result.return_code == 0 + + +client_setup_handlers = [ClientSetupHandlerClaudeCode()]