shopify-operations-mcp
Safe-write Shopify operations with plan-before-execute workflows, out-of-band approval, and tamper-evident audit logging.
Click on "Install Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@shopify-operations-mcpPlan a price update for all products in the Sale collection."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
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
flowchart TB
subgraph agent["Agent"]
A[Claude]
end
subgraph mcp["MCP stdio transport"]
T[tools/call]
end
subgraph core["safe-write-mcp-core"]
PS[PlanStore]
AS[ApprovalServer]
end
subgraph shopify["Shopify Admin API"]
GQL[GraphQL endpoint<br/>/admin/api/2026-04/graphql.json]
end
subgraph audit["Audit"]
LF[JSONL audit file<br/>hash-chained]
end
A -->|"MCP stdio"| T
T -->|create plan| PS
T -->|preview| PS
T -->|execute plan| PS
PS -->|awaiting_approval| AS
AS -->|approve/reject| PS
PS -->|execute| GQL
PS -->|record| LF
style PS fill:#e1f5fe
style AS fill:#fff3e0
style LF fill:#f3e5f5
style GQL fill:#e8f5e9The 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 athttp://127.0.0.1:4319/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).
Related MCP server: shopify-admin-mcp
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:
update_prices([...], newPrice: 1.5) ← meant 15.00, typed 1.5A 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
npm install
npm test
npm run buildSet 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):
{
"shopify": {
"storeDomain": "my-store.myshopify.com",
"apiVersion": "2026-04"
},
"plans": {
"planTtlMs": 60000,
"approvalRequiredAboveItems": 25,
"hardMaxItems": 250,
"maxPriceChangePct": 30,
"rollbackTtlMs": 86400000
},
"approvalServer": {
"enabled": true,
"port": 4319
},
"protectedTags": ["do-not-touch"],
"callerId": "shopify-operations-mcp"
}Config reference
Field | Type | Default | Description |
|
| (required) | MyShopify domain, e.g. |
|
|
| Pinned quarterly Admin API version |
|
| (env only) | Admin API token — never in config file, only |
|
|
| How long a plan token stays valid (ms). Overridable: |
|
|
| Plans touching this many items require human approval. Overridable: |
|
|
| Plans exceeding this item count are refused outright. Overridable: |
|
|
| Price changes exceeding this % require approval. Overridable: |
|
|
| Rollback window (ms, default 24h). Overridable: |
|
|
| Start localhost approval UI alongside MCP server. Overridable: |
|
|
| Port for localhost approval UI (127.0.0.1 only). Overridable: |
|
|
| Tags that plans may never modify. Overridable: |
|
|
| Identity recorded on every audit row. Overridable: |
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:
Field | Type | Description |
|
| Matches products whose title contains the term (Shopify fuzzy search) |
|
| Matches products with a variant whose SKU equals the term |
|
| Matches products from this vendor |
|
| Matches products carrying this tag |
|
| Page size passed to Admin API (default 50) |
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:
Field | Type | Description |
|
|
|
|
|
|
|
| Orders created at or after this datetime |
|
| Orders created at or before this datetime |
|
| Page size (default 250) |
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:
Field | Type | Description |
|
|
|
|
| Each |
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 pathPer-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:
Field | Type | Description |
|
|
|
|
|
|
|
| Return items to inventory |
|
| Send cancellation email |
Safety properties:
Always approval:
alwaysRequireApproval: trueis hardcoded in the tool — approval thresholds are never consultedNo snapshot: no
snapshotStore.capture()call — rollback is not opened for this operationRollback: 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:
Field | Type | Description |
|
|
|
|
| Line items and quantities to refund; absent = full refund of all fulfilled items |
|
| Human-readable reason recorded in audit |
Each RefundLineItem: {lineItemId, quantity, restockType?} where restockType is "RETURN" \| "NO_RESTOCK" \| "CANCEL".
Safety properties:
Always approval:
alwaysRequireApproval: truehardcoded in the toolPreview via
refundCalculate: zero-write GraphQL call returns exact suggested refund amounts for the approval surfaceNo 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:
Field | Type | Description |
|
| The token from the executed plan to roll back |
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 previewedKind guard:
ROLLBACK_UNSUPPORTEDwhen the plan kind iscancel_orderorrefund_orderInverse 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
Agent calls preview tool
│
▼
Manifest built
(pure reads, zero writes)
│
▼
Item count checked
│
├─── <= hardMaxItems ──► token issued
│ │
│ >= approvalRequiredAboveItems
│ │ or alwaysRequireApproval
│ ▼
│ status: "awaiting_approval"
│ │
│ human approves
│ │
▼ │
HARD_MAX_ITEMS_EXCEEDED │
(no token, refused) ▼
execute_plan
│
STATE_CHANGED check
(re-read, compare digest)
│
┌────┴────┐
success failure
│ │
per-item per-item
ledger ledger
│
snapshot stored
for rollbackLocalhost 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:
http://127.0.0.1:4319/(or configuredapprovalServer.port) on the machine running the server. Unreachable from other machines.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":1,"prev_hash":"0000...","hash":"ab12...","ts":1734567890000,"tool":"update_inventory","reason":"adjusting stock","planToken":"abc123","status":"executed","previewCount":10,"callerId":"shopify-ops","durationMs":234,"detail":"all 10 item(s) executed"}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:
SHOPIFY_STORE_DOMAIN=my-dev.myshopify.com SHOPIFY_ADMIN_TOKEN=shpat_... npm run seed -- --seed 42The 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):
Resource | Count | Notes |
Products | 300 | titled |
Variants | 768 | 1–4 per product, SKUs |
Locations | 2 | the first two locations in the store (locations are physical, not created) |
Customers | 20 |
|
Orders | 120 | Bogus-Gateway test orders ( |
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:
SHOPIFY_STORE_DOMAIN=my-dev.myshopify.com SHOPIFY_ADMIN_TOKEN=shpat_... npm run seed -- --seed 42
SHOPIFY_STORE_DOMAIN=my-dev.myshopify.com SHOPIFY_ADMIN_TOKEN=shpat_... npm run test:integrationRe-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):
File | Proves |
|
|
|
|
|
|
|
|
| DESTRUCTIVE — exactly one real |
| Cost-aware throttling: a parallel burst exceeding the API cost budget is absorbed by the default client (backoff); a no-retry client surfaces |
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 (or127.0.0.1:4319for approvals) can use it with the configured store credentials.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
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
Guarded MCP server for agent-readable business truth, provenance, readiness, and discovery.
MCP server for Boson Protocol — on-chain agentic commerce for physical & digital goods.
MCP server for Modern Treasury — payment orders, transactions, counterparties and ledgers.
Unified MCP server for 70+ eCommerce platforms: products, orders, customers, and more.
Related MCP Servers
- FlicenseNot gradedqualityCmaintenanceMCP server exposing Shopify commerce backend with ~22 typed tools for orders, inventory, logistics, and fulfillment, including read/write separation and structured errors.
- AlicenseNot gradedqualityDmaintenanceProduction-grade MCP server for the Shopify Admin GraphQL API, exposing typed tools for AI agents to manage products, orders, customers, and more.31MIT
- AlicenseNot gradedqualityBmaintenanceA read-only MCP server that exposes the full Shopify Admin GraphQL API through 6 universal tools, with multi-store support and mutation rejection at the parser level for safety.MIT
- AlicenseNot gradedqualityDmaintenanceMCP server for Shopify Admin API. Enables product, order, customer, and inventory management via natural language.191MIT
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/jpka/shopify-operations-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server