2 Commits

Author SHA1 Message Date
RNL
7e4108fb32 fix(mcp): repair sse->http transport mismatch, auto-provision API key
bambu_mcp_server was refactored from FastMCP/sse to mcp-gate's http|stdio
transport, but native/bambu-run-mcp.service, setup.sh, and bambu-run.sh
enable-mcp were never updated — the systemd unit crash-looped on an
invalid --transport sse flag on any Pi deployed after that change.

Also closes the follow-on gap: MCP over http requires
BAMBU_RUN_MCP_API_KEY, which nothing ever generated, so a fixed service
would just crash-loop on a different error instead. Both setup.sh and
enable-mcp now auto-generate and persist a key (same pattern as
DJANGO_SECRET_KEY), print it in the ready-to-use Claude Desktop config,
and respect a real environment variable over .env if one is already set.
setup.sh's MCP step also gained an idempotency guard so re-running it
doesn't re-prompt/reinstall MCP that's already enabled.

Claude-Session: https://claude.ai/code/session_017Yxt4rdkuSowToxEKGMG9x
2026-09-05 00:12:23 +10:00
RNL
70731c637f capability to point to local postgres db
:
2026-09-02 23:09:54 +10:00
6 changed files with 112 additions and 16 deletions

View File

@@ -153,6 +153,46 @@ cd Bambu-Run
bash setup.sh bash setup.sh
``` ```
### Pointing at an External Postgres Database (Read-Only)
If you already have printer data in a Postgres database (e.g. shared with
another Django project using the same `infrastructure_*` tables) and want this
install to act as a read-only viewer instead of collecting its own data:
```bash
cd ~/Bambu-Run
# 1. Stop and disable the collector — it's the only thing that writes to the
# database (PrinterMetrics, FilamentSnapshot, PrintJob). A read-only DB
# user will make it crash-loop, so turn it off.
systemctl --user stop bambu-run-collector.service
systemctl --user disable bambu-run-collector.service
# 2. Install the Postgres driver
.venv/bin/pip install ".[postgres]"
# 3. Edit .env — point at Postgres with a read-only user, and switch sessions
# to signed cookies so login doesn't need to write to django_session
cat >> .env <<'EOF'
DB_ENGINE=postgresql
DB_NAME=your_db_name
DB_USER=your_readonly_user
DB_PASSWORD=your_readonly_password
DB_HOST=your_db_host
DB_PORT=5432
SESSION_ENGINE=signed_cookies
EOF
# 4. Restart the web service (do NOT run `manage.py migrate` — the schema
# already exists in that database, and a read-only user can't apply
# migrations anyway)
systemctl --user restart bambu-run-web.service
```
You'll need a login that already exists in that database's `auth_user`
table — this install can't create one itself without write access. If MCP is
enabled, it becomes read-only too, since its tools only ever `SELECT`.
--- ---
## Docker Setup ## Docker Setup

View File

@@ -7,7 +7,7 @@ Type=exec
WorkingDirectory={{REPO_DIR}} WorkingDirectory={{REPO_DIR}}
EnvironmentFile={{REPO_DIR}}/.env EnvironmentFile={{REPO_DIR}}/.env
Environment=DJANGO_SETTINGS_MODULE=standalone.settings Environment=DJANGO_SETTINGS_MODULE=standalone.settings
ExecStart={{VENV_DIR}}/bin/python standalone/manage.py bambu_mcp_server --transport sse --host 0.0.0.0 --port 8808 ExecStart={{VENV_DIR}}/bin/python standalone/manage.py bambu_mcp_server --transport http --host 0.0.0.0 --port 8808
Restart=on-failure Restart=on-failure
RestartSec=10 RestartSec=10

View File

