Skip to main content
Glama

inventory-doctor

Multi-source inventory sync diagnostics — find the SKUs you're overselling without knowing it.

Compare two inventory snapshots (CSV exports or live Shopify stores) and get a report of everything that doesn't line up: SKU mismatches, oversell risk, blank cells masquerading as zeros, barcode conflicts, and more.

Your data never leaves your machine. CSV files are parsed locally; Shopify API calls go directly from your computer to your own stores over HTTPS. There is no server, no telemetry, no upload.

Install & run

npm install          # or: pnpm install
npm run build        # produces dist/cli.js (with shebang)

# zero-credential path: two CSV exports
npx tsx src/cli.ts diff fixtures/shopify-store-a.csv fixtures/shopify-store-b.csv
# or after build / npm link:
inventory-doctor diff a.csv b.csv

Related MCP server: DataClaw MCP Server

What it looks like

Running inventory-doctor diff fixtures/shopify-store-a.csv fixtures/shopify-store-b.csv against the bundled sample files (which deliberately contain one of every problem):

inventory-doctor — sync diagnosis
============================================================
Sources: shopify-store-a  vs  shopify-store-b
  shopify-store-a: 13 records
  shopify-store-b: 12 records

Sync health score: 33/100
  exact match: 5 · minor drift: 0 · severe drift: 4 · unmatched: 6 (of 15)

CRITICAL (8)
------------------------------------------------------------
[CRITICAL] SKU "DUP-100" appears 2 times in shopify-store-a with different quantities
[CRITICAL] SKU "ORPHAN-A" exists in shopify-store-a but is missing from shopify-store-b
[CRITICAL] "OVER-1" is out of stock in shopify-store-a (0) but shows 8 available in
           shopify-store-b — you may be selling stock you don't have
[CRITICAL] "BLANK-1" has a BLANK quantity cell in shopify-store-a (not "0") while
           shopify-store-b tracks a real quantity — an import may read this as 0
[CRITICAL] Barcode 9999999 maps to 2 different SKUs across sources
           ("BC-A" in shopify-store-a vs "BC-B" in shopify-store-b)
...

WARNING (5)
------------------------------------------------------------
[WARN]     SKU differs only by letter case: "ABC-123" (a) vs "abc-123" (b)
[WARN]     SKU matches only after removing whitespace/invisible characters
[WARN]     "CONT-1" allows overselling ("continue selling when out of stock") with quantity 0
...

INFO (3)
------------------------------------------------------------
[INFO]     Possible prefix/suffix variant: "GHI-789" (a) vs "SHOP-GHI-789" (b)
[INFO]     Sync health score: 33/100 — 5 exact, 0 minor drift, 4 severe drift, 6 unmatched
[INFO]     "UNTRACKED-1" has inventory tracking OFF in shopify-store-a but is
           stock-managed in shopify-store-b

The full output is checked in as fixtures/expected-report.md — it's a living document verified by the test suite.

The exit code is 1 when critical findings exist, so you can wire this into CI or a cron job.

The six diagnostic rules

Rule

What it catches

Severity

sku-mismatch

Case-only / whitespace / prefix-suffix SKU variants, orphan SKUs, one-to-many duplicates within one source

info → critical

oversell-risk

Quantity ≤ 0 on one side but > 0 on the other; "continue selling when out of stock" with empty stock; drift beyond threshold

warning → critical

blank-vs-zero

A blank quantity cell vs an explicit 0 — the classic "bulk import wiped my inventory" root cause

critical

barcode-crosscheck

Same barcode, different SKUs across sources — silent mapping misconfiguration

critical

quantity-drift

Overall sync health: % exact / minor drift / severe drift / unmatched → health score 0–100

info

untracked

Inventory tracking disabled in one source while another manages stock

info

Blank vs "0" is a first-class distinction. CSV parsers love turning empty cells into 0; this tool keeps quantity: null strictly separate from quantity: 0 all the way through.

Supported inputs

  • Shopify product CSV — both header generations are recognized (Variant SKU and the current SKU, Variant Inventory Qty and Inventory quantity, etc.)

  • Shopify inventory CSV — both layouts: the long "All states" table (one row per variant × location) and the wide "Available" table (location names as column headers, inferred automatically)

  • Anything else — alias-based column probing (Amazon-style seller-sku/quantity TSVs work out of the box), plus explicit mapping when guessing fails:

inventory-doctor diff shopify.csv erp.csv --map sku="Item Code" --map quantity="Stock Count"

