setup.sh•8.64 kB
#!/usr/bin/env bash
set -euo pipefail
# Improved setup script for MCP Server
# Usage: ./scripts/setup.sh [--target "$HOME/MCPServer"] [--user username]
TARGET_DIR=${TARGET_DIR:-$HOME/MCPServer}
INSTALL_USER="${INSTALL_USER:-}"
# parse args
while [[ $# -gt 0 ]]; do
case "$1" in
--target) TARGET_DIR="$2"; shift 2 ;;
--user) INSTALL_USER="$2"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
if [ "$TARGET_DIR" = "$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ]; then
echo "Refusing to install into source repository path: $TARGET_DIR" >&2
echo "Please specify --target \"$HOME/MCPServer\" or another directory under your home." >&2
exit 1
fi
echo "Setting up MCP server in: $TARGET_DIR"
# Ensure source dir
SRC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
echo "Source directory: $SRC_DIR"
mkdir -p "$TARGET_DIR/bin" "$TARGET_DIR/logs" "$TARGET_DIR/run" "$TARGET_DIR/config"
# If an install user was supplied, try to set ownership (best-effort)
if [ -n "$INSTALL_USER" ]; then
if id "$INSTALL_USER" >/dev/null 2>&1; then
chown -R "$INSTALL_USER":"$INSTALL_USER" "$TARGET_DIR" || true
else
echo "Warning: install user '$INSTALL_USER' not found; continuing as current user"
fi
fi
# Create venv
VENV_DIR="$TARGET_DIR/venv"
if [ ! -d "$VENV_DIR" ]; then
echo "Creating venv at $VENV_DIR"
if command -v python3 >/dev/null 2>&1; then
python3 -m venv "$VENV_DIR"
else
python -m venv "$VENV_DIR"
fi
fi
# Install requirements into venv using venv python
if [ -f "$SRC_DIR/requirements.txt" ]; then
echo "Installing pip requirements into venv..."
"$VENV_DIR/bin/python" -m pip install --upgrade pip setuptools wheel
"$VENV_DIR/bin/python" -m pip install -r "$SRC_DIR/requirements.txt"
else
echo "No requirements.txt found in source; skipping pip install."
fi
# Copy application code (exclude docs and probes by default)
echo "Copying application files to $TARGET_DIR/config (excluding docs/probes)..."
rsync -a --delete --exclude='.git' --exclude='*.md' --exclude='tools/mcp_probe.py' \
--exclude='__pycache__' --exclude='logs' --exclude='.vscode' \
"$SRC_DIR/" "$TARGET_DIR/config/"
# Ensure .env exists: prefer existing .env in repo, else copy .env.example
if [ -f "$SRC_DIR/.env" ]; then
cp -f "$SRC_DIR/.env" "$TARGET_DIR/config/.env"
elif [ -f "$SRC_DIR/.env.example" ]; then
echo "Copying .env.example to $TARGET_DIR/config/.env (please edit credentials)"
cp -f "$SRC_DIR/.env.example" "$TARGET_DIR/config/.env"
fi
echo "Generating runtime wrappers in $TARGET_DIR/bin"
cat > "$TARGET_DIR/bin/start.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
VENV="$DIR/venv"
# shellcheck source=/dev/null
source "$VENV/bin/activate"
export PYTHONUNBUFFERED=1
# Load env if present
if [ -f "$DIR/config/.env" ]; then
# Normalize CRLF to LF to avoid $'\r' errors
if command -v dos2unix >/dev/null 2>&1; then
dos2unix "$DIR/config/.env" >/dev/null 2>&1 || true
else
if grep -q $'\r' "$DIR/config/.env" >/dev/null 2>&1; then
sed -i 's/\r$//' "$DIR/config/.env" || true
fi
fi
set -o allexport
# shellcheck disable=SC1091
source "$DIR/config/.env"
set +o allexport
fi
# Logs and pid
LOG="$DIR/logs/mcp-server.log"
PIDFILE="$DIR/run/mcp-server.pid"
PGIDFILE="$DIR/run/mcp-server.pgid"
cmd="$VENV/bin/python -u $DIR/config/server.py"
if [ "${1:-}" = "foreground" ]; then
exec $cmd
fi
# Prevent duplicate starts
if [ -f "$PIDFILE" ]; then
EXISTING_PID=$(cat "$PIDFILE" 2>/dev/null || echo "")
if [ -n "$EXISTING_PID" ] && kill -0 "$EXISTING_PID" 2>/dev/null; then
echo "Server already running (PID $EXISTING_PID)."
exit 0
else
echo "Removing stale pidfile $PIDFILE" >&2
rm -f "$PIDFILE" || true
rm -f "$PGIDFILE" || true
fi
fi
echo "Starting MCP server..."
# Start in its own session so we can stop the group later
setsid nohup $cmd >> "$LOG" 2>&1 &
PID=$!
echo "$PID" > "$PIDFILE"
sleep 0.2
if command -v ps >/dev/null 2>&1; then
PGID=$(ps -o pgid= "$PID" | tr -d ' ' || true)
else
PGID=""
fi
if [ -n "$PGID" ]; then
echo "$PGID" > "$PGIDFILE"
fi
sleep 0.5
if kill -0 "$PID" >/dev/null 2>&1; then
echo "Started: PID $PID, PGID ${PGID:-unknown}, logs: $LOG"
else
echo "Failed to start server; check $LOG for errors." >&2
tail -n 50 "$LOG" >&2 || true
exit 1
fi
EOF
chmod +x "$TARGET_DIR/bin/start.sh"
cat > "$TARGET_DIR/bin/stop.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PIDFILE="$DIR/run/mcp-server.pid"
PGIDFILE="$DIR/run/mcp-server.pgid"
if [ ! -f "$PIDFILE" ]; then
echo "No PID file found; server may not be running."
exit 0
fi
PID="$(cat "$PIDFILE")"
if [ -f "/proc/$PID/cmdline" ]; then
CMDLINE=$(tr '\0' ' ' < /proc/$PID/cmdline)
if ! echo "$CMDLINE" | grep -E "(python|uvicorn|server.py)" >/dev/null 2>&1; then
echo "PID $PID does not look like our server process: $CMDLINE" >&2
echo "Removing stale pidfile." >&2
rm -f "$PIDFILE" || true
rm -f "$PGIDFILE" || true
exit 0
fi
fi
# If we have a PGID, prefer stopping the whole group
if [ -f "$PGIDFILE" ]; then
PGID=$(cat "$PGIDFILE" 2>/dev/null || echo "")
if [ -n "$PGID" ]; then
echo "Stopping process group $PGID..."
kill -TERM -"$PGID" 2>/dev/null || true
for i in {1..8}; do
if ! kill -0 -"$PGID" >/dev/null 2>&1; then
break
fi
sleep 1
done
if kill -0 -"$PGID" >/dev/null 2>&1; then
echo "Process group $PGID did not stop; force killing..."
kill -KILL -"$PGID" 2>/dev/null || true
fi
rm -f "$PGIDFILE" || true
fi
fi
if kill -0 "$PID" >/dev/null 2>&1; then
echo "Stopping PID $PID..."
kill -TERM "$PID" 2>/dev/null || true
for i in {1..6}; do
if ! kill -0 "$PID" >/dev/null 2>&1; then
break
fi
sleep 1
done
if kill -0 "$PID" >/dev/null 2>&1; then
echo "PID $PID did not exit, force killing..."
kill -KILL "$PID" 2>/dev/null || true
fi
else
echo "Process $PID not running; cleaning up pidfile." >&2
fi
rm -f "$PIDFILE" || true
echo "Stopped"
EOF
chmod +x "$TARGET_DIR/bin/stop.sh"
cat > "$TARGET_DIR/bin/health_check.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [ -f "$DIR/config/.env" ]; then
if command -v dos2unix >/dev/null 2>&1; then
dos2unix "$DIR/config/.env" >/dev/null 2>&1 || true
else
if grep -q $'\r' "$DIR/config/.env" >/dev/null 2>&1; then
sed -i 's/\r$//' "$DIR/config/.env" || true
fi
fi
set -o allexport
# shellcheck disable=SC1091
source "$DIR/config/.env"
set +o allexport
fi
HOST=${MCP_HOST:-127.0.0.1}
PORT=${MCP_PORT:-3000}
URL="http://$HOST:$PORT/health"
if command -v curl >/dev/null 2>&1; then
HTTP=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$URL" || true)
if [ "$HTTP" = "200" ]; then
echo "OK"
exit 0
else
echo "UNHEALTHY (HTTP $HTTP)"
exit 1
fi
else
PIDFILE="$DIR/run/mcp-server.pid"
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
echo "OK (process running)"
exit 0
fi
echo "UNHEALTHY (no curl and no running process)"
exit 1
fi
EOF
chmod +x "$TARGET_DIR/bin/health_check.sh"
cat > "$TARGET_DIR/bin/uninstall.sh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
echo "Stopping and removing installation at $DIR"
if [ -f "$DIR/run/mcp-server.pid" ]; then
"$DIR/bin/stop.sh" || true
fi
if [ "${FORCE:-}" = "yes" ]; then
echo "FORCE=yes set; removing installation and logs without prompting"
rm -rf "$DIR" || true
echo "Installation directory removed"
else
echo "You are about to remove: $DIR"
read -p "Type the exact path to confirm deletion: " -r CONFIRM_PATH
if [ "$CONFIRM_PATH" != "$DIR" ]; then
echo "Confirmation did not match. Aborting without changes."
exit 1
fi
rm -rf "$DIR"
echo "Installation directory removed"
fi
echo "Uninstalled."
EOF
chmod +x "$TARGET_DIR/bin/uninstall.sh"
echo "Setup complete. Runtime wrappers are in $TARGET_DIR/bin"
echo "To start: $TARGET_DIR/bin/start.sh | To stop: $TARGET_DIR/bin/stop.sh | Health: $TARGET_DIR/bin/health_check.sh"