Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"permissions": {
"allow": [
"Bash(uv run:*)",
"Bash(python3:*)",
"WebSearch",
"WebFetch(domain:github.com)",
"WebFetch(domain:raw.githubusercontent.com)",
"WebFetch(domain:pypi.org)",
"Bash(ls:*)",
"Bash(uv pip list:*)",
"Bash(.venv/bin/pip list:*)",
"Bash(.venv/bin/pip show:*)",
"Bash(.venv/bin/python:*)",
"WebFetch(domain:www.lumia.security)"
]
}
}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ client_secret_*.apps.googleusercontent.com.json
web-ui/frontend/node_modules
web-ui/backend/.venv-backend/
.pnpm-store/
scripts/publish_dev_image.sh
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ FROM python:3.11-slim
WORKDIR /app

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /usr/local/bin/
COPY --from=ghcr.io/astral-sh/uv:0.5.29 /uv /uvx /bin/

# Copy dependency files
COPY pyproject.toml .
Expand All @@ -27,7 +27,7 @@ ENV UNRAID_MCP_PORT=6970
ENV UNRAID_MCP_HOST="0.0.0.0"
ENV UNRAID_MCP_TRANSPORT="streamable-http"
ENV UNRAID_API_URL=""
ENV UNRAID_API_KEY=""

ENV UNRAID_VERIFY_SSL="true"
ENV UNRAID_MCP_LOG_LEVEL="INFO"

Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ docker compose logs -f unraid-mcp
uv sync

# Run development server
./dev.sh
./scripts/dev.sh
```

---
Expand Down Expand Up @@ -124,7 +124,7 @@ cp .env.example .env
# Edit .env with your settings

# Run development server
./dev.sh
./scripts/dev.sh
```

---
Expand Down Expand Up @@ -194,7 +194,7 @@ UNRAID_VERIFY_SSL=true # true, false, or path to CA bundle
### Monitoring & Diagnostics
- `health_check()` - Comprehensive system health assessment
- `get_notifications_overview()` - Notification counts by severity
- `list_notifications(type, offset, limit)` - Filtered notification listing
- `list_notifications(notification_type, offset, limit)` - Filtered notification listing
- `list_available_log_files()` - Available system logs
- `get_logs(path, tail_lines)` - Log file content retrieval

Expand Down Expand Up @@ -242,8 +242,12 @@ unraid-mcp/
│ │ ├── virtualization.py # VM management
│ │ └── rclone.py # Cloud storage
│ └── server.py # FastMCP server setup
├── scripts/ # Utility scripts
│ └── dev.sh # Development script
├── tests/ # Test suite
│ ├── integration/ # Integration & compliance tests
│ └── unit/ # Unit tests
├── logs/ # Log files (auto-created)
├── dev.sh # Development script
└── docker-compose.yml # Docker Compose deployment
```

Expand All @@ -265,10 +269,10 @@ uv run pytest
### Development Workflow
```bash
# Start development server (kills existing processes safely)
./dev.sh
./scripts/dev.sh