@@ -5,6 +5,7 @@ set -euo pipefail
REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)" REPO_DIR="$(cd "$(dirname "$0")/.." && pwd)"
VENV_DIR="$REPO_DIR/.venv" VENV_DIR="$REPO_DIR/.venv"
ENV_FILE="$REPO_DIR/.env"
MANAGE="$VENV_DIR/bin/python $REPO_DIR/standalone/manage.py" MANAGE="$VENV_DIR/bin/python $REPO_DIR/standalone/manage.py"
SERVICES="bambu-run-web.service bambu-run-collector.service" SERVICES="bambu-run-web.service bambu-run-collector.service"
@@ -44,6 +45,19 @@ case "${1:-help}" in
echo "Installing MCP dependencies..." echo "Installing MCP dependencies..."
"$VENV_DIR/bin/pip" install --quiet ".[mcp]" "$VENV_DIR/bin/pip" install --quiet ".[mcp]"
# Resolution order: a real process/systemd env var always wins and is
# never written to .env; otherwise reuse whatever's already in .env;
# otherwise generate one (same pattern setup.sh uses for DJANGO_SECRET_KEY).
if [ -n "${BAMBU_RUN_MCP_API_KEY:-}" ]; then
echo "Using BAMBU_RUN_MCP_API_KEY from the environment (not written to .env)."
elif grep -q '^BAMBU_RUN_MCP_API_KEY=' "$ENV_FILE" 2>/dev/null; then
echo "BAMBU_RUN_MCP_API_KEY already in .env, reusing it."
else
MCP_API_KEY=$("$VENV_DIR/bin/python" -c "import secrets; print(secrets.token_urlsafe(32))")
echo "BAMBU_RUN_MCP_API_KEY=$MCP_API_KEY" >> "$ENV_FILE"
echo "Generated BAMBU_RUN_MCP_API_KEY and saved it to .env."
fi
sed "s|{{REPO_DIR}}|$REPO_DIR|g; s|{{VENV_DIR}}|$VENV_DIR|g" \ sed "s|{{REPO_DIR}}|$REPO_DIR|g; s|{{VENV_DIR}}|$VENV_DIR|g" \
"$REPO_DIR/native/bambu-run-mcp.service" > "$SERVICE_DIR/bambu-run-mcp.service" "$REPO_DIR/native/bambu-run-mcp.service" > "$SERVICE_DIR/bambu-run-mcp.service"
@@ -52,10 +66,11 @@ case "${1:-help}" in
systemctl --user start bambu-run-mcp.service systemctl --user start bambu-run-mcp.service
PI_IP=$(hostname -I 2>/dev/null | awk '{print $1}') PI_IP=$(hostname -I 2>/dev/null | awk '{print $1}')
MCP_KEY_FOR_DISPLAY="${BAMBU_RUN_MCP_API_KEY:-$(grep '^BAMBU_RUN_MCP_API_KEY=' "$ENV_FILE" 2>/dev/null | cut -d= -f2-)}"
echo "MCP server enabled on port 8808." echo "MCP server enabled on port 8808."
echo echo
echo " Claude Desktop config:" echo " Claude Desktop config:"
echo " {\"mcpServers\":{\"bambu-run\":{\"url\":\"http://${PI_IP:-localhost}:8808/sse\"}}}" echo " {\"mcpServers\":{\"bambu-run\":{\"url\":\"http://${PI_IP:-localhost}:8808/mcp\",\"headers\":{\"Authorization\":\"Bearer ${MCP_KEY_FOR_DISPLAY}\"}}}}"
fi fi
;; ;;
disable-mcp) disable-mcp)

View File