SKU normalization (trim, case folding, full-width → half-width, zero-width character removal) is used only for comparison — reports always show your raw SKU values.

Multi-location aware. When both sources carry a location dimension (inventory CSV long/wide, or the API), quantities are compared per (SKU, location): a location-level stockout or drift is reported at that location and never hidden by summing across locations, and a SKU stocked at two locations is not mistaken for a duplicate. When one side has no location dimension (product CSV), its per-SKU quantity is compared against the other side's cross-location sum. See fixtures/shopify-inventory-c.csv / -d.csv (long format) and fixtures/shopify-inventory-wide-e.csv / -f.csv (wide format) for worked examples.

Shopify Admin API (optional)

Pull live snapshots instead of exporting CSVs. Create inventory-doctor.json in the project directory (or ~/.config/inventory-doctor/config.json):

{
  "stores": [
    // Mode 1: existing static token (shpat_... — still works if you already have one)
    { "name": "store-a", "domain": "a.myshopify.com", "accessToken": "env:STORE_A_TOKEN" },
    // Mode 2: client credentials grant (the current way to create app credentials)
    { "name": "store-b", "domain": "b.myshopify.com",
      "clientId": "env:STORE_B_CLIENT_ID", "clientSecret": "env:STORE_B_SECRET" }
  ]
}

Credentials support "env:VAR_NAME" references so secrets stay out of files. Then:

inventory-doctor diff --store store-a --store store-b   # store vs store
inventory-doctor diff --store store-a --csv b.csv       # mixed mode

Details that matter:

  • API version pinned to 2026-07 (/admin/api/2026-07/graphql.json).

  • Inventory is read via quantities(names: [...]) — the old InventoryLevel.available field no longer exists.

  • Tokens from client credentials live 24h; they're cached and refreshed 60s early, not re-requested per call.

  • Rate limiting is adaptive: every response's extensions.cost.throttleStatus.currentlyAvailable drives a slow-down before the bucket empties; HTTP 429 / THROTTLED backs off 1s and retries. No plan-specific rate numbers are hardcoded.

  • Read-only scopes only: read_inventory, read_products, read_locations.

Client credentials limitation: the app and the store must belong to the same Shopify org. That covers "a merchant building a tool for their own store". Agencies managing client stores will get shop_not_permitted and need full OAuth — which this tool does not implement (v1).

MCP server (use it from Claude Code and other agents)

Add to your .mcp.json:

{
  "mcpServers": {
    "inventory-doctor": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y", "inventory-doctor", "mcp"],
      "env": { "SHOPIFY_CLIENT_SECRET": "${SHOPIFY_CLIENT_SECRET}" }
    }
  }
}

Three tools are registered:

  • diff_inventory(sourceA, sourceB, configPath?) — full diagnosis, returns the JSON report

  • explain_sku(sku, sources, configPath?) — one SKU's raw values and findings across all sources (for follow-up questions)

  • inventory_health(sources, configPath?) — lightweight health-score summary

Every source argument accepts either a CSV file path or store:<name> (a store from inventory-doctor.json, credentials resolved from env vars) — so an agent can diff two live stores, or a store against a CSV, in one call. configPath is only needed when the config file is not in a default location.

stdout is reserved for JSON-RPC; all logging goes to stderr.

Honest limitations

  • Snapshot diffing only. This compares two snapshots taken now. It does not do time-series detection (e.g. "this SKU gets silently zeroed every night") — that needs snapshot history and is planned for v2.

  • Client credentials = same org only, as described above. No OAuth flow in v1.

  • Amazon report headers vary by marketplace and report options; detection is best-effort via column aliases, and --map is the escape hatch.

  • Product CSVs carry no per-location inventory; multi-location diagnosis needs the inventory CSV export or the API.

Development

npm test             # vitest — rules, CSV adapters, token cache, throttle logic
npm run dev -- diff fixtures/shopify-store-a.csv fixtures/shopify-store-b.csv
npm run build        # tsc + shebang
npm run check:stdout # guards the MCP stdout discipline

Architecture: src/core/ is a pure-function diagnostic kernel ((records: InventoryRecord[]) => Finding[], zero I/O); src/adapters/ turn CSVs and the Shopify API into that intermediate representation; CLI and MCP layers only parse arguments and format output. Adding a new source (WooCommerce, BigCommerce, …) means adding one adapter, no refactor.

Related MCP Connectors

Related MCP Servers