<# .SYNOPSIS Non-blocking server management script for local-mcp. .DESCRIPTION Manages the local-mcp server process without blocking the terminal. The server runs as a detached background process; stdout/stderr are written to logs/server.log. .USAGE .\server.ps1 start # Start (no-op if already running) .\server.ps1 stop # Kill the running server .\server.ps1 restart # Stop then start .\server.ps1 status # Show PID, port state, and tail 20 log lines .\server.ps1 logs [N] # Tail last N lines of the log (default 40) .\server.ps1 logs -f # Follow log live (Ctrl-C to quit) #> param( [Parameter(Position = 0)] [ValidateSet("start", "stop", "restart", "status", "logs")] [string]$Command = "status", [Parameter(Position = 1)] [string]$Arg = "" ) # ── Paths ───────────────────────────────────────────────────────────────── $Root = $PSScriptRoot $Python = Join-Path $Root ".venv\Scripts\python.exe" $Entry = Join-Path $Root "main.py" $LogDir = Join-Path $Root "logs" $LogOut = Join-Path $LogDir "server.log" $LogErr = Join-Path $LogDir "server.err.log" $PidFile = Join-Path $LogDir "server.pid" $Port = 8000 # ── Helpers ─────────────────────────────────────────────────────────────── function EnsureLogDir { if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir | Out-Null } } function GetServerPid { # Trust the PID file first; verify the process is actually alive if (Test-Path $PidFile) { $stored = Get-Content $PidFile -ErrorAction SilentlyContinue if ($stored -match '^\d+$') { $proc = Get-Process -Id ([int]$stored) -ErrorAction SilentlyContinue if ($proc) { return [int]$stored } } Remove-Item $PidFile -ErrorAction SilentlyContinue } # Fallback: find python process listening on the port $conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1 if ($conn) { return $conn.OwningProcess } return $null } function IsRunning { return $null -ne (GetServerPid) } function WriteLog([string]$msg, [string]$color = "White") { Write-Host $msg -ForegroundColor $color } function MergedLogTail([int]$n) { # Merge stdout + stderr logs sorted by content order and tail n lines $lines = @() if (Test-Path $LogOut) { $lines += Get-Content $LogOut -Tail ($n * 2) } if (Test-Path $LogErr) { $lines += Get-Content $LogErr -Tail ($n * 2) } # Return last $n lines (simple approach — interleaving is approximate) $lines | Select-Object -Last $n } # ── Commands ────────────────────────────────────────────────────────────── function Start-Server { if (IsRunning) { $pid_ = GetServerPid WriteLog "Server already running (PID $pid_ http://localhost:$Port)" "Green" return } if (-not (Test-Path $Python)) { WriteLog "ERROR: Python venv not found at $Python" "Red" WriteLog "Run: python -m venv .venv && .venv\Scripts\pip install -r requirements.txt" "Yellow" exit 1 } EnsureLogDir # Stamp both log files so they exist and have a separator $stamp = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [server.ps1] ---- starting ----" Add-Content $LogOut $stamp Add-Content $LogErr $stamp # Start python with stdout -> LogOut, stderr -> LogErr (separate files required on Windows) $proc = Start-Process ` -FilePath $Python ` -ArgumentList "-u `"$Entry`"" ` -WorkingDirectory $Root ` -RedirectStandardOutput $LogOut ` -RedirectStandardError $LogErr ` -WindowStyle Hidden ` -PassThru $proc.Id | Set-Content $PidFile # Wait up to 6 s for the port to open $deadline = (Get-Date).AddSeconds(6) $ready = $false while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 400 $conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue if ($conn) { $ready = $true; break } } if ($ready) { WriteLog "Server started (PID $($proc.Id) http://localhost:$Port)" "Green" } else { WriteLog "Server process launched (PID $($proc.Id)) but port $Port not yet open." "Yellow" WriteLog "Check logs: .\server.ps1 logs" "Yellow" } } function Stop-Server { $pid_ = GetServerPid if (-not $pid_) { WriteLog "Server is not running." "Yellow" return } Stop-Process -Id $pid_ -Force -ErrorAction SilentlyContinue Remove-Item $PidFile -ErrorAction SilentlyContinue # Wait up to 4 s for the port to free $deadline = (Get-Date).AddSeconds(4) while ((Get-Date) -lt $deadline) { Start-Sleep -Milliseconds 300 $conn = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue if (-not $conn) { break } } WriteLog "Server stopped (was PID $pid_)" "Yellow" } function Restart-Server { Stop-Server Start-Sleep -Milliseconds 500 Start-Server } function Show-Status { $pid_ = GetServerPid if ($pid_) { $proc = Get-Process -Id $pid_ -ErrorAction SilentlyContinue $mem = if ($proc) { "{0:N0} MB" -f ($proc.WorkingSet64 / 1MB) } else { "?" } WriteLog "" WriteLog " Status : RUNNING" "Green" WriteLog " PID : $pid_" WriteLog " Memory : $mem" WriteLog " URL : http://localhost:$Port" WriteLog " Logs : $LogOut" WriteLog " $LogErr" } else { WriteLog "" WriteLog " Status : STOPPED" "Red" } WriteLog "" WriteLog "--- Last 20 log lines (stdout) ---" "DarkGray" if (Test-Path $LogOut) { Get-Content $LogOut -Tail 20 | ForEach-Object { WriteLog $_ "DarkGray" } } else { WriteLog " (no log file yet)" "DarkGray" } if (Test-Path $LogErr) { $errLines = Get-Content $LogErr -Tail 5 | Where-Object { $_ -match "ERROR|Exception|Traceback" } if ($errLines) { WriteLog "" WriteLog "--- Recent errors (stderr) ---" "Red" $errLines | ForEach-Object { WriteLog $_ "Red" } } } WriteLog "" } function Tail-Logs { EnsureLogDir if ($Arg -eq "-f") { if (-not (Test-Path $LogOut)) { WriteLog "No log file yet. Start the server first." "Yellow" return } WriteLog "Following $LogOut (Ctrl-C to stop)" "Cyan" Get-Content $LogOut -Wait -Tail 30 } else { $n = if ($Arg -match '^\d+$') { [int]$Arg } else { 40 } WriteLog "--- stdout (last $n lines) ---" "Cyan" if (Test-Path $LogOut) { Get-Content $LogOut -Tail $n } else { WriteLog " (empty)" "DarkGray" } WriteLog "" WriteLog "--- stderr (last 20 lines) ---" "Yellow" if (Test-Path $LogErr) { Get-Content $LogErr -Tail 20 } else { WriteLog " (empty)" "DarkGray" } } } # ── Dispatch ────────────────────────────────────────────────────────────── switch ($Command) { "start" { Start-Server } "stop" { Stop-Server } "restart" { Restart-Server } "status" { Show-Status } "logs" { Tail-Logs } }