@@ -218,11 +218,29 @@ systemctl --user enable bambu-run-web.service bambu-run-collector.service
echo echo
MCP_ENABLED=false MCP_ENABLED=false
read -rp "Enable MCP server for AI agent access (Claude Desktop, Claude Code, etc.)? [y/N] " ENABLE_MCP if [ -f "$SERVICE_DIR/bambu-run-mcp.service" ]; then
if [[ "$ENABLE_MCP" =~ ^[Yy] ]]; then yellow "MCP server already enabled, skipping (use ./native/bambu-run.sh disable-mcp first to reconfigure)."
MCP_ENABLED=true
else
read -rp "Enable MCP server for AI agent access (Claude Desktop, Claude Code, etc.)? [y/N] " ENABLE_MCP
if [[ "$ENABLE_MCP" =~ ^[Yy] ]]; then
green "Installing MCP dependencies..." green "Installing MCP dependencies..."
"$VENV_DIR/bin/pip" install $PIP_QUIET_FLAG ".[mcp]" "$VENV_DIR/bin/pip" install $PIP_QUIET_FLAG ".[mcp]"
# Resolution order: a real process/systemd env var always wins and is
# never written to .env (lets deployments override without touching
# the file); otherwise reuse whatever's already in .env; otherwise
# generate one, same pattern as DJANGO_SECRET_KEY above.
if [ -n "${BAMBU_RUN_MCP_API_KEY:-}" ]; then
yellow "Using BAMBU_RUN_MCP_API_KEY from the environment (not written to .env)."
elif grep -q '^BAMBU_RUN_MCP_API_KEY=' "$ENV_FILE" 2>/dev/null; then
yellow "BAMBU_RUN_MCP_API_KEY already in .env, reusing it."
else
MCP_API_KEY=$("$VENV_DIR/bin/python" -c "import secrets; print(secrets.token_urlsafe(32))")
echo "BAMBU_RUN_MCP_API_KEY=$MCP_API_KEY" >> "$ENV_FILE"
green "Generated BAMBU_RUN_MCP_API_KEY and saved it to .env."
fi
sed "s|{{REPO_DIR}}|$REPO_DIR|g; s|{{VENV_DIR}}|$VENV_DIR|g" \ sed "s|{{REPO_DIR}}|$REPO_DIR|g; s|{{VENV_DIR}}|$VENV_DIR|g" \
"$REPO_DIR/native/bambu-run-mcp.service" > "$SERVICE_DIR/bambu-run-mcp.service" "$REPO_DIR/native/bambu-run-mcp.service" > "$SERVICE_DIR/bambu-run-mcp.service"
@@ -231,6 +249,7 @@ if [[ "$ENABLE_MCP" =~ ^[Yy] ]]; then
systemctl --user start bambu-run-mcp.service systemctl --user start bambu-run-mcp.service
MCP_ENABLED=true MCP_ENABLED=true
green "MCP server enabled on port 8808." green "MCP server enabled on port 8808."
fi
fi fi
# Enable linger so services survive SSH logout # Enable linger so services survive SSH logout
@@ -288,15 +307,16 @@ green "============================================"
echo echo
echo " Dashboard: $DASHBOARD_URL" echo " Dashboard: $DASHBOARD_URL"
if [ "$MCP_ENABLED" = true ]; then if [ "$MCP_ENABLED" = true ]; then
echo " MCP Server: http://${PI_IP:-localhost}:8808/sse" echo " MCP Server: http://${PI_IP:-localhost}:8808/mcp"
fi fi
echo " Status: systemctl --user status bambu-run-web bambu-run-collector" echo " Status: systemctl --user status bambu-run-web bambu-run-collector"
echo " Logs: journalctl --user -u bambu-run-web -u bambu-run-collector -f" echo " Logs: journalctl --user -u bambu-run-web -u bambu-run-collector -f"
echo " Helper: ./native/bambu-run.sh {start|stop|restart|status|logs|update}" echo " Helper: ./native/bambu-run.sh {start|stop|restart|status|logs|update}"
echo echo
if [ "$MCP_ENABLED" = true ]; then if [ "$MCP_ENABLED" = true ]; then
MCP_KEY_FOR_DISPLAY="${BAMBU_RUN_MCP_API_KEY:-$(grep '^BAMBU_RUN_MCP_API_KEY=' "$ENV_FILE" 2>/dev/null | cut -d= -f2-)}"
echo " Claude Desktop config:" echo " Claude Desktop config:"
echo " {\"mcpServers\":{\"bambu-run\":{\"url\":\"http://${PI_IP:-localhost}:8808/sse\"}}}" echo " {\"mcpServers\":{\"bambu-run\":{\"url\":\"http://${PI_IP:-localhost}:8808/mcp\",\"headers\":{\"Authorization\":\"Bearer ${MCP_KEY_FOR_DISPLAY}\"}}}}"
echo echo
fi fi
echo " Services auto-start on boot. Safe to close SSH." echo " Services auto-start on boot. Safe to close SSH."

View File

@@ -87,6 +87,15 @@ else:
} }
} }
# Session storage — DB-backed by default. Set SESSION_ENGINE=signed_cookies when
# pointing DB_ENGINE at a database whose user has read-only access, since the
# default engine writes a row to django_session on every login.
SESSION_ENGINE = (
"django.contrib.sessions.backends.signed_cookies"
if os.environ.get("SESSION_ENGINE") == "signed_cookies"
else "django.contrib.sessions.backends.db"
)
# Password validation # Password validation
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},

View File

@@ -7,3 +7,15 @@ from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "standalone.settings") os.environ.setdefault("DJANGO_SETTINGS_MODULE", "standalone.settings")
application = get_wsgi_application() application = get_wsgi_application()
# Read-only deployments (SESSION_ENGINE=signed_cookies) point at a database
# whose user has no write access. Django writes to auth_user.last_login on
# every successful login by default, which would error out against such a
# user and break login entirely — so skip that write here.
from django.conf import settings # noqa: E402
if settings.SESSION_ENGINE == "django.contrib.sessions.backends.signed_cookies":
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
user_logged_in.disconnect(update_last_login, dispatch_uid="update_last_login")