Skip to main content
Glama
mcp_client_installer.py10.7 kB
import argparse import json import os import sys # Import shared utilities try: # Try relative import first (when run as module) from ..plugin.utils.python_detection import ( copy_python_env, create_venv_with_system_python, get_python_executable, ) except ImportError: # Fallback for direct script execution sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "plugin")) from utils.python_detection import ( copy_python_env, create_venv_with_system_python, get_python_executable, ) # Unique key used in MCP client configs MCP_SERVER_KEY = "binary_ninja_mcp" def _repo_root() -> str: """Return the repository root (one level above this scripts directory).""" return os.path.dirname(os.path.dirname(os.path.realpath(__file__))) def _bridge_entrypoint() -> str: return os.path.join(_repo_root(), "bridge", "binja_mcp_bridge.py") def _venv_dir() -> str: return os.path.join(_repo_root(), ".venv") def _venv_python() -> str: d = _venv_dir() if sys.platform == "win32": # Always prefer a real Python interpreter for MCP stdio servers. # "binaryninja.exe" is an embedded interpreter launcher and does not # behave like a normal Python on stdio, causing MCP clients to fail. py = os.path.join(d, "Scripts", "python.exe") return py return os.path.join(d, "bin", "python3") def ensure_local_venv() -> str: """Create a local venv under the plugin root if missing and return its python.""" vdir = _venv_dir() req = os.path.join(_repo_root(), "bridge", "requirements.txt") try: py = create_venv_with_system_python(vdir, req if os.path.exists(req) else None) return py if os.path.exists(py) else get_python_executable() except Exception: return get_python_executable() # Note: get_python_executable and copy_python_env are now imported from utils.python_detection def print_mcp_config(): """Print a generic MCP config snippet users can copy to unsupported clients.""" mcp_config = { "command": ensure_local_venv(), "args": [ _bridge_entrypoint(), ], "timeout": 1800, "disabled": False, } env = {} if copy_python_env(env): mcp_config["env"] = env print(json.dumps({"mcpServers": {MCP_SERVER_KEY: mcp_config}}, indent=2)) def _config_targets() -> dict[str, tuple[str, str]]: """Return supported MCP client config locations per platform. Value is (config_dir, filename). """ home = os.path.expanduser("~") if sys.platform == "win32": appdata = os.getenv("APPDATA") or os.path.join(home, "AppData", "Roaming") return { "Cline": ( os.path.join( appdata, "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings" ), "cline_mcp_settings.json", ), "Roo Code": ( os.path.join( appdata, "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", ), "mcp_settings.json", ), "Claude": (os.path.join(appdata, "Claude"), "claude_desktop_config.json"), "Cursor": (os.path.join(home, ".cursor"), "mcp.json"), "Windsurf": (os.path.join(home, ".codeium", "windsurf"), "mcp_config.json"), "Claude Code": (home, ".claude.json"), "LM Studio": (os.path.join(home, ".lmstudio"), "mcp.json"), } elif sys.platform == "darwin": return { "Cline": ( os.path.join( home, "Library", "Application Support", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", ), "cline_mcp_settings.json", ), "Roo Code": ( os.path.join( home, "Library", "Application Support", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", ), "mcp_settings.json", ), "Claude": ( os.path.join(home, "Library", "Application Support", "Claude"), "claude_desktop_config.json", ), "Cursor": (os.path.join(home, ".cursor"), "mcp.json"), "Windsurf": (os.path.join(home, ".codeium", "windsurf"), "mcp_config.json"), "Claude Code": (home, ".claude.json"), "LM Studio": (os.path.join(home, ".lmstudio"), "mcp.json"), } elif sys.platform == "linux": return { "Cline": ( os.path.join( home, ".config", "Code", "User", "globalStorage", "saoudrizwan.claude-dev", "settings", ), "cline_mcp_settings.json", ), "Roo Code": ( os.path.join( home, ".config", "Code", "User", "globalStorage", "rooveterinaryinc.roo-cline", "settings", ), "mcp_settings.json", ), # Claude not supported on Linux "Cursor": (os.path.join(home, ".cursor"), "mcp.json"), "Windsurf": (os.path.join(home, ".codeium", "windsurf"), "mcp_config.json"), "Claude Code": (home, ".claude.json"), "LM Studio": (os.path.join(home, ".lmstudio"), "mcp.json"), } else: return {} def install_mcp_servers( *, uninstall: bool = False, quiet: bool = False, env: dict[str, str] | None = None ) -> int: """Install or remove MCP server entries for supported clients. Returns the number of configs modified. """ env = {} if env is None else dict(env) targets = _config_targets() if not targets: if not quiet: print(f"Unsupported platform: {sys.platform}") return 0 installed = 0 for name, (config_dir, config_file) in targets.items(): config_path = os.path.join(config_dir, config_file) action_word = "uninstall" if uninstall else "installation" if not os.path.exists(config_dir): if not quiet: print(f"Skipping {name} {action_word}\n Config: {config_path} (not found)") continue if not os.path.exists(config_path): config: dict = {} else: try: with open(config_path, encoding="utf-8") as f: data = f.read().strip() config = json.loads(data) if data else {} except json.decoder.JSONDecodeError: if not quiet: print(f"Skipping {name} uninstall\n Config: {config_path} (invalid JSON)") continue config.setdefault("mcpServers", {}) mcp_servers = config["mcpServers"] if uninstall: if MCP_SERVER_KEY not in mcp_servers: if not quiet: print(f"Skipping {name} uninstall\n Config: {config_path} (not installed)") continue del mcp_servers[MCP_SERVER_KEY] else: # Preserve any existing env overrides for this server if MCP_SERVER_KEY in mcp_servers: for key, value in mcp_servers[MCP_SERVER_KEY].get("env", {}).items(): env[key] = value bridge = _bridge_entrypoint() if copy_python_env(env) and not quiet: print("[WARNING] Custom Python environment variables detected") server_cfg = { "command": ensure_local_venv(), "args": [bridge], "timeout": 1800, "disabled": False, } if env: server_cfg["env"] = env mcp_servers[MCP_SERVER_KEY] = server_cfg # Write back os.makedirs(config_dir, exist_ok=True) with open(config_path, "w", encoding="utf-8") as f: json.dump(config, f, indent=2) if not quiet: print( ("Uninstalled" if uninstall else "Installed") + f" {name} MCP server (restart required)\n Config: {config_path}" ) installed += 1 if not uninstall and installed == 0 and not quiet: print("No MCP servers installed. For unsupported MCP clients, use the following config:\n") print_mcp_config() return installed def main(): parser = argparse.ArgumentParser( description="Binary Ninja MCP Max - MCP Client Installer (CLI)" ) parser.add_argument( "--install", action="store_true", help="Install MCP server entries for supported clients" ) parser.add_argument( "--uninstall", action="store_true", help="Remove MCP server entries from supported clients" ) parser.add_argument("--config", action="store_true", help="Print generic MCP config JSON") parser.add_argument("--quiet", action="store_true", help="Reduce output noise") args = parser.parse_args() if args.install and args.uninstall: print("Cannot install and uninstall at the same time") return if args.config: print_mcp_config() return if args.uninstall: install_mcp_servers(uninstall=True, quiet=args.quiet) # Also remove auto-setup sentinel so the plugin can re-run setup later sentinel = os.path.join(_repo_root(), ".mcp_auto_setup_done") try: os.remove(sentinel) if not args.quiet: print(f"Removed auto-setup marker: {sentinel}") except FileNotFoundError: pass except Exception as e: if not args.quiet: print(f"Warning: failed to remove auto-setup marker: {e}") return # Default action is install if no flag is provided if args.install or (not args.uninstall and not args.config): install_mcp_servers(quiet=args.quiet) if __name__ == "__main__": main()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/fosdickio/binary_ninja_mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server