Skip to main content
Glama
ekufta0530

lifesaver-mcp

by ekufta0530

lifesaver-mcp

Sales and customer-retention tooling for picture-framing stores on Lifesaver Software: one website for every store, fed by two kinds of data source.

Dir

What

webapp/

The website (React + FastAPI, Cloud Run): all-stores overview, a dashboard and a report builder / SQL console per store, and for on-prem stores an in-store detail section. Also the MCP server for Claude over every store (/mcp/<token>). README

stores.toml

Every store: id, name, tier (cloud / on-prem), and where its data lives. Adding a cloud store is an entry here + a secret

cloud/

Cloud tier. Everything that talks to Lifesaver Cloud (lsscloud.com): the report scraper (lifesaver/), the warehouse ingest that accumulates report pulls (warehouse/), one-off pull scripts

sqlite_extract/

On-prem tier. Turns a store's own LifeSaver data (uploaded to Google Drive by the owner) into lifesaver.sqlite, then into KPI tables -- no scraping

core/

Store-independent: visits, the five retention KPIs, the SQLite tables every store writes, the store registry (stores.py) and the pooled all-stores warehouse (aggregate.py)

lsreports/

The query layer behind the Reports tab and the MCP tools: named dimensions/metrics over each store's data, plus the cloud and pooled report DBs (build.py)

tests/

Offline suite, split as tests/{cloud,core,sqlite_extract,webapp}

Dependencies only point inward: cloud and sqlite_extract import core; core imports neither. Background: cloud/spec.md (phases 1-2) and docs/kpi-design.md (the KPI design; its renderer has since moved into webapp/).

How it fits together

CLOUD STORES (one lsscloud account each)        ON-PREM STORES (LifeSaver on their own machine)

lsscloud.com ── Work Order List report          owner drags LifeSaver folder / zip
   │                                               into a shared Google Drive folder
   │ refresh.yml (daily 09:17 UTC, one job         │
   │   per cloud store in stores.toml)             │ mason-ingest.yml (every 6 h)
   └─▶ cloud.warehouse.job sync                    └─▶ sqlite_extract/ingest.sh
         gs://frame-shop-511616-kpi-warehouse/                   gs://frame-shop-511616-lifesaver-data/
           stores/<id>/warehouse.db                        lifesaver.sqlite
                 │                                              │
                 └───────────────┬──────────────────────────────┘
                                 ▼   (re-read every 10 min; on-prem KPIs derived on arrival)
                       webapp (Cloud Run "frame-shop")
                         /              all stores: side by side + pooled
                         /stores/<id>   one store + its Reports (+ in-store detail if on-prem)
                         /mcp/<token>   the MCP server for Claude: every store, or all pooled

GitHub Actions drive everything; all authenticate to GCP as gha-deployer via Workload Identity Federation (no keys):

Workflow

When

Does

Setup

publish.yml

push to main

test (incl. frontend build) → website image to GHCR → redeploy the website (and with it the MCP server)

DEPLOY.md §1, webapp/README.md

refresh.yml

daily 09:17 UTC

every cloud store: pull new report rows → recompute KPIs → save its warehouse.db

DEPLOY.md §2

mason-ingest.yml

every 6 h (:41)

on-prem store: newest Drive upload → validated lifesaver.sqlite

DEPLOY.md §3

GCS buckets (project frame-shop-511616):

Bucket

Holds

Written by

frame-shop-511616-kpi-warehouse (private, versioned)

each cloud store's stores/<id>/warehouse.db + warehouse_raw/ — the system of record

refresh.yml

frame-shop-511616-lifesaver-data (private)

the on-prem store's live lifesaver.sqlite, archive/ (replaced copies), raw/<ts>/ (source .dbfs, deleted after 365 days), ingest/state.json; also users.json, the website's logins

mason-ingest.yml, webapp/users.sh

Related MCP server: connectwise-command-mcp

Layout

Path

What

cloud/lifesaver/client.py

the 3-step scrape as a class; one session, re-login + retry on expiry

cloud/lifesaver/parser.py

CSV bytes → typed rows (BOM, $ currency, M/d/yyyy dates)

