Skip to main content
Glama
AbdulRehman0004

Shopify Content Engine MCP Server

README.md
# Shopify Product Content Engine

[![CI](https://github.com/AbdulRehman0004/shopify-ai-content-engine/actions/workflows/ci.yml/badge.svg)](https://github.com/AbdulRehman0004/shopify-ai-content-engine/actions/workflows/ci.yml)
![n8n](https://img.shields.io/badge/n8n-workflow-EA4B71?logo=n8n&logoColor=white)
![OpenAI](https://img.shields.io/badge/OpenAI-Structured%20Outputs-412991?logo=openai&logoColor=white)
![MCP](https://img.shields.io/badge/MCP-server-000000)
![Python](https://img.shields.io/badge/python-3.11%2B-3776AB?logo=python&logoColor=white)
![License](https://img.shields.io/badge/license-MIT-green)

An n8n pipeline that turns a bare product (`product_name`, `product_description`) into a complete,
**review-ready marketing kit** — SEO research, product-page copy, a blog post, Google + Meta ads,
social posts, an email and a hero image — and writes one row per product to a **review sheet**.
Nothing publishes automatically; a human approves.

Alongside the workflow this repo ships the two things that make it operable rather than a demo:
an **eval harness** that scores every generated kit against the engine's own rules (length caps,
banned words, invented facts) and an **MCP server** so any agent — Claude Desktop, Claude Code,
Cursor, your own — can validate drafts and trigger runs.

```
product_name + product_description
        │
        ▼
 ┌────────────── n8n ──────────────┐
 │ idempotency gate → 3 LLM calls  │──▶ Review sheet (status=done | failed)
 │ strict JSON → validate → map    │──▶ hero image → Drive
 │ dry-run · retry · dead-letter   │──▶ batch summary email
 └─────────────────────────────────┘
        ▲                       │
   MCP server               eval harness
 (validate / run)      (rules + LLM judge, gates CI)
```

## Why it's built this way

| Concern | What the engine does |
|---|---|
| **Hallucinated specs** | Every prompt is facts-only; the eval flags any number/unit or claim word (`clinically`, `certified`, `vegan`, …) not present in the product input. |
| **Fragile parsing** | All three model calls use OpenAI **Structured Outputs** (`strict: true`) — schema-valid JSON, no regex. |
| **Re-runs / cost** | An idempotency gate skips anything already `status=done`; `forceRegenerate` overrides. Bulk model is `gpt-4o-mini`. |
| **One bad product** | Per-call retry + continue-on-error. Failures are written as `status=failed` rows with a `failure_reason` (dead-letter) — the batch never stops. |
| **Uncaught failures** | Routed to a separate Error Handler workflow → alert email. |
| **Testing without side effects** | `dryRun=true` generates and validates but writes nothing. |
| **Publishing** | Never automatic. Drafts land in the `Review` tab and a human approves. |
| **Platform limits** | Hard caps (SEO title ≤ 60, meta ≤ 155, Google headline ≤ 30, X ≤ 280, …) live in `config/`, are enforced in the Parse node, and are re-checked by the evals. |

## Flow

```
Manual / Schedule / Webhook ─▶ ⚙️ Config ─▶ 📥 Read Products [swap point → Shopify]
  ─▶ 📖 Read Done ─▶ 🚦 Idempotency Gate ─▶ 🔢 Limit ─▶ 🔁 Loop (1 product at a time)
        ─▶ 🤖 A: SEO + Product Page ─▶ 🤖 B: Blog ─▶ 🤖 C: Ads / Social / Email / Image prompt
        ─▶ 🧩 Parse, Validate & Map ─▶ 🧪 Dry run?
              ├─ yes ─▶ 📝 Dry-run log (no write) ─▶ loop
              └─ no  ─▶ 🖼️ Images? ─▶ 🎨 gpt-image-1 ─▶ ☁️ Drive ─▶ 📤 Upsert Review row ─▶ loop
  🔁 done ─▶ 📊 Batch summary ─▶ ✉️ Email (optional)
  (uncaught) ─▶ ⚠️ Error Handler workflow ─▶ alert email
```

Full Mermaid diagram: [`docs/architecture.mmd`](docs/architecture.mmd).
Product source is a Google Sheet today; `📥 Read Products` is a marked swap point for
*Shopify → Get Products* (`title → product_name`, `body_html → product_description`).

## Evals

`python -m evals` scores kits (Review-tab rows) against the rules the engine promises to keep.
Rules come straight from `config/generation.config.yaml` and `config/brand-voice.yaml`, so
changing a cap in config changes the eval.

| Check family | Examples |
|---|---|
| Completeness | `status=done`, all 19 content fields present |
| Hard caps | 8 character caps, 3 word ranges, ≤15 Google headlines each ≤30 chars, 5–10 hashtags |
| Structure | 4–6 bullets, H2/H3 in the blog, exactly one `{PRODUCT_URL}` CTA, slug format |
| SEO placement | primary keyword in title / meta / first sentence (warnings) |
| Brand voice | banned words from `brand-voice.yaml` |
| Facts-only | numbers+units and claim words in the output must exist in the product input |
| LLM judge (`--judge`) | 1–5 on facts-only, brand voice, SEO quality + quoted unsupported claims (Structured Outputs, opt-in, needs `OPENAI_API_KEY`) |

Current golden set (real kit from a live run, `evals/golden/kits.json`):

| Product | Score | What it caught |
|---|---:|---|
| Gentle Hydrating Gel Cleanser | 88% | description 66 words (want 150–250), blog 507 words (want 900–1200), email 28 words (want 60–120) — the model under-delivers on length; the Parse node truncates over-length text but has no minimum-length retry yet |

That finding is exactly why the harness exists — it's now an open item (see roadmap). Run it
against your own export with `python -m evals --input review-export.csv --min-score 0.9`; the
non-zero exit code gates CI.

## MCP server

`mcp_server/` exposes the engine over the Model Context Protocol (stdio):

| Tool | Purpose |
|---|---|
| `get_brand_voice` | voice, audience, banned words, facts-only rule |
| `get_generation_limits` | the hard caps + run flags |
| `list_sample_products` | the five fixture products |
| `validate_content_kit(row, product)` | score a draft with the same rules as the evals |
| `run_content_engine(dry_run=True, …)` | POST the workflow webhook with per-run flags — dry-run by default |

Resources: `content-engine://columns` (Review sheet column map), `content-engine://prompts/{A|B|C}`.

Claude Desktop / Claude Code config:

```json
{
  "mcpServers": {
    "shopify-content-engine": {
      "command": "content-engine-mcp",
      "env": { "N8N_WEBHOOK_URL": "https://<your-instance>/webhook/shopify-content-engine-run" }
    }
  }
}
```

## Repo layout

```
workflows/     shopify-content-engine.workflow.json · error-handler.workflow.json   (import these)
prompts/       system prompts A / B / C + prompt library (version-controlled mirror of ⚙️ Config)
config/        brand-voice.yaml · generation.config.yaml                            (limits, flags)
docs/          runbook.md · architecture.mmd · output-sheet-columns.md
test/          product-fixtures.json (5 products) · dry-run checklist · a real generated kit
evals/         checks.py (rules) · judge.py (LLM judge) · golden/ · tests/
mcp_server/    server.py · tests/
scripts/       check_workflows.py — static checks on the exported JSON (runs in CI)
```

## Quick start

**Workflow**
1. Import `workflows/error-handler.workflow.json`, then `workflows/shopify-content-engine.workflow.json`.
2. Re-select credentials on the OpenAI / Google Sheets / Drive / Gmail nodes (never stored in the JSON).
3. Point `📥 Read Products`, `📖 Read Done`, `📤 Write` at a sheet with tabs `Products` and `Review`.
4. Set *Settings → Error Workflow* to the imported error handler.
5. Dry-run the five fixtures ([`test/README.md`](test/README.md)), then run for real.
   Run flags can be posted in the webhook body: `{"dryRun": true, "enableImages": false}`.

**Evals + MCP**
```bash
pip install -e ".[mcp,dev]"
python -m evals                     # score the golden set
python -m evals --input export.csv  # score your own Review-tab export
pytest                              # 27 tests: rules, negative cases, MCP tools in-process
content-engine-mcp                  # start the MCP server (stdio)
```

## Roadmap
- Minimum-length guard + one self-correcting retry in the Parse step (the eval currently flags short copy after the fact).
- Header-auth on the webhook trigger (n8n credential) — today it relies on the unguessable path.
- Shopify Admin API as the product source and a `Draft product` write-back behind the same approval gate.
- Golden set: more real kits, one per fixture, and a nightly `--judge` run.

## License
MIT