diff --git a/README.md b/README.md index 3d96c5e..71156aa 100644 --- a/README.md +++ b/README.md @@ -488,6 +488,7 @@ Server-Sent Events endpoint for live UI updates. - [x] Dark / light theme toggle defaulting to OS colour-scheme preference. - [x] `default_wait_seconds` changed to fully server-controlled (agents can no longer override wait time). - [x] Non-blocking `server.ps1` management script (start / stop / restart / status / logs). + - [x] Non-blocking `server.sh` bash management script — identical feature set for macOS / Linux. - [x] MCP stateless/stateful mode configurable via `MCP_STATELESS` env var (default `true`). - [x] Per-agent generation counter prevents abandoned (timed-out) coroutines from silently consuming instructions meant for newer calls. - [x] `tests/test_wakeup.py` covers both immediate-wakeup timing and concurrent-call generation safety. @@ -518,8 +519,9 @@ pip install -r requirements.txt python main.py ``` -Or use the included management script (recommended — non-blocking): +Or use the included management scripts (recommended — non-blocking): +**PowerShell (Windows)** ```powershell .\server.ps1 start # start in background, logs to logs/ .\server.ps1 stop # graceful stop @@ -530,6 +532,18 @@ Or use the included management script (recommended — non-blocking): .\server.ps1 logs 100 # show last 100 lines ``` +**Bash (macOS / Linux)** +```bash +chmod +x server.sh # make executable once +./server.sh start # start in background, logs to logs/ +./server.sh stop # graceful stop +./server.sh restart # stop + start +./server.sh status # PID, memory, tail logs +./server.sh logs # show last 40 stdout lines +./server.sh logs -f # follow logs live +./server.sh logs 100 # show last 100 lines +``` + The server starts on `http://localhost:8000` by default. | URL | Description | diff --git a/server.sh b/server.sh new file mode 100644 index 0000000..21a0616 --- /dev/null +++ b/server.sh @@ -0,0 +1,250 @@ +#!/usr/bin/env bash +# server.sh — Non-blocking server management script for local-mcp. +# +# Usage: +# ./server.sh start # Start (no-op if already running) +# ./server.sh stop # Kill the running server +# ./server.sh restart # Stop then start +# ./server.sh status # Show PID, memory, and tail 20 log lines +# ./server.sh logs [N] # Tail last N lines of stdout log (default 40) +# ./server.sh logs -f # Follow log live (Ctrl-C to quit) + +set -euo pipefail + +# ── Paths ───────────────────────────────────────────────────────────────── + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PYTHON="$ROOT/.venv/bin/python" +ENTRY="$ROOT/main.py" +LOG_DIR="$ROOT/logs" +LOG_OUT="$LOG_DIR/server.log" +LOG_ERR="$LOG_DIR/server.err.log" +PID_FILE="$LOG_DIR/server.pid" +PORT=8000 + +# ── Colours ─────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +GRAY='\033[0;90m' +NC='\033[0m' # No Colour + +cecho() { printf "${1}${2}${NC}\n"; } + +# ── Helpers ─────────────────────────────────────────────────────────────── + +ensure_log_dir() { + mkdir -p "$LOG_DIR" +} + +get_server_pid() { + # Trust the PID file first; verify the process is alive + if [[ -f "$PID_FILE" ]]; then + local stored + stored=$(cat "$PID_FILE" 2>/dev/null || true) + if [[ "$stored" =~ ^[0-9]+$ ]]; then + if kill -0 "$stored" 2>/dev/null; then + echo "$stored" + return + fi + fi + rm -f "$PID_FILE" + fi + + # Fallback: find a process listening on the port + local pid + pid=$(lsof -ti "TCP:$PORT" -s "TCP:LISTEN" 2>/dev/null | head -1 || true) + if [[ -n "$pid" ]]; then + echo "$pid" + return + fi + + echo "" +} + +is_running() { + local pid + pid=$(get_server_pid) + [[ -n "$pid" ]] +} + +# ── Commands ────────────────────────────────────────────────────────────── + +cmd_start() { + local pid + pid=$(get_server_pid) + if [[ -n "$pid" ]]; then + cecho "$GREEN" "Server already running (PID $pid http://localhost:$PORT)" + return + fi + + if [[ ! -x "$PYTHON" ]]; then + cecho "$RED" "ERROR: Python venv not found at $PYTHON" + cecho "$YELLOW" "Run: python -m venv .venv && .venv/bin/pip install -r requirements.txt" + exit 1 + fi + + ensure_log_dir + + # Stamp the log files + local stamp + stamp="$(date '+%Y-%m-%d %H:%M:%S') [server.sh] ---- starting ----" + echo "$stamp" >> "$LOG_OUT" + echo "$stamp" >> "$LOG_ERR" + + # Start detached: stdout → LOG_OUT, stderr → LOG_ERR + nohup "$PYTHON" -u "$ENTRY" >> "$LOG_OUT" 2>> "$LOG_ERR" & + local new_pid=$! + echo "$new_pid" > "$PID_FILE" + + # Wait up to 6 s for the port to open + local deadline=$(( $(date +%s) + 6 )) + local ready=false + while [[ $(date +%s) -lt $deadline ]]; do + sleep 0.4 + if lsof -ti "TCP:$PORT" -s "TCP:LISTEN" &>/dev/null; then + ready=true + break + fi + done + + if $ready; then + cecho "$GREEN" "Server started (PID $new_pid http://localhost:$PORT)" + else + cecho "$YELLOW" "Server process launched (PID $new_pid) but port $PORT not yet open." + cecho "$YELLOW" "Check logs: ./server.sh logs" + fi +} + +cmd_stop() { + local pid + pid=$(get_server_pid) + if [[ -z "$pid" ]]; then + cecho "$YELLOW" "Server is not running." + return + fi + + kill "$pid" 2>/dev/null || true + rm -f "$PID_FILE" + + # Wait up to 4 s for the port to free + local deadline=$(( $(date +%s) + 4 )) + while [[ $(date +%s) -lt $deadline ]]; do + sleep 0.3 + if ! lsof -ti "TCP:$PORT" -s "TCP:LISTEN" &>/dev/null; then + break + fi + done + + cecho "$YELLOW" "Server stopped (was PID $pid)" +} + +cmd_restart() { + cmd_stop + sleep 0.5 + cmd_start +} + +cmd_status() { + local pid + pid=$(get_server_pid) + + echo "" + if [[ -n "$pid" ]]; then + local mem="?" + # Try to get RSS memory in MB (Linux: /proc, macOS: ps) + if [[ -f "/proc/$pid/status" ]]; then + local kb + kb=$(grep -i VmRSS "/proc/$pid/status" 2>/dev/null | awk '{print $2}' || echo "0") + mem="$(( kb / 1024 )) MB" + elif command -v ps &>/dev/null; then + local rss + rss=$(ps -o rss= -p "$pid" 2>/dev/null | tr -d ' ' || echo "0") + mem="$(( rss / 1024 )) MB" + fi + + cecho "$GREEN" " Status : RUNNING" + printf " PID : %s\n" "$pid" + printf " Memory : %s\n" "$mem" + printf " URL : http://localhost:%s\n" "$PORT" + printf " Logs : %s\n" "$LOG_OUT" + printf " %s\n" "$LOG_ERR" + else + cecho "$RED" " Status : STOPPED" + fi + + echo "" + cecho "$GRAY" "--- Last 20 log lines (stdout) ---" + if [[ -f "$LOG_OUT" ]]; then + tail -n 20 "$LOG_OUT" | while IFS= read -r line; do + cecho "$GRAY" "$line" + done + else + cecho "$GRAY" " (no log file yet)" + fi + + if [[ -f "$LOG_ERR" ]]; then + local err_lines + err_lines=$(grep -E "ERROR|Exception|Traceback" "$LOG_ERR" 2>/dev/null | tail -5 || true) + if [[ -n "$err_lines" ]]; then + echo "" + cecho "$RED" "--- Recent errors (stderr) ---" + echo "$err_lines" | while IFS= read -r line; do + cecho "$RED" "$line" + done + fi + fi + echo "" +} + +cmd_logs() { + local arg="${1:-}" + ensure_log_dir + + if [[ "$arg" == "-f" ]]; then + if [[ ! -f "$LOG_OUT" ]]; then + cecho "$YELLOW" "No log file yet. Start the server first." + return + fi + cecho "$CYAN" "Following $LOG_OUT (Ctrl-C to stop)" + tail -f -n 30 "$LOG_OUT" + else + local n=40 + if [[ "$arg" =~ ^[0-9]+$ ]]; then + n="$arg" + fi + cecho "$CYAN" "--- stdout (last $n lines) ---" + if [[ -f "$LOG_OUT" ]]; then + tail -n "$n" "$LOG_OUT" + else + cecho "$GRAY" " (empty)" + fi + echo "" + cecho "$YELLOW" "--- stderr (last 20 lines) ---" + if [[ -f "$LOG_ERR" ]]; then + tail -n 20 "$LOG_ERR" + else + cecho "$GRAY" " (empty)" + fi + fi +} + +# ── Dispatch ────────────────────────────────────────────────────────────── + +COMMAND="${1:-status}" +ARG="${2:-}" + +case "$COMMAND" in + start) cmd_start ;; + stop) cmd_stop ;; + restart) cmd_restart ;; + status) cmd_status ;; + logs) cmd_logs "$ARG" ;; + *) + echo "Usage: $0 {start|stop|restart|status|logs [N|-f]}" + exit 1 + ;; +esac +