@acodera/shopify-admin-mcp 0.0.0-stage → 1.0.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +22 -0
  3. package/README.md +389 -2
  4. package/dist/auth/provider.d.ts +13 -0
  5. package/dist/auth/provider.js +64 -0
  6. package/dist/auth/provider.js.map +1 -0
  7. package/dist/graphql/client.d.ts +25 -0
  8. package/dist/graphql/client.js +73 -0
  9. package/dist/graphql/client.js.map +1 -0
  10. package/dist/graphql/introspection.d.ts +38 -0
  11. package/dist/graphql/introspection.js +88 -0
  12. package/dist/graphql/introspection.js.map +1 -0
  13. package/dist/graphql/schema-index.d.ts +21 -0
  14. package/dist/graphql/schema-index.js +148 -0
  15. package/dist/graphql/schema-index.js.map +1 -0
  16. package/dist/index.d.ts +2 -0
  17. package/dist/index.js +42 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/server.d.ts +6 -0
  20. package/dist/server.js +47 -0
  21. package/dist/server.js.map +1 -0
  22. package/dist/tools/collections.d.ts +3 -0
  23. package/dist/tools/collections.js +113 -0
  24. package/dist/tools/collections.js.map +1 -0
  25. package/dist/tools/customers.d.ts +3 -0
  26. package/dist/tools/customers.js +83 -0
  27. package/dist/tools/customers.js.map +1 -0
  28. package/dist/tools/files.d.ts +4 -0
  29. package/dist/tools/files.js +230 -0
  30. package/dist/tools/files.js.map +1 -0
  31. package/dist/tools/graphql-proxy.d.ts +3 -0
  32. package/dist/tools/graphql-proxy.js +50 -0
  33. package/dist/tools/graphql-proxy.js.map +1 -0
  34. package/dist/tools/inventory.d.ts +3 -0
  35. package/dist/tools/inventory.js +77 -0
  36. package/dist/tools/inventory.js.map +1 -0
  37. package/dist/tools/markets.d.ts +3 -0
  38. package/dist/tools/markets.js +125 -0
  39. package/dist/tools/markets.js.map +1 -0
  40. package/dist/tools/metafields.d.ts +3 -0
  41. package/dist/tools/metafields.js +124 -0
  42. package/dist/tools/metafields.js.map +1 -0
  43. package/dist/tools/metaobjects.d.ts +3 -0
  44. package/dist/tools/metaobjects.js +168 -0
  45. package/dist/tools/metaobjects.js.map +1 -0
  46. package/dist/tools/orders.d.ts +3 -0
  47. package/dist/tools/orders.js +66 -0
  48. package/dist/tools/orders.js.map +1 -0
  49. package/dist/tools/products.d.ts +3 -0
  50. package/dist/tools/products.js +186 -0
  51. package/dist/tools/products.js.map +1 -0
  52. package/dist/tools/publishing.d.ts +3 -0
  53. package/dist/tools/publishing.js +60 -0
  54. package/dist/tools/publishing.js.map +1 -0
  55. package/dist/tools/schema-search.d.ts +3 -0
  56. package/dist/tools/schema-search.js +85 -0
  57. package/dist/tools/schema-search.js.map +1 -0
  58. package/dist/tools/shared.d.ts +18 -0
  59. package/dist/tools/shared.js +54 -0
  60. package/dist/tools/shared.js.map +1 -0
  61. package/dist/tools/themes.d.ts +4 -0
  62. package/dist/tools/themes.js +176 -0
  63. package/dist/tools/themes.js.map +1 -0
  64. package/dist/utils/cli.d.ts +22 -0
  65. package/dist/utils/cli.js +95 -0
  66. package/dist/utils/cli.js.map +1 -0
  67. package/dist/utils/sleep.d.ts +1 -0
  68. package/dist/utils/sleep.js +2 -0
  69. package/dist/utils/sleep.js.map +1 -0
  70. package/package.json +62 -4
