@anton.andrusenko/shopify-mcp-admin 2.0.0 → 2.0.1

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 CHANGED
@@ -129,6 +129,13 @@ Tokens are automatically refreshed every 24 hours.
129
129
  | `LOG_LEVEL` | No | `info` | Log level: `debug`, `info`, `warn`, `error` |
130
130
  | `SHOPIFY_MCP_LAZY_LOADING` | No | `false` | Enable modular lazy loading (set to `true` for on-demand module loading) |
131
131
  | `SHOPIFY_MCP_ROLE` | No | — | Role preset for automatic module loading (see [Role Presets](#-role-presets)) |
132
+ | `ALLOWED_HOSTS` | ✅ (remote mode) | — | Comma-separated list of allowed hostnames (e.g., `shopify-mcp.com,api.shopify-mcp.com`) |
133
+ | `ALLOWED_ORIGINS` | ✅ (remote mode) | — | Comma-separated list of allowed CORS origins (e.g., `https://shopify-mcp.com`) |
134
+ | `METRICS_ENDPOINT_ENABLED` | No | `false` | Enable Prometheus metrics endpoint at `/metrics` |
135
+ | `ENABLE_HSTS` | No | `false` | Enable HTTP Strict Transport Security headers |
136
+ | `SHUTDOWN_DRAIN_SECONDS` | No | `30` | Graceful shutdown drain timeout in seconds (1-300) |
137
+ | `LOG_FORMAT` | No | `json` | Log format: `json` (production) or `pretty` (development) |
138
+ | `SENTRY_DSN` | No | — | Sentry error tracking DSN (recommended for production) |
132
139
 
133
140
  ### Server Modes
134
141
 
@@ -987,9 +994,9 @@ shopify-mcp-admin uses a modular architecture to optimize AI agent performance.
987
994
 
988
995
  ## 🔄 Upgrading from Previous Versions
989
996
 
990
- ### What's New in v0.8.0+: Modular Tool Loading
997
+ ### What's New in v2.0.0+: Modular Tool Loading
991
998
 
992
- Starting with v0.8.0, shopify-mcp-admin supports **modular lazy loading** to optimize AI agent performance. Research shows that presenting more than 30 tools to an AI agent can degrade performance by up to 85%.
999
+ Starting with v2.0.0, shopify-mcp-admin supports **modular lazy loading** to optimize AI agent performance. Research shows that presenting more than 30 tools to an AI agent can degrade performance by up to 85%.
993
1000
 
994
1001
  **Default Behavior (All Tools Loaded):**
995
1002
  - All 79 domain tools are loaded at startup for maximum compatibility
@@ -1227,56 +1234,6 @@ Enable these settings for the `main` branch in **Settings → Branches**:
1227
1234
 
1228
1235
  ---
1229
1236
 
1230
- ## 🚀 Deployment
1231
-
1232
- shopify-mcp-admin supports multiple deployment options for different environments:
1233
-
1234
- ### Railway (Recommended for Quick Start)
1235
-
1236
- Deploy to [Railway](https://railway.app) with the included `railway.json` configuration:
1237
-
1238
- 1. Connect your GitHub repository to Railway
1239
- 2. Add environment variables (DATABASE_URL, ENCRYPTION_KEY, etc.)
1240
- 3. Deploy — Railway auto-detects the Dockerfile
1241
-
1242
- ### Kubernetes
1243
-
1244
- For self-hosted or cloud Kubernetes clusters, see the [Kubernetes Deployment Guide](docs/kubernetes.md).
1245
-
1246
- Quick start:
1247
- ```bash
1248
- # Build and push image
1249
- docker build -t yourregistry/shopify-mcp-admin:v1.0.0 .
1250
- docker push yourregistry/shopify-mcp-admin:v1.0.0
1251
-
1252
- # Configure and deploy
1253
- cd k8s
1254
- kustomize edit set image shopify-mcp-admin=yourregistry/shopify-mcp-admin:v1.0.0
1255
- kubectl apply -k .
1256
- ```
1257
-
1258
- ### Docker (Local/Development)
1259
-
1260
- ```bash
1261
- # Build image
1262
- docker build -t shopify-mcp-admin .
1263
-
1264
- # Run container
1265
- docker run -p 3000:3000 \
1266
- -e DATABASE_URL="postgresql://..." \
1267
- -e ENCRYPTION_KEY="..." \
1268
- shopify-mcp-admin
1269
- ```
1270
-
1271
- ### Image Size
1272
-
1273
- The production Docker image is optimized for minimal size:
1274
- - Multi-stage build with Alpine Linux
1275
- - Production dependencies only (`npm prune --production`)
1276
- - Target size: <150MB
1277
-
1278
- ---
1279
-
1280
1237
  ## 🎨 Tenant Dashboard (Remote Mode)
1281
1238
 
1282
1239
  When running in remote mode (`SERVER_MODE=remote`), shopify-mcp-admin includes a web-based dashboard for tenant management. The dashboard provides a modern, responsive UI for:
@@ -1383,6 +1340,8 @@ This project is licensed under the **MIT License** — see the [LICENSE](LICENSE
1383
1340
 
1384
1341
  ## 🚀 Deployment
1385
1342
 
1343
+ shopify-mcp-admin supports multiple deployment options for different environments.
1344
+
1386
1345
  ### Production Deployment
1387
1346
 
1388
1347
  For production deployment on Railway with Cloudflare, Sentry, and uptime monitoring, see the comprehensive deployment guide:
@@ -1396,6 +1355,79 @@ The guide includes:
1396
1355
  - **Staging Environment**: Separate staging deployment configuration
1397
1356
  - **Monitoring**: Sentry error tracking and uptime monitoring setup
1398
1357
 
1358
+ ### Production URLs
1359
+
1360
+ The production service is available at:
1361
+
1362
+ **Root Domain:**
1363
+ - **Dashboard:** `https://shopify-mcp.com/` → redirects to `/app`
1364
+ - **Dashboard UI:** `https://shopify-mcp.com/app`
1365
+ - **Health Check:** `https://shopify-mcp.com/health`
1366
+ - **Metrics:** `https://shopify-mcp.com/metrics`
1367
+ - **MCP Endpoint:** `https://shopify-mcp.com/mcp`
1368
+
1369
+ **API Subdomain:**
1370
+ - **Dashboard:** `https://api.shopify-mcp.com/app`
1371
+ - **Health Check:** `https://api.shopify-mcp.com/health`
1372
+ - **Metrics:** `https://api.shopify-mcp.com/metrics`
1373
+ - **MCP Endpoint:** `https://api.shopify-mcp.com/mcp`
1374
+
1375
+ **Available Endpoints:**
1376
+
1377
+ | Endpoint | Description | Authentication |
1378
+ |----------|-------------|----------------|
1379
+ | `/` | Root path - redirects to `/app` (dashboard) | None |
1380
+ | `/app` | Dashboard UI (React SPA) | Session cookie |
1381
+ | `/health` | Health check endpoint | None |
1382
+ | `/metrics` | Prometheus metrics | None |
1383
+ | `/mcp` | MCP protocol endpoint | API key or session |
1384
+ | `/api/tenants` | Tenant management API | Session cookie |
1385
+ | `/api/shops` | Shop connection API | Session cookie |
1386
+ | `/api/keys` | API key management | Session cookie |
1387
+
1388
+ **Note:** The root domain (`shopify-mcp.com`) redirects to the dashboard (`/app`) for a better user experience. All API endpoints are also available on the root domain.
1389
+
1390
+ ### Deployment Options
1391
+
1392
+ #### Railway (Recommended for Quick Start)
1393
+
1394
+ Deploy to [Railway](https://railway.app) with the included `railway.json` configuration:
1395
+
1396
+ 1. Connect your GitHub repository to Railway
1397
+ 2. Add environment variables (DATABASE_URL, ENCRYPTION_KEY, etc.)
1398
+ 3. Deploy — Railway auto-detects the Dockerfile
1399
+
1400
+ #### Kubernetes
1401
+
1402
+ For self-hosted or cloud Kubernetes clusters, see the [Kubernetes Deployment Guide](docs/kubernetes.md).
1403
+
1404
+ Quick start:
1405
+ ```bash
1406
+ # Build and push image
1407
+ docker build -t yourregistry/shopify-mcp-admin:v2.0.0 .
1408
+ docker push yourregistry/shopify-mcp-admin:v2.0.0
1409
+
1410
+ # Configure and deploy
1411
+ cd k8s
1412
+ kustomize edit set image shopify-mcp-admin=yourregistry/shopify-mcp-admin:v2.0.0
1413
+ kubectl apply -k .
1414
+ ```
1415
+
1416
+ #### Docker (Local/Development)
1417
+
1418
+ ```bash
1419
+ # Build image
1420
+ docker build -t shopify-mcp-admin .
1421
+
1422
+ # Run container
1423
+ docker run -p 3000:3000 \
1424
+ -e DATABASE_URL="postgresql://..." \
1425
+ -e ENCRYPTION_KEY="..." \
1426
+ shopify-mcp-admin
1427
+ ```
1428
+
1429
+ **Image Size:** The production Docker image is optimized for minimal size (<150MB) with multi-stage Alpine Linux build.
1430
+
1399
1431
  ### Environment Variables
1400
1432
 
1401
1433
  For production environment variables, see:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anton.andrusenko/shopify-mcp-admin",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "MCP server for Shopify Admin API - enables AI agents to manage Shopify stores with 79 tools for products, inventory, collections, content, SEO, metafields, markets & translations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,124 +0,0 @@
1
- import {
2
- log
3
- } from "./chunk-GKGHMPEC.js";
4
-
5
- // src/middleware/mcp-auth.ts
6
- var MCP_AUTH_ERROR_CODES = {
7
- AUTH_REQUIRED: -32001,
8
- // Missing authentication
9
- AUTH_INVALID: -32002,
10
- // Invalid API key format or key
11
- AUTH_REVOKED: -32003,
12
- // API key has been revoked
13
- AUTH_FORBIDDEN: -32004
14
- // Tenant suspended or no access
15
- };
16
- function createJsonRpcError(code, message, hint) {
17
- const error = {
18
- jsonrpc: "2.0",
19
- error: {
20
- code,
21
- message
22
- },
23
- id: null
24
- };
25
- if (hint) {
26
- error.error.data = { hint };
27
- }
28
- return error;
29
- }
30
- function parseBearerToken(authHeader) {
31
- if (!authHeader) {
32
- return null;
33
- }
34
- const parts = authHeader.split(" ");
35
- if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") {
36
- return null;
37
- }
38
- const token = parts[1];
39
- if (!token || token.trim() === "") {
40
- return null;
41
- }
42
- return token;
43
- }
44
- async function validateMcpApiKey(authHeader, apiKeyService, prisma) {
45
- const token = parseBearerToken(authHeader);
46
- if (!token) {
47
- return {
48
- valid: false,
49
- error: "Authentication required",
50
- httpStatus: 401
51
- };
52
- }
53
- const validationResult = await apiKeyService.validate(token);
54
- if (!validationResult.valid) {
55
- const isRevoked = validationResult.error?.includes("revoked");
56
- return {
57
- valid: false,
58
- error: validationResult.error || "Invalid API key",
59
- httpStatus: isRevoked ? 403 : 401
60
- };
61
- }
62
- const tenant = validationResult.tenant;
63
- const tenantShops = await prisma.tenantShop.findMany({
64
- where: {
65
- tenantId: tenant.tenantId,
66
- uninstalledAt: null
67
- // Only active shops
68
- },
69
- select: {
70
- shopDomain: true,
71
- scopes: true
72
- }
73
- });
74
- const allowedShops = tenantShops.map((shop) => shop.shopDomain);
75
- const mcpTenantContext = {
76
- tenantId: tenant.tenantId,
77
- email: tenant.email,
78
- defaultShop: validationResult.shop,
79
- allowedShops
80
- };
81
- return {
82
- valid: true,
83
- tenant: mcpTenantContext
84
- };
85
- }
86
- function createMcpAuthMiddleware(options) {
87
- const { apiKeyService, prisma, isRemote } = options;
88
- return async (req, res, next) => {
89
- if (!isRemote) {
90
- log.debug("[mcp-auth] Local mode: skipping API key validation");
91
- next();
92
- return;
93
- }
94
- const authResult = await validateMcpApiKey(req.headers.authorization, apiKeyService, prisma);
95
- if (!authResult.valid) {
96
- log.debug(`[mcp-auth] Auth failed: ${authResult.error}`);
97
- let errorCode;
98
- let hint;
99
- if (authResult.httpStatus === 401) {
100
- errorCode = authResult.error === "Authentication required" ? MCP_AUTH_ERROR_CODES.AUTH_REQUIRED : MCP_AUTH_ERROR_CODES.AUTH_INVALID;
101
- hint = "Include Authorization: Bearer sk_live_xxx header";
102
- } else {
103
- errorCode = authResult.error?.includes("revoked") ? MCP_AUTH_ERROR_CODES.AUTH_REVOKED : MCP_AUTH_ERROR_CODES.AUTH_FORBIDDEN;
104
- hint = "API key has been revoked or tenant is inactive";
105
- }
106
- res.status(authResult.httpStatus || 401).json(createJsonRpcError(errorCode, authResult.error || "Authentication failed", hint));
107
- return;
108
- }
109
- req.mcpTenantContext = authResult.tenant;
110
- log.debug(
111
- `[mcp-auth] Auth successful for tenant: ${authResult.tenant?.tenantId?.substring(0, 8)}...`
112
- );
113
- next();
114
- };
115
- }
116
- var MCP_AUTH_ERRORS = MCP_AUTH_ERROR_CODES;
117
-
118
- export {
119
- createJsonRpcError,
120
- parseBearerToken,
121
- validateMcpApiKey,
122
- createMcpAuthMiddleware,
123
- MCP_AUTH_ERRORS
124
- };
@@ -1,208 +0,0 @@
1
- import {
2
- log
3
- } from "./chunk-GKGHMPEC.js";
4
-
5
- // src/db/client.ts
6
- import { PrismaClient } from "@prisma/client";
7
- import { PrismaClient as PrismaClient2 } from "@prisma/client";
8
- var prismaInstance = null;
9
- function getPrismaClient() {
10
- if (!prismaInstance) {
11
- log.debug("Creating new Prisma client instance");
12
- prismaInstance = new PrismaClient({
13
- log: process.env.DEBUG === "true" ? ["query", "info", "warn", "error"] : ["error"]
14
- });
15
- }
16
- return prismaInstance;
17
- }
18
- async function disconnectPrisma() {
19
- if (prismaInstance) {
20
- log.debug("Disconnecting Prisma client");
21
- await prismaInstance.$disconnect();
22
- prismaInstance = null;
23
- }
24
- }
25
- var prisma = getPrismaClient();
26
-
27
- // src/session/store.ts
28
- var SessionStore = class {
29
- cleanupInterval = null;
30
- prisma = getPrismaClient();
31
- SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
32
- // 24 hours
33
- /**
34
- * Retrieve session data by session ID
35
- * @param sessionId - Unique session identifier
36
- * @returns SessionData if valid and not expired, null otherwise
37
- */
38
- async get(sessionId) {
39
- try {
40
- const session = await this.prisma.tenantSession.findUnique({
41
- where: { id: sessionId },
42
- select: {
43
- tenantId: true,
44
- expiresAt: true
45
- }
46
- });
47
- if (!session) {
48
- return null;
49
- }
50
- const expiresAt = session.expiresAt.getTime();
51
- if (expiresAt < Date.now()) {
52
- await this.prisma.tenantSession.delete({
53
- where: { id: sessionId }
54
- }).catch(() => {
55
- });
56
- return null;
57
- }
58
- return {
59
- tenantId: session.tenantId,
60
- expiresAt
61
- };
62
- } catch (error) {
63
- log.error("Failed to retrieve session", error instanceof Error ? error : void 0);
64
- return null;
65
- }
66
- }
67
- /**
68
- * Store session data with automatic 24-hour expiration
69
- * @param sessionId - Unique session identifier
70
- * @param data - Session data (tenantId)
71
- */
72
- async set(sessionId, data) {
73
- const expiresAt = new Date(Date.now() + this.SESSION_TTL_MS);
74
- try {
75
- await this.prisma.tenantSession.upsert({
76
- where: { id: sessionId },
77
- update: {
78
- expiresAt
79
- },
80
- create: {
81
- id: sessionId,
82
- tenantId: data.tenantId,
83
- expiresAt
84
- }
85
- });
86
- } catch (error) {
87
- log.error("Failed to store session", error instanceof Error ? error : void 0);
88
- throw error;
89
- }
90
- }
91
- /**
92
- * Delete a specific session
93
- * @param sessionId - Session identifier to delete
94
- */
95
- async delete(sessionId) {
96
- try {
97
- await this.prisma.tenantSession.delete({
98
- where: { id: sessionId }
99
- });
100
- } catch (error) {
101
- if (error && typeof error === "object" && "code" in error && error.code !== "P2025") {
102
- log.error("Failed to delete session", error instanceof Error ? error : void 0);
103
- }
104
- }
105
- }
106
- /**
107
- * Delete all sessions for a specific tenant
108
- * Used when a tenant changes their password for security
109
- * @param tenantId - Tenant identifier
110
- */
111
- async deleteByTenantId(tenantId) {
112
- try {
113
- await this.prisma.tenantSession.deleteMany({
114
- where: { tenantId }
115
- });
116
- log.debug(`Deleted all sessions for tenant ${tenantId}`);
117
- } catch (error) {
118
- log.error("Failed to delete tenant sessions", error instanceof Error ? error : void 0);
119
- throw error;
120
- }
121
- }
122
- /**
123
- * Remove all expired sessions from the store
124
- */
125
- async cleanup() {
126
- try {
127
- const result = await this.prisma.tenantSession.deleteMany({
128
- where: {
129
- expiresAt: {
130
- lt: /* @__PURE__ */ new Date()
131
- }
132
- }
133
- });
134
- if (result.count > 0) {
135
- log.debug(`Cleaned up ${result.count} expired sessions`);
136
- }
137
- } catch (error) {
138
- log.error("Session cleanup failed", error instanceof Error ? error : void 0);
139
- }
140
- }
141
- /**
142
- * Start automatic cleanup task (runs every 10 minutes)
143
- * @returns Timer handle for cleanup task
144
- */
145
- startCleanup() {
146
- if (this.cleanupInterval) {
147
- return this.cleanupInterval;
148
- }
149
- this.cleanupInterval = setInterval(
150
- () => {
151
- this.cleanup().catch((error) => {
152
- log.error("Background session cleanup error", error instanceof Error ? error : void 0);
153
- });
154
- },
155
- 10 * 60 * 1e3
156
- );
157
- this.cleanupInterval.unref();
158
- log.info("Session cleanup background task started (runs every 10 minutes)");
159
- return this.cleanupInterval;
160
- }
161
- /**
162
- * Stop the automatic cleanup task
163
- */
164
- stopCleanup() {
165
- if (this.cleanupInterval) {
166
- clearInterval(this.cleanupInterval);
167
- this.cleanupInterval = null;
168
- log.info("Session cleanup background task stopped");
169
- }
170
- }
171
- /**
172
- * Get the number of active sessions (for testing/monitoring)
173
- */
174
- async size() {
175
- try {
176
- return await this.prisma.tenantSession.count({
177
- where: {
178
- expiresAt: {
179
- gte: /* @__PURE__ */ new Date()
180
- }
181
- }
182
- });
183
- } catch (error) {
184
- log.error("Failed to count sessions", error instanceof Error ? error : void 0);
185
- return 0;
186
- }
187
- }
188
- /**
189
- * Clear all sessions (for testing)
190
- */
191
- async clear() {
192
- try {
193
- await this.prisma.tenantSession.deleteMany();
194
- } catch (error) {
195
- log.error("Failed to clear sessions", error instanceof Error ? error : void 0);
196
- throw error;
197
- }
198
- }
199
- };
200
- var sessionStore = new SessionStore();
201
-
202
- export {
203
- getPrismaClient,
204
- disconnectPrisma,
205
- prisma,
206
- SessionStore,
207
- sessionStore
208
- };