Skip to content

Commit eee17cd

Browse files
committed
Add linters for shell, and add improved run-gunicorn-v2.sh - not yet used
Signed-off-by: Mihai Criveti <[email protected]>
1 parent 3e2d395 commit eee17cd

File tree

3 files changed

+341
-1
lines changed

3 files changed

+341
-1
lines changed

Makefile

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,3 +1835,66 @@ devpi-delete: devpi-setup-user ## Delete mcpgateway==$(VER) from
18351835
devpi use $(DEVPI_INDEX) && \
18361836
devpi remove -y mcpgateway==$(VER) || true"
18371837
@echo "✅ Delete complete (if it existed)"
1838+
1839+
1840+
# =============================================================================
1841+
# 🐚 LINT SHELL FILES
1842+
# =============================================================================
1843+
# help: 🐚 LINT SHELL FILES
1844+
# help: shell-linters-install - Install ShellCheck, shfmt & bashate (best-effort per OS)
1845+
# help: shell-lint - Run shfmt (check-only) + ShellCheck + bashate on every *.sh
1846+
# help: shfmt-fix - AUTO-FORMAT all *.sh in-place with shfmt -w
1847+
# -----------------------------------------------------------------------------
1848+
1849+
# ──────────────────────────
1850+
# Which shell files to scan
1851+
# ──────────────────────────
1852+
SHELL_SCRIPTS := $(shell find . -type f -name '*.sh' -not -path './node_modules/*')
1853+
1854+
.PHONY: shell-linters-install shell-lint shfmt-fix shellcheck bashate
1855+
1856+
shell-linters-install: ## 🔧 Install shellcheck, shfmt, bashate
1857+
@echo "🔧 Installing/ensuring shell linters are present…"
1858+
@set -e ; \
1859+
# -------- ShellCheck -------- \
1860+
if ! command -v shellcheck >/dev/null 2>&1 ; then \
1861+
echo "🛠 Installing ShellCheck…" ; \
1862+
case "$$(uname -s)" in \
1863+
Darwin) brew install shellcheck ;; \
1864+
Linux) { command -v apt-get && sudo apt-get update -qq && sudo apt-get install -y shellcheck ; } || \
1865+
{ command -v dnf && sudo dnf install -y ShellCheck ; } || \
1866+
{ command -v pacman && sudo pacman -Sy --noconfirm shellcheck ; } || true ;; \
1867+
*) echo "⚠️ Please install ShellCheck manually" ;; \
1868+
esac ; \
1869+
fi ; \
1870+
# -------- shfmt (Go) -------- \
1871+
if ! command -v shfmt >/dev/null 2>&1 ; then \
1872+
echo "🛠 Installing shfmt…" ; \
1873+
GO111MODULE=on go install mvdan.cc/sh/v3/cmd/shfmt@latest || \
1874+
{ echo "⚠️ go not found – install Go or brew/apt shfmt package manually"; } ; \
1875+
export PATH=$$PATH:$$HOME/go/bin ; \
1876+
fi ; \
1877+
# -------- bashate (pip) ----- \
1878+
if ! $(VENV_DIR)/bin/bashate -h >/dev/null 2>&1 ; then \
1879+
echo "🛠 Installing bashate (into venv)…" ; \
1880+
test -d "$(VENV_DIR)" || $(MAKE) venv ; \
1881+
/bin/bash -c "source $(VENV_DIR)/bin/activate && python -m pip install --quiet bashate" ; \
1882+
fi
1883+
@echo "✅ Shell linters ready."
1884+
1885+
# -----------------------------------------------------------------------------
1886+
1887+
shell-lint: shell-linters-install ## 🔍 Run shfmt, ShellCheck & bashate
1888+
@echo "🔍 Running shfmt (diff-only)…"
1889+
@shfmt -d -i 4 -ci $(SHELL_SCRIPTS) || true
1890+
@echo "🔍 Running ShellCheck…"
1891+
@shellcheck $(SHELL_SCRIPTS) || true
1892+
@echo "🔍 Running bashate…"
1893+
@$(VENV_DIR)/bin/bashate -C $(SHELL_SCRIPTS) || true
1894+
@echo "✅ Shell lint complete."
1895+
1896+
1897+
shfmt-fix: shell-linters-install ## 🎨 Auto-format *.sh in place
1898+
@echo "🎨 Formatting shell scripts with shfmt -w…"
1899+
@shfmt -w -i 4 -ci $(SHELL_SCRIPTS)
1900+
@echo "✅ shfmt formatting done."

