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:
parent
7f7c67b9b4
commit
d82d81c85a
|
|
@ -0,0 +1,3 @@
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
18
README.md
18
README.md
|
|
@ -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
133
setup.py
|
|
@ -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__":
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue