Files
my-pal-mcp-server/run-server.sh
2025-06-19 05:42:12 +04:00

869 lines
26 KiB
Bash
Executable File

#!/bin/bash
set -euo pipefail
# ============================================================================
# Zen MCP Server Setup Script
#
# A platform-agnostic setup script that works on macOS, Linux, and WSL.
# Handles environment setup, dependency installation, and configuration.
# ============================================================================
# ----------------------------------------------------------------------------
# Constants and Configuration
# ----------------------------------------------------------------------------
# Colors for output (ANSI codes work on all platforms)
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly RED='\033[0;31m'
readonly NC='\033[0m' # No Color
# Configuration
readonly VENV_PATH=".zen_venv"
readonly DOCKER_CLEANED_FLAG=".docker_cleaned"
readonly DESKTOP_CONFIG_FLAG=".desktop_configured"
readonly LOG_DIR="logs"
readonly LOG_FILE="mcp_server.log"
# ----------------------------------------------------------------------------
# Utility Functions
# ----------------------------------------------------------------------------
# Print colored output
print_success() {
echo -e "${GREEN}${NC} $1" >&2
}
print_error() {
echo -e "${RED}${NC} $1" >&2
}
print_warning() {
echo -e "${YELLOW}!${NC} $1" >&2
}
print_info() {
echo -e "${YELLOW}$1${NC}" >&2
}
# Get the script's directory (works on all platforms)
get_script_dir() {
cd "$(dirname "$0")" && pwd
}
# Extract version from config.py
get_version() {
grep -E '^__version__ = ' config.py 2>/dev/null | sed 's/__version__ = "\(.*\)"/\1/' || echo "unknown"
}
# ----------------------------------------------------------------------------
# Platform Detection Functions
# ----------------------------------------------------------------------------
# Detect the operating system
detect_os() {
case "$OSTYPE" in
darwin*) echo "macos" ;;
linux*)
if grep -qi microsoft /proc/version 2>/dev/null; then
echo "wsl"
else
echo "linux"
fi
;;
msys*|cygwin*|win32) echo "windows" ;;
*) echo "unknown" ;;
esac
}
# Get Claude config path based on platform
get_claude_config_path() {
local os_type=$(detect_os)
case "$os_type" in
macos)
echo "$HOME/Library/Application Support/Claude/claude_desktop_config.json"
;;
linux)
echo "$HOME/.config/Claude/claude_desktop_config.json"
;;
wsl)
echo "/mnt/c/Users/$USER/AppData/Roaming/Claude/claude_desktop_config.json"
;;
windows)
echo "$APPDATA/Claude/claude_desktop_config.json"
;;
*)
echo ""
;;
esac
}
# ----------------------------------------------------------------------------
# Docker Cleanup Functions
# ----------------------------------------------------------------------------
# Clean up old Docker artifacts
cleanup_docker() {
# Skip if already cleaned or Docker not available
[[ -f "$DOCKER_CLEANED_FLAG" ]] && return 0
if ! command -v docker &> /dev/null || ! docker info &> /dev/null 2>&1; then
return 0
fi
local found_artifacts=false
# Define containers to remove
local containers=(
"gemini-mcp-server"
"gemini-mcp-redis"
"zen-mcp-server"
"zen-mcp-redis"
"zen-mcp-log-monitor"
)
# Remove containers
for container in "${containers[@]}"; do
if docker ps -a --format "{{.Names}}" | grep -q "^${container}$" 2>/dev/null; then
if [[ "$found_artifacts" == false ]]; then
echo "One-time Docker cleanup..."
found_artifacts=true
fi
echo " Removing container: $container"
docker stop "$container" >/dev/null 2>&1 || true
docker rm "$container" >/dev/null 2>&1 || true
fi
done
# Remove images
local images=("gemini-mcp-server:latest" "zen-mcp-server:latest")
for image in "${images[@]}"; do
if docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${image}$" 2>/dev/null; then
if [[ "$found_artifacts" == false ]]; then
echo "One-time Docker cleanup..."
found_artifacts=true
fi
echo " Removing image: $image"
docker rmi "$image" >/dev/null 2>&1 || true
fi
done
# Remove volumes
local volumes=("redis_data" "mcp_logs")
for volume in "${volumes[@]}"; do
if docker volume ls --format "{{.Name}}" | grep -q "^${volume}$" 2>/dev/null; then
if [[ "$found_artifacts" == false ]]; then
echo "One-time Docker cleanup..."
found_artifacts=true
fi
echo " Removing volume: $volume"
docker volume rm "$volume" >/dev/null 2>&1 || true
fi
done
if [[ "$found_artifacts" == true ]]; then
print_success "Docker cleanup complete"
fi
touch "$DOCKER_CLEANED_FLAG"
}
# ----------------------------------------------------------------------------
# Python Environment Functions
# ----------------------------------------------------------------------------
# Find suitable Python command
find_python() {
# Prefer Python 3.12 for best compatibility
local python_cmds=("python3.12" "python3.13" "python3.11" "python3.10" "python3" "python" "py")
for cmd in "${python_cmds[@]}"; do
if command -v "$cmd" &> /dev/null; then
local version=$($cmd --version 2>&1)
if [[ $version =~ Python\ 3\.([0-9]+)\.([0-9]+) ]]; then
local major_version=${BASH_REMATCH[1]}
local minor_version=${BASH_REMATCH[2]}
# Check minimum version (3.10) for better library compatibility
if [[ $major_version -ge 10 ]]; then
echo "$cmd"
print_success "Found Python: $version"
# Recommend Python 3.12
if [[ $major_version -ne 12 ]]; then
print_info "Note: Python 3.12 is recommended for best compatibility."
fi
return 0
fi
fi
fi
done
print_error "Python 3.10+ not found. Please install Python 3.10 or newer (3.12 recommended)."
return 1
}
# Setup virtual environment
setup_venv() {
local python_cmd="$1"
local venv_python=""
# Create venv if it doesn't exist
if [[ ! -d "$VENV_PATH" ]]; then
print_info "Creating isolated environment..."
if $python_cmd -m venv "$VENV_PATH" 2>/dev/null; then
print_success "Created isolated environment"
else
print_error "Failed to create virtual environment"
exit 1
fi
fi
# Get venv Python path based on platform
local os_type=$(detect_os)
case "$os_type" in
windows)
venv_python="$VENV_PATH/Scripts/python.exe"
;;
*)
venv_python="$VENV_PATH/bin/python"
;;
esac
# Always use venv Python
if [[ -f "$venv_python" ]]; then
if [[ -n "${VIRTUAL_ENV:-}" ]]; then
print_success "Using activated virtual environment"
fi
# Convert to absolute path for MCP registration
local abs_venv_python=$(cd "$(dirname "$venv_python")" && pwd)/$(basename "$venv_python")
echo "$abs_venv_python"
return 0
else
print_error "Virtual environment Python not found"
exit 1
fi
}
# Check if package is installed
check_package() {
local python_cmd="$1"
local package="$2"
$python_cmd -c "import $package" 2>/dev/null
}
# Install dependencies
install_dependencies() {
local python_cmd="$1"
local deps_needed=false
# Check required packages
local packages=("mcp" "google.generativeai" "openai" "pydantic")
for package in "${packages[@]}"; do
local import_name=${package%%.*} # Get first part before dot
if ! check_package "$python_cmd" "$import_name"; then
deps_needed=true
break
fi
done
if [[ "$deps_needed" == false ]]; then
print_success "Dependencies already installed"
return 0
fi
echo ""
print_info "Setting up Zen MCP Server..."
echo "Installing required components:"
echo " • MCP protocol library"
echo " • AI model connectors"
echo " • Data validation tools"
echo ""
# Determine if we're in a venv
local install_cmd
if [[ -n "${VIRTUAL_ENV:-}" ]] || [[ "$python_cmd" == *"$VENV_PATH"* ]]; then
install_cmd="$python_cmd -m pip install -q -r requirements.txt"
else
install_cmd="$python_cmd -m pip install -q --user -r requirements.txt"
fi
# Install packages
echo -n "Downloading packages..."
if $install_cmd 2>&1 | grep -i error | grep -v warning; then
echo -e "\r${RED}✗ Setup failed${NC} "
echo ""
echo "Try running manually:"
echo " $python_cmd -m pip install mcp google-genai openai pydantic"
return 1
else
echo -e "\r${GREEN}✓ Setup complete!${NC} "
return 0
fi
}
# ----------------------------------------------------------------------------
# Environment Configuration Functions
# ----------------------------------------------------------------------------
# Setup .env file
setup_env_file() {
if [[ -f .env ]]; then
print_success ".env file already exists"
migrate_env_file
return 0
fi
if [[ ! -f .env.example ]]; then
print_error ".env.example not found!"
return 1
fi
cp .env.example .env
print_success "Created .env from .env.example"
# Detect sed version for cross-platform compatibility
local sed_cmd
if sed --version >/dev/null 2>&1; then
sed_cmd="sed -i" # GNU sed (Linux)
else
sed_cmd="sed -i ''" # BSD sed (macOS)
fi
# Update API keys from environment if present
local api_keys=(
"GEMINI_API_KEY:your_gemini_api_key_here"
"OPENAI_API_KEY:your_openai_api_key_here"
"XAI_API_KEY:your_xai_api_key_here"
"OPENROUTER_API_KEY:your_openrouter_api_key_here"
)
for key_pair in "${api_keys[@]}"; do
local key_name="${key_pair%%:*}"
local placeholder="${key_pair##*:}"
local key_value="${!key_name:-}"
if [[ -n "$key_value" ]]; then
$sed_cmd "s/$placeholder/$key_value/" .env
print_success "Updated .env with $key_name from environment"
fi
done
return 0
}
# Migrate .env file from Docker to standalone format
migrate_env_file() {
# Check if migration is needed
if ! grep -q "host\.docker\.internal" .env 2>/dev/null; then
return 0
fi
print_warning "Migrating .env from Docker to standalone format..."
# Create backup
cp .env .env.backup_$(date +%Y%m%d_%H%M%S)
# Detect sed version for cross-platform compatibility
local sed_cmd
if sed --version >/dev/null 2>&1; then
sed_cmd="sed -i" # GNU sed (Linux)
else
sed_cmd="sed -i ''" # BSD sed (macOS)
fi
# Replace host.docker.internal with localhost
$sed_cmd 's/host\.docker\.internal/localhost/g' .env
print_success "Migrated Docker URLs to localhost in .env"
echo " (Backup saved as .env.backup_*)"
}
# Validate API keys
validate_api_keys() {
local has_key=false
local api_keys=(
"GEMINI_API_KEY:your_gemini_api_key_here"
"OPENAI_API_KEY:your_openai_api_key_here"
"XAI_API_KEY:your_xai_api_key_here"
"OPENROUTER_API_KEY:your_openrouter_api_key_here"
)
for key_pair in "${api_keys[@]}"; do
local key_name="${key_pair%%:*}"
local placeholder="${key_pair##*:}"
local key_value="${!key_name:-}"
if [[ -n "$key_value" ]] && [[ "$key_value" != "$placeholder" ]]; then
print_success "$key_name configured"
has_key=true
fi
done
# Check custom API URL
if [[ -n "${CUSTOM_API_URL:-}" ]]; then
print_success "CUSTOM_API_URL configured: $CUSTOM_API_URL"
has_key=true
fi
if [[ "$has_key" == false ]]; then
print_error "No API keys found in .env!"
echo "" >&2
echo "Please edit .env and add at least one API key:" >&2
echo " GEMINI_API_KEY=your-actual-key" >&2
echo " OPENAI_API_KEY=your-actual-key" >&2
echo " XAI_API_KEY=your-actual-key" >&2
echo " OPENROUTER_API_KEY=your-actual-key" >&2
echo "" >&2
return 1
fi
return 0
}
# ----------------------------------------------------------------------------
# Claude Integration Functions
# ----------------------------------------------------------------------------
# Check if MCP is added to Claude CLI and verify it's correct
check_claude_cli_integration() {
local python_cmd="$1"
local server_path="$2"
if ! command -v claude &> /dev/null; then
echo ""
print_warning "Claude CLI not found"
echo ""
read -p "Would you like to add Zen to Claude Code? (Y/n): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Nn]$ ]]; then
print_info "Skipping Claude Code integration"
return 0
fi
echo ""
echo "Please install Claude Code first:"
echo " Visit: https://docs.anthropic.com/en/docs/claude-code/cli-usage"
echo ""
echo "Then run this script again to register MCP."
return 1
fi
# Check if zen is registered
local mcp_list=$(claude mcp list 2>/dev/null)
if echo "$mcp_list" | grep -q "zen"; then
# Check if it's using the old Docker command
if echo "$mcp_list" | grep -E "zen.*docker|zen.*compose" &>/dev/null; then
print_warning "Found old Docker-based Zen registration, updating..."
claude mcp remove zen -s user 2>/dev/null || true
# Re-add with correct Python command
if claude mcp add zen -s user -- "$python_cmd" "$server_path" 2>/dev/null; then
print_success "Updated Zen to become a standalone script"
return 0
else
echo ""
echo "Failed to update MCP registration. Please run manually:"
echo " claude mcp remove zen -s user"
echo " claude mcp add zen -s user -- $python_cmd $server_path"
return 1
fi
else
# Verify the registered path matches current setup
local expected_cmd="$python_cmd $server_path"
if echo "$mcp_list" | grep -F "$server_path" &>/dev/null; then
return 0
else
print_warning "Zen registered with different path, updating..."
claude mcp remove zen -s user 2>/dev/null || true
if claude mcp add zen -s user -- "$python_cmd" "$server_path" 2>/dev/null; then
print_success "Updated Zen with current path"
return 0
else
echo ""
echo "Failed to update MCP registration. Please run manually:"
echo " claude mcp remove zen -s user"
echo " claude mcp add zen -s user -- $python_cmd $server_path"
return 1
fi
fi
fi
else
# Not registered at all, ask user if they want to add it
echo ""
read -p "Add Zen to Claude Code? (Y/n): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Nn]$ ]]; then
print_info "To add manually later, run:"
echo " claude mcp add zen -s user -- $python_cmd $server_path"
return 0
fi
print_info "Registering Zen with Claude Code..."
if claude mcp add zen -s user -- "$python_cmd" "$server_path" 2>/dev/null; then
print_success "Successfully added Zen to Claude Code"
return 0
else
echo ""
echo "Failed to add automatically. To add manually, run:"
echo " claude mcp add zen -s user -- $python_cmd $server_path"
return 1
fi
fi
}
# Check and update Claude Desktop configuration
check_claude_desktop_integration() {
local python_cmd="$1"
local server_path="$2"
# Skip if already configured (check flag)
if [[ -f "$DESKTOP_CONFIG_FLAG" ]]; then
return 0
fi
local config_path=$(get_claude_config_path)
if [[ -z "$config_path" ]]; then
print_warning "Unable to determine Claude Desktop config path for this platform"
return 0
fi
echo ""
read -p "Configure Zen for Claude Desktop? (Y/n): " -n 1 -r
echo ""
if [[ $REPLY =~ ^[Nn]$ ]]; then
print_info "Skipping Claude Desktop integration"
touch "$DESKTOP_CONFIG_FLAG" # Don't ask again
return 0
fi
# Create config directory if it doesn't exist
local config_dir=$(dirname "$config_path")
mkdir -p "$config_dir" 2>/dev/null || true
# Handle existing config
if [[ -f "$config_path" ]]; then
print_info "Updating existing Claude Desktop config..."
# Check for old Docker config and remove it
if grep -q "docker.*compose.*zen\|zen.*docker" "$config_path" 2>/dev/null; then
print_warning "Removing old Docker-based MCP configuration..."
# Create backup
cp "$config_path" "${config_path}.backup_$(date +%Y%m%d_%H%M%S)"
# Remove old zen config using a more robust approach
local temp_file=$(mktemp)
python3 -c "
import json
import sys
try:
with open('$config_path', 'r') as f:
config = json.load(f)
# Remove zen from mcpServers if it exists
if 'mcpServers' in config and 'zen' in config['mcpServers']:
del config['mcpServers']['zen']
print('Removed old zen MCP configuration')
with open('$temp_file', 'w') as f:
json.dump(config, f, indent=2)
except Exception as e:
print(f'Error processing config: {e}', file=sys.stderr)
sys.exit(1)
" && mv "$temp_file" "$config_path"
fi
# Add new config
local temp_file=$(mktemp)
python3 -c "
import json
import sys
try:
with open('$config_path', 'r') as f:
config = json.load(f)
except:
config = {}
# Ensure mcpServers exists
if 'mcpServers' not in config:
config['mcpServers'] = {}
# Add zen server
config['mcpServers']['zen'] = {
'command': '$python_cmd',
'args': ['$server_path']
}
with open('$temp_file', 'w') as f:
json.dump(config, f, indent=2)
" && mv "$temp_file" "$config_path"
else
print_info "Creating new Claude Desktop config..."
cat > "$config_path" << EOF
{
"mcpServers": {
"zen": {
"command": "$python_cmd",
"args": ["$server_path"]
}
}
}
EOF
fi
if [[ $? -eq 0 ]]; then
print_success "Successfully configured Claude Desktop"
echo " Config: $config_path"
echo " Restart Claude Desktop to use the new MCP server"
touch "$DESKTOP_CONFIG_FLAG"
else
print_error "Failed to update Claude Desktop config"
echo "Manual config location: $config_path"
echo "Add this configuration:"
cat << EOF
{
"mcpServers": {
"zen": {
"command": "$python_cmd",
"args": ["$server_path"]
}
}
}
EOF
fi
}
# Display configuration instructions
display_config_instructions() {
local python_cmd="$1"
local server_path="$2"
echo ""
local config_header="ZEN MCP SERVER CONFIGURATION"
echo "===== $config_header ====="
printf '%*s\n' "$((${#config_header} + 12))" | tr ' ' '='
echo ""
echo "To use Zen MCP Server with your Claude clients:"
echo ""
print_info "1. For Claude Code (CLI):"
echo -e " ${GREEN}claude mcp add zen -s user -- $python_cmd $server_path${NC}"
echo ""
print_info "2. For Claude Desktop:"
echo " Add this configuration to your Claude Desktop config file:"
echo ""
cat << EOF
{
"mcpServers": {
"zen": {
"command": "$python_cmd",
"args": ["$server_path"]
}
}
}
EOF
# Show platform-specific config location
local config_path=$(get_claude_config_path)
if [[ -n "$config_path" ]]; then
echo ""
print_info " Config file location:"
echo -e " ${YELLOW}$config_path${NC}"
fi
echo ""
print_info "3. Restart Claude Desktop after updating the config file"
echo ""
}
# Display setup instructions
display_setup_instructions() {
local python_cmd="$1"
local server_path="$2"
echo ""
local setup_header="SETUP COMPLETE"
echo "===== $setup_header ====="
printf '%*s\n' "$((${#setup_header} + 12))" | tr ' ' '='
echo ""
print_success "Zen is ready to use!"
}
# ----------------------------------------------------------------------------
# Log Management Functions
# ----------------------------------------------------------------------------
# Show help message
show_help() {
local version=$(get_version)
local header="🤖 Zen MCP Server v$version"
echo "$header"
printf '%*s\n' "${#header}" | tr ' ' '='
echo ""
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " -h, --help Show this help message"
echo " -v, --version Show version information"
echo " -f, --follow Follow server logs in real-time"
echo " -c, --config Show configuration instructions for Claude clients"
echo ""
echo "Examples:"
echo " $0 Setup and start the MCP server"
echo " $0 -f Setup and follow logs"
echo " $0 -c Show configuration instructions"
echo " $0 --version Show version only"
echo ""
echo "For more information, visit:"
echo " https://github.com/BeehiveInnovations/zen-mcp-server"
}
# Show version only
show_version() {
local version=$(get_version)
echo "$version"
}
# Follow logs
follow_logs() {
local log_path="$LOG_DIR/$LOG_FILE"
echo "Following server logs (Ctrl+C to stop)..."
echo ""
# Create logs directory and file if they don't exist
mkdir -p "$LOG_DIR"
touch "$log_path"
# Follow the log file
tail -f "$log_path"
}
# ----------------------------------------------------------------------------
# Main Function
# ----------------------------------------------------------------------------
main() {
# Parse command line arguments
local arg="${1:-}"
case "$arg" in
-h|--help)
show_help
exit 0
;;
-v|--version)
show_version
exit 0
;;
-c|--config)
# Setup minimal environment to get paths for config display
local python_cmd
python_cmd=$(find_python) || exit 1
local new_python_cmd
new_python_cmd=$(setup_venv "$python_cmd")
python_cmd="$new_python_cmd"
local script_dir=$(get_script_dir)
local server_path="$script_dir/server.py"
display_config_instructions "$python_cmd" "$server_path"
exit 0
;;
-f|--follow)
# Continue with normal setup then follow logs
;;
"")
# Normal setup without following logs
;;
*)
print_error "Unknown option: $arg"
echo "" >&2
show_help
exit 1
;;
esac
# Display header
local main_header="🤖 Zen MCP Server"
echo "$main_header"
printf '%*s\n' "${#main_header}" | tr ' ' '='
# Get and display version
local version=$(get_version)
echo "Version: $version"
echo ""
# Check if venv exists
if [[ ! -d "$VENV_PATH" ]]; then
echo "Setting up Python environment for first time..."
fi
# Step 1: Docker cleanup
cleanup_docker
# Step 2: Find Python
local python_cmd
python_cmd=$(find_python) || exit 1
# Step 3: Setup environment file
setup_env_file || exit 1
# Step 4: Source .env file
if [[ -f .env ]]; then
set -a
source .env
set +a
fi
# Step 5: Validate API keys
validate_api_keys || exit 1
# Step 6: Setup virtual environment
local new_python_cmd
new_python_cmd=$(setup_venv "$python_cmd")
python_cmd="$new_python_cmd"
# Step 7: Install dependencies
install_dependencies "$python_cmd" || exit 1
# Step 8: Get absolute server path
local script_dir=$(get_script_dir)
local server_path="$script_dir/server.py"
# Step 9: Display setup instructions
display_setup_instructions "$python_cmd" "$server_path"
# Step 10: Check Claude integrations
check_claude_cli_integration "$python_cmd" "$server_path"
check_claude_desktop_integration "$python_cmd" "$server_path"
# Step 11: Display log information
echo ""
echo "Logs will be written to: $script_dir/$LOG_DIR/$LOG_FILE"
echo ""
# Step 12: Handle command line arguments
if [[ "$arg" == "-f" ]] || [[ "$arg" == "--follow" ]]; then
follow_logs
else
echo "To follow logs: ./run-server.sh -f"
echo "To show config: ./run-server.sh -c"
echo "To update: git pull, then run ./run-server.sh again"
echo ""
echo "Happy Clauding! 🎉"
fi
}
# ----------------------------------------------------------------------------
# Script Entry Point
# ----------------------------------------------------------------------------
# Run main function with all arguments
main "$@"