cloud/lifesaver/reports.py

per-report config (page path + date-field control IDs); WorkOrderList only

cloud/lifesaver/models.py

pydantic row/response schemas

cloud/lifesaver/api.py

FastAPI app (local dev; not deployed)

cloud/warehouse/

main store's ingest: raw landing → line_items → name-matched customers, then core for visits/KPIs. CLI: python -m cloud.warehouse.job. See its README

cloud/lifesaver_report_pull.py

original standalone reference script (still runnable); also has pull_invoice() (single invoice as PDF) and pull_customer_export() (full customer list as CSV)

cloud/scripts/pull_invoices.py

bulk-pull invoices as PDFs (invoice_<id>.pdf), from a WorkOrderList CSV or a plain list of invoice numbers

cloud/scripts/pull_customers.py

pull the full customer list as CSV (every customer, not scoped to any date range)

cloud/scripts/inspect/

one-off recon of the other SSRS report pages (parked — see its README); also has verify_invoice_endpoint.py and verify_customer_export.py, which confirmed those two flows live before they were promoted into lifesaver_report_pull.py

cloud/DEPLOY.md

CI/CD, the GitHub Actions identity, and the two data workflows' setup

sqlite_extract/drive.py

finds the newest LifeSaver upload (zip or plain folder) in the shared Drive folder and downloads only what's needed

sqlite_extract/extract.py

LifeSaver .dbf tables (folder or zip) → lifesaver.sqlite (python -m sqlite_extract.extract <dir|zip> lifesaver.sqlite)

sqlite_extract/ingest.py / ingest.sh

Drive → build → sanity-check against the live copy → publish to GCS; what mason-ingest.yml runs

sqlite_extract/importer.py

lifesaver.sqlite → core warehouse (python -m sqlite_extract.importer lifesaver.sqlite mason.db). See its README

core/

visits.py, kpis.py, months.py (pure functions), store.py (the shared SQLite tables), models.py, config.py

webapp/

the website: server.py (API), data.py (per-store data + pooled warehouse), views.py, insights.py, mcp_server.py (the MCP server for Claude), frontend/ (React). See its README

stores.toml, core/stores.py

the store registry

lsreports/

the report query layer (semantic.py, presets.py) and the cloud/pooled report DBs (build.py)

.github/workflows/publish.yml

on push to main: test → build+push the website image to GHCR → deploy it to Cloud Run

.github/workflows/refresh.yml

daily: every cloud store → sync → recompute KPIs → save its warehouse

.github/workflows/mason-ingest.yml

every 6 h: Mason Drive upload → validated lifesaver.sqlite

Setup

python3 -m venv .venv
.venv/bin/pip install -r requirements-dev.txt   # runtime + pytest
cp .env.example .env   # fill in LIFESAVER_USERNAME / LIFESAVER_PASSWORD

