diff --git a/pyproject.toml b/pyproject.toml index 99e5f6a..632ad48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,8 @@ dependencies = [ "deprecated>=1.2.18", "inquirerpy>=0.3.4", "rich-gradient>=0.3.2", + "tomli>=2.2.1", + "tomli-w>=1.2.0", ] [project.urls] diff --git a/src/mcpm/clients/client_registry.py b/src/mcpm/clients/client_registry.py index 88bf218..b583331 100644 --- a/src/mcpm/clients/client_registry.py +++ b/src/mcpm/clients/client_registry.py @@ -13,9 +13,11 @@ from mcpm.clients.managers.claude_code import ClaudeCodeManager from mcpm.clients.managers.claude_desktop import ClaudeDesktopManager from mcpm.clients.managers.cline import ClineManager, RooCodeManager +from mcpm.clients.managers.codex_cli import CodexCliManager from mcpm.clients.managers.continue_extension import ContinueManager from mcpm.clients.managers.cursor import CursorManager from mcpm.clients.managers.fiveire import FiveireManager +from mcpm.clients.managers.gemini_cli import GeminiCliManager from mcpm.clients.managers.goose import GooseClientManager from mcpm.clients.managers.trae import TraeManager from mcpm.clients.managers.vscode import VSCodeManager @@ -46,6 +48,8 @@ class ClientRegistry: "roo-code": RooCodeManager, "trae": TraeManager, "vscode": VSCodeManager, + "gemini-cli": GeminiCliManager, + "codex-cli": CodexCliManager, } @classmethod diff --git a/src/mcpm/clients/managers/__init__.py b/src/mcpm/clients/managers/__init__.py index 4a53577..789081c 100644 --- a/src/mcpm/clients/managers/__init__.py +++ b/src/mcpm/clients/managers/__init__.py @@ -7,11 +7,14 @@ from mcpm.clients.managers.claude_code import ClaudeCodeManager from mcpm.clients.managers.claude_desktop import ClaudeDesktopManager from mcpm.clients.managers.cline import ClineManager +from mcpm.clients.managers.codex_cli import CodexCliManager from mcpm.clients.managers.continue_extension import ContinueManager from mcpm.clients.managers.cursor import CursorManager from mcpm.clients.managers.fiveire import FiveireManager +from mcpm.clients.managers.gemini_cli import GeminiCliManager from mcpm.clients.managers.goose import GooseClientManager from mcpm.clients.managers.trae import TraeManager +from mcpm.clients.managers.vscode import VSCodeManager from mcpm.clients.managers.windsurf import WindsurfManager __all__ = [ @@ -24,4 +27,7 @@ "FiveireManager", "GooseClientManager", "TraeManager", + "VSCodeManager", + "GeminiCliManager", + "CodexCliManager", ] diff --git a/src/mcpm/clients/managers/codex_cli.py b/src/mcpm/clients/managers/codex_cli.py new file mode 100644 index 0000000..fc65f2d --- /dev/null +++ b/src/mcpm/clients/managers/codex_cli.py @@ -0,0 +1,110 @@ +""" +Codex CLI integration utilities for MCP +""" + +import logging +import os +import shutil +from typing import Any, Dict + +import tomli +import tomli_w + +from mcpm.clients.base import JSONClientManager + +logger = logging.getLogger(__name__) + + +class CodexCliManager(JSONClientManager): + """Manages Codex CLI MCP server configurations""" + + # Client information + client_key = "codex-cli" + display_name = "Codex CLI" + download_url = "https://github.com/openai/codex" + configure_key_name = "mcp_servers" # Codex uses mcp_servers instead of mcpServers + + def __init__(self, config_path_override: str | None = None): + """Initialize the Codex CLI client manager + + Args: + config_path_override: Optional path to override the default config file location + """ + super().__init__(config_path_override=config_path_override) + + if config_path_override: + self.config_path = config_path_override + else: + # Codex CLI stores its settings in ~/.codex/config.toml + self.config_path = os.path.expanduser("~/.codex/config.toml") + + def _get_empty_config(self) -> Dict[str, Any]: + """Get empty config structure for Codex CLI""" + return {"mcp_servers": {}} + + def is_client_installed(self) -> bool: + """Check if Codex CLI is installed + Returns: + bool: True if codex command is available, False otherwise + """ + codex_executable = "codex.exe" if self._system == "Windows" else "codex" + return shutil.which(codex_executable) is not None + + def get_client_info(self) -> Dict[str, str]: + """Get information about this client + + Returns: + Dict: Information about the client including display name, download URL, and config path + """ + return { + "name": self.display_name, + "download_url": self.download_url, + "config_file": self.config_path, + "description": "OpenAI's Codex CLI tool", + } + + def _load_config(self) -> Dict[str, Any]: + """Load client configuration file + + Returns: + Dict containing the client configuration with at least {"mcp_servers": {}} + """ + try: + # Check if config file exists + if not os.path.exists(self.config_path): + # Create empty config + return self._get_empty_config() + + # Codex uses TOML format instead of JSON + with open(self.config_path, "rb") as f: + config = tomli.load(f) + + # Ensure mcp_servers key exists + if self.configure_key_name not in config: + config[self.configure_key_name] = {} + + return config + except Exception as e: + logger.error(f"Error loading Codex config: {e}") + return self._get_empty_config() + + def _save_config(self, config: Dict[str, Any]) -> bool: + """Save configuration to client config file + + Args: + config: Configuration to save + + Returns: + bool: Success or failure + """ + try: + # Create directory if it doesn't exist + os.makedirs(os.path.dirname(self.config_path), exist_ok=True) + + # Codex uses TOML format instead of JSON + with open(self.config_path, "wb") as f: + tomli_w.dump(config, f) + return True + except Exception as e: + logger.error(f"Error saving Codex config: {e}") + return False diff --git a/src/mcpm/clients/managers/gemini_cli.py b/src/mcpm/clients/managers/gemini_cli.py new file mode 100644 index 0000000..80b655b --- /dev/null +++ b/src/mcpm/clients/managers/gemini_cli.py @@ -0,0 +1,66 @@ +""" +Gemini CLI integration utilities for MCP +""" + +import logging +import os +import shutil +from typing import Any, Dict + +from mcpm.clients.base import JSONClientManager + +logger = logging.getLogger(__name__) + + +class GeminiCliManager(JSONClientManager): + """Manages Gemini CLI MCP server configurations""" + + # Client information + client_key = "gemini-cli" + display_name = "Gemini CLI" + download_url = "https://github.com/google-gemini/gemini-cli" + + def __init__(self, config_path_override: str | None = None): + """Initialize the Gemini CLI client manager + + Args: + config_path_override: Optional path to override the default config file location + """ + super().__init__(config_path_override=config_path_override) + + if config_path_override: + self.config_path = config_path_override + else: + # Gemini CLI stores its settings in ~/.gemini/settings.json + self.config_path = os.path.expanduser("~/.gemini/settings.json") + + def _get_empty_config(self) -> Dict[str, Any]: + """Get empty config structure for Gemini CLI""" + return { + "mcpServers": {}, + # Include other default settings that Gemini CLI expects + "contextFileName": "GEMINI.md", + "autoAccept": False, + "theme": "Default" + } + + def is_client_installed(self) -> bool: + """Check if Gemini CLI is installed + Returns: + bool: True if gemini command is available, False otherwise + """ + gemini_executable = "gemini.exe" if self._system == "Windows" else "gemini" + return shutil.which(gemini_executable) is not None + + def get_client_info(self) -> Dict[str, str]: + """Get information about this client + + Returns: + Dict: Information about the client including display name, download URL, and config path + """ + return { + "name": self.display_name, + "download_url": self.download_url, + "config_file": self.config_path, + "description": "Google's Gemini CLI tool", + } diff --git a/uv.lock b/uv.lock index 55cad57..8f167c2 100644 --- a/uv.lock +++ b/uv.lock @@ -636,6 +636,8 @@ dependencies = [ { name = "rich-click" }, { name = "rich-gradient" }, { name = "ruamel-yaml" }, + { name = "tomli" }, + { name = "tomli-w" }, { name = "watchfiles" }, ] @@ -665,6 +667,8 @@ requires-dist = [ { name = "rich-click", specifier = ">=1.8.0" }, { name = "rich-gradient", specifier = ">=0.3.2" }, { name = "ruamel-yaml", specifier = ">=0.18.10" }, + { name = "tomli", specifier = ">=2.2.1" }, + { name = "tomli-w", specifier = ">=1.2.0" }, { name = "watchfiles", specifier = ">=1.0.4" }, ] @@ -1345,6 +1349,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245 }, ] +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "tomli-w" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675 }, +] + [[package]] name = "tqdm" version = "4.67.1"