shopify-store-sync-mcp 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +578 -3
- package/dist/apply.js +185 -0
- package/dist/apply.js.map +1 -0
- package/dist/cli.js +259 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.js +107 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.js +104 -0
- package/dist/diff.js.map +1 -0
- package/dist/engine.js +157 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/plan.js +163 -0
- package/dist/plan.js.map +1 -0
- package/dist/refs.js +330 -0
- package/dist/refs.js.map +1 -0
- package/dist/resources/collections.js +109 -0
- package/dist/resources/collections.js.map +1 -0
- package/dist/resources/index.js +31 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/resources/menus.js +81 -0
- package/dist/resources/menus.js.map +1 -0
- package/dist/resources/metafieldDefinitions.js +143 -0
- package/dist/resources/metafieldDefinitions.js.map +1 -0
- package/dist/resources/metafields.js +85 -0
- package/dist/resources/metafields.js.map +1 -0
- package/dist/resources/metaobjectDefinitions.js +153 -0
- package/dist/resources/metaobjectDefinitions.js.map +1 -0
- package/dist/resources/metaobjects.js +91 -0
- package/dist/resources/metaobjects.js.map +1 -0
- package/dist/resources/pages.js +48 -0
- package/dist/resources/pages.js.map +1 -0
- package/dist/resources/products.js +256 -0
- package/dist/resources/products.js.map +1 -0
- package/dist/resources/redirects.js +28 -0
- package/dist/resources/redirects.js.map +1 -0
- package/dist/resources/shopPolicies.js +30 -0
- package/dist/resources/shopPolicies.js.map +1 -0
- package/dist/resources/translations.js +86 -0
- package/dist/resources/translations.js.map +1 -0
- package/dist/resources/types.js +18 -0
- package/dist/resources/types.js.map +1 -0
- package/dist/resources/util.js +23 -0
- package/dist/resources/util.js.map +1 -0
- package/dist/server.js +271 -0
- package/dist/server.js.map +1 -0
- package/dist/shopify/cliClient.js +185 -0
- package/dist/shopify/cliClient.js.map +1 -0
- package/dist/shopify/client.js +123 -0
- package/dist/shopify/client.js.map +1 -0
- package/dist/snapshot.js +29 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/snapshotFiles.js +100 -0
- package/dist/snapshotFiles.js.map +1 -0
- package/dist/themes.js +88 -0
- package/dist/themes.js.map +1 -0
- package/package.json +66 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Youssef Idder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,578 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/images/hero.png" alt="shopify-store-sync-mcp: diff and sync configuration between Shopify stores from your AI assistant" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/shopify-store-sync-mcp"><img src="https://img.shields.io/npm/v/shopify-store-sync-mcp?color=cb3837" alt="npm version"></a>
|
|
7
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-57c26e" alt="MIT license"></a>
|
|
8
|
+
<img src="https://img.shields.io/badge/node-%3E%3D20-6cb6ff" alt="Node 20+">
|
|
9
|
+
<img src="https://img.shields.io/badge/MCP-server-a371f7" alt="MCP server">
|
|
10
|
+
<img src="https://img.shields.io/badge/Shopify%20Admin%20API-2026--10-e3b341" alt="Shopify Admin API 2026-10">
|
|
11
|
+
</p>
|
|
12
|
+
|
|
13
|
+
<h3 align="center">Keep your Shopify stores in sync by asking for it.</h3>
|
|
14
|
+
|
|
15
|
+
<p align="center">Compare a dev store with production, see every difference field by field, review a dry-run plan, then apply it.<br>From Claude, Cursor or any MCP client, or straight from the command line.</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="#get-started">Get started</a> ·
|
|
19
|
+
<a href="#what-it-syncs">What it syncs</a> ·
|
|
20
|
+
<a href="#why-this-tool">Why this tool</a> ·
|
|
21
|
+
<a href="#safe-on-production">Safety</a> ·
|
|
22
|
+
<a href="#tested-on-real-stores">Tested</a> ·
|
|
23
|
+
<a href="#roadmap">Roadmap</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<p align="center">
|
|
27
|
+
<img src="docs/images/quickstart.png" alt="Three steps: connect your stores with npx shopify-store-sync-mcp connect, add the server to your AI client, ask in plain language" width="100%">
|
|
28
|
+
</p>
|
|
29
|
+
|
|
30
|
+
<p align="center">
|
|
31
|
+
<img src="docs/images/demo.png" alt="diff_stores, plan_sync and apply_plan output: 6 created, 3 updated, 1 conflict skipped, protected store refused without confirmation" width="100%">
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<sub>Real output of the server's tools on two sample stores.</sub>
|
|
35
|
+
|
|
36
|
+
## What it syncs
|
|
37
|
+
|
|
38
|
+
The store setup that usually gets rebuilt by hand, plus the images it uses. References between items (a size guide pointing at a product, a menu linking to a page) are carried across by handle, so they land on the right item instead of a broken ID.
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<img src="docs/images/what-it-syncs.png" alt="Metafield definitions, metaobject definitions and entries, collections, pages, menus, redirects, store policies, files and images, and opt-in products, metafield values and translations" width="100%">
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<details>
|
|
45
|
+
<summary><b>Details per resource</b></summary>
|
|
46
|
+
|
|
47
|
+
| Resource | Keyed by | Notes |
|
|
48
|
+
|---|---|---|
|
|
49
|
+
| `metafield_definitions` | `OWNER_TYPE.namespace.key` | Name, description, pinning, storefront and customer account access, capabilities, validations. A type change is reported as a conflict, since Shopify can't change a definition's type in place. |
|
|
50
|
+
| `metaobject_definitions` | `type` | Schema, access, capabilities. Fields are added or updated, never removed. Self-referencing definitions are handled. |
|
|
51
|
+
| `pages` | `handle` | Title, body HTML, template, published state. |
|
|
52
|
+
| `metaobjects` | `type/handle` | All field values, publish status. References to products, collections, pages and other metaobjects are remapped. |
|
|
53
|
+
| `menus` | `handle` | Full item tree (3 levels), including customer account menus. Links to resources are remapped; custom URLs are kept; links to things that can't be matched are left out and reported. |
|
|
54
|
+
| `redirects` | `path` | Thousands are fine. |
|
|
55
|
+
| `collections` | `handle` | Title, description, sort order, template, SEO and smart-collection rules (rules on a metafield definition are remapped). Manual collections keep their own product list; sales-channel publishing is not copied. Manual versus smart is reported as a conflict. |
|
|
56
|
+
| `shop_policies` | `type` | Refund, privacy, terms, shipping and contact policy text. |
|
|
57
|
+
| `products` *(opt-in)* | `handle` | Title, description, vendor, type, tags, template, SEO, options, variants (matched by option values, with SKU) and images. **New products are created as drafts.** Prices only with `prices: true` (`--with-prices`); inventory and sales-channel publishing are never copied. On update, variants, images and option values that only the target has are kept, and don't count as differences. Gift cards are skipped. |
|
|
58
|
+
| `metafields` *(opt-in)* | `OWNER/handle/namespace.key` | Metafield **values** on products and collections, landed on the item with the same handle. Reference values are remapped. Filter with `namespaces`. |
|
|
59
|
+
| `translations` *(opt-in)* | `locale:Type/handle` | Translations of pages, collections, metaobjects, menus and policies. Registered against the target's own text, after the content itself is synced. The plan warns when a language isn't added to the target. |
|
|
60
|
+
|
|
61
|
+
`products`, `metafields` and `translations` can be large, so they're read only when you ask for them (`resources: ["products"]`). When an include filter names exact product handles (or handle prefixes like `linen-*`), only those products are read. App-owned (`app--…`) and Shopify-reserved (`shopify--…`) data is skipped and listed in the report.
|
|
62
|
+
|
|
63
|
+
**Files and images** referenced by any of the above are matched by file name in the target. When one is missing, the plan says so and `apply_plan` uploads it from the source store.
|
|
64
|
+
|
|
65
|
+
</details>
|
|
66
|
+
|
|
67
|
+
## Use cases
|
|
68
|
+
|
|
69
|
+
<p align="center">
|
|
70
|
+
<img src="docs/images/use-cases.png" alt="Dev to production, multi-region brands, new market launch, drift audits" width="100%">
|
|
71
|
+
</p>
|
|
72
|
+
|
|
73
|
+
## Why this tool
|
|
74
|
+
|
|
75
|
+
Plenty of Shopify MCP servers read and write *one* store. This one is built for the job *between* stores, where every ID is different:
|
|
76
|
+
|
|
77
|
+
| | Generic Admin MCP servers | shopify-store-sync-mcp |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| Compare two stores | ❌ | ✅ field-level diff |
|
|
80
|
+
| Cross-store ID remapping | ❌ you copy raw IDs | ✅ products, collections, pages, blogs, metaobjects, definitions, customer account pages and shop policies |
|
|
81
|
+
| Dry run before writing | ❌ | ✅ every write goes through a reviewed plan |
|
|
82
|
+
| Detects changes made after planning | ❌ | ✅ re-reads the target and skips anything that drifted |
|
|
83
|
+
| Deletes | sometimes | **never** |
|
|
84
|
+
| Production guardrails | ❌ | ✅ writes are off by default; protected stores need typed confirmation |
|
|
85
|
+
| Audit trail | ❌ | ✅ JSONL log of every write |
|
|
86
|
+
|
|
87
|
+
**Why not just Shopify CLI?** The CLI runs one query you wrote, on one store. This tool uses the CLI only to sign in, and does the comparing, ID remapping, ordering, uploads and safety checks for you.
|
|
88
|
+
|
|
89
|
+
<p align="center">
|
|
90
|
+
<img src="docs/images/vs-shopify-cli.png" alt="Left: raw shopify store execute commands per store with manual comparison and ID rewriting. Right: diff_stores, plan_sync and apply_plan doing it with remapping, ordering, uploads and drift checks" width="100%">
|
|
91
|
+
</p>
|
|
92
|
+
|
|
93
|
+
## Safe on production
|
|
94
|
+
|
|
95
|
+
<p align="center">
|
|
96
|
+
<img src="docs/images/safety.png" alt="Read-only by default, plan then apply, drift protection, never deletes, protected stores, conflicts not forced, audit log, no broken IDs" width="100%">
|
|
97
|
+
</p>
|
|
98
|
+
|
|
99
|
+
<details>
|
|
100
|
+
<summary><b>How each safeguard works</b></summary>
|
|
101
|
+
|
|
102
|
+
1. **Read-only by default.** `allowWrites` is `false` until you set it, per store.
|
|
103
|
+
2. **Plan, then apply.** `apply_plan` only runs a plan produced by `plan_sync`. Plans expire after an hour.
|
|
104
|
+
3. **Drift protection.** Before writing, the target is re-read. Any item that changed since planning is skipped, not overwritten.
|
|
105
|
+
4. **No deletes.** Items that exist only in the target are listed, never removed. Fields that exist only in the target are left alone.
|
|
106
|
+
5. **Protected stores.** Production needs the exact domain typed as confirmation.
|
|
107
|
+
6. **Conflicts aren't forced.** A metafield definition whose type differs is reported and skipped.
|
|
108
|
+
7. **Audit log.** Every write is appended to `~/.shopify-store-sync/audit.log` (override with `SYNC_AUDIT_LOG`).
|
|
109
|
+
|
|
110
|
+
</details>
|
|
111
|
+
|
|
112
|
+
## Get started
|
|
113
|
+
|
|
114
|
+
### 1. Connect your stores
|
|
115
|
+
|
|
116
|
+
Most people only need the first option: no app to create. The other two are for scheduled or unattended runs.
|
|
117
|
+
|
|
118
|
+
<p align="center">
|
|
119
|
+
<img src="docs/images/connect.png" alt="connect output: browser approval, Connected, and the stores.json line to add; then the three ways to connect compared" width="100%">
|
|
120
|
+
</p>
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm install -g @shopify/cli
|
|
124
|
+
npx shopify-store-sync-mcp connect my-dev-store # read-only
|
|
125
|
+
npx shopify-store-sync-mcp connect my-staging-store --writes
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
<details>
|
|
129
|
+
<summary><b>More about the Shopify CLI sign-in</b></summary>
|
|
130
|
+
|
|
131
|
+
Each `connect` opens Shopify in your browser; click **Install** to approve. It asks for read-only scopes unless you pass `--writes` (or the store is configured with `"allowWrites": true`), then prints the line to add to `stores.json`:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
{ "stores": { "my-dev-store": { "domain": "my-dev-store.myshopify.com", "auth": "shopify-cli" } } }
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Requests then run through `shopify store execute`, Shopify CLI's supported command, using the sign-in it saved. Each request starts Shopify CLI (about a second), so a full diff takes tens of seconds instead of one or two with an app. That sign-in belongs to you and expires after a while; when it does, the tool says so and you run `connect` again. Because of that, use option 2 or 3 for scheduled or unattended runs.
|
|
138
|
+
|
|
139
|
+
</details>
|
|
140
|
+
|
|
141
|
+
<details>
|
|
142
|
+
<summary><b>Option 2: a Dev Dashboard app (automation on your own stores)</b></summary>
|
|
143
|
+
|
|
144
|
+
One app covers every store in the same Shopify organization, and tokens are fetched and refreshed for you.
|
|
145
|
+
|
|
146
|
+
1. Open the [Dev Dashboard](https://dev.shopify.com/dashboard), go to **Apps**, click **Create app**, then **Create app manually**.
|
|
147
|
+
2. Give it a name, for example `Store Sync`. Any URL works as the App URL (it's never opened); untick **Embed app in Shopify admin**.
|
|
148
|
+
3. In **Scopes**, paste the scopes from "Scopes needed per resource" below, then click **Create app** and **Release**.
|
|
149
|
+
4. Install it on each store: open `https://admin.shopify.com/store/<store-handle>/oauth/install?client_id=<your client ID>` and click **Install**.
|
|
150
|
+
5. Under the app's **Settings**, copy the **Client ID** and **Client secret** into environment variables (step 2 reads them from there).
|
|
151
|
+
|
|
152
|
+
Adding a scope later means releasing a new version and approving the update on each store (same install link).
|
|
153
|
+
|
|
154
|
+
</details>
|
|
155
|
+
|
|
156
|
+
<details>
|
|
157
|
+
<summary><b>Option 3: an access token from a custom app (automation on a client's store)</b></summary>
|
|
158
|
+
|
|
159
|
+
Client credentials only work on stores in the app's own organization; elsewhere Shopify answers `shop_not_permitted`. For unattended runs on such a store, ask the owner to create a custom app in their admin (**Settings > Apps > Develop apps**), grant the scopes listed below, install it and share the Admin API access token (`shpat_...`). Use it as `accessToken` in step 2.
|
|
160
|
+
|
|
161
|
+
</details>
|
|
162
|
+
|
|
163
|
+
<details>
|
|
164
|
+
<summary><b>Scopes needed per resource</b></summary>
|
|
165
|
+
|
|
166
|
+
| Resource | Read scope | Write scope |
|
|
167
|
+
|---|---|---|
|
|
168
|
+
| metafield definitions | `read_products` (and the owner type's read scope) | `write_products` (and the owner type's write scope) |
|
|
169
|
+
| metaobjects and definitions | `read_metaobjects`, `read_metaobject_definitions` | `write_metaobjects`, `write_metaobject_definitions` |
|
|
170
|
+
| pages | `read_online_store_pages` | `write_online_store_pages` |
|
|
171
|
+
| menus | `read_online_store_navigation` | `write_online_store_navigation` |
|
|
172
|
+
| redirects | `read_online_store_navigation` | `write_online_store_navigation` |
|
|
173
|
+
| collections | `read_products` | `write_products` |
|
|
174
|
+
| products | `read_products` | `write_products` (plus `read_files`/`write_files` for their images) |
|
|
175
|
+
| shop policies | `read_legal_policies` | `write_legal_policies` |
|
|
176
|
+
| metafield values | `read_products` | `write_products` |
|
|
177
|
+
| translations | `read_translations`, `read_locales` | `write_translations` |
|
|
178
|
+
| files | `read_files` | `write_files` (to upload missing files) |
|
|
179
|
+
| theme diff | `read_themes` | none, never writes |
|
|
180
|
+
| ID remapping | `read_products`, `read_content` | none |
|
|
181
|
+
|
|
182
|
+
Everything at once (the set used for the live tests), ready to paste:
|
|
183
|
+
|
|
184
|
+
```text
|
|
185
|
+
read_products,write_products,read_metaobjects,write_metaobjects,read_metaobject_definitions,write_metaobject_definitions,read_online_store_pages,write_online_store_pages,read_online_store_navigation,write_online_store_navigation,read_legal_policies,write_legal_policies,read_translations,write_translations,read_locales,read_files,write_files,read_themes,read_content
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Give source stores read-only scopes when you can.
|
|
189
|
+
|
|
190
|
+
</details>
|
|
191
|
+
|
|
192
|
+
### 2. Describe your stores
|
|
193
|
+
|
|
194
|
+
`connect` prints the line to add. A `stores.json` mixing all three ways to connect:
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"stores": {
|
|
199
|
+
"dev": { "domain": "my-brand-dev.myshopify.com", "auth": "shopify-cli" },
|
|
200
|
+
"eu": { "domain": "my-brand-eu.myshopify.com", "clientId": "env:EU_CLIENT_ID", "clientSecret": "env:EU_CLIENT_SECRET", "allowWrites": true, "protected": true },
|
|
201
|
+
"uk": { "domain": "my-brand-uk.myshopify.com", "accessToken": "env:UK_TOKEN" }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
<details>
|
|
207
|
+
<summary><b>All store options</b></summary>
|
|
208
|
+
|
|
209
|
+
Create `stores.json`. Secrets can come from environment variables with `env:NAME`:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"stores": {
|
|
214
|
+
"dev": {
|
|
215
|
+
"domain": "my-brand-dev.myshopify.com",
|
|
216
|
+
"accessToken": "env:DEV_TOKEN",
|
|
217
|
+
"label": "Development"
|
|
218
|
+
},
|
|
219
|
+
"eu": {
|
|
220
|
+
"domain": "my-brand-eu.myshopify.com",
|
|
221
|
+
"clientId": "env:EU_CLIENT_ID",
|
|
222
|
+
"clientSecret": "env:EU_CLIENT_SECRET",
|
|
223
|
+
"allowWrites": true,
|
|
224
|
+
"protected": true
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
| Option | Default | Meaning |
|
|
231
|
+
|---|---|---|
|
|
232
|
+
| `domain` | required | `*.myshopify.com` domain |
|
|
233
|
+
| `auth` | | `"shopify-cli"`: use the sign-in saved by `connect` (no token needed), **or** |
|
|
234
|
+
| `accessToken` | | Static Admin API token, **or** |
|
|
235
|
+
| `clientId` + `clientSecret` | | Dev Dashboard app; tokens are fetched and refreshed automatically |
|
|
236
|
+
| `apiVersion` | `2026-10` | Admin API version |
|
|
237
|
+
| `allowWrites` | `false` | Must be `true` for `apply_plan` to write to this store |
|
|
238
|
+
| `protected` | `false` | `apply_plan` also requires `confirm: "<domain>"` |
|
|
239
|
+
|
|
240
|
+
You can also pass the whole config as JSON in `SHOPIFY_STORES`, or point `SHOPIFY_STORES_FILE` at a file elsewhere. For a single store, `SHOPIFY_STORE_DOMAIN` plus `SHOPIFY_ACCESS_TOKEN` works.
|
|
241
|
+
|
|
242
|
+
</details>
|
|
243
|
+
|
|
244
|
+
### 3. Add it to your AI client
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"mcpServers": {
|
|
249
|
+
"shopify-store-sync": {
|
|
250
|
+
"command": "npx",
|
|
251
|
+
"args": ["-y", "shopify-store-sync-mcp"],
|
|
252
|
+
"env": { "SHOPIFY_STORES_FILE": "/absolute/path/to/stores.json" }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
<details>
|
|
259
|
+
<summary><b>Claude Desktop, Claude Code and Cursor</b></summary>
|
|
260
|
+
|
|
261
|
+
**Claude Desktop** (`claude_desktop_config.json`):
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
{
|
|
265
|
+
"mcpServers": {
|
|
266
|
+
"shopify-store-sync": {
|
|
267
|
+
"command": "npx",
|
|
268
|
+
"args": ["-y", "shopify-store-sync-mcp"],
|
|
269
|
+
"env": {
|
|
270
|
+
"SHOPIFY_STORES_FILE": "/absolute/path/to/stores.json",
|
|
271
|
+
"DEV_TOKEN": "shpat_...",
|
|
272
|
+
"EU_CLIENT_ID": "...",
|
|
273
|
+
"EU_CLIENT_SECRET": "..."
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Claude Code**
|
|
281
|
+
|
|
282
|
+
```bash
|
|
283
|
+
claude mcp add shopify-store-sync \
|
|
284
|
+
-e SHOPIFY_STORES_FILE=/absolute/path/to/stores.json \
|
|
285
|
+
-e DEV_TOKEN=shpat_... \
|
|
286
|
+
-- npx -y shopify-store-sync-mcp
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Cursor** (`.cursor/mcp.json`) takes the same `command`, `args` and `env` as Claude Desktop.
|
|
290
|
+
|
|
291
|
+
</details>
|
|
292
|
+
|
|
293
|
+
### 4. Ask
|
|
294
|
+
|
|
295
|
+
- *"Compare dev and eu. Only metaobjects and menus."*
|
|
296
|
+
- *"Which product metafield definitions in namespace custom are missing from eu?"* (uses `include: ["PRODUCT.custom.*"]`)
|
|
297
|
+
- *"Plan a sync of the faq and size_guide metaobjects from dev to eu."*
|
|
298
|
+
- *"Apply only ops 1 to 4 of that plan."*
|
|
299
|
+
- *"Copy the linen-shirt product to eu, with its prices."* (uses `resources: ["products"], include: ["linen-shirt"], prices: true`)
|
|
300
|
+
- *"Copy the French translations from dev to eu."* (uses `resources: ["translations"], locales: ["fr"]`)
|
|
301
|
+
- *"Export eu's configuration to ./store-config so I can commit it."*
|
|
302
|
+
- *"Has eu drifted from ./store-config?"*
|
|
303
|
+
- *"What's different in the theme settings between dev and eu?"*
|
|
304
|
+
|
|
305
|
+
## Tools
|
|
306
|
+
|
|
307
|
+
What `diff_stores` reports, field by field (real output):
|
|
308
|
+
|
|
309
|
+
<p align="center">
|
|
310
|
+
<img src="docs/images/field-diff.png" alt="Field-level diff: page body, menu items, definition name, and a type conflict reported but not forced" width="100%">
|
|
311
|
+
</p>
|
|
312
|
+
|
|
313
|
+
<details>
|
|
314
|
+
<summary><b>All 9 tools</b></summary>
|
|
315
|
+
|
|
316
|
+
| Tool | Writes? | What it does |
|
|
317
|
+
|---|---|---|
|
|
318
|
+
| `list_stores` | no | Configured stores and their write permissions. Secrets are never returned. |
|
|
319
|
+
| `list_resource_types` | no | What can be synced and how it's keyed. |
|
|
320
|
+
| `snapshot_store` | no | Read one store's configuration in portable form. |
|
|
321
|
+
| `diff_stores` | no | Only-in-source, changed (field by field), only-in-target. |
|
|
322
|
+
| `plan_sync` | no | Dry-run plan: creates, updates, conflicts, missing references. Returns a `plan_id`. |
|
|
323
|
+
| `get_plan` | no | Show a plan, or list recent ones. |
|
|
324
|
+
| `apply_plan` | **yes** | Execute a plan, with every safety check below. |
|
|
325
|
+
| `export_snapshot` | local files only | Write a store's configuration to a folder for git ([config as code](#config-as-code)). |
|
|
326
|
+
| `diff_themes` | no | Compare theme templates and settings between two stores ([theme settings](#theme-settings-diff)). |
|
|
327
|
+
|
|
328
|
+
Every tool except `apply_plan` is read-only toward Shopify and annotated as such, so clients can auto-approve them.
|
|
329
|
+
|
|
330
|
+
</details>
|
|
331
|
+
|
|
332
|
+
## Command line
|
|
333
|
+
|
|
334
|
+
Everything also works without an AI client, and `--exit-code` turns `diff` into a scheduled drift check.
|
|
335
|
+
|
|
336
|
+
<p align="center">
|
|
337
|
+
<img src="docs/images/cli.png" alt="CLI output of diff, plan and themes commands" width="100%">
|
|
338
|
+
</p>
|
|
339
|
+
|
|
340
|
+
<details>
|
|
341
|
+
<summary><b>Commands</b></summary>
|
|
342
|
+
|
|
343
|
+
Everything also works without an AI client. Run a command instead of starting the server:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
npx shopify-store-sync-mcp stores
|
|
347
|
+
npx shopify-store-sync-mcp diff dev eu --resources pages,menus
|
|
348
|
+
npx shopify-store-sync-mcp plan dev eu
|
|
349
|
+
npx shopify-store-sync-mcp apply <plan-id> --confirm eu.myshopify.com
|
|
350
|
+
npx shopify-store-sync-mcp export eu ./store-config
|
|
351
|
+
npx shopify-store-sync-mcp themes dev eu
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
`--json` gives machine-readable output and `--exit-code` makes `diff` and `themes` exit with 2 when they find differences, handy for a scheduled drift check. Run `npx shopify-store-sync-mcp help` for every option.
|
|
355
|
+
|
|
356
|
+
</details>
|
|
357
|
+
|
|
358
|
+
## Config as code
|
|
359
|
+
|
|
360
|
+
Export a store's setup as small, sorted JSON files, commit it, and use the folder as a source anywhere (`dir:./store-config`) to spot drift, review changes as a pull request, or restore.
|
|
361
|
+
|
|
362
|
+
<p align="center">
|
|
363
|
+
<img src="docs/images/config-as-code.png" alt="Exported store-config folder and one translation file" width="100%">
|
|
364
|
+
</p>
|
|
365
|
+
|
|
366
|
+
<details>
|
|
367
|
+
<summary><b>How to use it</b></summary>
|
|
368
|
+
|
|
369
|
+
Export a store's configuration as one small JSON file per item, with sorted keys and no store-specific IDs, then commit it:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npx shopify-store-sync-mcp export prod ./store-config
|
|
373
|
+
git add store-config && git commit -m "Snapshot prod configuration"
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Any command or tool that takes a source also takes `dir:<path>`:
|
|
377
|
+
|
|
378
|
+
- **Spot drift:** `diff dir:./store-config prod` shows what changed in production since the commit.
|
|
379
|
+
- **Review changes:** edit the files or re-export from staging, and review the change as a pull request.
|
|
380
|
+
- **Restore or promote:** `plan dir:./store-config prod`, then `apply`, with all the usual safety checks.
|
|
381
|
+
|
|
382
|
+
</details>
|
|
383
|
+
|
|
384
|
+
<details>
|
|
385
|
+
<summary><b>Theme settings diff (read-only)</b></summary>
|
|
386
|
+
|
|
387
|
+
`diff_themes` (or `themes` on the command line) compares the published themes of two stores: JSON templates, section groups and `config/settings_data.json`, setting by setting. Pass theme ids to compare other themes, and `files` to narrow it down (for example `templates/product*.json`). It is read-only: themes are never written to.
|
|
388
|
+
|
|
389
|
+
</details>
|
|
390
|
+
|
|
391
|
+
## How it works
|
|
392
|
+
|
|
393
|
+
<p align="center">
|
|
394
|
+
<img src="docs/images/how-it-works.png" alt="Snapshot, diff, plan, apply, with safety checks before every write" width="100%">
|
|
395
|
+
</p>
|
|
396
|
+
|
|
397
|
+
<p align="center">
|
|
398
|
+
<img src="docs/images/remapping.png" alt="gid://shopify/Product/101 in dev becomes shopify://Product/linen-shirt, then gid://shopify/Product/9101 in eu" width="100%">
|
|
399
|
+
</p>
|
|
400
|
+
|
|
401
|
+
<details>
|
|
402
|
+
<summary><b>How ID remapping works</b></summary>
|
|
403
|
+
|
|
404
|
+
Every Shopify ID is store-specific, so the server never compares or copies raw IDs. When it reads a store, each `gid://shopify/...` value is rewritten into a portable reference:
|
|
405
|
+
|
|
406
|
+
```
|
|
407
|
+
gid://shopify/Product/8123456 → shopify://Product/linen-shirt
|
|
408
|
+
gid://shopify/Metaobject/2918 → shopify://Metaobject/faq/returns
|
|
409
|
+
gid://shopify/MetaobjectDefinition/77 → shopify://MetaobjectDefinition/faq
|
|
410
|
+
gid://shopify/CustomerAccountPage/12 → shopify://CustomerAccountPage/orders
|
|
411
|
+
gid://shopify/ShopPolicy/5 → shopify://ShopPolicy/PRIVACY_POLICY
|
|
412
|
+
gid://shopify/Menu/41 → shopify://Menu/main-menu
|
|
413
|
+
gid://shopify/MetafieldDefinition/9 → shopify://MetafieldDefinition/PRODUCT.custom.fabric
|
|
414
|
+
gid://shopify/MediaImage/730 → shopify://File/size-chart.png
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
This covers JSON list values (`list.product_reference`) and definition validations (`metaobject_definition_id`). When it writes, references are resolved by handle in the target. A reference that doesn't exist there is flagged in the plan before anything is written.
|
|
418
|
+
|
|
419
|
+
Things in the same plan can depend on each other, for example a metaobject pointing at another one being created, or a definition that references itself. These are written in two passes: first without the pending references, then complete.
|
|
420
|
+
|
|
421
|
+
Files (images, generic files and videos) are matched by file name and uploaded to the target when missing. Collection rules that filter on a metafield are remapped to the target's definition.
|
|
422
|
+
|
|
423
|
+
**Not remapped yet:** product variants and articles. Their values are left out of the sync and listed as warnings, never copied as broken IDs.
|
|
424
|
+
|
|
425
|
+
Two references followed from the source store to the target, as the sample sync above wrote them:
|
|
426
|
+
|
|
427
|
+
</details>
|
|
428
|
+
|
|
429
|
+
## Tested on real stores
|
|
430
|
+
|
|
431
|
+
<p align="center">
|
|
432
|
+
<img src="docs/images/tested.png" alt="68 automated tests, 52 GraphQL operations validated, 11 item types written live, 2 sign-in methods used live" width="100%">
|
|
433
|
+
</p>
|
|
434
|
+
|
|
435
|
+
<details>
|
|
436
|
+
<summary><b>Every live test and its result</b></summary>
|
|
437
|
+
|
|
438
|
+
| Feature | Live result |
|
|
439
|
+
|---|---|
|
|
440
|
+
| Diff of all default resource types | both stores read in about 10 seconds |
|
|
441
|
+
| Create: metafield and metaobject definitions, metaobjects, smart collection, page, redirect, French translation | applied, second diff empty |
|
|
442
|
+
| File reference on a metaobject | image uploaded to the target by file name and linked |
|
|
443
|
+
| Update: page title and body, collection description | applied, second diff empty |
|
|
444
|
+
| Shop policies | synced; a policy Shopify manages automatically is reported with how to unblock it |
|
|
445
|
+
| Metafield values, including a `product_reference` | applied, reference remapped to the target's own product |
|
|
446
|
+
| Values on products missing from the target | skipped up front with the reason |
|
|
447
|
+
| Export to a folder, then diff that folder against the store | no differences |
|
|
448
|
+
| Theme settings diff | read-only, 15 files compared in about 1 second |
|
|
449
|
+
| Products (Shopify CLI sign-in, Windows) | a real product copied as a draft with its 4 images and alt text; after editing the copy and adding a variant and an image there, the update restored the title and kept both; the next diff was identical |
|
|
450
|
+
| Products: reading only named handles | planning one product went from 97 to 6 seconds |
|
|
451
|
+
| Safety: write to a read-only store | refused |
|
|
452
|
+
| Safety: target edited after the plan was made | step skipped, nothing overwritten |
|
|
453
|
+
| Shopify CLI sign-in on Windows (CLI 4.9.4), both stores | `connect` read-only and `--writes`; plan, apply (7 creates incl. file upload and translation), update and re-diff all exact; also through the MCP server over stdio |
|
|
454
|
+
|
|
455
|
+
Behaviours of the live API found this way are covered by tests: `nodes` returns `null` for customer account pages (they're looked up by listing instead), `ShopPolicy.type` has to be aliased next to `Metaobject.type`, Shopify refuses edits to auto-managed policies, and a failed token request carries its reason (such as `app_not_installed`) in an HTML page.
|
|
456
|
+
|
|
457
|
+
</details>
|
|
458
|
+
|
|
459
|
+
## Roadmap
|
|
460
|
+
|
|
461
|
+
<p align="center">
|
|
462
|
+
<img src="docs/images/roadmap.png" alt="Roadmap: shipped (syncing, safety, ways to use it, quality) and next (collection sources, variants and articles, more metafield owners, menu translations, markets, bulk mode, undo, optional deletes)" width="100%">
|
|
463
|
+
</p>
|
|
464
|
+
|
|
465
|
+
<details>
|
|
466
|
+
<summary><b>The full list</b></summary>
|
|
467
|
+
|
|
468
|
+
**Shipped**
|
|
469
|
+
|
|
470
|
+
*Syncing*
|
|
471
|
+
- [x] Metafield definitions, metaobject definitions and metaobject entries
|
|
472
|
+
- [x] Products: content, options, variants by option values and SKU, images; new ones as drafts, prices only when asked, target-only variants and images kept
|
|
473
|
+
- [x] Collections with smart-collection rules, pages, menus (3 levels, customer account menus too), redirects, store policies
|
|
474
|
+
- [x] Metafield values on products and collections, translations per language (opt-in)
|
|
475
|
+
- [x] Images and files matched by file name, uploaded to the target when missing
|
|
476
|
+
- [x] Cross-store ID remapping by handle, including list references and rules on metafield definitions
|
|
477
|
+
- [x] Two-pass writes for items that point at each other or at themselves
|
|
478
|
+
- [x] Filters: resource types, include and exclude patterns, namespaces, metaobject types, owner types, languages
|
|
479
|
+
|
|
480
|
+
*Safety*
|
|
481
|
+
- [x] Dry-run plans that expire after an hour, applied in full or step by step (`--only`)
|
|
482
|
+
- [x] Drift check before every write, never deletes, read-only by default, typed confirmation for protected stores
|
|
483
|
+
- [x] Conflicts reported instead of forced (type changes, manual versus smart collections, values on items missing from the target)
|
|
484
|
+
- [x] Audit log of every write
|
|
485
|
+
|
|
486
|
+
*Ways to use it*
|
|
487
|
+
- [x] MCP server with 9 tools, read-only tools annotated for auto-approval
|
|
488
|
+
- [x] Command line with `--json` output and `--exit-code` for scheduled drift checks
|
|
489
|
+
- [x] Config as code: export a store to files and use the folder as a source (`dir:`)
|
|
490
|
+
- [x] Read-only theme settings diff
|
|
491
|
+
- [x] Connect without creating an app (Shopify CLI sign-in), or with a Dev Dashboard app or an access token
|
|
492
|
+
- [x] Published on npm, runs with `npx`
|
|
493
|
+
|
|
494
|
+
*Quality*
|
|
495
|
+
- [x] 68 automated tests against an in-memory Shopify, plus MCP protocol tests
|
|
496
|
+
- [x] Every GraphQL operation validated against Shopify's schema for 2026-04, 2026-10 and 2027-01
|
|
497
|
+
- [x] Tested live between two stores on Linux and Windows, Shopify CLI 4.9 and 5
|
|
498
|
+
- [x] Parallel reads with throttle-aware retries; clear errors for missing installs, expired sign-ins and auto-managed policies
|
|
499
|
+
|
|
500
|
+
**Next**
|
|
501
|
+
- [ ] Move collection rules to Shopify's new `sources` model (`ruleSet` is deprecated but supported through 2027-01)
|
|
502
|
+
- [ ] Product prices per market and inventory levels, as explicit opt-ins
|
|
503
|
+
- [ ] Remap product variants and blog articles, so references to them sync too
|
|
504
|
+
- [ ] Metafield values on variants, pages, blogs and the shop
|
|
505
|
+
- [ ] Menu item (link) translations
|
|
506
|
+
- [ ] Markets and market web presences
|
|
507
|
+
- [ ] Bulk operations for very large stores
|
|
508
|
+
- [ ] Undo: restore the previous values of an applied plan from the audit log
|
|
509
|
+
- [ ] Optional deletes, behind an explicit flag
|
|
510
|
+
|
|
511
|
+
</details>
|
|
512
|
+
|
|
513
|
+
Ideas and PRs are welcome. Issues tagged `good first issue` are a good place to start.
|
|
514
|
+
|
|
515
|
+
<details>
|
|
516
|
+
<summary><b>Development</b></summary>
|
|
517
|
+
|
|
518
|
+
```bash
|
|
519
|
+
npm install
|
|
520
|
+
npm test # vitest: unit, sync engine against an in-memory Shopify, and MCP protocol tests
|
|
521
|
+
npm run typecheck
|
|
522
|
+
npm run build
|
|
523
|
+
|
|
524
|
+
# Validate every GraphQL operation against Shopify's real Admin schema
|
|
525
|
+
npm run validate:graphql # defaults to 2026-10
|
|
526
|
+
npm run validate:graphql -- 2027-01
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
Layout:
|
|
530
|
+
|
|
531
|
+
```
|
|
532
|
+
src/
|
|
533
|
+
index.ts entrypoint: MCP server on stdio, or the CLI when given a command
|
|
534
|
+
engine.ts everything the tools and the CLI do, in one place
|
|
535
|
+
server.ts MCP tools
|
|
536
|
+
cli.ts command line
|
|
537
|
+
snapshotFiles.ts export and read snapshot folders (config as code)
|
|
538
|
+
themes.ts read-only theme settings diff
|
|
539
|
+
config.ts store config loading and validation
|
|
540
|
+
shopify/client.ts Admin GraphQL client (token refresh, throttling, retries)
|
|
541
|
+
shopify/cliClient.ts the same requests through Shopify CLI's sign-in (`shopify store execute`)
|
|
542
|
+
refs.ts GID <-> portable reference mapping
|
|
543
|
+
snapshot.ts read a store into portable form
|
|
544
|
+
diff.ts field-level diff
|
|
545
|
+
plan.ts dry-run plans and plan storage
|
|
546
|
+
apply.ts guarded, drift-checked, two-pass writes
|
|
547
|
+
resources/ one adapter per resource type
|
|
548
|
+
test/
|
|
549
|
+
fakeShop.ts in-memory Admin API used by the tests
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
Adding a resource type means writing one adapter (`fetch`, `create`, `update`) in `src/resources/`. See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
553
|
+
|
|
554
|
+
</details>
|
|
555
|
+
|
|
556
|
+
<details>
|
|
557
|
+
<summary><b>Related projects</b></summary>
|
|
558
|
+
|
|
559
|
+
Syncing definitions between stores isn't a new idea, and these tools are worth knowing:
|
|
560
|
+
|
|
561
|
+
| Project | Type | What it covers |
|
|
562
|
+
|---|---|---|
|
|
563
|
+
| [groupthought/metasync](https://github.com/groupthought/metasync) | CLI | Metafield and metaobject definitions plus resource data; dry run by default; has a delete mode |
|
|
564
|
+
| [TeamDijon/meta-sync](https://github.com/TeamDijon/meta-sync) | CLI | Metafield and metaobject definitions and entries; manifest-based copy; bulk mode deletes the target first |
|
|
565
|
+
| [shopify-metadata-migrator](https://github.com/warriorachilles/shopify-metadata-migrator) | CLI | Copies metafield and metaobject definitions |
|
|
566
|
+
| MetaMaintain, Metora, Rewind Staging | Shopify apps (paid or freemium) | Definition and entry sync through an embedded app |
|
|
567
|
+
|
|
568
|
+
What this project adds: it runs inside your AI client as an MCP server (or as a CLI) on your own machine, signed in as you through Shopify CLI or with your own private app, so there's no third-party app from the App Store and no data goes anywhere except Shopify. You can ask for a diff in plain language; it also covers pages, menus, redirects, collections, policies, metafield values and translations, remaps references and files across stores, keeps store configuration in git, checks for drift before writing and never deletes. If you only need a scripted one-shot copy of definitions, one of the CLIs above may be all you need.
|
|
569
|
+
|
|
570
|
+
</details>
|
|
571
|
+
|
|
572
|
+
## Disclaimer
|
|
573
|
+
|
|
574
|
+
Not affiliated with, endorsed by or sponsored by Shopify Inc. "Shopify" is a trademark of Shopify Inc. Test on development stores first; you are responsible for what you apply to production.
|
|
575
|
+
|
|
576
|
+
## License
|
|
577
|
+
|
|
578
|
+
[MIT](LICENSE) © Youssef Idder
|