(The website's container installs webapp/requirements.txt.)

Run the API

.venv/bin/uvicorn cloud.lifesaver.api:app --port 8000
  • GET /reports/work-order-list?start=2025-08-01&end=2025-08-31 → JSON

  • add &format=csv for the raw export

  • interactive docs at http://localhost:8000/docs

Claude connector (MCP)

The website serves it at /mcp/<token>: every store, or all of them pooled, from the same data the site shows. Tools, tiers and the connector URL: webapp/README.md.

Retention warehouse + website (Phase 3)

A cloud store (its login in LIFESAVER_USERNAME / LIFESAVER_PASSWORD; WAREHOUSE_DB_PATH picks the file, default warehouse.db):

export LIFESAVER_USERNAME=... LIFESAVER_PASSWORD=...
.venv/bin/python -m cloud.warehouse.job backfill   # full history, month by month, resumable
.venv/bin/python -m cloud.warehouse.job kpis       # eyeball against the baselines

An on-prem store. In production this is hands-off: the owner uploads to Drive and mason-ingest.yml does the rest. By hand:

# LifeSaver folder or zip -> lifesaver.sqlite (or: gcloud storage cp gs://frame-shop-511616-lifesaver-data/lifesaver.sqlite mason.sqlite)
.venv/bin/python -m sqlite_extract.extract ~/Downloads/LifeSaver.zip mason.sqlite

Then see it in the website, locally (webapp/README.md has the full recipe):

(cd webapp/frontend && npm install && npm run build)
STORE_DANS_DATA=warehouse.db STORE_WHEATONS_DATA=mason.sqlite AUTH_DISABLED=1 \
  .venv/bin/uvicorn webapp.server:app --port 8765

How the Drive ingest works, the lifesaver.sqlite data model, and the runbook (rejected uploads, force, rollback): sqlite_extract/README.md.

Full CLI and layer model: cloud/warehouse/README.md. Design, KPI definitions, and open questions: docs/kpi-design.md. warehouse.db and warehouse_raw/ are gitignored — each cloud store's system of record is the GCS warehouse bucket.

Standalone script (no server)

export LIFESAVER_USERNAME=... LIFESAVER_PASSWORD=...
.venv/bin/python cloud/lifesaver_report_pull.py --start 8/1/2025 --end 8/31/2025 --out aug.csv

Pulling invoices as PDFs

A single invoice (lifesaver_report_pull.pull_invoice(session, invoice_number)) hits a different endpoint than WorkOrderList — /Reports/Invoice/ needs no postback, but the human-readable "Invoice #" must first be resolved to an internal id via a separate POST (resolve_invoice_id()) before it works; using the human number directly renders a silently-empty PDF ($0.00 everywhere, #Error in place of the invoice number). See cloud/spec.md → "Invoices — a separate, simpler flow" for how both of these were confirmed.

Bulk-pulling, from a WorkOrderList CSV export's invoiceNumber column or a plain list:

export LIFESAVER_USERNAME=... LIFESAVER_PASSWORD=...
.venv/bin/python cloud/scripts/pull_invoices.py --csv aug.csv --out-dir invoices
.venv/bin/python cloud/scripts/pull_invoices.py --ids 584,591,602 --out-dir invoices

Writes invoices/invoice_<id>.pdf per invoice, one login/session for the whole batch, a delay between requests (--delay, default 1.5s), and one invoice failing doesn't stop the rest.

Pulling the customer list

Don't scrape it from the invoice PDFs (name + cell phone only, no email/address, one request per invoice) — the report catalog has a report built for exactly this. Filter: "No filter, show all customers." returns every customer in the database with real columns (name, email, phone, address, first/last purchase, total spend, ...), not scoped to any date range. See cloud/spec.md → "Customer list — a report built for exactly this, once found" for how that was confirmed.

export LIFESAVER_USERNAME=... LIFESAVER_PASSWORD=...
.venv/bin/python cloud/scripts/pull_customers.py --out customers.csv

One session per user

LifeSaver only allows one active session per user (license-limited). If a run doesn't log out, the next login is blocked with UserAlreadyLoggedIn. The client handles this automatically: it terminates its own stale session (never another user's) and retries. To clear one manually:

LIFESAVER_USERNAME=... LIFESAVER_PASSWORD=... .venv/bin/python cloud/scripts/terminate_my_sessions.py

Set LIFESAVER_TERMINATE_OWN_SESSION=false to disable the auto-clear.

Tests

.venv/bin/python -m pytest -q

Offline only — no network, no credentials, no GCP. The fixtures in tests/fixtures/ approximate the live responses; export_sample.csv is a real capture. After any live run, refresh the HTML fixtures from real responses to make the suite true regression coverage.

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    Pulls golf data from Arccos, GHIN, and 18Birdies, and generates interactive HTML and PDF round reports with shot maps and stats via local MCP tools.
    -
  • F
    license
    Not graded
    quality
    C
    maintenance
    MCP server for Bvoip / 1Stream that exposes call-reporting, phone-status, and CRM-extension-mapping endpoints as MCP tools.
    -
  • A
    license
    Not graded
    quality
    D
    maintenance
    MCP server for read-only queries and reports from the Astrea (Aurum) legal software. Enables retrieving case information by client, new cases per month, tags, financials (fees/expenses), schedule (hearings, meetings, deadlines, tasks), and case updates for client reports.
    10
    MIT