Skip to main content
Glama

Shopify Demand & Inventory Copilot

A production-grade demand forecasting and inventory-risk system for Shopify merchants: a live GraphQL Admin API integration (OAuth install, webhooks, bulk operations), per-SKU forecasting with an eval-gated promotion process, and every forecast translated into an estimated dollar impact versus a naive baseline -- not just an accuracy score.

This is built as an installable, multi-tenant Shopify app (any merchant can install it via OAuth), not a tool wired to one personal store.

Status: Phase 4 of 4 (complete)

Phase

Scope

Status

0

Repo scaffold, schema, local Postgres

Done

1

Seasonality-aware data generation, Shopify OAuth install flow, webhook ingestion (HMAC-verified), bulk-operation reconciliation

Done

2

dbt aggregation (daily_sku_sales), naive baseline, backtest harness, $-impact methodology

Done

3

Per-SKU LightGBM model, MLflow tracking, promotion gate

Done -- see REPORTS.md

4

FastAPI serving (/forecast, /reorder, /impact-report), MCP tools, Streamlit dashboard, Evidently monitoring, full Docker Compose stack

Done (this pass)

Headline result (see REPORTS.md for the full, real MLflow-logged history): the promoted forecasting model has worse raw accuracy than the naive baseline (6.69 vs. 5.39 units MAE) but delivers +$11,593.21 over the baseline across a 12-week/24-SKU backtest, because it was trained toward the newsvendor-optimal quantile instead of the conditional mean. Three earlier, accuracy-competitive candidates were correctly rejected by the promotion gate before this fix, along with a deliberately weak model on every run -- real evidence the gate evaluates dollars, not accuracy.

Related MCP server: shopops-mcp

What's real vs. what needs live Shopify credentials

There's no Shopify Partner account or dev store yet, so everything is built in two tracks:

  • Runs today, no Shopify credentials needed: local Postgres, the seasonality/promo/catalog generators, data_generator/seed_local.py (writes synthetic order history directly into Postgres), the webhook listener + HMAC verification (tested with locally-crafted signed payloads).

  • Implemented against the real Shopify API contract, but requires a live Partner dev store (or a real merchant install) to actually run: shopify_client/oauth.py (app install flow), data_generator/shopify_seed.py (seeds history via real orderCreate mutations), ingestion/bulk_backfill.py (weekly reconciliation via bulkOperationRunQuery).

Creating a Shopify Partner account and a free development store (no credit card, ~10 minutes) is what unlocks live end-to-end testing of the second track. Until then, the first track proves the ingestion/storage contract works correctly on its own.

Repo layout

data_generator/    seasonality + promo + catalog models, local and live seeding
shopify_client/    Admin GraphQL client, OAuth install flow, bulk operations
ingestion/         FastAPI app: webhook listener (HMAC-verified) + OAuth routes, bulk reconciliation job
dbt/               staging models + daily_sku_sales mart
forecasting/       naive baseline, $-value dollarization, backtest harness, LightGBM training, MLflow promotion gate
serving/           GET /forecast, /reorder, /impact-report business logic, mounted into ingestion's FastAPI app
mcp_server/        MCP tool wrappers over serving/, for use from Claude Desktop/Code or any MCP client
dashboard/         Streamlit dashboard leading with the live $-impact headline number
monitoring/        Evidently feature-drift report + a custom $-impact-degradation trend check
db/                schema.sql, tiny psycopg2 connection helper
tests/             pytest suite
.github/workflows/ CI (tests on push/PR) + scheduled weekly retrain (+ drift check)
Dockerfile         image shared by the api/dashboard/mlflow-ui docker-compose services
docker-compose.yml postgres + api + dashboard + mlflow-ui for local dev
REPORTS.md         Phase 3 baseline-vs-model comparison, accuracy and dollar terms

