@anton.andrusenko/shopify-mcp-admin 0.1.0 → 0.2.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/README.md +71 -11
- package/dist/index.js +376 -89
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# shopify-mcp-admin
|
|
1
|
+
# @anton.andrusenko/shopify-mcp-admin
|
|
2
2
|
|
|
3
3
|
> 🛍️ **MCP Server for Shopify Admin API** — Enable AI agents to manage Shopify stores with 40 powerful tools
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@anton.andrusenko/shopify-mcp-admin)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://nodejs.org/)
|
|
8
8
|
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
|
|
33
33
|
```bash
|
|
34
34
|
# Run directly with npx (recommended)
|
|
35
|
-
npx shopify-mcp-admin
|
|
35
|
+
npx @anton.andrusenko/shopify-mcp-admin
|
|
36
36
|
|
|
37
37
|
# Or install globally
|
|
38
|
-
npm install -g shopify-mcp-admin
|
|
38
|
+
npm install -g @anton.andrusenko/shopify-mcp-admin
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
### Configuration
|
|
@@ -57,18 +57,58 @@ The AI will use the `list-products` tool to fetch your catalog.
|
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
60
|
+
## 🔐 Authentication Methods
|
|
61
|
+
|
|
62
|
+
shopify-mcp-admin supports two authentication methods:
|
|
63
|
+
|
|
64
|
+
### Option 1: Legacy Custom App Token (Recommended for existing apps)
|
|
65
|
+
|
|
66
|
+
If you have a Custom App with a static access token:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
SHOPIFY_STORE_URL=mystore.myshopify.com
|
|
70
|
+
SHOPIFY_ACCESS_TOKEN=shpat_xxxxx
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Option 2: Dev Dashboard (OAuth 2.0)
|
|
74
|
+
|
|
75
|
+
If you're using Shopify's new Dev Dashboard with client credentials:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
SHOPIFY_STORE_URL=mystore.myshopify.com
|
|
79
|
+
SHOPIFY_CLIENT_ID=xxxxx
|
|
80
|
+
SHOPIFY_CLIENT_SECRET=xxxxx
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Tokens are automatically refreshed every 24 hours.
|
|
84
|
+
|
|
85
|
+
### Which Should I Use?
|
|
86
|
+
|
|
87
|
+
| Scenario | Method |
|
|
88
|
+
|----------|--------|
|
|
89
|
+
| Existing Custom App with static token | Legacy (Option 1) |
|
|
90
|
+
| New Dev Dashboard app | Client Credentials (Option 2) |
|
|
91
|
+
| Development/Partner store | Either works |
|
|
92
|
+
| Production store (after Jan 2026) | Client Credentials required |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
60
96
|
## ⚙️ Configuration Reference
|
|
61
97
|
|
|
62
98
|
| Variable | Required | Default | Description |
|
|
63
99
|
|----------|----------|---------|-------------|
|
|
64
100
|
| `SHOPIFY_STORE_URL` | ✅ Yes | — | Your Shopify store domain (e.g., `your-store.myshopify.com`) |
|
|
65
|
-
| `SHOPIFY_ACCESS_TOKEN` |
|
|
101
|
+
| `SHOPIFY_ACCESS_TOKEN` | ⚡ See below | — | Admin API access token from your Custom App |
|
|
102
|
+
| `SHOPIFY_CLIENT_ID` | ⚡ See below | — | Client ID from Dev Dashboard app |
|
|
103
|
+
| `SHOPIFY_CLIENT_SECRET` | ⚡ See below | — | Client Secret from Dev Dashboard app |
|
|
66
104
|
| `SHOPIFY_API_VERSION` | No | `2025-10` | Shopify API version |
|
|
67
105
|
| `TRANSPORT` | No | `stdio` | Transport mode: `stdio` or `http` |
|
|
68
106
|
| `PORT` | No | `3000` | HTTP server port (when `TRANSPORT=http`) |
|
|
69
107
|
| `DEBUG` | No | — | Enable debug logging (`1` or `true`) |
|
|
70
108
|
| `LOG_LEVEL` | No | `info` | Log level: `debug`, `info`, `warn`, `error` |
|
|
71
109
|
|
|
110
|
+
**⚡ Authentication:** Provide EITHER `SHOPIFY_ACCESS_TOKEN` (legacy) OR both `SHOPIFY_CLIENT_ID` and `SHOPIFY_CLIENT_SECRET` (Dev Dashboard).
|
|
111
|
+
|
|
72
112
|
### Required Shopify Scopes
|
|
73
113
|
|
|
74
114
|
Configure these scopes when creating your Custom App:
|
|
@@ -95,12 +135,14 @@ Edit your Claude Desktop configuration file:
|
|
|
95
135
|
|
|
96
136
|
### Step 2: Add the MCP Server
|
|
97
137
|
|
|
138
|
+
**Option 1: Legacy Token Authentication**
|
|
139
|
+
|
|
98
140
|
```json
|
|
99
141
|
{
|
|
100
142
|
"mcpServers": {
|
|
101
143
|
"shopify": {
|
|
102
144
|
"command": "npx",
|
|
103
|
-
"args": ["shopify-mcp-admin"],
|
|
145
|
+
"args": ["@anton.andrusenko/shopify-mcp-admin"],
|
|
104
146
|
"env": {
|
|
105
147
|
"SHOPIFY_STORE_URL": "your-store.myshopify.com",
|
|
106
148
|
"SHOPIFY_ACCESS_TOKEN": "shpat_xxxxx"
|
|
@@ -110,6 +152,24 @@ Edit your Claude Desktop configuration file:
|
|
|
110
152
|
}
|
|
111
153
|
```
|
|
112
154
|
|
|
155
|
+
**Option 2: Dev Dashboard (OAuth 2.0)**
|
|
156
|
+
|
|
157
|
+
```json
|
|
158
|
+
{
|
|
159
|
+
"mcpServers": {
|
|
160
|
+
"shopify": {
|
|
161
|
+
"command": "npx",
|
|
162
|
+
"args": ["@anton.andrusenko/shopify-mcp-admin"],
|
|
163
|
+
"env": {
|
|
164
|
+
"SHOPIFY_STORE_URL": "your-store.myshopify.com",
|
|
165
|
+
"SHOPIFY_CLIENT_ID": "xxxxx",
|
|
166
|
+
"SHOPIFY_CLIENT_SECRET": "xxxxx"
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
113
173
|
### Step 3: Restart Claude Desktop
|
|
114
174
|
|
|
115
175
|
Quit and reopen Claude Desktop. You should see "shopify" in the MCP servers list.
|
|
@@ -135,7 +195,7 @@ TRANSPORT=http \
|
|
|
135
195
|
PORT=3000 \
|
|
136
196
|
SHOPIFY_STORE_URL=your-store.myshopify.com \
|
|
137
197
|
SHOPIFY_ACCESS_TOKEN=shpat_xxxxx \
|
|
138
|
-
npx shopify-mcp-admin
|
|
198
|
+
npx @anton.andrusenko/shopify-mcp-admin
|
|
139
199
|
```
|
|
140
200
|
|
|
141
201
|
### Step 2: Test the Connection
|
|
@@ -186,7 +246,7 @@ Each tool can be converted to OpenAI function format:
|
|
|
186
246
|
|
|
187
247
|
## 🛠️ Available Tools
|
|
188
248
|
|
|
189
|
-
shopify-mcp-admin provides **40 tools** organized into 7 categories:
|
|
249
|
+
@anton.andrusenko/shopify-mcp-admin provides **40 tools** organized into 7 categories:
|
|
190
250
|
|
|
191
251
|
<details>
|
|
192
252
|
<summary><strong>📦 Product Management (7 tools)</strong></summary>
|
|
@@ -303,7 +363,7 @@ shopify-mcp-admin provides **40 tools** organized into 7 categories:
|
|
|
303
363
|
| `401 Unauthorized` | Invalid access token | Regenerate token in Shopify Admin |
|
|
304
364
|
| `403 Forbidden` | Missing API scopes | Add required scopes to Custom App |
|
|
305
365
|
| `429 Too Many Requests` | Rate limited | Wait; shopify-mcp-admin auto-retries |
|
|
306
|
-
| `ECONNREFUSED` | Server not running | Start server with `npx shopify-mcp-admin` |
|
|
366
|
+
| `ECONNREFUSED` | Server not running | Start server with `npx @anton.andrusenko/shopify-mcp-admin` |
|
|
307
367
|
| `Invalid store URL` | Wrong URL format | Use `store.myshopify.com` format |
|
|
308
368
|
|
|
309
369
|
### Debug Mode
|
|
@@ -312,10 +372,10 @@ Enable verbose logging to diagnose issues:
|
|
|
312
372
|
|
|
313
373
|
```bash
|
|
314
374
|
# Option 1: DEBUG flag
|
|
315
|
-
DEBUG=1 npx shopify-mcp-admin
|
|
375
|
+
DEBUG=1 npx @anton.andrusenko/shopify-mcp-admin
|
|
316
376
|
|
|
317
377
|
# Option 2: LOG_LEVEL
|
|
318
|
-
LOG_LEVEL=debug npx shopify-mcp-admin
|
|
378
|
+
LOG_LEVEL=debug npx @anton.andrusenko/shopify-mcp-admin
|
|
319
379
|
```
|
|
320
380
|
|
|
321
381
|
### FAQ
|
package/dist/index.js
CHANGED
|
@@ -10,8 +10,11 @@ import { z } from "zod";
|
|
|
10
10
|
var configSchema = z.object({
|
|
11
11
|
// Required - store identity
|
|
12
12
|
SHOPIFY_STORE_URL: z.string().min(1, "SHOPIFY_STORE_URL is required").regex(/\.myshopify\.com$/, "Must be a valid myshopify.com domain"),
|
|
13
|
-
//
|
|
14
|
-
SHOPIFY_ACCESS_TOKEN: z.string().
|
|
13
|
+
// Authentication Option 1: Legacy Custom App (static token)
|
|
14
|
+
SHOPIFY_ACCESS_TOKEN: z.string().optional(),
|
|
15
|
+
// Authentication Option 2: Dev Dashboard (OAuth 2.0 client credentials)
|
|
16
|
+
SHOPIFY_CLIENT_ID: z.string().optional(),
|
|
17
|
+
SHOPIFY_CLIENT_SECRET: z.string().optional(),
|
|
15
18
|
// Optional with defaults
|
|
16
19
|
SHOPIFY_API_VERSION: z.string().default("2025-10"),
|
|
17
20
|
DEBUG: z.string().optional(),
|
|
@@ -20,7 +23,48 @@ var configSchema = z.object({
|
|
|
20
23
|
// Transport selection (AC-2.2.6, AC-2.2.7)
|
|
21
24
|
// Default: stdio for Claude Desktop compatibility
|
|
22
25
|
TRANSPORT: z.enum(["stdio", "http"]).default("stdio")
|
|
23
|
-
})
|
|
26
|
+
}).refine(
|
|
27
|
+
(data) => {
|
|
28
|
+
const hasLegacyAuth = !!data.SHOPIFY_ACCESS_TOKEN;
|
|
29
|
+
const hasClientId = !!data.SHOPIFY_CLIENT_ID;
|
|
30
|
+
const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
|
|
31
|
+
const hasClientCredentials = hasClientId && hasClientSecret;
|
|
32
|
+
return hasLegacyAuth || hasClientCredentials;
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
message: "Authentication required: Provide either SHOPIFY_ACCESS_TOKEN (legacy) OR both SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET (Dev Dashboard)"
|
|
36
|
+
}
|
|
37
|
+
).refine(
|
|
38
|
+
(data) => {
|
|
39
|
+
const hasClientId = !!data.SHOPIFY_CLIENT_ID;
|
|
40
|
+
const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
|
|
41
|
+
if (hasClientId && !hasClientSecret) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
message: "Incomplete client credentials: SHOPIFY_CLIENT_SECRET is required when SHOPIFY_CLIENT_ID is provided"
|
|
48
|
+
}
|
|
49
|
+
).refine(
|
|
50
|
+
(data) => {
|
|
51
|
+
const hasClientId = !!data.SHOPIFY_CLIENT_ID;
|
|
52
|
+
const hasClientSecret = !!data.SHOPIFY_CLIENT_SECRET;
|
|
53
|
+
if (hasClientSecret && !hasClientId) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
message: "Incomplete client credentials: SHOPIFY_CLIENT_ID is required when SHOPIFY_CLIENT_SECRET is provided"
|
|
60
|
+
}
|
|
61
|
+
);
|
|
62
|
+
function getAuthMode(config) {
|
|
63
|
+
if (config.SHOPIFY_ACCESS_TOKEN) {
|
|
64
|
+
return "token";
|
|
65
|
+
}
|
|
66
|
+
return "client_credentials";
|
|
67
|
+
}
|
|
24
68
|
function isDebugEnabled(debugValue) {
|
|
25
69
|
if (!debugValue) return false;
|
|
26
70
|
const normalized = debugValue.toLowerCase().trim();
|
|
@@ -54,8 +98,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
54
98
|
// src/utils/logger.ts
|
|
55
99
|
var SANITIZATION_PATTERNS = [
|
|
56
100
|
{ pattern: /shpat_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
|
|
57
|
-
{ pattern: /
|
|
58
|
-
{ pattern: /
|
|
101
|
+
{ pattern: /shpua_[a-zA-Z0-9]+/g, replacement: "[REDACTED]" },
|
|
102
|
+
{ pattern: /Bearer\s+[a-zA-Z0-9_-]+/g, replacement: "Bearer [REDACTED]" },
|
|
103
|
+
{ pattern: /access_token[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "access_token=[REDACTED]" },
|
|
104
|
+
{ pattern: /client_secret[=:]\s*[a-zA-Z0-9_-]+/gi, replacement: "client_secret=[REDACTED]" }
|
|
59
105
|
];
|
|
60
106
|
function sanitizeLogMessage(message) {
|
|
61
107
|
let result = message;
|
|
@@ -313,6 +359,283 @@ async function withRateLimit(operation, context, config = DEFAULT_RATE_LIMIT_CON
|
|
|
313
359
|
// src/shopify/client.ts
|
|
314
360
|
import "@shopify/shopify-api/adapters/node";
|
|
315
361
|
import { shopifyApi } from "@shopify/shopify-api";
|
|
362
|
+
|
|
363
|
+
// src/utils/errors.ts
|
|
364
|
+
var sanitizeErrorMessage = sanitizeLogMessage;
|
|
365
|
+
var ToolError = class _ToolError extends Error {
|
|
366
|
+
/** AI-friendly suggestion for error recovery */
|
|
367
|
+
suggestion;
|
|
368
|
+
/**
|
|
369
|
+
* Create a new ToolError
|
|
370
|
+
*
|
|
371
|
+
* @param message - The error message describing what went wrong
|
|
372
|
+
* @param suggestion - A helpful suggestion for how to resolve the error
|
|
373
|
+
*/
|
|
374
|
+
constructor(message, suggestion) {
|
|
375
|
+
super(message);
|
|
376
|
+
this.name = "ToolError";
|
|
377
|
+
this.suggestion = suggestion;
|
|
378
|
+
if (Error.captureStackTrace) {
|
|
379
|
+
Error.captureStackTrace(this, _ToolError);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
function extractErrorMessage(error) {
|
|
384
|
+
if (error instanceof Error) {
|
|
385
|
+
return error.message;
|
|
386
|
+
}
|
|
387
|
+
if (typeof error === "string") {
|
|
388
|
+
return error;
|
|
389
|
+
}
|
|
390
|
+
return "Unknown error";
|
|
391
|
+
}
|
|
392
|
+
function createToolError(error, suggestion) {
|
|
393
|
+
const message = extractErrorMessage(error);
|
|
394
|
+
const safeMessage = sanitizeErrorMessage(message);
|
|
395
|
+
const safeSuggestion = sanitizeErrorMessage(suggestion);
|
|
396
|
+
return {
|
|
397
|
+
isError: true,
|
|
398
|
+
content: [
|
|
399
|
+
{
|
|
400
|
+
type: "text",
|
|
401
|
+
text: `Error: ${safeMessage}
|
|
402
|
+
|
|
403
|
+
Suggestion: ${safeSuggestion}`
|
|
404
|
+
}
|
|
405
|
+
]
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function safeStringify2(data) {
|
|
409
|
+
try {
|
|
410
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
411
|
+
return JSON.stringify(
|
|
412
|
+
data,
|
|
413
|
+
(_key, value) => {
|
|
414
|
+
if (typeof value === "object" && value !== null) {
|
|
415
|
+
if (seen.has(value)) {
|
|
416
|
+
return "[Circular]";
|
|
417
|
+
}
|
|
418
|
+
seen.add(value);
|
|
419
|
+
}
|
|
420
|
+
return value;
|
|
421
|
+
},
|
|
422
|
+
2
|
|
423
|
+
);
|
|
424
|
+
} catch {
|
|
425
|
+
return "[Unable to stringify]";
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
function createToolSuccess(data) {
|
|
429
|
+
const text = safeStringify2(data);
|
|
430
|
+
return {
|
|
431
|
+
content: [
|
|
432
|
+
{
|
|
433
|
+
type: "text",
|
|
434
|
+
text
|
|
435
|
+
}
|
|
436
|
+
],
|
|
437
|
+
structuredContent: data
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// src/shopify/token-manager.ts
|
|
442
|
+
var TokenManager = class _TokenManager {
|
|
443
|
+
config;
|
|
444
|
+
cachedToken = null;
|
|
445
|
+
refreshPromise = null;
|
|
446
|
+
/**
|
|
447
|
+
* Refresh buffer: 5 minutes before expiry
|
|
448
|
+
*
|
|
449
|
+
* Tokens are refreshed when within this window of expiration
|
|
450
|
+
* to ensure requests don't fail due to token expiry mid-request.
|
|
451
|
+
*/
|
|
452
|
+
static REFRESH_BUFFER_MS = 5 * 60 * 1e3;
|
|
453
|
+
/**
|
|
454
|
+
* Create a new TokenManager instance
|
|
455
|
+
*
|
|
456
|
+
* @param config - Configuration containing store URL and OAuth credentials
|
|
457
|
+
*/
|
|
458
|
+
constructor(config) {
|
|
459
|
+
this.config = config;
|
|
460
|
+
log.debug("TokenManager initialized", { storeUrl: config.storeUrl });
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Get a valid access token
|
|
464
|
+
*
|
|
465
|
+
* Returns a cached token if valid and not expiring soon.
|
|
466
|
+
* Automatically fetches a new token if:
|
|
467
|
+
* - No token is cached
|
|
468
|
+
* - Cached token is within 5 minutes of expiration
|
|
469
|
+
*
|
|
470
|
+
* Concurrent calls are deduplicated - only one OAuth request
|
|
471
|
+
* is made even if multiple callers request a token simultaneously.
|
|
472
|
+
*
|
|
473
|
+
* @returns Promise resolving to a valid access token string
|
|
474
|
+
* @throws ToolError if token acquisition fails
|
|
475
|
+
*/
|
|
476
|
+
async getAccessToken() {
|
|
477
|
+
if (this.refreshPromise) {
|
|
478
|
+
log.debug("Token refresh in progress, waiting...");
|
|
479
|
+
return this.refreshPromise;
|
|
480
|
+
}
|
|
481
|
+
if (this.cachedToken && !this.needsRefresh()) {
|
|
482
|
+
log.debug("Using cached token");
|
|
483
|
+
return this.cachedToken.accessToken;
|
|
484
|
+
}
|
|
485
|
+
log.debug("Fetching new OAuth token");
|
|
486
|
+
this.refreshPromise = this.fetchNewToken().then((token) => {
|
|
487
|
+
this.cachedToken = token;
|
|
488
|
+
log.debug("Token cached successfully", {
|
|
489
|
+
expiresIn: Math.round((token.expiresAt - Date.now()) / 1e3)
|
|
490
|
+
});
|
|
491
|
+
return token.accessToken;
|
|
492
|
+
}).finally(() => {
|
|
493
|
+
this.refreshPromise = null;
|
|
494
|
+
});
|
|
495
|
+
return this.refreshPromise;
|
|
496
|
+
}
|
|
497
|
+
/**
|
|
498
|
+
* Fetch a new token from Shopify OAuth endpoint
|
|
499
|
+
*
|
|
500
|
+
* Implements OAuth 2.0 client credentials grant flow:
|
|
501
|
+
* - POST to /admin/oauth/access_token
|
|
502
|
+
* - Content-Type: application/x-www-form-urlencoded
|
|
503
|
+
* - Body: grant_type, client_id, client_secret
|
|
504
|
+
*
|
|
505
|
+
* @returns Promise resolving to cached token with expiry
|
|
506
|
+
* @throws ToolError on OAuth failure (401, 400, network error)
|
|
507
|
+
* @private
|
|
508
|
+
*/
|
|
509
|
+
async fetchNewToken() {
|
|
510
|
+
const tokenEndpoint = this.buildTokenEndpoint();
|
|
511
|
+
try {
|
|
512
|
+
const response = await fetch(tokenEndpoint, {
|
|
513
|
+
method: "POST",
|
|
514
|
+
headers: {
|
|
515
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
516
|
+
},
|
|
517
|
+
body: new URLSearchParams({
|
|
518
|
+
grant_type: "client_credentials",
|
|
519
|
+
client_id: this.config.clientId,
|
|
520
|
+
client_secret: this.config.clientSecret
|
|
521
|
+
})
|
|
522
|
+
});
|
|
523
|
+
if (!response.ok) {
|
|
524
|
+
const errorText = await response.text();
|
|
525
|
+
throw this.handleOAuthError(response.status, errorText);
|
|
526
|
+
}
|
|
527
|
+
const data = await response.json();
|
|
528
|
+
if (!data.access_token) {
|
|
529
|
+
throw new ToolError(
|
|
530
|
+
"Invalid OAuth response: missing access_token",
|
|
531
|
+
"This may be a temporary Shopify API issue. Try again in a few minutes."
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
const expiresAt = Date.now() + data.expires_in * 1e3;
|
|
535
|
+
return {
|
|
536
|
+
accessToken: data.access_token,
|
|
537
|
+
expiresAt
|
|
538
|
+
};
|
|
539
|
+
} catch (error) {
|
|
540
|
+
if (error instanceof ToolError) {
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
544
|
+
throw new ToolError(
|
|
545
|
+
`OAuth token request failed: ${message}`,
|
|
546
|
+
"Check your network connection and ensure SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET are correct."
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Build the OAuth token endpoint URL
|
|
552
|
+
*
|
|
553
|
+
* @returns Full URL to the Shopify OAuth token endpoint
|
|
554
|
+
* @private
|
|
555
|
+
*/
|
|
556
|
+
buildTokenEndpoint() {
|
|
557
|
+
const storeUrl = this.config.storeUrl.includes("://") ? this.config.storeUrl : `https://${this.config.storeUrl}`;
|
|
558
|
+
const baseUrl = storeUrl.replace(/\/$/, "");
|
|
559
|
+
return `${baseUrl}/admin/oauth/access_token`;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Check if the cached token needs to be refreshed
|
|
563
|
+
*
|
|
564
|
+
* Returns true if:
|
|
565
|
+
* - No token is cached
|
|
566
|
+
* - Token expires within REFRESH_BUFFER_MS (5 minutes)
|
|
567
|
+
*
|
|
568
|
+
* @returns true if token should be refreshed
|
|
569
|
+
* @private
|
|
570
|
+
*/
|
|
571
|
+
needsRefresh() {
|
|
572
|
+
if (!this.cachedToken) {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
const refreshThreshold = Date.now() + _TokenManager.REFRESH_BUFFER_MS;
|
|
576
|
+
return refreshThreshold >= this.cachedToken.expiresAt;
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Handle OAuth error responses
|
|
580
|
+
*
|
|
581
|
+
* Creates appropriate ToolError with actionable suggestions
|
|
582
|
+
* based on HTTP status code.
|
|
583
|
+
*
|
|
584
|
+
* @param status - HTTP status code
|
|
585
|
+
* @param responseText - Error response body
|
|
586
|
+
* @returns ToolError with appropriate message and suggestion
|
|
587
|
+
* @private
|
|
588
|
+
*/
|
|
589
|
+
handleOAuthError(status, responseText) {
|
|
590
|
+
switch (status) {
|
|
591
|
+
case 401:
|
|
592
|
+
return new ToolError(
|
|
593
|
+
"OAuth authentication failed (HTTP 401)",
|
|
594
|
+
"Verify that SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET are correct. Ensure the app has the required scopes configured in the Dev Dashboard."
|
|
595
|
+
);
|
|
596
|
+
case 400:
|
|
597
|
+
return new ToolError(
|
|
598
|
+
"OAuth bad request (HTTP 400)",
|
|
599
|
+
"The OAuth request parameters are invalid. Ensure SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET are set correctly and the app is properly configured in the Dev Dashboard."
|
|
600
|
+
);
|
|
601
|
+
case 403:
|
|
602
|
+
return new ToolError(
|
|
603
|
+
"OAuth forbidden (HTTP 403)",
|
|
604
|
+
"The app may not have permission to access this store. Ensure the app is installed on the store and has the required access scopes."
|
|
605
|
+
);
|
|
606
|
+
case 404:
|
|
607
|
+
return new ToolError(
|
|
608
|
+
"OAuth endpoint not found (HTTP 404)",
|
|
609
|
+
`Verify SHOPIFY_STORE_URL is correct and includes the .myshopify.com domain. Current value appears incorrect. Received: ${responseText.slice(0, 100)}`
|
|
610
|
+
);
|
|
611
|
+
case 429:
|
|
612
|
+
return new ToolError(
|
|
613
|
+
"OAuth rate limited (HTTP 429)",
|
|
614
|
+
"Too many token requests. Wait a few minutes before trying again."
|
|
615
|
+
);
|
|
616
|
+
default:
|
|
617
|
+
return new ToolError(
|
|
618
|
+
`OAuth request failed (HTTP ${status})`,
|
|
619
|
+
`Unexpected error from Shopify OAuth endpoint. Response: ${responseText.slice(0, 200)}`
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Clear the cached token
|
|
625
|
+
*
|
|
626
|
+
* Use this for:
|
|
627
|
+
* - Testing: Reset state between tests
|
|
628
|
+
* - Error recovery: Force new token after auth failures
|
|
629
|
+
*
|
|
630
|
+
* The next call to getAccessToken() will fetch a new token.
|
|
631
|
+
*/
|
|
632
|
+
clearCache() {
|
|
633
|
+
this.cachedToken = null;
|
|
634
|
+
log.debug("Token cache cleared");
|
|
635
|
+
}
|
|
636
|
+
};
|
|
637
|
+
|
|
638
|
+
// src/shopify/client.ts
|
|
316
639
|
var DEFAULT_API_VERSION = "2025-10";
|
|
317
640
|
var SHOP_QUERY = `
|
|
318
641
|
query {
|
|
@@ -322,6 +645,7 @@ var SHOP_QUERY = `
|
|
|
322
645
|
}
|
|
323
646
|
`;
|
|
324
647
|
var _defaultClient = null;
|
|
648
|
+
var _tokenManager = null;
|
|
325
649
|
async function verifyConnectivity(client) {
|
|
326
650
|
const response = await client.graphql.request(SHOP_QUERY);
|
|
327
651
|
if (response.errors && response.errors.length > 0) {
|
|
@@ -377,18 +701,59 @@ async function createShopifyClient(credentials, options = {}) {
|
|
|
377
701
|
}
|
|
378
702
|
return client;
|
|
379
703
|
}
|
|
704
|
+
async function createClientWithTokenRefresh(credentials, tokenManager) {
|
|
705
|
+
const baseClient = buildClient(credentials);
|
|
706
|
+
let currentToken = credentials.accessToken;
|
|
707
|
+
return {
|
|
708
|
+
...baseClient,
|
|
709
|
+
graphql: {
|
|
710
|
+
request: async (query, options) => {
|
|
711
|
+
const token = await tokenManager.getAccessToken();
|
|
712
|
+
if (token !== currentToken) {
|
|
713
|
+
log.debug("Token refreshed, updating client credentials");
|
|
714
|
+
currentToken = token;
|
|
715
|
+
credentials.accessToken = token;
|
|
716
|
+
const refreshedClient = buildClient(credentials);
|
|
717
|
+
return refreshedClient.graphql.request(query, options);
|
|
718
|
+
}
|
|
719
|
+
return baseClient.graphql.request(query, options);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
};
|
|
723
|
+
}
|
|
380
724
|
async function getShopifyClient() {
|
|
381
725
|
if (_defaultClient !== null) {
|
|
382
726
|
return _defaultClient;
|
|
383
727
|
}
|
|
384
728
|
const config = getConfig();
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
accessToken: config.SHOPIFY_ACCESS_TOKEN,
|
|
388
|
-
apiVersion: config.SHOPIFY_API_VERSION
|
|
389
|
-
};
|
|
729
|
+
const authMode = getAuthMode(config);
|
|
730
|
+
log.info(`Initializing Shopify client with auth mode: ${authMode}`);
|
|
390
731
|
try {
|
|
391
|
-
|
|
732
|
+
if (authMode === "token") {
|
|
733
|
+
const credentials2 = {
|
|
734
|
+
storeUrl: config.SHOPIFY_STORE_URL,
|
|
735
|
+
accessToken: config.SHOPIFY_ACCESS_TOKEN,
|
|
736
|
+
apiVersion: config.SHOPIFY_API_VERSION
|
|
737
|
+
};
|
|
738
|
+
const client2 = await createShopifyClient(credentials2);
|
|
739
|
+
_defaultClient = client2;
|
|
740
|
+
return _defaultClient;
|
|
741
|
+
}
|
|
742
|
+
log.debug("Creating TokenManager for client_credentials auth");
|
|
743
|
+
_tokenManager = new TokenManager({
|
|
744
|
+
storeUrl: config.SHOPIFY_STORE_URL,
|
|
745
|
+
clientId: config.SHOPIFY_CLIENT_ID,
|
|
746
|
+
clientSecret: config.SHOPIFY_CLIENT_SECRET
|
|
747
|
+
});
|
|
748
|
+
const initialToken = await _tokenManager.getAccessToken();
|
|
749
|
+
log.debug("Initial OAuth token acquired successfully");
|
|
750
|
+
const credentials = {
|
|
751
|
+
storeUrl: config.SHOPIFY_STORE_URL,
|
|
752
|
+
accessToken: initialToken,
|
|
753
|
+
apiVersion: config.SHOPIFY_API_VERSION
|
|
754
|
+
};
|
|
755
|
+
const client = await createClientWithTokenRefresh(credentials, _tokenManager);
|
|
756
|
+
await verifyConnectivity(client);
|
|
392
757
|
_defaultClient = client;
|
|
393
758
|
return _defaultClient;
|
|
394
759
|
} catch (error) {
|
|
@@ -1967,84 +2332,6 @@ function createSingleTenantContext(client, shopDomain) {
|
|
|
1967
2332
|
};
|
|
1968
2333
|
}
|
|
1969
2334
|
|
|
1970
|
-
// src/utils/errors.ts
|
|
1971
|
-
var sanitizeErrorMessage = sanitizeLogMessage;
|
|
1972
|
-
var ToolError = class _ToolError extends Error {
|
|
1973
|
-
/** AI-friendly suggestion for error recovery */
|
|
1974
|
-
suggestion;
|
|
1975
|
-
/**
|
|
1976
|
-
* Create a new ToolError
|
|
1977
|
-
*
|
|
1978
|
-
* @param message - The error message describing what went wrong
|
|
1979
|
-
* @param suggestion - A helpful suggestion for how to resolve the error
|
|
1980
|
-
*/
|
|
1981
|
-
constructor(message, suggestion) {
|
|
1982
|
-
super(message);
|
|
1983
|
-
this.name = "ToolError";
|
|
1984
|
-
this.suggestion = suggestion;
|
|
1985
|
-
if (Error.captureStackTrace) {
|
|
1986
|
-
Error.captureStackTrace(this, _ToolError);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
};
|
|
1990
|
-
function extractErrorMessage(error) {
|
|
1991
|
-
if (error instanceof Error) {
|
|
1992
|
-
return error.message;
|
|
1993
|
-
}
|
|
1994
|
-
if (typeof error === "string") {
|
|
1995
|
-
return error;
|
|
1996
|
-
}
|
|
1997
|
-
return "Unknown error";
|
|
1998
|
-
}
|
|
1999
|
-
function createToolError(error, suggestion) {
|
|
2000
|
-
const message = extractErrorMessage(error);
|
|
2001
|
-
const safeMessage = sanitizeErrorMessage(message);
|
|
2002
|
-
const safeSuggestion = sanitizeErrorMessage(suggestion);
|
|
2003
|
-
return {
|
|
2004
|
-
isError: true,
|
|
2005
|
-
content: [
|
|
2006
|
-
{
|
|
2007
|
-
type: "text",
|
|
2008
|
-
text: `Error: ${safeMessage}
|
|
2009
|
-
|
|
2010
|
-
Suggestion: ${safeSuggestion}`
|
|
2011
|
-
}
|
|
2012
|
-
]
|
|
2013
|
-
};
|
|
2014
|
-
}
|
|
2015
|
-
function safeStringify2(data) {
|
|
2016
|
-
try {
|
|
2017
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
2018
|
-
return JSON.stringify(
|
|
2019
|
-
data,
|
|
2020
|
-
(_key, value) => {
|
|
2021
|
-
if (typeof value === "object" && value !== null) {
|
|
2022
|
-
if (seen.has(value)) {
|
|
2023
|
-
return "[Circular]";
|
|
2024
|
-
}
|
|
2025
|
-
seen.add(value);
|
|
2026
|
-
}
|
|
2027
|
-
return value;
|
|
2028
|
-
},
|
|
2029
|
-
2
|
|
2030
|
-
);
|
|
2031
|
-
} catch {
|
|
2032
|
-
return "[Unable to stringify]";
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
function createToolSuccess(data) {
|
|
2036
|
-
const text = safeStringify2(data);
|
|
2037
|
-
return {
|
|
2038
|
-
content: [
|
|
2039
|
-
{
|
|
2040
|
-
type: "text",
|
|
2041
|
-
text
|
|
2042
|
-
}
|
|
2043
|
-
],
|
|
2044
|
-
structuredContent: data
|
|
2045
|
-
};
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
2335
|
// src/tools/registration.ts
|
|
2049
2336
|
var KEBAB_CASE_REGEX = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
2050
2337
|
var registeredTools = /* @__PURE__ */ new Map();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anton.andrusenko/shopify-mcp-admin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "MCP server for Shopify Admin API - enables AI agents to manage Shopify stores with 40+ tools for products, inventory, collections, content, and SEO",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|