run-gunicorn-v2.sh

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
#!/usr/bin/env bash
2+
#───────────────────────────────────────────────────────────────────────────────
3+
# Script : run-gunicorn.sh
4+
# Author : Mihai Criveti
5+
# Purpose: Launch the MCP Gateway API under Gunicorn with optional TLS support
6+
#
7+
# Description:
8+
# This script provides a robust way to launch a production API server using
9+
# Gunicorn with the following features:
10+
#
11+
# • Portable Python detection across different distros (python vs python3)
12+
# • Virtual environment handling (activates project venv if available)
13+
# • Configurable via environment variables for CI/CD pipelines
14+
# • Optional TLS/SSL support for secure connections
15+
# • Database initialization before server start
16+
# • Comprehensive error handling and user feedback
17+
#
18+
# Environment Variables:
19+
# PYTHON : Path to Python interpreter (optional)
20+
# VIRTUAL_ENV : Path to active virtual environment (auto-detected)
21+
# GUNICORN_WORKERS : Number of worker processes (default: 2 × CPU cores + 1)
22+
# GUNICORN_TIMEOUT : Worker timeout in seconds (default: 600)
23+
# GUNICORN_MAX_REQUESTS : Max requests per worker before restart (default: 1000)
24+
# GUNICORN_MAX_REQUESTS_JITTER : Random jitter for max requests (default: 100)
25+
# SSL : Enable TLS/SSL (true/false, default: false)
26+
# CERT_FILE : Path to SSL certificate (default: certs/cert.pem)
27+
# KEY_FILE : Path to SSL private key (default: certs/key.pem)
28+
#
29+
# Usage:
30+
# ./run-gunicorn.sh # Run with defaults
31+
# SSL=true ./run-gunicorn.sh # Run with TLS enabled
32+
# GUNICORN_WORKERS=16 ./run-gunicorn.sh # Run with 16 workers
33+
#───────────────────────────────────────────────────────────────────────────────
34+
35+
# Exit immediately on error, undefined variable, or pipe failure
36+
set -euo pipefail
37+
38+
#────────────────────────────────────────────────────────────────────────────────
39+
# SECTION 1: Script Location Detection
40+
# Determine the absolute path to this script's directory for relative path resolution
41+
#────────────────────────────────────────────────────────────────────────────────
42+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
43+
44+
# Change to script directory to ensure relative paths work correctly
45+
# This ensures gunicorn.config.py and cert paths resolve properly
46+
cd "${SCRIPT_DIR}" || {
47+
echo "❌ FATAL: Cannot change to script directory: ${SCRIPT_DIR}"
48+
exit 1
49+
}
50+
51+
#────────────────────────────────────────────────────────────────────────────────
52+
# SECTION 2: Virtual Environment Activation
53+
# Check if a virtual environment is already active. If not, try to activate one
54+
# from known locations. This ensures dependencies are properly isolated.
55+
#────────────────────────────────────────────────────────────────────────────────
56+
if [[ -z "${VIRTUAL_ENV:-}" ]]; then
57+
# Check for virtual environment in user's home directory (preferred location)
58+
if [[ -f "${HOME}/.venv/mcpgateway/bin/activate" ]]; then
59+
echo "🔧 Activating virtual environment: ${HOME}/.venv/mcpgateway"
60+
# shellcheck disable=SC1090
61+
source "${HOME}/.venv/mcpgateway/bin/activate"
62+
63+
# Check for virtual environment in script directory (development setup)
64+
elif [[ -f "${SCRIPT_DIR}/.venv/bin/activate" ]]; then
65+
echo "🔧 Activating virtual environment in script directory"
66+
# shellcheck disable=SC1090
67+
source "${SCRIPT_DIR}/.venv/bin/activate"
68+
69+
# No virtual environment found - warn but continue
70+
else
71+
echo "⚠️ WARNING: No virtual environment found!"
72+
echo " This may lead to dependency conflicts."
73+
echo " Consider creating a virtual environment with:"
74+
echo " python3 -m venv ~/.venv/mcpgateway"
75+
76+
# Optional: Uncomment the following lines to enforce virtual environment usage
77+
# echo "❌ FATAL: Virtual environment required for production deployments"
78+
# echo " This ensures consistent dependency versions."
79+
# exit 1
80+
fi
81+
else
82+
echo "✓ Virtual environment already active: ${VIRTUAL_ENV}"
83+
fi
84+
85+
#────────────────────────────────────────────────────────────────────────────────
86+
# SECTION 3: Python Interpreter Detection
87+
# Locate a suitable Python interpreter with the following precedence:
88+
# 1. User-provided PYTHON environment variable
89+
# 2. 'python' binary in active virtual environment
90+
# 3. 'python3' binary on system PATH
91+
# 4. 'python' binary on system PATH
92+
#────────────────────────────────────────────────────────────────────────────────
93+
if [[ -z "${PYTHON:-}" ]]; then
94+
# If virtual environment is active, prefer its Python binary
95+
if [[ -n "${VIRTUAL_ENV:-}" && -x "${VIRTUAL_ENV}/bin/python" ]]; then
96+
PYTHON="${VIRTUAL_ENV}/bin/python"
97+
echo "🐍 Using Python from virtual environment"
98+
99+
# Otherwise, search for Python in system PATH
100+
else
101+
# Try python3 first (more common on modern systems)
102+
if command -v python3 &> /dev/null; then
103+
PYTHON="$(command -v python3)"
104+
echo "🐍 Found system Python3: ${PYTHON}"
105+
106+
# Fall back to python if python3 not found
107+
elif command -v python &> /dev/null; then
108+
PYTHON="$(command -v python)"
109+
echo "🐍 Found system Python: ${PYTHON}"
110+
111+
# No Python found at all
112+
else
113+
PYTHON=""
114+
fi
115+
fi
116+
fi
117+
118+
# Verify Python interpreter exists and is executable
119+
if [[ -z "${PYTHON}" ]] || [[ ! -x "${PYTHON}" ]]; then
120+
echo "❌ FATAL: Could not locate a Python interpreter!"
121+
echo " Searched for: python3, python"
122+
echo " Please install Python 3.x or set the PYTHON environment variable."
123+
echo " Example: PYTHON=/usr/bin/python3.9 $0"
124+
exit 1
125+
fi
126+
127+
# Display Python version for debugging
128+
PY_VERSION="$("${PYTHON}" --version 2>&1)"
129+
echo "📋 Python version: ${PY_VERSION}"
130+
131+
# Verify this is Python 3.x (not Python 2.x)
132+
if ! "${PYTHON}" -c "import sys; sys.exit(0 if sys.version_info[0] >= 3 else 1)" 2>/dev/null; then
133+
echo "❌ FATAL: Python 3.x is required, but Python 2.x was found!"
134+
echo " Please install Python 3.x or update the PYTHON environment variable."
135+
exit 1
136+
fi
137+
138+
#────────────────────────────────────────────────────────────────────────────────
139+
# SECTION 4: Display Application Banner
140+
# Show a fancy ASCII art banner for the MCP Gateway
141+
#────────────────────────────────────────────────────────────────────────────────
142+
cat <<'EOF'
143+
███╗ ███╗ ██████╗██████╗ ██████╗ █████╗ ████████╗███████╗██╗ ██╗ █████╗ ██╗ ██╗
144+
████╗ ████║██╔════╝██╔══██╗ ██╔════╝ ██╔══██╗╚══██╔══╝██╔════╝██║ ██║██╔══██╗╚██╗ ██╔╝
145+
██╔████╔██║██║ ██████╔╝ ██║ ███╗███████║ ██║ █████╗ ██║ █╗ ██║███████║ ╚████╔╝
146+
██║╚██╔╝██║██║ ██╔═══╝ ██║ ██║██╔══██║ ██║ ██╔══╝ ██║███╗██║██╔══██║ ╚██╔╝
147+
██║ ╚═╝ ██║╚██████╗██║ ╚██████╔╝██║ ██║ ██║ ███████╗╚███╔███╔╝██║ ██║ ██║
148+
╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚══╝╚══╝ ╚═╝ ╚═╝ ╚═╝
149+
EOF
150+
151+
#────────────────────────────────────────────────────────────────────────────────
152+
# SECTION 5: Configure Gunicorn Settings
153+
# Set up Gunicorn parameters with sensible defaults that can be overridden
154+
# via environment variables for different deployment scenarios
155+
#────────────────────────────────────────────────────────────────────────────────
156+
157+
# Number of worker processes (adjust based on CPU cores and expected load)
158+
# Default: 2 × CPU cores + 1 (automatically detected)
159+
if [[ -z "${GUNICORN_WORKERS:-}" ]]; then
160+
# Try to detect CPU count
161+
if command -v nproc &>/dev/null; then
162+
CPU_COUNT=$(nproc)
163+
elif command -v sysctl &>/dev/null && sysctl -n hw.ncpu &>/dev/null; then
164+
CPU_COUNT=$(sysctl -n hw.ncpu)
165+
else
166+
CPU_COUNT=4 # Fallback to reasonable default
167+
fi
168+
GUNICORN_WORKERS=$((CPU_COUNT * 2 + 1))
169+
fi
170+
171+
# Worker timeout in seconds (increase for long-running requests)
172+
GUNICORN_TIMEOUT=${GUNICORN_TIMEOUT:-600}
173+
174+
# Maximum requests a worker will process before restarting (prevents memory leaks)
175+
GUNICORN_MAX_REQUESTS=${GUNICORN_MAX_REQUESTS:-1000}
176+
177+
# Random jitter for max requests (prevents all workers restarting simultaneously)
178+
GUNICORN_MAX_REQUESTS_JITTER=${GUNICORN_MAX_REQUESTS_JITTER:-100}
179+
180+
echo "📊 Gunicorn Configuration:"
181+
echo " Workers: ${GUNICORN_WORKERS}"
182+
echo " Timeout: ${GUNICORN_TIMEOUT}s"
183+
echo " Max Requests: ${GUNICORN_MAX_REQUESTS}${GUNICORN_MAX_REQUESTS_JITTER})"
184+
185+
#────────────────────────────────────────────────────────────────────────────────
186+
# SECTION 6: Configure TLS/SSL Settings
187+
# Handle optional TLS configuration for secure HTTPS connections
188+
#────────────────────────────────────────────────────────────────────────────────
189+
190+
# SSL/TLS configuration
191+
SSL=${SSL:-false} # Enable/disable SSL (default: false)
192+
CERT_FILE=${CERT_FILE:-certs/cert.pem} # Path to SSL certificate file
193+
KEY_FILE=${KEY_FILE:-certs/key.pem} # Path to SSL private key file
194+
195+
# Verify SSL settings if enabled
196+
if [[ "${SSL}" == "true" ]]; then
197+
echo "🔐 Configuring TLS/SSL..."
198+
199+
# Verify certificate files exist
200+
if [[ ! -f "${CERT_FILE}" ]]; then
201+
echo "❌ FATAL: SSL certificate file not found: ${CERT_FILE}"
202+
exit 1
203+
fi
204+
205+
if [[ ! -f "${KEY_FILE}" ]]; then
206+
echo "❌ FATAL: SSL private key file not found: ${KEY_FILE}"
207+
exit 1
208+
fi
209+
210+
# Verify certificate and key files are readable
211+
if [[ ! -r "${CERT_FILE}" ]]; then
212+
echo "❌ FATAL: Cannot read SSL certificate file: ${CERT_FILE}"
213+
exit 1
214+
fi
215+
216+
if [[ ! -r "${KEY_FILE}" ]]; then
217+
echo "❌ FATAL: Cannot read SSL private key file: ${KEY_FILE}"
218+
exit 1
219+
fi
220+
221+
echo "✓ TLS enabled – using:"
222+
echo " Certificate: ${CERT_FILE}"
223+
echo " Private Key: ${KEY_FILE}"
224+
else
225+
echo "🔓 Running without TLS (HTTP only)"
226+
fi
227+
228+
#────────────────────────────────────────────────────────────────────────────────
229+
# SECTION 7: Database Initialization
230+
# Run database setup/migrations before starting the server
231+
#────────────────────────────────────────────────────────────────────────────────
232+
echo "🗄️ Initializing database..."
233+
if ! "${PYTHON}" -m mcpgateway.db; then
234+
echo "❌ FATAL: Database initialization failed!"
235+
echo " Please check your database configuration and connection."
236+
exit 1
237+
fi
238+
echo "✓ Database initialized successfully"
239+
240+
#────────────────────────────────────────────────────────────────────────────────
241+
# SECTION 8: Launch Gunicorn Server
242+
# Start the Gunicorn server with all configured options
243+
# Using 'exec' replaces this shell process with Gunicorn for cleaner process management
244+
#────────────────────────────────────────────────────────────────────────────────
245+
echo "🚀 Starting Gunicorn server..."
246+
echo "─────────────────────────────────────────────────────────────────────"
247+
248+
# Check if gunicorn is available
249+
if ! command -v gunicorn &> /dev/null; then
250+
echo "❌ FATAL: gunicorn command not found!"
251+
echo " Please install it with: pip install gunicorn"
252+
exit 1
253+
fi
254+
255+
# Build command array to handle spaces in paths properly
256+
cmd=(
257+
gunicorn
258+
-c gunicorn.config.py
259+
--worker-class uvicorn.workers.UvicornWorker
260+
--workers "${GUNICORN_WORKERS}"
261+
--timeout "${GUNICORN_TIMEOUT}"
262+
--max-requests "${GUNICORN_MAX_REQUESTS}"
263+
--max-requests-jitter "${GUNICORN_MAX_REQUESTS_JITTER}"
264+
--access-logfile -
265+
--error-logfile -
266+
)
267+
268+
# Add SSL arguments if enabled
269+
if [[ "${SSL}" == "true" ]]; then
270+
cmd+=( --certfile "${CERT_FILE}" --keyfile "${KEY_FILE}" )
271+
fi
272+
273+
# Add the application module
274+
cmd+=( "mcpgateway.main:app" )
275+
276+
# Launch Gunicorn with all configured options
277+
exec "${cmd[@]}"

run-gunicorn.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ if [[ "${SSL}" == "true" ]]; then
5555
fi
5656

5757
# Initialize databases
58-
python -m mcpgateway.db
58+
"$PYTHON" -m mcpgateway.db
5959

6060
exec gunicorn -c gunicorn.config.py \
6161
--worker-class uvicorn.workers.UvicornWorker \

0 commit comments

Comments
 (0)