Skip to main content
Glama
petrycz

ecommerce-mcp-automation

by petrycz
README.md
# Ecommerce MCP Automation

**A sample Claude Code + MCP integration:** Shopify and Meta Ads exposed as
MCP tools, plus a reporting agent that pulls both into one formatted daily
P&L + ad-performance spreadsheet — no manual copy-paste between platforms.

> **This is a demonstration built against the public Shopify Admin API and
> Meta Marketing API docs — not something that has run a real business.** It's
> a clean-room sample: real endpoints, real auth, real pagination, real error
> handling, written fresh to show exactly how this kind of automation gets
> built. It runs end-to-end in **mock mode** with zero credentials (realistic
> fixture data standing in for live responses), and switches to **live mode**
> per-integration the moment real credentials are set — see
> [How to run](#how-to-run).
>
> The Shopify client has been run live against a real Shopify Partners
> **development store** (a sandbox store, not a production business) —
> real auth, a real order, real API responses. That process surfaced and
> fixed two real nullability edge cases (see
> [Known simplifications](#known-simplifications)) that the mock fixtures
> alone hadn't covered. Meta Ads runs against the mock transport by default
> in this repo; the client code is written the same way and switches to live
> the moment `META_ACCESS_TOKEN`/`META_AD_ACCOUNT_ID` are set.

## What it does

- Exposes Shopify orders, revenue, and COGS as MCP tools (`get_orders`,
  `get_daily_pnl`)
- Exposes Meta Ads spend, impressions, purchases, and ROAS as MCP tools
  (`get_insights`, `get_daily_ad_performance`)
- Runs a reporting agent (`daily_report.py`) that pulls both **concurrently**
  and writes a formatted `.xlsx` — Summary, Orders, and Ad Performance sheets
- Ships a [Claude Code Skill](skills/daily-report/SKILL.md) that wraps the
  whole workflow behind a natural-language trigger ("run the daily report")
- Includes a committed [sample output](examples/sample_daily_report.xlsx) so
  the result is visible without running anything

## Sample output

<img src="examples/summary_preview.png" alt="Summary tab preview" width="480">

*Rendered preview of the Summary tab — [open the actual generated
workbook](examples/sample_daily_report.xlsx) for the live file (with the
Orders and Ad Performance sheets, currency/ROAS formatting, and frozen
header rows).*

## Architecture

```mermaid
flowchart LR
    subgraph Shopify["Shopify Admin API"]
        SO[orders.json]
        SI[inventory_items.json]
    end
    subgraph Meta["Meta Marketing API"]
        MI[act_id/insights]
    end

    SO --> SC[shopify_client.py]
    SI --> SC
    MI --> MC[meta_ads_client.py]

    SC --> SS[shopify_server.py<br/>MCP tools]
    MC --> MS[meta_ads_server.py<br/>MCP tools]

    SC --> DR[daily_report.py]
    MC --> DR
    DR --> SPX[spreadsheet.py]
    SPX --> XLSX[(sample_daily_report.xlsx)]

    Mock[["mock_api.py<br/>(ASGITransport, in-process)"]] -.mock mode.-> SC
    Mock -.mock mode.-> MC
```

The two API clients (`clients/shopify_client.py`, `clients/meta_ads_client.py`)
are genuine integration code — real endpoint URLs, real auth headers, real
pagination loops, real 429 backoff. The **only** thing that changes between
mock and live mode is the HTTP transport (`clients/http.py`):

- **Live:** `httpx.AsyncClient` opens a real connection to Shopify / Meta.
- **Mock:** `httpx.AsyncClient` is given an `httpx.ASGITransport` pointed at
  an in-process FastAPI app (`fixtures/mock_api.py`) serving realistic
  fixture payloads. No port is bound, no subprocess runs — but requests still
  travel through genuine HTTP/ASGI routing, headers, and JSON encoding.

That means the client code a reviewer reads is the same code that would run
against a live store — not a mock dressed up to look like one. See
[CLAUDE.md](CLAUDE.md) for the full conventions.

## How to run

### Mock mode (default — zero credentials)

```bash
git clone <this-repo> && cd ecommerce-mcp-automation
python -m venv .venv && source .venv/bin/activate   # or: uv sync && source .venv/bin/activate
pip install -e ".[dev]"

python -m ecommerce_mcp.reporting.daily_report
# -> Wrote examples/sample_daily_report.xlsx
```

Run the test suite the same way, no setup needed:

```bash
pytest
```

### Live mode

Copy `.env.example` to `.env` and fill in what you have — each integration
switches to live independently the moment its own credentials are present,
so you can run Shopify live with Meta still mocked (or vice versa):

```bash
cp .env.example .env
# SHOPIFY_STORE_DOMAIN=your-dev-store.myshopify.com
# SHOPIFY_ACCESS_TOKEN=shpat_...          (Partners dev store -> custom app -> Admin API token)
# META_ACCESS_TOKEN=EAA...                (System User token, ads_read scope)
# META_AD_ACCOUNT_ID=act_1234567890
```

### As MCP servers (Claude Code / Claude Desktop)

Add to your MCP config (`.mcp.json` for Claude Code, or Claude Desktop's
config file). Point `command` at the project's venv interpreter directly —
MCP clients don't source your shell profile, so a bare `python` won't see an
activated venv:

```json
{
  "mcpServers": {
    "shopify": {
      "command": "/path/to/ecommerce-mcp-automation/.venv/bin/python",
      "args": ["-m", "ecommerce_mcp.mcp_servers.shopify_server"],
      "cwd": "/path/to/ecommerce-mcp-automation"
    },
    "meta-ads": {
      "command": "/path/to/ecommerce-mcp-automation/.venv/bin/python",
      "args": ["-m", "ecommerce_mcp.mcp_servers.meta_ads_server"],
      "cwd": "/path/to/ecommerce-mcp-automation"
    }
  }
}
```

Then ask Claude things like "what's today's Shopify P&L?" or "get me
yesterday's Meta ad performance" — it'll call the tools directly, in mock
mode by default.

### As a Skill

`skills/daily-report/SKILL.md` wraps the report-generation workflow so
Claude Code runs it on a natural-language trigger ("run the daily report")
rather than needing the exact CLI command. **The full-report path doesn't
need the MCP config above at all** — it runs `daily_report.py` directly,
which calls the clients as plain Python, no MCP involved. MCP config is only
needed for the Skill's other path: answering a one-off single-metric
question ("what's today's ROAS?") by calling `get_daily_pnl` /
`get_daily_ad_performance` as MCP tools instead of running the whole report.

## Project layout

```
src/ecommerce_mcp/
  clients/         Typed, async API clients (Shopify + Meta), transport-swappable
  mcp_servers/      MCP tool servers wrapping the clients
  reporting/        daily_report.py (orchestration) + spreadsheet.py (openpyxl)
  fixtures/         Realistic mock payloads + the in-process mock API app
skills/daily-report/ Claude Code Skill for the reporting workflow
tests/              pytest suite (all run against mock mode)
examples/           Committed sample .xlsx + README preview image
```

## Known simplifications

Documented here rather than hidden, since precision matters more than
polish for a sample like this:

- **COGS** uses Shopify's `InventoryItem.cost` field via the real two-hop
  lookup (variant → `inventory_item_id` → batched `inventory_items` fetch)
  — Shopify doesn't expose cost on the order line item directly. Both `cost`
  and line-item `sku` are nullable on a live store (a merchant may never have
  set them) — found via live testing against a real dev store, not from the
  docs alone. Both are handled as zero-cost / missing-SKU rather than erroring.
- **Refunded orders** are excluded entirely from revenue/COGS/order count in
  `daily_pnl()`. Partial refunds/returns accounting would need the Refund
  resource — out of scope here.
- **Meta purchase attribution** uses the `purchase` action type from the
  `actions`/`action_values` arrays at whatever attribution window the ad
  account is configured with — this client doesn't override it.
- The reporting agent currently pulls **all** available orders/insights
  rather than filtering by date range; a production daily cron would pass
  `created_at_min`/`time_range` for the target day.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A3.9/5.0

Scored across 2 tools

Disambiguation5/5

get_orders returns raw order data, while get_daily_pnl computes financial metrics. Their purposes are entirely distinct with no overlap, making tool selection unambiguous.

Naming Consistency5/5

Both tools follow the consistent get_<noun> pattern, which is clear and predictable. There is no mixing of styles or ambiguous verbs.

Tool Count3/5

With only two tools, the set feels thin for a server labeled 'ecommerce automation'. However, it is not a single trivial tool, so borderline is appropriate.

Completeness1/5

The server provides only read-only order access and a PnL report. There are no create/update/delete operations, product or customer tools, refund handling, or broader automation workflows, making the surface severely incomplete for ecommerce automation.

Maintenance

ActivityMaintained
ResponsivenessNo issues