
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 分