# Stop server only
./dev.sh --kill
./scripts/dev.sh --kill
```

---
Expand Down Expand Up @@ -301,7 +305,7 @@ uv run pytest

**🔥 Port Already in Use**
```bash
./dev.sh # Automatically kills existing processes
./scripts/dev.sh # Automatically kills existing processes
```

**🔧 Connection Refused**
Expand Down
142 changes: 122 additions & 20 deletions dev.sh → scripts/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ set -euo pipefail

# Configuration
DEFAULT_PORT=6970
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
LOG_DIR="/tmp"
LOG_FILE="$LOG_DIR/unraid-mcp.log"
PID_FILE="$LOG_DIR/dev.pid"
Expand All @@ -23,9 +23,15 @@ log() {
local indent="${3:-0}"
local file_timestamp="$(date +'%Y-%m-%d %H:%M:%S')"

# Use unified Rich logger for beautiful console output - escape single quotes
local escaped_message="${message//\'/\'\"\'\"\'}"
uv run python -c "from unraid_mcp.config.logging import log_with_level_and_indent; log_with_level_and_indent('$escaped_message', '$level', $indent)"
# Use unified Rich logger for beautiful console output - use env vars to avoid injection
export LOG_MESSAGE="$message"
export LOG_LEVEL="$level"
export LOG_INDENT="$indent"

uv run python -c "import os; from unraid_mcp.config.logging import log_with_level_and_indent; log_with_level_and_indent(os.environ.get('LOG_MESSAGE', ''), os.environ.get('LOG_LEVEL', 'info'), int(os.environ.get('LOG_INDENT', 0)))"

# Unset env vars
unset LOG_MESSAGE LOG_LEVEL LOG_INDENT

# File output without color
printf "[%s] %s\n" "$file_timestamp" "$message" >> "$LOG_FILE"
Expand Down Expand Up @@ -81,6 +87,24 @@ cleanup_pid_file() {
fi
}

# Check if array contains element (exact match)
contains_elem() {
local e match="$1"
shift
for e; do [[ "$e" == "$match" ]] && return 0; done
return 1
}

# Get file size portably
get_file_size() {
local file="$1"
if [[ "$OSTYPE" == "darwin"* ]]; then
stat -f%z "$file" 2>/dev/null || echo 0
else
stat -c%s "$file" 2>/dev/null || echo 0
fi
}

# Get PID from PID file if valid, otherwise return empty
get_valid_pid_from_file() {
local pid=$(read_pid_file)
Expand Down Expand Up @@ -110,7 +134,7 @@ find_server_processes() {
if [[ -n "$line" ]]; then
local pid=$(echo "$line" | awk '{print $2}')
# Add to pids if not already present
if [[ ! " ${pids[@]} " =~ " $pid " ]]; then
if ! contains_elem "$pid" "${pids[@]}"; then
pids+=("$pid")
fi
fi
Expand All @@ -122,7 +146,7 @@ find_server_processes() {
if [[ -n "$line" ]]; then
local pid=$(echo "$line" | awk '{print $2}')
# Add to pids if not already present
if [[ ! " ${pids[@]} " =~ " $pid " ]]; then
if ! contains_elem "$pid" "${pids[@]}"; then
pids+=("$pid")
fi
fi
Expand Down Expand Up @@ -250,9 +274,17 @@ stop_servers() {
log_info "⏳ Waiting for port $port to be released..."
local port_wait=0
while [[ $port_wait -lt 3 ]]; do
if ! lsof -i ":$port" >/dev/null 2>&1; then
log_success "✅ Port $port released" 1
break
if command -v lsof >/dev/null 2>&1; then
if ! lsof -i ":$port" >/dev/null 2>&1; then
log_success "✅ Port $port released" 1
break
fi
else
# Fallback if lsof is missing: assume success or check netstat (omitted for simplicity, just breaking loop)
# Realistically if lsof is missing we can't easily check, so we trust kill worked or wait blindly.
# For now, just break since we can't verify.
log_warning "⚠️ lsof not found, skipping port release check" 1
break
fi
sleep 1
((port_wait++))
Expand Down Expand Up @@ -281,13 +313,54 @@ start_modular_server() {
fi

# Clear the log file and add a startup marker to capture fresh logs
echo "=== Server Starting at $(date) ===" > "$LOG_FILE"
# Rotate logs if too large (e.g., >10MB) or just simple rotation
if [[ -f "$LOG_FILE" ]]; then
local fsize
fsize=$(get_file_size "$LOG_FILE")
if [[ $fsize -gt 10485760 ]]; then # 10MB
mv "$LOG_FILE" "$LOG_FILE.old"
fi
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Append startup marker
echo "=== Server Starting at $(date) ===" >> "$LOG_FILE"

# Start server in background using module syntax
log_info "→ Executing: uv run -m unraid_mcp.main" 1
# Start server in new process group to isolate it from parent signals
# Start server in new process group to isolate it from parent signals
# Use setsid to detach, but tracking the PID is tricky.
# New approach: Run in background and find the child PID.
setsid nohup uv run -m unraid_mcp.main >> "$LOG_FILE" 2>&1 &
local pid=$!
local shell_pid=$!

# Wait for the python process to appear
local attempts=0
local pid=""
while [[ $attempts -lt 20 && -z "$pid" ]]; do
sleep 0.1
# Try to find the child process of the shell_pid or the setsid process
# This is tricky because setsid executes the program.
# Actually setsid execs, so the shell_pid should be the pid of setsid which becomes the pid of uv which becomes...
# Wait, setsid forks? No, setsid command runs a program in a new session.
# If we used `setsid program &`, $! is the pid of `setsid`.
# If `setsid` execs, then $! is the PID we want.
# But `uv run` might spawn python as a child.
# Let's try to match by command line as requested by user fallback.

# Look for the python process running unraid_mcp.main
# Look for the python process running unraid_mcp.main
# Constrain to current user and newest process to avoid unrelated matches
pid=$(pgrep -u "$(id -u)" -n -f "unraid_mcp\.main")

# Verify it's new (not an old one we failed to kill) - risky if improper cleanup, but we did cleanup.
((attempts++))
done

if [[ -z "$pid" ]]; then
# Fallback to the shell PID if we can't find specific python one, though it might be wrong.
pid=$shell_pid
fi

# Write PID to file
write_pid_file "$pid"
Expand Down Expand Up @@ -338,13 +411,42 @@ start_original_server() {
fi

# Clear the log file and add a startup marker to capture fresh logs
echo "=== Server Starting at $(date) ===" > "$LOG_FILE"
# Rotate logs if too large (e.g., >10MB) or just simple rotation
if [[ -f "$LOG_FILE" ]]; then
local fsize
fsize=$(get_file_size "$LOG_FILE")
if [[ $fsize -gt 10485760 ]]; then # 10MB
mv "$LOG_FILE" "$LOG_FILE.old"
fi
fi

# Append startup marker
echo "=== Server Starting at $(date) ===" >> "$LOG_FILE"

# Start server in background
log_info "→ Executing: uv run unraid_mcp_server.py" 1
# Start server in new process group to isolate it from parent signals
# Use setsid to detach, but tracking the PID is tricky.
# New approach: Run in background and find the child PID.
setsid nohup uv run unraid_mcp_server.py >> "$LOG_FILE" 2>&1 &
local pid=$!
local shell_pid=$!

# Wait for the python process to appear
local attempts=0
local pid=""
while [[ $attempts -lt 20 && -z "$pid" ]]; do
sleep 0.1
# Look for the python process running unraid_mcp_server.py
# Look for the python process running unraid_mcp_server.py
# Constrain to current user and newest process to avoid unrelated matches
pid=$(pgrep -u "$(id -u)" -n -f "unraid_mcp_server\.py")
((attempts++))
done

if [[ -z "$pid" ]]; then
# Fallback to the shell PID if we can't find specific python one
pid=$shell_pid
fi

# Write PID to file
write_pid_file "$pid"
Expand Down Expand Up @@ -400,13 +502,13 @@ show_usage() {
echo " UNRAID_MCP_PORT Port for server (default: $DEFAULT_PORT)"
echo ""
echo "EXAMPLES:"
echo " ./dev.sh # Restart with modular server"
echo " ./dev.sh --old # Restart with original server"
echo " ./dev.sh --kill # Stop all servers"
echo " ./dev.sh --status # Check server status"
echo " ./dev.sh --logs # Show last 50 lines of logs"
echo " ./dev.sh --logs 100 # Show last 100 lines of logs"
echo " ./dev.sh --tail # Follow logs in real-time"
echo " ./scripts/dev.sh # Restart with modular server"
echo " ./scripts/dev.sh --old # Restart with original server"
echo " ./scripts/dev.sh --kill # Stop all servers"
echo " ./scripts/dev.sh --status # Check server status"
echo " ./scripts/dev.sh --logs # Show last 50 lines of logs"
echo " ./scripts/dev.sh --logs 100 # Show last 100 lines of logs"
echo " ./scripts/dev.sh --tail # Follow logs in real-time"
}

# Show server status
Expand Down