package/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @acodera/shopify-admin-mcp
2
+
3
+ ## 1.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - First release as `@acodera/shopify-admin-mcp`, forked from [shopify-graphql-admin-mcp](https://github.com/colbymchenry/shopify-graphql-admin-mcp) by Colby McHenry.
8
+
9
+ - Targets Shopify Admin API 2026-10 and migrates every tool off removed and deprecated fields (`metafieldsDelete`, `productCreate`/`productUpdate` with `product`, collection `sources`, `defaultEmailAddress`, `featuredMedia`). Inventory adjustments send `changeFromQuantity` and an idempotency key.
10
+ - Adds tools for themes, file uploads, markets, publishing, product variants, and metafield and metaobject definitions, grouped into selectable toolsets (`--toolsets`).
11
+ - Security: credentials are only sent to `*.myshopify.com`, a read-only mode blocks mutations, local uploads are confined to `--upload-dir`, live-theme writes need `--allow-live-theme-writes`, and every tool carries read-only / destructive annotations.
12
+ - Retries throttled requests, adds request timeouts, and reports `userErrors` as tool errors.
13
+ - Requires Node.js 22.12 or later. Bin renamed to `shopify-admin-mcp`.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Colby McHenry
4
+ Copyright (c) 2026 acoderacom
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,390 @@
1
- # Temporary Holding Version
1
+ # shopify-admin-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![npm](https://img.shields.io/npm/v/@acodera/shopify-admin-mcp)](https://www.npmjs.com/package/@acodera/shopify-admin-mcp)
4
+ [![CI](https://github.com/acoderacom/shopify-admin-mcp/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/acoderacom/shopify-admin-mcp/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
6
+
7
+ MCP server providing full access to Shopify's Admin GraphQL API. Targets API version **2026-10** by default and introspects the live schema on startup, so your AI assistant always sees the exact API your store is serving.
8
+
9
+ ## Features
10
+
11
+ - **Raw GraphQL execution** — run any query or mutation against the Admin API
12
+ - **Live schema introspection** — search and explore the full GraphQL schema directly from your AI assistant
13
+ - **50 convenience tools** — typed, no-GraphQL-needed tools for products and variants, collections, publishing, metafields and metaobjects (including definitions), customers, orders, inventory, file uploads, themes, and markets
14
+ - **Toolsets** — register only the areas you need to keep the assistant's tool list short
15
+ - **Read-only mode** — one flag hides every write tool and blocks mutations in raw GraphQL
16
+ - **Tool annotations** — every tool is marked read-only, write, or destructive so MCP clients can ask before risky calls
17
+ - **Dual auth** — OAuth client credentials (Dev Dashboard apps) and legacy access tokens (`shpat_`)
18
+ - **Auto token refresh** — client-credentials tokens are refreshed automatically before they expire
19
+ - **Rate-limit aware** — throttled requests are retried after the cost bucket refills
20
+
21
+ ## Quick Start
22
+
23
+ Credentials are read from environment variables, which keeps them out of process listings and shell history:
24
+
25
+ ```bash
26
+ # With a legacy access token
27
+ SHOPIFY_STORE=mystore.myshopify.com \
28
+ SHOPIFY_ACCESS_TOKEN=shpat_xxxxx \
29
+ npx -y @acodera/shopify-admin-mcp
30
+
31
+ # With OAuth client credentials (Dev Dashboard app)
32
+ SHOPIFY_STORE=mystore.myshopify.com \
33
+ SHOPIFY_CLIENT_ID=your_client_id \
34
+ SHOPIFY_CLIENT_SECRET=your_client_secret \
35
+ npx -y @acodera/shopify-admin-mcp
36
+ ```
37
+
38
+ Add `--read-only` (or `SHOPIFY_READ_ONLY=true`) when the assistant only needs to look at store data.
39
+
40
+ ## Tools
41
+
42
+ ### Core
43
+
44
+ | Tool | Description |
45
+ |------|-------------|
46
+ | `shopify_graphql` | Execute any raw GraphQL query or mutation (queries only in read-only mode) |
47
+ | `shopify_schema_search` | Search the live schema by keyword (types, queries, mutations) |
48
+ | `shopify_schema_details` | Get full details for a specific type, query, or mutation |
49
+
50
+ Convenience tools are grouped into toolsets (shown in brackets), which you can select with `--toolsets`.
51
+
52
+ ### Products `[products]`
53
+
54
+ | Tool | Description |
55
+ |------|-------------|
56
+ | `shopify_products_list` | List/search products with pagination |
57
+ | `shopify_product_get` | Get product by ID with variants and metafields |
58
+ | `shopify_product_create` | Create a product, optionally with options such as Size or Color (created unpublished) |
59
+ | `shopify_product_update` | Update an existing product |
60
+ | `shopify_product_delete` | Delete a product |
61
+ | `shopify_product_variants_create` | Add variants by option values, with price, compare-at price, and SKU |
62
+ | `shopify_product_variants_update` | Update variant prices, compare-at prices, SKUs, or inventory policy |
63
+
64
+ ### Collections `[collections]`
65
+
66
+ | Tool | Description |
67
+ |------|-------------|
68
+ | `shopify_collections_list` | List/search collections with pagination |
69
+ | `shopify_collection_get` | Get collection by ID with its sources and products |
70
+ | `shopify_collection_create` | Create a collection, optionally with product sources |
71
+ | `shopify_collection_update` | Update title or description, and add or remove product sources |
72
+ | `shopify_collection_delete` | Delete a collection |
73
+
74
+ ### Publishing `[publishing]`
75
+
76
+ | Tool | Description |
77
+ |------|-------------|
78
+ | `shopify_publications_list` | List sales channels and catalogs that can be published to |
79
+ | `shopify_publish` | Publish a product or collection, optionally on a schedule |
80
+ | `shopify_unpublish` | Unpublish a product or collection |
81
+
82
+ ### Metafields `[metafields]`
83
+
84
+ | Tool | Description |
85
+ |------|-------------|
86
+ | `shopify_metafields_list` | List metafields on any resource with pagination |
87
+ | `shopify_metafields_set` | Set (upsert) up to 25 metafields on any resources |
88
+ | `shopify_metafield_delete` | Delete a metafield by owner ID, namespace, and key |
89
+ | `shopify_metafield_definitions_list` | List metafield definitions for a resource type |
90
+ | `shopify_metafield_definition_create` | Create a typed, validated metafield definition |
91
+
92
+ ### Metaobjects `[metaobjects]`
93
+
94
+ | Tool | Description |
95
+ |------|-------------|
96
+ | `shopify_metaobject_definitions_list` | List metaobject type definitions with pagination |
97
+ | `shopify_metaobject_definition_create` | Create a metaobject definition with typed fields |
98
+ | `shopify_metaobjects_list` | List entries of a specific metaobject type |
99
+ | `shopify_metaobject_get` | Get a single metaobject entry |
100
+ | `shopify_metaobject_create` | Create a new metaobject entry |
101
+ | `shopify_metaobject_upsert` | Create or update an entry by type and handle |
102
+ | `shopify_metaobject_update` | Update an existing metaobject entry |
103
+ | `shopify_metaobject_delete` | Delete a metaobject entry |
104
+
105
+ ### Customers `[customers]`
106
+
107
+ | Tool | Description |
108
+ |------|-------------|
109
+ | `shopify_customers_list` | List/search customers with pagination |
110
+ | `shopify_customer_get` | Get customer by ID with addresses and orders |
111
+ | `shopify_customer_update` | Update customer details |
112
+
113
+ ### Orders `[orders]`
114
+
115
+ | Tool | Description |
116
+ |------|-------------|
117
+ | `shopify_orders_list` | List/search orders with pagination |
118
+ | `shopify_order_get` | Get order by ID with line items and fulfillments |
119
+
120
+ ### Inventory `[inventory]`
121
+
122
+ | Tool | Description |
123
+ |------|-------------|
124
+ | `shopify_inventory_get_levels` | Get inventory levels across locations |
125
+ | `shopify_inventory_adjust` | Adjust available quantity at a location, with a compare-and-swap check |
126
+
127
+ ### Files `[files]`
128
+
129
+ | Tool | Description |
130
+ |------|-------------|
131
+ | `shopify_files_list` | List/search the Files library |
132
+ | `shopify_file_upload` | Upload from a URL or a local file, wait until processed, optionally attach to a product |
133
+ | `shopify_file_delete` | Delete files |
134
+
135
+ ### Themes `[themes]`
136
+
137
+ | Tool | Description |
138
+ |------|-------------|
139
+ | `shopify_themes_list` | List themes and their roles |
140
+ | `shopify_theme_files_list` | List a theme's files, filtered by patterns such as `sections/*` |
141
+ | `shopify_theme_files_get` | Read theme file contents |
142
+ | `shopify_theme_files_upsert` | Create or overwrite up to 50 theme files |
143
+ | `shopify_theme_files_delete` | Delete theme files |
144
+ | `shopify_theme_duplicate` | Copy a theme as a new unpublished theme |
145
+ | `shopify_theme_delete` | Delete an unpublished theme |
146
+
147
+ ### Markets `[markets]`
148
+
149
+ | Tool | Description |
150
+ |------|-------------|
151
+ | `shopify_markets_list` | List markets with regions, currency settings, and web presences |
152
+ | `shopify_market_get` | Get a market with its catalogs and price lists |
153
+ | `shopify_market_create` | Create a market for a set of countries |
154
+ | `shopify_market_update` | Rename a market, change its status, add or remove countries, or change currency settings |
155
+ | `shopify_market_delete` | Delete a market |
156
+
157
+ ## API Version 2026-10
158
+
159
+ The server defaults to `2026-10`. The convenience tools follow the current API, which changes a few tool inputs compared to older releases:
160
+
161
+ | Tool | What changed |
162
+ |------|--------------|
163
+ | `shopify_collection_create` | Product membership is defined with `sources` (typed conditions and manual selections) instead of the deprecated `ruleSet`. Use `shopify_schema_details` on `CollectionCreateSourceTargetInput` to see the shape. |
164
+ | `shopify_collection_update` | Adds and removes products by creating or deleting sources (`sourcesToCreate`, `sourcesToDelete`); `collectionAddProducts` no longer applies. |
165
+ | `shopify_collections_list` / `shopify_collection_get` | Return `sources` instead of `ruleSet`. |
166
+ | `shopify_metafield_delete` | Takes `ownerId`, `namespace`, and `key` (the old `metafieldDelete` mutation by ID no longer exists). |
167
+ | `shopify_inventory_adjust` | Requires `changeFromQuantity`: the quantity you expect before the change, or `null` to skip the check. An idempotency key is added automatically. |
168
+ | Customer and order tools | Return `defaultEmailAddress` / `defaultPhoneNumber` instead of the deprecated `email` / `phone` fields. |
169
+
170
+ If you pin an older version with `--api-version`, raw GraphQL still works against that version, but some convenience tools may not. When Shopify serves a different version than the one requested (for example because the requested version has been retired), the server logs a warning to stderr.
171
+
172
+ ## Read-Only Mode
173
+
174
+ Start with `--read-only` or `SHOPIFY_READ_ONLY=true` to:
175
+
176
+ - register only the read-only tools (list/get tools, schema search, inventory levels, theme file reads)
177
+ - reject `mutation` and `subscription` operations in `shopify_graphql`; the document is parsed, so comments and multiple operations can't slip one through
178
+
179
+ Use it whenever the assistant doesn't need to change the store. Product descriptions, customer notes, and order notes are written by merchants and customers, and an assistant reading them can be prompted to take actions you didn't ask for.
180
+
181
+ ## Toolsets
182
+
183
+ All toolsets are registered by default. To keep the assistant's tool list short, pass the ones you need:
184
+
185
+ ```bash
186
+ --toolsets products,collections,publishing,files
187
+ ```
188
+
189
+ Available toolsets: `products`, `collections`, `publishing`, `metafields`, `metaobjects`, `customers`, `orders`, `inventory`, `files`, `themes`, `markets`. Raw GraphQL and schema search are always available.
190
+
191
+ ## File Uploads
192
+
193
+ `shopify_file_upload` accepts either a public `url`, which Shopify fetches itself, or a local `path`, which is sent through a staged upload. The tool waits for Shopify to finish processing and returns the file's CDN URL. Pass `productId` to attach an image, video, or 3D model to a product as media.
194
+
195
+ Local uploads are disabled until you choose a directory with `--upload-dir` (or `SHOPIFY_UPLOAD_DIR`). Only files inside that directory can be uploaded, with symlinks resolved, so a prompt can't make the server publish other files from your machine to the store's public CDN.
196
+
197
+ ## Themes
198
+
199
+ Theme file tools read any theme, but by default they refuse to write to or delete from the live (`MAIN`) theme. The safe workflow is:
200
+
201
+ 1. `shopify_theme_duplicate` the live theme
202
+ 2. edit the copy with `shopify_theme_files_upsert`
203
+ 3. preview and publish it from the Shopify admin
204
+
205
+ Start the server with `--allow-live-theme-writes` (or `SHOPIFY_ALLOW_LIVE_THEME_WRITES=true`) to edit the live theme directly. Writing theme files needs `write_themes` and Shopify's theme-code exemption on the app.
206
+
207
+ ## Required API Scopes
208
+
209
+ Configure these scopes on your app to enable all tools:
210
+
211
+ | Scope | Tools |
212
+ |-------|-------|
213
+ | `read_products`, `write_products` | Products, variants, collections |
214
+ | `read_publications`, `write_publications` | Publishing |
215
+ | `read_metaobjects`, `write_metaobjects` | Metaobject entries |
216
+ | `read_metaobject_definitions`, `write_metaobject_definitions` | Metaobject definitions |
217
+ | Scope of the owning resource | Metafields (e.g. `read_products` for product metafields, `read_customers` for customer metafields) |
218
+ | `read_customers`, `write_customers` | Customers |
219
+ | `read_orders` | Orders (last 60 days; add `read_all_orders` for older orders) |
220
+ | `read_inventory`, `write_inventory`, `read_locations` | Inventory |
221
+ | `read_files`, `write_files` | Files |
222
+ | `read_themes`, `write_themes` (+ theme-code exemption for writes) | Themes |
223
+ | `read_markets`, `write_markets` | Markets |
224
+
225
+ You only need scopes for the tools you plan to use. Customer names, emails, phone numbers, and addresses are [protected customer data](https://shopify.dev/docs/apps/launch/protected-customer-data), so your app must also be granted access to those fields.
226
+
227
+ ## Authentication
228
+
229
+ ### OAuth Client Credentials (recommended)
230
+
231
+ For apps created in the [Shopify Dev Dashboard](https://dev.shopify.com/dashboard):
232
+
233
+ 1. Create an app in the Dev Dashboard
234
+ 2. Configure the Admin API scopes listed above
235
+ 3. Release a version and install the app on your store
236
+ 4. Set the Client ID and Client secret:
237
+
238
+ ```bash
239
+ SHOPIFY_STORE=mystore.myshopify.com \
240
+ SHOPIFY_CLIENT_ID=your_client_id \
241
+ SHOPIFY_CLIENT_SECRET=your_client_secret \
242
+ npx -y @acodera/shopify-admin-mcp
243
+ ```
244
+
245
+ The client credentials grant only works when the app and the store belong to the same Shopify organization. Tokens last 24 hours and are refreshed automatically.
246
+
247
+ ### Legacy Access Token
248
+
249
+ For existing custom apps with a `shpat_` token:
250
+
251
+ ```bash
252
+ SHOPIFY_STORE=mystore.myshopify.com \
253
+ SHOPIFY_ACCESS_TOKEN=shpat_xxxxx \
254
+ npx -y @acodera/shopify-admin-mcp
255
+ ```
256
+
257
+ ## Configuration
258
+
259
+ | Flag | Env Variable | Description |
260
+ |------|-------------|-------------|
261
+ | `--store` | `SHOPIFY_STORE` | Store domain (required). Accepts `mystore` or `mystore.myshopify.com` |
262
+ | `--access-token` | `SHOPIFY_ACCESS_TOKEN` | Legacy access token (`shpat_...`) |
263
+ | `--client-id` | `SHOPIFY_CLIENT_ID` | OAuth client ID |
264
+ | `--client-secret` | `SHOPIFY_CLIENT_SECRET` | OAuth client secret |
265
+ | `--api-version` | `SHOPIFY_API_VERSION` | API version (default: `2026-10`) |
266
+ | `--read-only` | `SHOPIFY_READ_ONLY` | Expose only read tools and block mutations (`true` / `1`) |
267
+ | `--toolsets` | `SHOPIFY_TOOLSETS` | Comma-separated toolsets to register (default: all) |
268
+ | `--upload-dir` | `SHOPIFY_UPLOAD_DIR` | Directory that local file uploads are confined to (default: local uploads disabled) |
269
+ | `--allow-live-theme-writes` | `SHOPIFY_ALLOW_LIVE_THEME_WRITES` | Allow theme file writes and deletes on the live theme (`true` / `1`) |
270
+
271
+ The store must be a `*.myshopify.com` domain, so credentials are only ever sent to Shopify. Secrets can still be passed as flags, but the server prints a warning because flags are visible to other processes on the machine.
272
+
273
+ ## Usage with Claude Code
274
+
275
+ ```bash
276
+ claude mcp add shopify \
277
+ -e SHOPIFY_STORE=mystore.myshopify.com \
278
+ -e SHOPIFY_ACCESS_TOKEN=shpat_xxxxx \
279
+ -- npx -y @acodera/shopify-admin-mcp
280
+ ```
281
+
282
+ Or add it to your project's `.mcp.json`:
283
+
284
+ ```json
285
+ {
286
+ "mcpServers": {
287
+ "shopify": {
288
+ "command": "npx",
289
+ "args": ["-y", "@acodera/shopify-admin-mcp"],
290
+ "env": {
291
+ "SHOPIFY_STORE": "mystore.myshopify.com",
292
+ "SHOPIFY_ACCESS_TOKEN": "shpat_xxxxx"
293
+ }
294
+ }
295
+ }
296
+ }
297
+ ```
298
+
299
+ Don't commit real tokens. Claude Code expands `${VAR}` in `.mcp.json`, so you can reference variables from your shell instead.
300
+
301
+ ## Usage with Claude Desktop
302
+
303
+ Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json`):
304
+
305
+ ```json
306
+ {
307
+ "mcpServers": {
308
+ "shopify": {
309
+ "command": "npx",
310
+ "args": ["-y", "@acodera/shopify-admin-mcp"],
311
+ "env": {
312
+ "SHOPIFY_STORE": "mystore.myshopify.com",
313
+ "SHOPIFY_ACCESS_TOKEN": "shpat_xxxxx"
314
+ }
315
+ }
316
+ }
317
+ }
318
+ ```
319
+
320
+ If you install the package globally (`npm install -g @acodera/shopify-admin-mcp`), you can use `"command": "shopify-admin-mcp"` with no `args`.
321
+
322
+ ## Schema Exploration
323
+
324
+ The server introspects Shopify's live GraphQL schema on startup, so your AI assistant can discover API capabilities in real time.
325
+
326
+ ```
327
+ You: "What mutations are available for metaobjects?"
328
+ → AI uses shopify_schema_search with query "metaobject" filter "mutations"
329
+ → Returns: metaobjectCreate, metaobjectUpdate, metaobjectDelete, metaobjectUpsert, ...
330
+
331
+ You: "What fields does MetaobjectCreateInput take?"
332
+ → AI uses shopify_schema_details with name "MetaobjectCreateInput"
333
+ → Returns: full type definition with all fields, types, and descriptions
334
+ ```
335
+
336
+ Deprecated fields are left out of the index, so the assistant is steered toward the current API.
337
+
338
+ ## Development
339
+
340
+ ```bash
341
+ git clone https://github.com/acoderacom/shopify-admin-mcp.git
342
+ cd shopify-admin-mcp
343
+ git checkout dev
344
+ npm install
345
+ npm run build
346
+ npm run dev # watch mode
347
+ ```
348
+
349
+ Requires Node.js 22.12 or later.
350
+
351
+ ## Testing
352
+
353
+ ```bash
354
+ npm test # unit tests; live tests are skipped without credentials
355
+ npm run lint # type-check src and tests
356
+ ```
357
+
358
+ The unit tests need no network. They cover CLI validation, the HTTP client (throttle retry, token refresh), every tool's request over a real MCP connection, read-only mode, toolsets, the live-theme guard, and upload-directory confinement.
359
+
360
+ Live integration tests run against a real store when credentials are set:
361
+
362
+ ```bash
363
+ SHOPIFY_STORE=your-dev-store.myshopify.com \
364
+ SHOPIFY_ACCESS_TOKEN=shpat_xxxxx \
365
+ npm test
366
+ ```
367
+
368
+ Add `SHOPIFY_TEST_WRITES=1` to also run the write tests. They create `[MCP test]` products, variants, collections, files, metafield and metaobject definitions, customers, a duplicate of the live theme, and a draft market, run every write tool against them, and delete them afterwards. The live theme itself is only read. Only run write tests against a development store.
369
+
370
+ ## Contributing and Releases
371
+
372
+ Work happens on the `dev` branch; `main` holds what's been released.
373
+
374
+ 1. Branch from `dev`, make your change, and add a changeset describing it:
375
+
376
+ ```bash
377
+ npx changeset
378
+ ```
379
+
380
+ Pick `patch` for fixes, `minor` for new tools or options, and `major` for breaking changes to tool inputs or behaviour. Changes that don't affect the published package (tests, docs, CI) don't need one.
381
+
382
+ 2. Open a pull request into `dev`. CI type-checks, tests, and builds on Node.js 22 and 24, and a bot comments on whether the pull request has a changeset.
383
+ 3. To release, open a pull request from `dev` into `main` and merge it. The release workflow turns the pending changesets into a **Version Packages** pull request that bumps the version and updates [CHANGELOG.md](CHANGELOG.md).
384
+ 4. Merge the Version Packages pull request. The workflow publishes to npm through [trusted publishing](https://docs.npmjs.com/trusted-publishers) (no npm token is stored in GitHub), with provenance, and creates a GitHub release.
385
+
386
+ ## License
387
+
388
+ MIT. See [LICENSE](LICENSE).
389
+
390
+ Originally created by [Colby McHenry](https://github.com/colbymchenry) as [shopify-graphql-admin-mcp](https://github.com/colbymchenry/shopify-graphql-admin-mcp), and maintained by [acoderacom](https://github.com/acoderacom).
@@ -0,0 +1,13 @@
1
+ import type { Config } from "../utils/cli.js";
2
+ export declare class AuthProvider {
3
+ private config;
4
+ private tokenState;
5
+ private pendingToken;
6
+ constructor(config: Config);
7
+ /** Whether a rejected token can be replaced by requesting a new one. */
8
+ get canRefresh(): boolean;
9
+ getAccessToken(): Promise<string>;
10
+ forceRefresh(): Promise<string>;
11
+ private fetchToken;
12
+ private requestToken;
13
+ }
@@ -0,0 +1,64 @@
1
+ const REFRESH_MARGIN_MS = 300_000;
2
+ const TOKEN_REQUEST_TIMEOUT_MS = 30_000;
3
+ export class AuthProvider {
4
+ config;
5
+ tokenState = null;
6
+ pendingToken = null;
7
+ constructor(config) {
8
+ this.config = config;
9
+ }
10
+ /** Whether a rejected token can be replaced by requesting a new one. */
11
+ get canRefresh() {
12
+ return this.config.auth.mode === "client-credentials";
13
+ }
14
+ async getAccessToken() {
15
+ if (this.config.auth.mode === "access-token") {
16
+ return this.config.auth.accessToken;
17
+ }
18
+ if (this.tokenState && Date.now() < this.tokenState.expiresAt - REFRESH_MARGIN_MS) {
19
+ return this.tokenState.token;
20
+ }
21
+ return this.fetchToken();
22
+ }
23
+ async forceRefresh() {
24
+ if (this.config.auth.mode === "access-token") {
25
+ return this.config.auth.accessToken;
26
+ }
27
+ this.tokenState = null;
28
+ return this.fetchToken();
29
+ }
30
+ // Concurrent callers share a single in-flight token request
31
+ fetchToken() {
32
+ this.pendingToken ??= this.requestToken().finally(() => {
33
+ this.pendingToken = null;
34
+ });
35
+ return this.pendingToken;
36
+ }
37
+ async requestToken() {
38
+ if (this.config.auth.mode !== "client-credentials") {
39
+ throw new Error("Cannot fetch token in access-token mode");
40
+ }
41
+ const { clientId, clientSecret } = this.config.auth;
42
+ const res = await fetch(`https://${this.config.store}/admin/oauth/access_token`, {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
45
+ body: new URLSearchParams({
46
+ grant_type: "client_credentials",
47
+ client_id: clientId,
48
+ client_secret: clientSecret,
49
+ }),
50
+ signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS),
51
+ });
52
+ if (!res.ok) {
53
+ const text = await res.text();
54
+ throw new Error(`OAuth token exchange failed (${res.status}): ${text.slice(0, 200)}`);
55
+ }
56
+ const data = (await res.json());
57
+ this.tokenState = {
58
+ token: data.access_token,
59
+ expiresAt: Date.now() + data.expires_in * 1000,
60
+ };
61
+ return this.tokenState.token;
62
+ }
63
+ }
64
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../../src/auth/provider.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import type { AuthProvider } from "../auth/provider.js";
2
+ import type { Config } from "../utils/cli.js";
3
+ export interface GraphQLResponse {
4
+ data?: Record<string, unknown>;
5
+ errors?: Array<{
6
+ message: string;
7
+ locations?: Array<{
8
+ line: number;
9
+ column: number;
10
+ }>;
11
+ path?: string[];
12
+ extensions?: Record<string, unknown>;
13
+ }>;
14
+ extensions?: Record<string, unknown>;
15
+ }
16
+ export declare class GraphQLClient {
17
+ private auth;
18
+ private endpoint;
19
+ private apiVersion;
20
+ private versionMismatchReported;
21
+ constructor(auth: AuthProvider, config: Config);
22
+ execute(query: string, variables?: Record<string, unknown>): Promise<GraphQLResponse>;
23
+ private post;
24
+ private checkServedVersion;
25
+ }
@@ -0,0 +1,73 @@
1
+ import { sleep } from "../utils/sleep.js";
2
+ const REQUEST_TIMEOUT_MS = 60_000;
3
+ const MAX_THROTTLE_RETRIES = 3;
4
+ function isThrottled(res) {
5
+ return res.errors?.some((e) => e.extensions?.code === "THROTTLED") ?? false;
6
+ }
7
+ // Wait until the leaky bucket has restored enough points for the request
8
+ function throttleDelayMs(res, attempt) {
9
+ const cost = res.extensions?.cost;
10
+ const status = cost?.throttleStatus;
11
+ if (cost?.requestedQueryCost && status?.restoreRate) {
12
+ const deficit = cost.requestedQueryCost - status.currentlyAvailable;
13
+ return Math.max(1000, Math.ceil((deficit / status.restoreRate) * 1000));
14
+ }
15
+ return 1000 * (attempt + 1);
16
+ }
17
+ export class GraphQLClient {
18
+ auth;
19
+ endpoint;
20
+ apiVersion;
21
+ versionMismatchReported = false;
22
+ constructor(auth, config) {
23
+ this.auth = auth;
24
+ this.apiVersion = config.apiVersion;
25
+ this.endpoint = `https://${config.store}/admin/api/${config.apiVersion}/graphql.json`;
26
+ }
27
+ async execute(query, variables) {
28
+ const body = JSON.stringify(variables ? { query, variables } : { query });
29
+ // Throttled requests are rejected before execution, so retrying them is safe even for mutations
30
+ for (let attempt = 0;; attempt++) {
31
+ let res = await this.post(body, await this.auth.getAccessToken());
32
+ if (res.status === 401 && this.auth.canRefresh) {
33
+ res = await this.post(body, await this.auth.forceRefresh());
34
+ }
35
+ this.checkServedVersion(res);
36
+ if (res.status === 429 && attempt < MAX_THROTTLE_RETRIES) {
37
+ await sleep(1000 * (attempt + 1));
38
+ continue;
39
+ }
40
+ if (!res.ok) {
41
+ const text = await res.text();
42
+ throw new Error(`Shopify API request failed (${res.status}): ${text.slice(0, 500)}`);
43
+ }
44
+ const json = (await res.json());
45
+ if (isThrottled(json) && attempt < MAX_THROTTLE_RETRIES) {
46
+ await sleep(throttleDelayMs(json, attempt));
47
+ continue;
48
+ }
49
+ return json;
50
+ }
51
+ }
52
+ post(body, token) {
53
+ return fetch(this.endpoint, {
54
+ method: "POST",
55
+ headers: {
56
+ "Content-Type": "application/json",
57
+ Accept: "application/json",
58
+ "X-Shopify-Access-Token": token,
59
+ },
60
+ body,
61
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
62
+ });
63
+ }
64
+ // Shopify silently serves the oldest supported version when the requested one is retired
65
+ checkServedVersion(res) {
66
+ const served = res.headers.get("x-shopify-api-version");
67
+ if (served && served !== this.apiVersion && !this.versionMismatchReported) {
68
+ this.versionMismatchReported = true;
69
+ console.error(`Warning: requested API version ${this.apiVersion} but Shopify served ${served}. The requested version is unsupported or not yet released.`);
70
+ }
71
+ }
72
+ }
73
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/graphql/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ import type { GraphQLClient } from "./client.js";
2
+ export interface IntrospectedTypeRef {
3
+ kind: string;
4
+ name: string | null;
5
+ ofType?: IntrospectedTypeRef | null;
6
+ }
7
+ export interface IntrospectedField {
8
+ name: string;
9
+ description: string | null;
10
+ args?: IntrospectedArg[];
11
+ type: IntrospectedTypeRef;
12
+ }
13
+ export interface IntrospectedArg {
14
+ name: string;
15
+ description: string | null;
16
+ type: IntrospectedTypeRef;
17
+ defaultValue: string | null;
18
+ }
19
+ export interface IntrospectedEnumValue {
20
+ name: string;
21
+ description: string | null;
22
+ }
23
+ export interface IntrospectedType {
24
+ kind: string;
25
+ name: string;
26
+ description: string | null;
27
+ fields: IntrospectedField[] | null;
28
+ inputFields: IntrospectedArg[] | null;
29
+ interfaces: IntrospectedTypeRef[] | null;
30
+ enumValues: IntrospectedEnumValue[] | null;
31
+ possibleTypes: IntrospectedTypeRef[] | null;
32
+ }
33
+ export interface IntrospectedSchema {
34
+ queryTypeName: string;
35
+ mutationTypeName: string;
36
+ types: IntrospectedType[];
37
+ }
38
+ export declare function runIntrospection(client: GraphQLClient): Promise<IntrospectedSchema>;