@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.
Files changed (3) hide show
  1. package/README.md +71 -11
  2. package/dist/index.js +376 -89
  3. 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
- [![npm version](https://badge.fury.io/js/shopify-mcp-admin.svg)](https://badge.fury.io/js/shopify-mcp-admin)
5
+ [![npm version](https://badge.fury.io/js/@anton.andrusenko%2Fshopify-mcp-admin.svg)](https://www.npmjs.com/package/@anton.andrusenko/shopify-mcp-admin)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D20.0.0-brightgreen)](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` | Yes | — | Admin API access token from your Custom App |
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
- // Required - authentication
14
- SHOPIFY_ACCESS_TOKEN: z.string().min(1, "SHOPIFY_ACCESS_TOKEN is required"),
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: /Bearer\s+[a-zA-Z0-9]+/g, replacement: "Bearer [REDACTED]" },
58
- { pattern: /access_token[=:]\s*[a-zA-Z0-9]+/gi, replacement: "access_token=[REDACTED]" }
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 credentials = {
386
- storeUrl: config.SHOPIFY_STORE_URL,
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
- const client = await createShopifyClient(credentials);
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.1.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",