
shopify-operations-mcp
io.github.jpka由 GitHub 帳號 jpka 發布v0.1.2更新於 Oct 24, 2026
Safe-write Shopify operations: plan-before-execute writes with out-of-band approval and audit.
概覽
讓助理透過先預覽後執行的寫入計畫、人工審批與稽核日誌,安全地讀取與修改 Shopify 商店。
- 可以做什麼
- 將助理連接到 Shopify Admin API 進行讀寫操作。讀取工具可搜尋商品、列出訂單;寫入工具可更新庫存、調整價格、建立折扣、取消訂單與退款。每次寫入都遵循兩階段流程:預覽在不進行任何變更的情況下計算前後差異清單,接著簽發計畫權杖,執行時會重新讀取狀態,若已變動則拒絕執行。規模較大或不可逆的計畫需在本地網頁由人工審批,所有動作都會寫入雜湊鏈式 JSONL 稽核檔案。回滾工具可在時間窗內復原可逆計畫。
- 適用情境
- 適合讓助理管理 Shopify 商店的商品、庫存、價格、折扣與訂單,但批量或不可逆的變更需要先經人工確認的情境。適用於單店部署,且更重視預覽、審批門檻與稽核紀錄而非速度的情況。
- 執行需求
- 以 npm 套件形式透過 stdio 在本機執行,需要 Node.js。必須提供 SHOPIFY_ADMIN_TOKEN 與 SHOPIFY_STORE_DOMAIN,以及具備寫入權限的 Shopify Admin API 權杖。選用設定包括審批伺服器連接埠、稽核檔案路徑、呼叫方識別、設定檔路徑、計畫有效期與保護標籤。審批介面僅綁定 127.0.0.1。
安裝
在 SourceWeft 中
- 開啟 儀表板中的 shopify-operations-mcp,將其新增到工作區。
- 為需要使用其工具的對話啟用該服務。
Desktop only,透過 STDIO。 STDIO 服務會啟動本機處理程序,因此需要 SourceWeft 桌面主機。
其他 MCP 客戶端
參照 儲存庫 中的啟動說明。
README
Safe-write MCP server for Shopify Admin API operations
An agent can read and modify a Shopify store without being able to cause an unrecoverable accident. The safety layer is the differentiator: every write previews before it commits, large or irreversible changes require out-of-band human approval, and every action is recorded to a tamper-evident hash-chained audit file.
Architecture
The two-phase pattern (preview → token → execute) is the core discipline. Every write tool:
- Preview — reads current state and computes what would change, performing zero mutation calls
- Token — issues a plan token bound to the exact previewed manifest via a SHA-256 fingerprint
- Approval — plans exceeding
approvalRequiredAboveItems(default 25) or containing always-gated operations wait for human approval at the token-bearing URL the server prints on startup (e.g.http://127.0.0.1:4319/?token=<token>) - Execute — re-reads current values, refuses if they drifted from the preview (
STATE_CHANGED), then applies mutations per-item with a full success/failure ledger
A plan whose manifest exceeds hardMaxItems (default 250) is refused outright — no token, no approval path.
Irreversible operations (cancel_order, refund_order) always require approval regardless of item count and cannot be rolled back. Reversible operations (price changes, inventory adjustments) support rollback within a configurable window (default 24 hours).
Threat model
The risk is not a malicious agent — the agent is trusted to author correct GraphQL. The risk is a trusted-but-fallible agent: syntactically perfect, well-formed operations whose scope is the problem.
The killer scenario — a syntactically perfect bulk reprice with a misplaced decimal:
A 500-product bulk update that runs without preview-and-approve, or where the agent's price calculation contains a typo, produces exactly the wrong result at scale. Approval would catch it: a human sees "change 500 prices from $X to $1.50" and flags the不对劲. Without approval, or without the preview that makes the damage visible before it happens, the error lands silently in Shopify.
Three mechanisms carry the safety guarantee:
1. Preview-first, computed-diff. Every write tool reads current state and computes the manifest ({ref, before, after} pairs) without calling any mutation. A STATE_CHANGED re-read at execute time refuses the write if the world moved since preview. The blast radius is visible before anything changes.
2. Approval gating above the threshold. Plans touching >= approvalRequiredAboveItems items (default 25) require human approval. The threshold is sized for "is this large enough to warrant a human eye?" — meaningful for bulk value changes; irrelevant for one-item operations (which get unconditional approval for irreversible ops instead).
3. Plan token bound to exact manifest. The token is a SHA-256 fingerprint of the exact previewed manifest — not an opaque ID. Swapping in a wider set of items or a different price at execute time produces a different fingerprint and is refused as STATEMENT_MISMATCH.
Rollback provides recovery for reversible mistakes (wrong price, wrong inventory level) within the rollback window. It does not recover from the irreversible operations: a cancelled order stays cancelled, a refunded payment stays refunded.
Quick start
Set the required environment variable and point Claude Desktop at the server (see Configuration below). node dist/index.js starts the localhost approval UI alongside the MCP stdio server.
Demo: the step-by-step walkthrough script (store-wide reprice refused → approval-gated reprice → one-call rollback → hash-chained audit) is in docs/demo-runbook.md.
Configuration
Configuration file (default config.json in the working directory, or path via SHOPIFY_CONFIG):
Config reference
Invariant: plans.hardMaxItems must be >= plans.approvalRequiredAboveItems. The loader throws if violated.
Environment variables
All config fields are overridable by environment variables (precedence: env > config file > default). SHOPIFY_ADMIN_TOKEN is required and only ever read from the environment.
Tools
Read tools
search_products
Search products by title, SKU, vendor, or tag. Returns products with variants, current prices, and per-location inventory levels.
Arguments:
Returns: products[] with id, title, vendor, tags, variants (each with id, sku, price, inventoryItemId, inventoryLevels), plus flags.protected / flags.protectedTags indicating whether the product carries a protected tag.
Safety properties: Pure read — zero mutation calls. Protected-tagged products are returned (never filtered out) so a later write plan that touches them is refused.
list_orders
List orders filtered by financial status, fulfillment status, and date range.
Arguments:
Returns: orders[] with id, name, financialStatus, fulfillmentStatus, totalPrice, lineItems[].
Safety properties: Pure read — zero mutation calls.
Write tools (two-phase)
All write tools go through preview → token → (approval) → execute.
update_inventory
Set absolute inventory quantities at a named location for multiple inventory items. Preview reads current levels; execute calls inventorySetQuantities.
Arguments:
Safety properties:
- Threshold gating: requires approval when
adjustments.length >= approvalRequiredAboveItems(default 25); refused outright when> hardMaxItems(default 250) - Protected-tag enforcement: plans touching a product with a protected tag throw
PROTECTED_RESOURCEbefore a token is issued — no approval path - Per-item ledger: partial failure is recorded, never hidden
- Rollback: supported — restores
before.availablequantities via the snapshot
cancel_order
Cancel a Shopify order. Always requires approval regardless of item count. Cannot be rolled back.
Arguments:
Safety properties:
- Always approval:
alwaysRequireApproval: trueis hardcoded in the tool — approval thresholds are never consulted - No snapshot: no
snapshotStore.capture()call — rollback is not opened for this operation - Rollback: refused with
ROLLBACK_UNSUPPORTED— cancellation is a state transition, not a value change
refund_order
Refund a Shopify order. Always requires approval regardless of item count. Cannot be rolled back.
Arguments:
Each RefundLineItem: {lineItemId, quantity, restockType?} where restockType is "RETURN" \| "NO_RESTOCK" \| "CANCEL".
Safety properties:
- Always approval:
alwaysRequireApproval: truehardcoded in the tool - Preview via
refundCalculate: zero-write GraphQL call returns exact suggested refund amounts for the approval surface - No snapshot: no rollback support
- PII-free audit: only order ID and refund amount are recorded; customer name/email are never in the audit trail
rollback_plan
Undo an executed reversible plan within the rollback window (default 24 hours).
Arguments:
Safety properties:
- No approval required: restoring the prior state is the safe direction
- Window guard:
ROLLBACK_WINDOW_EXPIREDwhen the snapshot has expired or the plan was never previewed - Kind guard:
ROLLBACK_UNSUPPORTEDwhen the plan kind iscancel_orderorrefund_order - Inverse mutations only on refs that succeeded at execute time — a ref that failed is left untouched
- Per-item ledger: partial rollback failure is recorded honestly
Plan lifecycle
Localhost approval UI
A plain-HTML page for a human to approve or reject plans above the threshold. Runs as its own local-only HTTP server, started alongside the MCP server.
- Access: the token-bearing URL the server prints on startup (e.g.
http://127.0.0.1:4319/?token=<token>) on the machine running the server. Unreachable from other machines. - Auth: every route — including the read-only GET ones — requires a per-session bearer token (
Authorization: Bearer <token>header, or the?token=query fallback the printed URL uses). The token is generated per start unlessSHOPIFY_APPROVAL_SERVER_AUTH_TOKENsets a stable one; scripted callers pass it as a header (curl -H "Authorization: Bearer $TOKEN" ...). Prefer the env token for scripted or long-lived use rather than scraping the startup log — stderr is routinely captured to log files, where the credential would persist. Loopback binding plus Host/Origin checks stop a hostile browser page, but only the bearer token stops another local process that knows a plan token. SetapprovalServer.requireAuth: falseto fall back to pre-0.4.0 behavior (not recommended). - API:
GET /api/plansreturns pending plans as JSON;POST /api/plans/:token/approveandPOST /api/plans/:token/rejecthandle approval. - Security boundary: approval/rejection is never exposed as an MCP tool — the agent cannot approve its own plans.
Audit log
Every preview, approval, execution, rejection, and refusal writes one JSON object to the audit file:
seq— monotonically increasing per-file sequenceprev_hash— SHA-256 of the previous row (genesis = 64 zero chars)hash— SHA-256 of this row (excluding thehashfield itself)- Tamper-evident, not tamper-proof: editing, reordering, or deleting a non-terminal row breaks the chain at that row. Suffix truncation or replacing the whole file with a valid chain is not detectable from the file alone.
- PII defense-in-depth: top-level keys matching
/customerEmail|customerName/iare stripped before hashing. Free-textreason/detailstrings are not scanned — hosts must sanitize those before callingrecord(). - Restart safety: on open, the sink verifies the existing chain and resumes
seq/prev_hashfrom the last line.
Verify the chain with scripts/verify-audit.ts.
Dev-store seeder
scripts/seed-store.ts generates a realistic store to point the server at — deterministic, like sw-postgres-mcp's seeder. Everything is derived from a seeded PRNG (mulberry32), so two runs with the same seed produce identical data and identical counts.
Run it:
The seeder reuses the server's loadConfig, so SHOPIFY_STORE_DOMAIN / SHOPIFY_ADMIN_TOKEN / SHOPIFY_API_VERSION (and any config file) are honored exactly as for the server; --seed defaults to 42. Flags:
--seed <number>— PRNG seed; default42. Any two runs with the same seed are identical.--dry-run— prints the plan counts and verifies the sizing invariants without making any API call (no credentials needed).--order-delay-ms <ms>— sleep between order creates. Development stores caporderCreateat five per minute, so plan ~24 minutes for the 120 orders; default is no delay.
Data shape (seed 42):
Order-state mix: 116 paid / 4 pending; 40 fulfilled / 80 unfulfilled; 12 carry a fixed-amount discount code. Every product, customer, and order is tagged seeded-store.
Sizing invariants (asserted against the loaded config before any API call — the seeder fails fast if they don't hold):
- The full variant set (768) exceeds
hardMaxItems(default 250), so a store-wide reprice is refused (HARD_MAX_ITEMS_EXCEEDED). - The
saletag covers 156 variants — betweenapprovalRequiredAboveItems(default 25) andhardMaxItems(250) — so a reprice scoped totag:'sale'requests approval but is not refused.
The structural sizing is seed-independent: every 5th product (60 of 300) carries sale, and each product has 1–4 variants, so the sale tag always covers between 60 and 240 variants.
Idempotency: a re-run first wipes every previously-seeded order, customer, and product tagged seeded-store (orders first — customers can only be deleted once their orders are gone), then regenerates. Products delete their variants and inventory items with them; the two locations are reused, never deleted. The script prints counts at the end so you can diff two runs.
Live integration suite
tests/integration/ is a manual-only, env-gated suite that proves the server against the real Admin API and the seeded dev store. It is deliberately kept out of CI: it needs real store credentials (secrets must never reach CI) and it makes rate-limited calls that would flake.
Run it:
Re-seed before each run so the destructive tests find fresh candidate orders. With no credentials, the whole suite skips itself with a console note and exits 0 — npm run test:integration and the default npm test both pass as a no-op, and it never runs on CI.
What it covers (each file is describe.skip-gated unless SHOPIFY_STORE_DOMAIN and SHOPIFY_ADMIN_TOKEN are set):
Files run serially (fileParallelism: false) because the suite shares one mutable store — destructive writes must never race the read counts. Config: vitest.integration.config.ts.
Limitations
Stated plainly, not hidden:
-
Order cancels/refunds are irreversible. Approval protects them — it does not enable rollback. A cancelled order cannot be uncancelled; a refunded payment cannot be unwired. RollbackPlan refuses
cancel_orderandrefund_ordertokens withROLLBACK_UNSUPPORTED. -
Rollback is best-effort snapshot restoration. The snapshot captures the before-state at preview time; it is only usable as an inverse-mutation target while the world hasn't changed. After
rollbackTtlMs(default 24h), rollback is refused withROLLBACK_WINDOW_EXPIRED. Rollback restores values, not external side effects (e.g., a refund notification already sent). -
Partial failure leaves a per-item ledger, not an exception. When a batch mutation fails for some items and succeeds for others, the executor records each outcome honestly. There is no all-or-nothing rollback across items — only per-item inverse mutations at rollback time for the items that succeeded.
-
Plan state is in-memory and process-scoped. The PlanStore and SnapshotStore hold all pending and executed plan state in process memory. A server restart loses every pending plan (re-preview required) and every rollback window. The audit log is the durable record of what happened.
-
Single store, no multi-tenancy. One server process talks to one Shopify store. Running for multiple stores means running multiple server instances with separate credentials and audit files.
-
No per-user auth.
callerIdidentifies the deployment (default"unknown"), not an individual person. There is no per-MCP-session or per-user authentication in v1. Anyone who can reach the server's stdio transport can use it with the configured store credentials; anyone holding the approval-server bearer token (or reaching the UI with auth disabled) can approve plans. -
STATE_CHANGEDis a pre-write drift check, not a universal compare-and-swap. The re-read catches drift that exists before the mutation is sent. It does not close the window between re-read and write. Shopify exposes provider-level compare-and-swap for some operations (e.g.changeFromQuantityfor inventory) but not all (plain price updates are last-write-wins).
License
來源:README.md,提交 c836fe5
工具
0版本歷史
1- v0.1.2最新Sep 16, 2026
評分
已滿足適用檢查項的 50%
該評分衡量此收錄條目的完整程度,依據註冊中心條目、端點與公開來源計算得出。它不是對伺服器本身的評價,也不表示伺服器安全或目前可用:可連通性是帶日期的檢查結果,評分也不包含安裝量或使用者評價。
- 近 30 天內可連通25 分
請保持端點能夠回應 MCP 握手;SourceWeft 每 30 天探測一次。
- 已宣告認證方式10 分
請在註冊中心條目中宣告憑證請求標頭,或保持端點能夠回應探測,以便判定其認證方式。
- 已列出工具15 分
請讓伺服器在無憑證時回應 tools/list,或發布可在沙箱中無憑證啟動的軟體包。
- 描述必需15 分
- 圖示10 分
- 文件15 分
- 授權條款10 分