fix: use local venv instead of system pip

Avoids 'externally managed environment' errors on Mac (Homebrew Python 3.12+)
and permission issues on Linux. setup.py now creates .venv inside the repo,
installs into it, and points claude_desktop_config.json to the venv Python.
Added .gitignore to exclude .venv and __pycache__.
This commit is contained in:
Spaike 2026-05-11 16:28:29 +02:00
parent 7f7c67b9b4
commit d82d81c85a
3 changed files with 93 additions and 61 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.venv/
__pycache__/
*.pyc

View File

@ -11,10 +11,11 @@ python setup.py
``` ```
The wizard will: The wizard will:
1. Install dependencies automatically 1. Create a local `.venv` virtual environment (no system-level changes)
2. Open the browser to the API Keys page on the portal 2. Install dependencies into it
3. Read the key you paste into the terminal 3. Open the browser to the API Keys page on the portal
4. Write `claude_desktop_config.json` without touching your other settings 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. Then restart Claude Desktop — done.
@ -30,6 +31,13 @@ Then restart Claude Desktop — done.
### Install dependencies ### Install dependencies
```bash ```bash
python -m venv .venv
# Mac / Linux
source .venv/bin/activate
# Windows
.venv\Scripts\activate
pip install mcp httpx pip install mcp httpx
``` ```
@ -54,7 +62,7 @@ Add the `mcpServers` block:
{ {
"mcpServers": { "mcpServers": {
"cauldron": { "cauldron": {
"command": "python", "command": "/absolute/path/to/cauldron-mcp/.venv/bin/python",
"args": ["/absolute/path/to/cauldron-mcp/server.py"], "args": ["/absolute/path/to/cauldron-mcp/server.py"],
"env": { "env": {
"CAULDRON_API_KEY": "cldrn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "CAULDRON_API_KEY": "cldrn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

133
setup.py
View File

@ -1,7 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
Cauldron Cloud MCP Setup wizard 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 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 err(msg: str) -> None: print(f"{RED}{RESET}{msg}")
def bold(msg: str) -> str: return f"{BOLD}{msg}{RESET}" def bold(msg: str) -> str: return f"{BOLD}{msg}{RESET}"
# ── Helpers ─────────────────────────────────────────────────────────────────── # ── Paths ─────────────────────────────────────────────────────────────────────
def _python_executable() -> str: REPO_DIR = Path(__file__).resolve().parent
"""Return the Python executable that is running this script.""" VENV_DIR = REPO_DIR / ".venv"
return sys.executable 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: def _config_path() -> Path:
@ -48,7 +57,6 @@ def _config_path() -> Path:
return base / "Claude" / "claude_desktop_config.json" return base / "Claude" / "claude_desktop_config.json"
if system == "Darwin": if system == "Darwin":
return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json" return Path.home() / "Library" / "Application Support" / "Claude" / "claude_desktop_config.json"
# Linux / other
xdg = os.environ.get("XDG_CONFIG_HOME", "") xdg = os.environ.get("XDG_CONFIG_HOME", "")
base = Path(xdg) if xdg else Path.home() / ".config" base = Path(xdg) if xdg else Path.home() / ".config"
return base / "Claude" / "claude_desktop_config.json" return base / "Claude" / "claude_desktop_config.json"
@ -59,7 +67,7 @@ def _read_config(path: Path) -> dict:
try: try:
return json.loads(path.read_text(encoding="utf-8")) return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError: 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") backup = path.with_suffix(".json.bak")
shutil.copy(path, backup) shutil.copy(path, backup)
info(f"Backup saved to {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") path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
def _install_deps() -> bool: def _validate_key(key: str) -> bool:
packages = ["mcp>=1.0.0", "httpx>=0.27.0"] key = key.strip()
info("Installing Python dependencies...") 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( result = subprocess.run(
[_python_executable(), "-m", "pip", "install", "--quiet", *packages], [sys.executable, "-m", "venv", str(VENV_DIR)],
capture_output=True, capture_output=True, text=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: if result.returncode != 0:
err("pip install failed:") err("pip install failed:")
@ -86,19 +113,17 @@ def _install_deps() -> bool:
return True return True
def _check_deps() -> bool: def _deps_ok() -> bool:
try: python = _venv_python()
import mcp # noqa: F401 if not python.exists():
import httpx # noqa: F401
return True
except ImportError:
return False 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 ────────────────────────────────────────────────────────────────────── # ── Main ──────────────────────────────────────────────────────────────────────
def main() -> None: def main() -> None:
@ -108,32 +133,35 @@ def main() -> None:
print(bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")) print(bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))
print() print()
# ── 1. Python version check ─────────────────────────────────────────────── # ── 1. Python version ─────────────────────────────────────────────────────
if sys.version_info < (3, 10): if sys.version_info < (3, 10):
err(f"Python 3.10+ required (you have {platform.python_version()}).") err(f"Python 3.10+ required (you have {platform.python_version()}).")
sys.exit(1) sys.exit(1)
ok(f"Python {platform.python_version()}") ok(f"Python {platform.python_version()}")
# ── 2. Dependencies ─────────────────────────────────────────────────────── # ── 2. server.py present ──────────────────────────────────────────────────
if _check_deps(): if not SERVER_PY.exists():
ok("Dependencies already installed (mcp, httpx)") err(f"server.py not found at {SERVER_PY}")
else:
if not _install_deps():
err("Could not install dependencies. Try manually: pip install mcp httpx")
sys.exit(1) sys.exit(1)
if not _check_deps(): ok(f"Server script: {SERVER_PY}")
err("Dependencies installed but import still fails. Check your Python environment.")
# ── 3. Virtual environment + dependencies ─────────────────────────────────
if _deps_ok():
ok("Virtual environment already set up")
else:
if VENV_DIR.exists():
info("Refreshing existing virtual environment...")
elif not _create_venv():
sys.exit(1)
if not _install_deps() or not _deps_ok():
err("Could not install dependencies. Check your Python installation.")
sys.exit(1) sys.exit(1)
ok("Dependencies installed (mcp, httpx)") ok("Dependencies installed (mcp, httpx)")
# ── 3. Locate server.py ─────────────────────────────────────────────────── python_cmd = str(_venv_python())
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}")
# ── 4. Get the API key ──────────────────────────────────────────────────── # ── 4. API key ────────────────────────────────────────────────────────────
print() print()
print(bold(" Step: Generate your API key")) print(bold(" Step: Generate your API key"))
print() print()
@ -142,13 +170,12 @@ def main() -> None:
print() print()
input(" Press ENTER to open the browser...") input(" Press ENTER to open the browser...")
portal_url = "https://cauldron.cloud/mcpKeys" webbrowser.open("https://cauldron.cloud/mcpKeys")
webbrowser.open(portal_url) info("Opened: https://cauldron.cloud/mcpKeys")
info(f"Opened: {portal_url}")
print() print()
api_key = "" api_key = ""
for attempt in range(3): for _ in range(3):
raw = input(" Paste your API key here: ").strip() raw = input(" Paste your API key here: ").strip()
if _validate_key(raw): if _validate_key(raw):
api_key = raw api_key = raw
@ -160,28 +187,20 @@ def main() -> None:
ok(f"Key accepted: {api_key[:14]}") ok(f"Key accepted: {api_key[:14]}")
# ── 5. Detect Claude Desktop config ─────────────────────────────────────── # ── 5. Write Claude Desktop config ────────────────────────────────────────
config_path = _config_path() config_path = _config_path()
print() print()
info(f"Config file: {config_path}") 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 ───────────────────────────────────────────── config.setdefault("mcpServers", {})["cauldron"] = {
python_cmd = _python_executable()
mcp_entry = {
"command": python_cmd, "command": python_cmd,
"args": [str(server_py)], "args": [str(SERVER_PY)],
"env": {"CAULDRON_API_KEY": api_key}, "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) _write_config(config_path, config)
if existing: if existing:
@ -189,7 +208,7 @@ def main() -> None:
else: else:
ok("Added cauldron entry to Claude Desktop config") ok("Added cauldron entry to Claude Desktop config")
# ── 7. Done ─────────────────────────────────────────────────────────────── # ── 6. Done ───────────────────────────────────────────────────────────────
print() print()
print(bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")) print(bold("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"))
print(bold(" Setup complete!")) 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}\"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(f" {CYAN}\"Are there any incoming Requests for Help we haven't answered?\"{RESET}")
print() print()
print(f" To update in the future: {bold('git pull')} (then restart Claude Desktop)")
print()
if __name__ == "__main__": if __name__ == "__main__":