Running Phase 1 locally

  1. Start Postgres

    docker compose up -d postgres

    This applies db/schema.sql automatically on first start.

  2. Set up a virtualenv and install deps

    python -m venv .venv
    .venv/Scripts/activate   # or source .venv/bin/activate on macOS/Linux
    pip install -r requirements.txt
    cp .env.example .env

    Generate a TOKEN_ENCRYPTION_KEY:

    python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
  3. Seed 18-24 months of synthetic order history

    python -m data_generator.seed_local --months 20

    Re-run with --reset to regenerate. This writes a dev shop, a 24-SKU catalog with Pareto-ish popularity, and daily orders driven by a trend + weekday seasonality + quarterly/BFCM promo-spike curve.

  4. Run the ingestion API

    uvicorn ingestion.main:app --reload

    GET /health, GET /auth?shop=... (install flow), POST /webhooks (HMAC-verified orders/create / orders/updated / inventory_levels/update).

  5. Run the tests

    pytest tests/

    Covers: HMAC accepts a correctly-signed payload and rejects a forged or missing signature (both as pure-function checks and as full webhook-endpoint integration checks that assert nothing is written to the DB on rejection).

  6. Build the daily_sku_sales mart with dbt

    dbt run --project-dir dbt --profiles-dir dbt

    Aggregates orders + order_line_items into daily per-SKU unit sales. The dbt profile defaults to the docker-compose Postgres credentials, so no extra config is needed for local dev.

  7. Run the backtest harness

    python -m forecasting.backtest

    Replays the last 12 weeks for every product, using a trailing 4-week moving average as the naive baseline (forecasting/baseline.py), and writes each SKU-week's actual/predicted/baseline/dollar-impact into backtest_results. Phase 2 has no trained model yet, so predicted_units is deliberately set equal to the baseline -- the headline dollar impact should print as $0.00, which is the expected, correct result: it proves the harness and the dollarization math (forecasting/backtest.py::dollarize_forecast_error) are wired correctly without a real model muddying the signal. Phase 3's trained model plugs into this same harness as a genuinely different predicted_units and is expected to produce a non-zero (ideally positive) number.

  8. Train the model, run the promotion gate, and see the real comparison

    python -m forecasting.train

    Trains a global LightGBM model (product_id as a categorical feature) plus a deliberately weak DummyRegressor "control" candidate, backtests both through the exact same harness as the baseline, logs everything to MLflow (MLFLOW_TRACKING_URI, defaults to a local sqlite:///mlflow.db so Model Registry stage transitions work without a real server), and only promotes a candidate to the Production stage if it beats both the baseline and whatever's currently deployed, in dollars (forecasting/registry.py). Prints a comparison table at the end; see REPORTS.md for the full real run history from this project's own development, including multiple rejections.

    mlflow ui --backend-store-uri sqlite:///mlflow.db

    to browse runs/versions/stage transitions in the MLflow UI.

Running Phase 4 locally

  1. Serve live forecasts (after step 8 has promoted a model -- otherwise these fall back to the naive baseline)

    uvicorn ingestion.main:app --reload

    GET /forecast?shop_domain=...&sku=..., GET /reorder?shop_domain=...&sku=..., GET /impact-report?shop_domain=....

  2. Run the dashboard

    streamlit run dashboard/app.py

    Leads with the live $-impact-vs-baseline headline number (serving/impact_report_service.py), then a per-SKU breakdown and the reorder-recommendations table.

  3. Register the MCP server (wraps the same serving/ logic as three MCP tools: get_forecast_tool, get_reorder_recommendation_tool, get_impact_report_tool)

    claude mcp add shopico -- python -m mcp_server.server

    Or run directly with python -m mcp_server.server (stdio transport) to register with any other MCP client.

  4. Check for degrading $-impact or feature drift

    python -m monitoring.drift_monitor

    Prints an early-half-vs-recent-half $-impact trend for the active model and writes an Evidently feature-drift HTML report to monitoring/reports/. Wired into the weekly retrain workflow as a post-training step.

  5. Or run the whole stack in Docker

    docker compose up -d --build

    api (port 8000, ingestion + serving), dashboard (port 8501), mlflow-ui (port 5000) all bind-mount the host's ./mlflow.db, so they see whatever step 8 already promoted rather than starting from an empty registry.

Once a Partner dev store exists

python -m data_generator.shopify_seed --months 20   # seeds via real orderCreate mutations
python -m ingestion.bulk_backfill --shop-domain your-store.myshopify.com --access-token <token> --since-days 7

And point the app's /auth?shop=your-store.myshopify.com at a running ingestion.main:app (with SHOPIFY_APP_URL set to a publicly reachable URL, e.g. via ngrok for local dev) to exercise the full install flow.

Design notes worth knowing

  • products.shopify_inventory_item_id is an addition beyond the original spec's schema: inventory_levels/update webhooks key off inventory_item_id, not SKU, so there's no way to route that event to a product row without it.

  • Webhook ingestion and bulk-operation reconciliation intentionally write through the same handle_order_webhook function (ingestion/webhook_listener.py), so both paths agree by construction on overlapping records.

  • Everything uses a single psycopg2 (sync) driver, no ORM -- matches the spec's "right-sized, not cloud-scale" philosophy (see original spec §7).

  • dollarize_forecast_error's sign convention was corrected vs. the original spec pseudocode. The pseudocode's cost_of_error returns a positive cost for the stockout branch but a negative value for the overstock branch. Walk through a case where the model forecasts perfectly and the baseline over-forecasts: baseline_cost - model_cost should be positive (model saved money by avoiding the baseline's excess inventory), but the pseudocode's sign makes it negative -- and more negative the worse the baseline overstocks, inverting the stated "positive = value created" meaning. Fixed by having cost_of_error always return a positive cost magnitude in both branches. See forecasting/backtest.py's module docstring and tests/test_dollar_impact_calc.py::test_overstock_scenario_* for the hand-verified case that pins this down.

  • The naive baseline (forecasting/baseline.py) is a trailing 4-week moving average of weekly units sold, computed strictly from weeks before the target week (no leakage) -- the standard, easy-to-hand-verify "seasonal naive" baseline for weekly-bucketed retail demand.

  • A single global LightGBM model, not per-SKU models. The spec allows "per-SKU (or per-SKU-cluster, for low-volume SKUs)"; with ~600-900 days of history per SKU, one shared model with product_id as a categorical feature borrows statistical strength across SKUs far better than fitting 24 independent models in isolation, especially for the long-tail ones.

  • Trained toward a cost-optimal quantile, not the conditional mean. dollarize_forecast_error's cost is asymmetric (stockouts cost a full unit's margin, overstock only a small holding fee), so a plain L2 regression model has no way to know that slight over-forecasting is usually cheaper than under-forecasting. This is the classic newsvendor problem; forecasting/train.py::newsvendor_critical_fractile derives the optimal LightGBM quantile objective alpha from each product's real unit economics rather than guessing. See REPORTS.md for what this fix actually changed (rejected → promoted, despite worse MAE).

  • forecasting/backtest.py::write_backtest_results deletes existing rows for a model_version before inserting -- discovered this needed to be idempotent while iterating during Phase 3 development, when re-running training for the same day's model label was silently accumulating duplicate rows and corrupting total_dollar_impact's aggregation.

  • The promotion gate uses MLflow's Model Registry Production stage (via MlflowClient.transition_model_version_stage) as the single source of truth for "what's currently deployed," rather than a custom state table. That API is deprecated as of MLflow 2.9 in favor of model aliases, but still functional as of 2.19 (used here) -- worth revisiting if this project upgrades MLflow significantly later.

  • serving/model_loader.py reuses forecasting/train.py::make_predict_fn directly (loading the model via mlflow.lightgbm.load_model, not mlflow.pyfunc) so live inference goes through the exact same feature-building code path already validated in backtest -- no separate, potentially-drifted serving-time implementation. It caches the loaded model by MLflow registry version number (immutable once registered, unlike "Production" which changes meaning on every promotion), since callers that loop over many SKUs for one shop (dashboard/app.py, monitoring/drift_monitor.py) would otherwise reload the same model from disk once per SKU.

  • forecasting/train.py never persisted the naive baseline's own backtest_results rows (only trained candidates go through write_backtest_results) -- a real gap this pass's serving/impact_report_service.py had to work around: if the baseline has no rows for a shop, it's recomputed live via forecasting.backtest.run_backtest instead of silently reporting a zero MAE.

  • The reorder policy (serving/reorder_service.py) is a documented assumption, not real Shopify data: LEAD_TIME_WEEKS/SAFETY_STOCK_WEEKS/ OVERSTOCK_WEEKS_OF_COVER are a standard weeks-of-cover policy, overridable via env vars, until a live merchant's actual lead times exist.

  • fastapi was bumped from 0.115.6 to 0.139.0 (and pydantic 2.10.5 to 2.11.0) in this pass: fastapi 0.115.6 pinned starlette<0.42, which can't coexist in one virtualenv with the starlette version streamlit 1.59.2 requires, and mcp 1.28.1 requires pydantic>=2.11. Existing tests (tests/test_webhook_hmac.py, the OAuth routes) still pass unchanged.

  • The Docker image needs libgomp1 (apt-get install) -- python:3.11-slim doesn't include it, and LightGBM's native library won't load without it (OSError: libgomp.so.1: cannot open shared object file).

  • Evidently alone only compares feature distributions, which wouldn't catch a model that's still "in-distribution" but has quietly stopped beating the baseline in dollars -- the metric this whole project is built around. So monitoring/drift_monitor.py runs a custom early-half-vs-recent-half $-impact trend check (flags >30% relative degradation, or a positive→negative flip) alongside Evidently's feature-drift report, rather than relying on feature drift as a proxy for the thing that actually matters.

All four planned phases are now complete. Natural next steps beyond the original scope: replacing MLflow's deprecated stage-based registry with aliases, real Shopify Partner dev-store credentials to exercise the OAuth/bulk paths end-to-end, and per-SKU (rather than catalog-average) lead times once real merchant inventory data exists.

F
license - not found
-
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • Shopify MCP Pack — wraps the Shopify Admin REST API (2024-01)

  • Connect e-commerce and marketing data to AI assistants via MCP.

  • Remote MCP connector for eBay, Shopify, Best Buy & Etsy marketplace data via the Commerce API

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/HamzaOuadid/shopico'

If you have feedback or need assistance with the MCP directory API, please join our Discord server