diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..77ac754 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.venv/ +__pycache__/ +*.pyc diff --git a/README.md b/README.md index 5ed4a27..5d18364 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,11 @@ python setup.py ``` The wizard will: -1. Install dependencies automatically -2. Open the browser to the API Keys page on the portal -3. Read the key you paste into the terminal -4. Write `claude_desktop_config.json` without touching your other settings +1. Create a local `.venv` virtual environment (no system-level changes) +2. Install dependencies into it +3. Open the browser to the API Keys page on the portal +4. Read the key you paste into the terminal +5. Write `claude_desktop_config.json` without touching your other settings Then restart Claude Desktop — done. @@ -30,6 +31,13 @@ Then restart Claude Desktop — done. ### Install dependencies ```bash +python -m venv .venv + +# Mac / Linux +source .venv/bin/activate +# Windows +.venv\Scripts\activate + pip install mcp httpx ``` @@ -54,7 +62,7 @@ Add the `mcpServers` block: { "mcpServers": { "cauldron": { - "command": "python", + "command": "/absolute/path/to/cauldron-mcp/.venv/bin/python", "args": ["/absolute/path/to/cauldron-mcp/server.py"], "env": { "CAULDRON_API_KEY": "cldrn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/setup.py b/setup.py index 439c811..ca9664c 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,8 @@ #!/usr/bin/env python3 """ Cauldron Cloud MCP — Setup wizard -Installs dependencies and writes Claude Desktop config automatically. +Creates a local virtual environment, installs dependencies, +and writes Claude Desktop config automatically. """ import json @@ -34,11 +35,19 @@ def warn(msg: str) -> None: print(f"{YELLOW} ⚠ {RESET}{msg}") def err(msg: str) -> None: print(f"{RED} ✗ {RESET}{msg}") def bold(msg: str) -> str: return f"{BOLD}{msg}{RESET}" -# ── Helpers ─────────────────────────────────────────────────────────────────── +# ── Paths ───────────────────────────────────────────────────────────────────── -def _python_executable() -> str: - """Return the Python executable that is running this script.""" - return sys.executable +REPO_DIR = Path(__file__).resolve().parent +VENV_DIR = REPO_DIR / ".venv" +SERVER_PY = REPO_DIR / "server.py" +PACKAGES = ["mcp>=1.0.0", "httpx>=0.27.0"] + + +def _venv_python() -> Path: + """Path to the Python executable inside the local venv.""" + if platform.system() == "Windows": + return VENV_DIR / "Scripts" / "python.exe" + return VENV_DIR / "bin" / "python" def _config_path() -> Path: @@ -48,7 +57,6 @@ def _config_path() -> Path: return base / "Claude" / "claude_desktop_config.json" if system == "Darwin": return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json" - # Linux / other xdg = os.environ.get("XDG_CONFIG_HOME", "") base = Path(xdg) if xdg else Path.home() / ".config" return base / "Claude" / "claude_desktop_config.json" @@ -59,7 +67,7 @@ def _read_config(path: Path) -> dict: try: return json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError: - warn("Existing config file contains invalid JSON — it will be backed up and rewritten.") + warn("Existing config file contains invalid JSON — backing it up.") backup = path.with_suffix(".json.bak") shutil.copy(path, backup) info(f"Backup saved to {backup}") @@ -71,13 +79,32 @@ def _write_config(path: Path, data: dict) -> None: path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8") -def _install_deps() -> bool: - packages = ["mcp>=1.0.0", "httpx>=0.27.0"] - info("Installing Python dependencies...") +def _validate_key(key: str) -> bool: + key = key.strip() + return key.startswith("cldrn_") and len(key) >= 20 + + +# ── Venv + deps ─────────────────────────────────────────────────────────────── + +def _create_venv() -> bool: + info(f"Creating virtual environment in {VENV_DIR} ...") result = subprocess.run( - [_python_executable(), "-m", "pip", "install", "--quiet", *packages], - capture_output=True, - text=True, + [sys.executable, "-m", "venv", str(VENV_DIR)], + capture_output=True, text=True, + ) + if result.returncode != 0: + err("Could not create virtual environment:") + print(result.stderr.strip()) + return False + return True + + +def _install_deps() -> bool: + python = _venv_python() + info("Installing dependencies into virtual environment...") + result = subprocess.run( + [str(python), "-m", "pip", "install", "--quiet", "--upgrade", *PACKAGES], + capture_output=True, text=True, ) if result.returncode != 0: err("pip install failed:") @@ -86,19 +113,17 @@ def _install_deps() -> bool: return True -def _check_deps() -> bool: - try: - import mcp # noqa: F401 - import httpx # noqa: F401 - return True - except ImportError: +def _deps_ok() -> bool: + python = _venv_python() + if not python.exists(): return False + result = subprocess.run( + [str(python), "-c", "import mcp, httpx"], + capture_output=True, + ) + return result.returncode == 0 -def _validate_key(key: str) -> bool: - key = key.strip() - return key.startswith("cldrn_") and len(key) >= 20 - # ── Main ────────────────────────────────────────────────────────────────────── def main() -> None: @@ -108,32 +133,35 @@ def main() -> None: print(bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")) print() - # ── 1. Python version check ─────────────────────────────────────────────── + # ── 1. Python version ───────────────────────────────────────────────────── if sys.version_info < (3, 10): err(f"Python 3.10+ required (you have {platform.python_version()}).") sys.exit(1) ok(f"Python {platform.python_version()}") - # ── 2. Dependencies ─────────────────────────────────────────────────────── - if _check_deps(): - ok("Dependencies already installed (mcp, httpx)") + # ── 2. server.py present ────────────────────────────────────────────────── + if not SERVER_PY.exists(): + err(f"server.py not found at {SERVER_PY}") + sys.exit(1) + ok(f"Server script: {SERVER_PY}") + + # ── 3. Virtual environment + dependencies ───────────────────────────────── + if _deps_ok(): + ok("Virtual environment already set up") else: - if not _install_deps(): - err("Could not install dependencies. Try manually: pip install mcp httpx") + if VENV_DIR.exists(): + info("Refreshing existing virtual environment...") + elif not _create_venv(): sys.exit(1) - if not _check_deps(): - err("Dependencies installed but import still fails. Check your Python environment.") + + if not _install_deps() or not _deps_ok(): + err("Could not install dependencies. Check your Python installation.") sys.exit(1) ok("Dependencies installed (mcp, httpx)") - # ── 3. Locate server.py ─────────────────────────────────────────────────── - server_py = Path(__file__).resolve().parent / "server.py" - if not server_py.exists(): - err(f"server.py not found at {server_py}") - sys.exit(1) - ok(f"Server script: {server_py}") + python_cmd = str(_venv_python()) - # ── 4. Get the API key ──────────────────────────────────────────────────── + # ── 4. API key ──────────────────────────────────────────────────────────── print() print(bold(" Step: Generate your API key")) print() @@ -142,13 +170,12 @@ def main() -> None: print() input(" Press ENTER to open the browser...") - portal_url = "https://cauldron.cloud/mcpKeys" - webbrowser.open(portal_url) - info(f"Opened: {portal_url}") + webbrowser.open("https://cauldron.cloud/mcpKeys") + info("Opened: https://cauldron.cloud/mcpKeys") print() api_key = "" - for attempt in range(3): + for _ in range(3): raw = input(" Paste your API key here: ").strip() if _validate_key(raw): api_key = raw @@ -160,28 +187,20 @@ def main() -> None: ok(f"Key accepted: {api_key[:14]}…") - # ── 5. Detect Claude Desktop config ─────────────────────────────────────── + # ── 5. Write Claude Desktop config ──────────────────────────────────────── config_path = _config_path() print() info(f"Config file: {config_path}") - config = _read_config(config_path) + config = _read_config(config_path) + existing = config.get("mcpServers", {}).get("cauldron") - # ── 6. Merge mcpServers entry ───────────────────────────────────────────── - python_cmd = _python_executable() - - mcp_entry = { + config.setdefault("mcpServers", {})["cauldron"] = { "command": python_cmd, - "args": [str(server_py)], + "args": [str(SERVER_PY)], "env": {"CAULDRON_API_KEY": api_key}, } - if "mcpServers" not in config: - config["mcpServers"] = {} - - existing = config["mcpServers"].get("cauldron") - config["mcpServers"]["cauldron"] = mcp_entry - _write_config(config_path, config) if existing: @@ -189,7 +208,7 @@ def main() -> None: else: ok("Added cauldron entry to Claude Desktop config") - # ── 7. Done ─────────────────────────────────────────────────────────────── + # ── 6. Done ─────────────────────────────────────────────────────────────── print() print(bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")) print(bold(" Setup complete!")) @@ -204,6 +223,8 @@ def main() -> None: print(f" {CYAN}\"Show me all open Sell Side deals in the Automotive sector\"{RESET}") print(f" {CYAN}\"Are there any incoming Requests for Help we haven't answered?\"{RESET}") print() + print(f" To update in the future: {bold('git pull')} (then restart Claude Desktop)") + print() if __name__ == "__main__":