dtc-mcp 0.1.3 → 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 CHANGED
@@ -1,16 +1,16 @@
1
1
  # dtc-mcp
2
2
 
3
- Context-optimized MCP server for DTC e-commerce brands. Connect Claude (or any MCP client) to your Klaviyo and Shopify data with 16 pre-built analytics tools.
3
+ Context-optimized MCP server for DTC e-commerce brands. Connect Claude (or any MCP client) to your Klaviyo and Shopify data with 22 pre-built analytics tools.
4
4
 
5
5
  Unlike raw API wrappers, dtc-mcp pre-aggregates data server-side and returns only actionable fields — using ~80% less context than dumping raw API responses into your conversation.
6
6
 
7
7
  ## Features
8
8
 
9
9
  - **8 Klaviyo tools** — campaign performance, flow breakdowns, subscriber health, profile search, event activity
10
- - **7 Shopify tools** — sales summaries, daily/weekly/monthly trends, product performance, inventory alerts, customer cohorts, order search
10
+ - **12 Shopify tools** — sales summaries, time series, product performance, inventory alerts, customer cohorts & segments, sales breakdowns by country/channel/vendor, traffic sources, returns analysis, order search
11
11
  - **2 cross-platform tools** — email revenue attribution, full DTC health dashboard
12
12
  - **Dual revenue metrics** — both gross and net revenue on every sales query
13
- - **Smart fallbacks** — ShopifyQL when available, GraphQL pagination when not
13
+ - **ShopifyQL-powered analytics** — fast aggregated queries for sales, customers, and sessions
14
14
  - **Aggressive caching** — respects Klaviyo's strict rate limits (1 req/s on reporting)
15
15
 
16
16
  ## Quick Start
@@ -32,7 +32,7 @@ npx dtc-mcp
32
32
  1. Download the latest `dtc-mcp.mcpb` from [GitHub Releases](https://github.com/rafaelsztutman/dtc-mcp/releases)
33
33
  2. Double-click the `.mcpb` file — Claude Desktop will open an install dialog
34
34
  3. Enter your API credentials when prompted (Klaviyo key required, Shopify optional)
35
- 4. The 16 tools will appear in the hammer menu automatically
35
+ 4. The 22 tools will appear in the hammer menu automatically
36
36
 
37
37
  ### Option B: Manual Configuration
38
38
 
@@ -57,8 +57,8 @@ npx dtc-mcp
57
57
  }
58
58
  ```
59
59
 
60
- 4. Restart Claude Desktop
61
- 5. Look for the hammer icon in the chat input — that confirms the MCP tools are loaded
60
+ 1. Restart Claude Desktop
61
+ 2. Look for the hammer icon in the chat input — that confirms the MCP tools are loaded
62
62
 
63
63
  ### Klaviyo-only mode
64
64
 
@@ -78,13 +78,13 @@ ChatGPT supports MCP servers via remote connections. Since dtc-mcp uses stdio tr
78
78
  4. Click **Create Private API Key**
79
79
  5. Give it a name (e.g., "dtc-mcp")
80
80
  6. Select **Read-only** access for these scopes:
81
- - `campaigns:read`
82
- - `flows:read`
83
- - `lists:read`
84
- - `segments:read`
85
- - `profiles:read`
86
- - `metrics:read`
87
- - `events:read`
81
+ - `campaigns:read`
82
+ - `flows:read`
83
+ - `lists:read`
84
+ - `segments:read`
85
+ - `profiles:read`
86
+ - `metrics:read`
87
+ - `events:read`
88
88
  7. Copy the key (starts with `pk_`)
89
89
 
90
90
  ### Shopify Credentials
@@ -101,13 +101,14 @@ For apps created in the [Shopify Partners Dashboard](https://partners.shopify.co
101
101
  4. Your store URL is your `*.myshopify.com` domain
102
102
 
103
103
  Set these environment variables:
104
+
104
105
  ```
105
106
  SHOPIFY_STORE=your-store.myshopify.com
106
107
  SHOPIFY_CLIENT_ID=your_client_id
107
108
  SHOPIFY_CLIENT_SECRET=shpss_your_secret
108
109
  ```
109
110
 
110
- **Required scopes:** `read_orders`, `read_products`, `read_customers`, `read_inventory`
111
+ **Required scopes:** `read_orders`, `read_products`, `read_customers`, `read_inventory`, `read_reports`
111
112
 
112
113
  #### Option B: Legacy Custom App
113
114
 
@@ -118,6 +119,7 @@ For custom apps created directly in Shopify Admin (apps created before January 2
118
119
  3. Go to **API credentials** and copy the **Admin API access token**
119
120
 
120
121
  Set these environment variables:
122
+
121
123
  ```
122
124
  SHOPIFY_STORE=your-store.myshopify.com
123
125
  SHOPIFY_ACCESS_TOKEN=shpat_your_token_here
@@ -127,194 +129,318 @@ SHOPIFY_ACCESS_TOKEN=shpat_your_token_here
127
129
 
128
130
  ## Environment Variables
129
131
 
130
- | Variable | Required | Description |
131
- |----------|----------|-------------|
132
- | `KLAVIYO_API_KEY` | Yes | Klaviyo private API key (starts with `pk_`) |
133
- | `SHOPIFY_STORE` | For Shopify | Your `*.myshopify.com` domain |
134
- | `SHOPIFY_CLIENT_ID` | For Shopify (Dev Dashboard) | App client ID |
135
- | `SHOPIFY_CLIENT_SECRET` | For Shopify (Dev Dashboard) | App client secret (starts with `shpss_`) |
136
- | `SHOPIFY_ACCESS_TOKEN` | For Shopify (Legacy) | Admin API access token (starts with `shpat_`) |
137
- | `SHOPIFY_API_VERSION` | No | Shopify API version (default: `2026-01`) |
138
- | `KLAVIYO_CONVERSION_METRIC_ID` | No | Override auto-discovered "Placed Order" metric ID |
139
- | `LOG_LEVEL` | No | `debug` \| `info` \| `warn` \| `error` (default: `info`) |
132
+
133
+ | Variable | Required | Description |
134
+ | ------------------------------ | --------------------------- | ----------------------------------------------------- |
135
+ | `KLAVIYO_API_KEY` | Yes | Klaviyo private API key (starts with `pk_`) |
136
+ | `SHOPIFY_STORE` | For Shopify | Your `*.myshopify.com` domain |
137
+ | `SHOPIFY_CLIENT_ID` | For Shopify (Dev Dashboard) | App client ID |
138
+ | `SHOPIFY_CLIENT_SECRET` | For Shopify (Dev Dashboard) | App client secret (starts with `shpss_`) |
139
+ | `SHOPIFY_ACCESS_TOKEN` | For Shopify (Legacy) | Admin API access token (starts with `shpat_`) |
140
+ | `SHOPIFY_API_VERSION` | No | Shopify API version (default: `2026-01`) |
141
+ | `KLAVIYO_CONVERSION_METRIC_ID` | No | Override auto-discovered "Placed Order" metric ID |
142
+ | `LOG_LEVEL` | No | `debug` | `info` | `warn` | `error` (default: `info`) |
143
+
140
144
 
141
145
  ## Tool Reference
142
146
 
143
147
  ### Klaviyo Tools
144
148
 
145
149
  #### `klaviyo_campaign_summary`
150
+
146
151
  Top campaigns ranked by metric. Returns name, send date, opens, clicks, revenue.
147
152
 
148
- | Parameter | Type | Default | Description |
149
- |-----------|------|---------|-------------|
150
- | `channel` | `"email"` \| `"sms"` | required | Channel filter |
151
- | `metric` | `"revenue"` \| `"open_rate"` \| `"click_rate"` \| `"recipients"` | `"revenue"` | Rank by |
152
- | `days` | 1-365 | 30 | Lookback period |
153
- | `limit` | 1-25 | 10 | Max results |
153
+
154
+ | Parameter | Type | Default | Description |
155
+ | --------- | ------------------------------------------------------------- | ----------- | --------------- |
156
+ | `channel` | `"email"` | `"sms"` | required | Channel filter |
157
+ | `metric` | `"revenue"` | `"open_rate"` | `"click_rate"` | `"recipients"` | `"revenue"` | Rank by |
158
+ | `days` | 1-365 | 30 | Lookback period |
159
+ | `limit` | 1-25 | 10 | Max results |
160
+
154
161
 
155
162
  > "Show me my top email campaigns by revenue this month"
156
163
 
157
164
  #### `klaviyo_campaign_detail`
165
+
158
166
  Deep dive on one campaign: full metrics, subject line, audiences, send time.
159
167
 
160
- | Parameter | Type | Default | Description |
161
- |-----------|------|---------|-------------|
168
+
169
+ | Parameter | Type | Default | Description |
170
+ | ------------- | ------ | -------- | ------------------- |
162
171
  | `campaign_id` | string | required | Klaviyo campaign ID |
163
172
 
173
+
164
174
  > "Give me the full breakdown on my Black Friday campaign"
165
175
 
166
176
  #### `klaviyo_flow_summary`
177
+
167
178
  Top flows by metric. Returns name, status, trigger, message count, revenue.
168
179
 
169
- | Parameter | Type | Default | Description |
170
- |-----------|------|---------|-------------|
171
- | `metric` | `"revenue"` \| `"click_rate"` \| `"conversion_rate"` \| `"recipients"` | `"revenue"` | Rank by |
172
- | `days` | 1-365 | 30 | Lookback period |
173
- | `status` | `"live"` \| `"draft"` \| `"manual"` \| `"all"` | `"live"` | Filter by status |
174
- | `limit` | 1-25 | 10 | Max results |
180
+
181
+ | Parameter | Type | Default | Description |
182
+ | --------- | ------------------------------------------------------------------- | ----------- | ---------------- |
183
+ | `metric` | `"revenue"` | `"click_rate"` | `"conversion_rate"` | `"recipients"` | `"revenue"` | Rank by |
184
+ | `days` | 1-365 | 30 | Lookback period |
185
+ | `status` | `"live"` | `"draft"` | `"manual"` | `"all"` | `"live"` | Filter by status |
186
+ | `limit` | 1-25 | 10 | Max results |
187
+
175
188
 
176
189
  > "Which of my flows generates the most revenue?"
177
190
 
178
191
  #### `klaviyo_flow_detail`
192
+
179
193
  Deep dive on one flow: per-message performance breakdown.
180
194
 
181
- | Parameter | Type | Default | Description |
182
- |-----------|------|---------|-------------|
195
+
196
+ | Parameter | Type | Default | Description |
197
+ | --------- | ------ | -------- | --------------- |
183
198
  | `flow_id` | string | required | Klaviyo flow ID |
184
- | `days` | 1-365 | 30 | Lookback period |
199
+ | `days` | 1-365 | 30 | Lookback period |
200
+
185
201
 
186
202
  > "Show me the per-email breakdown of my welcome flow"
187
203
 
188
204
  #### `klaviyo_subscriber_health`
205
+
189
206
  List growth and engagement tier breakdown.
190
207
 
191
- | Parameter | Type | Default | Description |
192
- |-----------|------|---------|-------------|
208
+
209
+ | Parameter | Type | Default | Description |
210
+ | --------- | ------ | -------- | --------------------------- |
193
211
  | `list_id` | string | optional | Specific list, or all lists |
194
212
 
213
+
195
214
  > "What's the health of my email list?"
196
215
 
197
216
  #### `klaviyo_list_segments`
217
+
198
218
  All lists and segments with sizes.
199
219
 
200
- | Parameter | Type | Default | Description |
201
- |-----------|------|---------|-------------|
202
- | `type` | `"lists"` \| `"segments"` \| `"all"` | `"all"` | Filter by type |
203
- | `cursor` | string | optional | Pagination cursor |
220
+
221
+ | Parameter | Type | Default | Description |
222
+ | --------- | ---------------------------------- | -------- | ----------------- |
223
+ | `type` | `"lists"` | `"segments"` | `"all"` | `"all"` | Filter by type |
224
+ | `cursor` | string | optional | Pagination cursor |
225
+
204
226
 
205
227
  > "List all my Klaviyo segments and their sizes"
206
228
 
207
229
  #### `klaviyo_search_profiles`
230
+
208
231
  Find profiles by email, phone, or name.
209
232
 
210
- | Parameter | Type | Default | Description |
211
- |-----------|------|---------|-------------|
212
- | `query` | string | required | Email, phone, or name |
213
- | `limit` | 1-10 | 5 | Max results |
214
233
 
215
- > "Look up the profile for john@example.com"
234
+ | Parameter | Type | Default | Description |
235
+ | --------- | ------ | -------- | --------------------- |
236
+ | `query` | string | required | Email, phone, or name |
237
+ | `limit` | 1-10 | 5 | Max results |
238
+
239
+
240
+ > "Look up the profile for [john@example.com](mailto:john@example.com)"
216
241
 
217
242
  #### `klaviyo_recent_activity`
243
+
218
244
  Recent events for a metric (e.g., Placed Order, Opened Email).
219
245
 
220
- | Parameter | Type | Default | Description |
221
- |-----------|------|---------|-------------|
222
- | `metric_name` | string | `"Placed Order"` | Metric name |
223
- | `days` | 1-90 | 7 | Lookback period |
224
- | `limit` | 1-25 | 10 | Max events |
225
- | `profile_email` | string | optional | Filter to one profile |
246
+
247
+ | Parameter | Type | Default | Description |
248
+ | --------------- | ------ | ---------------- | --------------------- |
249
+ | `metric_name` | string | `"Placed Order"` | Metric name |
250
+ | `days` | 1-90 | 7 | Lookback period |
251
+ | `limit` | 1-25 | 10 | Max events |
252
+ | `profile_email` | string | optional | Filter to one profile |
253
+
226
254
 
227
255
  > "Show me the last 10 orders placed"
228
256
 
229
257
  ### Shopify Tools
230
258
 
231
259
  #### `shopify_sales_summary`
260
+
232
261
  Revenue (gross + net), orders, AOV for a period with comparison.
233
262
 
234
- | Parameter | Type | Default | Description |
235
- |-----------|------|---------|-------------|
236
- | `days` | 1-90 | 30 | Lookback period |
237
- | `compare_previous` | boolean | true | Include previous period comparison |
263
+
264
+ | Parameter | Type | Default | Description |
265
+ | ------------------ | ------- | ------- | ---------------------------------- |
266
+ | `days` | 1-90 | 30 | Lookback period |
267
+ | `compare_previous` | boolean | true | Include previous period comparison |
268
+
238
269
 
239
270
  > "What were my sales last month compared to the month before?"
240
271
 
241
272
  #### `shopify_sales_timeseries`
273
+
242
274
  Revenue and orders broken down by day, week, or month.
243
275
 
244
- | Parameter | Type | Default | Description |
245
- |-----------|------|---------|-------------|
246
- | `days` | 1-365 | 30 | Lookback period |
247
- | `granularity` | `"daily"` \| `"weekly"` \| `"monthly"` | `"daily"` | Bucket size |
276
+
277
+ | Parameter | Type | Default | Description |
278
+ | ------------- | ------------------------------------ | --------- | --------------- |
279
+ | `days` | 1-365 | 30 | Lookback period |
280
+ | `granularity` | `"daily"` | `"weekly"` | `"monthly"` | `"daily"` | Bucket size |
281
+
248
282
 
249
283
  > "Show me daily revenue for this month"
250
284
 
251
285
  #### `shopify_product_performance`
286
+
252
287
  Top products by revenue or units sold.
253
288
 
254
- | Parameter | Type | Default | Description |
255
- |-----------|------|---------|-------------|
256
- | `days` | 1-90 | 7 | Lookback period |
257
- | `metric` | `"revenue"` \| `"units"` | `"revenue"` | Rank by |
258
- | `limit` | 1-25 | 10 | Max results |
289
+
290
+ | Parameter | Type | Default | Description |
291
+ | --------- | ----------------------- | ----------- | --------------- |
292
+ | `days` | 1-90 | 7 | Lookback period |
293
+ | `metric` | `"revenue"` | `"units"` | `"revenue"` | Rank by |
294
+ | `limit` | 1-25 | 10 | Max results |
295
+
259
296
 
260
297
  > "Which products sold the most units this week?"
261
298
 
262
299
  #### `shopify_order_search`
300
+
263
301
  Find orders by number, email, or status.
264
302
 
265
- | Parameter | Type | Default | Description |
266
- |-----------|------|---------|-------------|
267
- | `query` | string | required | Order number, email, or `financial_status:paid` |
268
- | `limit` | 1-25 | 10 | Max results |
303
+
304
+ | Parameter | Type | Default | Description |
305
+ | --------- | ------ | -------- | ----------------------------------------------- |
306
+ | `query` | string | required | Order number, email, or `financial_status:paid` |
307
+ | `limit` | 1-25 | 10 | Max results |
308
+
269
309
 
270
310
  > "Find order #1234"
271
311
 
272
312
  #### `shopify_inventory_alerts`
313
+
273
314
  Products with low or zero stock, sorted by most urgent.
274
315
 
275
- | Parameter | Type | Default | Description |
276
- |-----------|------|---------|-------------|
277
- | `threshold` | number | 10 | Alert at or below this quantity |
278
- | `limit` | 1-50 | 20 | Max results |
316
+
317
+ | Parameter | Type | Default | Description |
318
+ | ----------- | ------ | ------- | ------------------------------- |
319
+ | `threshold` | number | 10 | Alert at or below this quantity |
320
+ | `limit` | 1-50 | 20 | Max results |
321
+
279
322
 
280
323
  > "Which products are running low on stock?"
281
324
 
282
- #### `shopify_customer_cohort`
283
- New vs returning buyers. First-time vs repeat split.
325
+ #### `shopify_customer_cohorts`
326
+
327
+ Monthly or quarterly acquisition cohorts with LTV and retention signals.
328
+
329
+
330
+ | Parameter | Type | Default | Description |
331
+ | ------------- | --------------------------- | ----------- | --------------- |
332
+ | `granularity` | `"monthly"` | `"quarterly"` | `"monthly"` | Cohort grouping |
333
+ | `months` | 1-24 | 12 | Lookback period |
334
+
335
+
336
+ > "Show me customer cohorts by month — which cohort has the best LTV?"
337
+
338
+ #### `shopify_customer_segments`
339
+
340
+ Customer distribution by RFM group, spend tier, country, or tags.
341
+
342
+
343
+ | Parameter | Type | Default | Description |
344
+ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------- |
345
+ | `dimension` | `"rfm_group"` | `"predicted_spend_tier"` | `"customer_email_subscription_status"` | `"customer_sms_subscription_status"` | `"customer_country"` | `"customer_tag"` | `"customer_number_of_orders"` | required | Segmentation dimension |
346
+ | `months` | 1-24 | 12 | Lookback period |
347
+ | `limit` | 1-50 | 20 | Max segments |
348
+
349
+
350
+ > "Break down my customers by RFM group — who are my champions vs at-risk?"
351
+
352
+ #### `shopify_sales_breakdown`
353
+
354
+ Revenue and orders broken down by country, channel, vendor, or traffic source.
355
+
356
+
357
+ | Parameter | Type | Default | Description |
358
+ | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | ------------------- |
359
+ | `dimension` | `"billing_country"` | `"billing_region"` | `"channel_name"` | `"product_vendor"` | `"referrer_source"` | `"referring_channel"` | `"referring_platform"` | `"traffic_type"` | `"shipping_country"` | required | Breakdown dimension |
360
+ | `days` | 1-365 | 30 | Lookback period |
361
+ | `metric` | `"total_sales"` | `"net_sales"` | `"orders"` | `"average_order_value"` | `"gross_profit"` | `"net_sales"` | Metric to rank by |
362
+ | `limit` | 1-50 | 10 | Max results |
363
+
364
+
365
+ > "What are my top countries by revenue?" or "Break down sales by channel"
366
+
367
+ #### `shopify_product_analytics`
368
+
369
+ Product performance with margins, returns, and quantities via ShopifyQL.
284
370
 
285
- | Parameter | Type | Default | Description |
286
- |-----------|------|---------|-------------|
287
- | `days` | 1-365 | 90 | Lookback period |
288
- | `limit` | 1-500 | 250 | Max customers to analyze |
289
371
 
290
- > "What percentage of orders this quarter came from new customers?"
372
+ | Parameter | Type | Default | Description |
373
+ | --------- | --------------------------------------------------------------- | ------------- | ---------------- |
374
+ | `days` | 1-365 | 30 | Lookback period |
375
+ | `metric` | `"net_sales"` | `"gross_sales"` | `"orders"` | `"gross_profit"` | `"net_sales"` | Sort products by |
376
+ | `limit` | 1-50 | 10 | Max results |
377
+
378
+
379
+ > "Which products have the best margins?" or "Show product performance with return rates"
380
+
381
+ #### `shopify_traffic_sources`
382
+
383
+ Session analytics by source, landing page, or daily trend.
384
+
385
+
386
+ | Parameter | Type | Default | Description |
387
+ | --------- | ------------------------------------------- | ----------- | --------------- |
388
+ | `mode` | `"sources"` | `"landing_pages"` | `"trend"` | `"sources"` | Analysis mode |
389
+ | `days` | 1-365 | 30 | Lookback period |
390
+ | `limit` | 1-50 | 10 | Max results |
391
+
392
+
393
+ > "Where is my traffic coming from?" or "What are my top landing pages?"
394
+
395
+ #### `shopify_returns_analysis`
396
+
397
+ Return rates, costs, and most-returned products.
398
+
399
+
400
+ | Parameter | Type | Default | Description |
401
+ | --------- | ---------------------------- | ----------- | --------------------------------------- |
402
+ | `mode` | `"summary"` | `"by_product"` | `"summary"` | Summary totals or per-product breakdown |
403
+ | `days` | 1-365 | 30 | Lookback period |
404
+ | `limit` | 1-50 | 10 | Max results (by_product mode) |
405
+
406
+
407
+ > "What's my return rate?" or "Which products get returned the most?"
291
408
 
292
409
  #### `shopify_recent_orders`
410
+
293
411
  Most recent orders. Quick snapshot of store activity.
294
412
 
413
+
295
414
  | Parameter | Type | Default | Description |
296
- |-----------|------|---------|-------------|
297
- | `limit` | 1-25 | 10 | Max results |
415
+ | --------- | ---- | ------- | ----------- |
416
+ | `limit` | 1-25 | 10 | Max results |
417
+
298
418
 
299
419
  > "Show me the last 10 orders"
300
420
 
301
421
  ### Cross-Platform Tools
302
422
 
303
423
  #### `dtc_email_revenue_attribution`
424
+
304
425
  Email/SMS revenue vs total Shopify revenue. Shows email marketing contribution.
305
426
 
306
- | Parameter | Type | Default | Description |
307
- |-----------|------|---------|-------------|
308
- | `days` | 1-365 | 30 | Lookback period |
427
+
428
+ | Parameter | Type | Default | Description |
429
+ | --------- | ----- | ------- | --------------- |
430
+ | `days` | 1-365 | 30 | Lookback period |
431
+
309
432
 
310
433
  > "What percentage of my revenue came from email?"
311
434
 
312
435
  #### `dtc_dashboard`
436
+
313
437
  Complete DTC health dashboard: sales + email + subscriber metrics in one call.
314
438
 
315
- | Parameter | Type | Default | Description |
316
- |-----------|------|---------|-------------|
317
- | `days` | 7-90 | 30 | Lookback period |
439
+
440
+ | Parameter | Type | Default | Description |
441
+ | --------- | ---- | ------- | --------------- |
442
+ | `days` | 7-90 | 30 | Lookback period |
443
+
318
444
 
319
445
  > "Give me the full business dashboard for last month"
320
446
 
@@ -330,7 +456,12 @@ Here are questions you can ask Claude once dtc-mcp is connected:
330
456
  - "Are any products running low on stock?"
331
457
  - "What percentage of my revenue comes from email marketing?"
332
458
  - "How many new vs returning customers did I have this quarter?"
333
- - "Look up the customer profile for sarah@example.com"
459
+ - "Show me customer cohorts by month — which has the best LTV?"
460
+ - "Break down my customers by RFM group"
461
+ - "What are my top countries by revenue?"
462
+ - "Where is my traffic coming from?"
463
+ - "What's my return rate? Which products get returned the most?"
464
+ - "Which products have the best margins?"
334
465
  - "Give me a complete health dashboard for my business"
335
466
  - "Compare this month's sales to last month"
336
467
  - "What are my top SMS campaigns by click rate?"
@@ -343,11 +474,11 @@ cd dtc-mcp
343
474
  npm install
344
475
  cp .env.example .env # Fill in your API credentials
345
476
  npm run build # Compile TypeScript
346
- npm test # Run tests (31 tests)
477
+ npm test # Run tests (44 tests)
347
478
  npm run dev # Watch mode
348
479
  npm run inspect # Open MCP Inspector for interactive testing
349
480
  ```
350
481
 
351
482
  ## License
352
483
 
353
- MIT
484
+ MIT
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // CommonJS bootstrap — runs BEFORE any ESM module evaluation.
4
+ // Catches errors during ESM module loading and writes to a debug log file.
5
+ // This exists because Claude Desktop's built-in Node.js does not capture stderr.
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const fs = require("fs");
8
+ const os = require("os");
9
+ const path = require("path");
10
+ const LOG = path.join(os.tmpdir(), "dtc-mcp-debug.log");
11
+ function debugLog(msg) {
12
+ try {
13
+ fs.appendFileSync(LOG, `${new Date().toISOString()} ${msg}\n`);
14
+ }
15
+ catch {
16
+ // can't write to log file
17
+ }
18
+ }
19
+ try {
20
+ fs.writeFileSync(LOG, "");
21
+ }
22
+ catch {
23
+ // ignore
24
+ }
25
+ debugLog("=== dtc-mcp bootstrap ===");
26
+ debugLog(`node: ${process.version}`);
27
+ debugLog(`platform: ${process.platform} ${process.arch}`);
28
+ debugLog(`argv: ${JSON.stringify(process.argv)}`);
29
+ debugLog(`cwd: ${process.cwd()}`);
30
+ debugLog(`env KLAVIYO_API_KEY set: ${!!process.env.KLAVIYO_API_KEY} (length: ${process.env.KLAVIYO_API_KEY?.length ?? 0})`);
31
+ debugLog(`env SHOPIFY_STORE set: ${!!process.env.SHOPIFY_STORE}`);
32
+ debugLog(`env SHOPIFY_CLIENT_ID set: ${!!process.env.SHOPIFY_CLIENT_ID}`);
33
+ debugLog(`env SHOPIFY_CLIENT_SECRET set: ${!!process.env.SHOPIFY_CLIENT_SECRET}`);
34
+ debugLog(`env SHOPIFY_ACCESS_TOKEN set: ${!!process.env.SHOPIFY_ACCESS_TOKEN}`);
35
+ process.on("uncaughtException", (err) => {
36
+ debugLog(`uncaughtException: ${err?.stack || err}`);
37
+ });
38
+ process.on("unhandledRejection", (err) => {
39
+ debugLog(`unhandledRejection: ${err instanceof Error ? err.stack : String(err)}`);
40
+ });
41
+ process.on("exit", (code) => {
42
+ debugLog(`process exit: code=${code}`);
43
+ });
44
+ debugLog("loading ESM module...");
45
+ import("./index.js")
46
+ .then(() => {
47
+ debugLog("ESM module loaded OK");
48
+ })
49
+ .catch((err) => {
50
+ debugLog(`ESM module load FAILED: ${err?.stack || err}`);
51
+ process.exit(1);
52
+ });
53
+ //# sourceMappingURL=bootstrap.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap.cjs","sourceRoot":"","sources":["../src/bootstrap.cts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/config.js CHANGED
@@ -10,7 +10,14 @@ function getRequired(name) {
10
10
  return value;
11
11
  }
12
12
  function getOptional(name) {
13
- return process.env[name] || null;
13
+ const raw = process.env[name];
14
+ if (!raw)
15
+ return null;
16
+ // Claude Desktop may pass placeholder values for unfilled optional fields
17
+ const trimmed = raw.trim();
18
+ if (!trimmed || trimmed === "undefined" || trimmed === "null")
19
+ return null;
20
+ return trimmed;
14
21
  }
15
22
  function loadConfig() {
16
23
  const klaviyoApiKey = getRequired("KLAVIYO_API_KEY");
@@ -21,36 +28,41 @@ function loadConfig() {
21
28
  // Determine auth mode
22
29
  const hasLegacy = shopifyAccessToken !== null;
23
30
  const hasClientCredentials = shopifyClientId !== null && shopifyClientSecret !== null;
31
+ let shopifyAuthMode = null;
24
32
  if (hasLegacy && hasClientCredentials) {
25
- throw new Error("Ambiguous Shopify config: both SHOPIFY_ACCESS_TOKEN and SHOPIFY_CLIENT_ID/SHOPIFY_CLIENT_SECRET are set. Use one auth mode, not both.");
33
+ // Both set prefer client_credentials (recommended), ignore legacy token
34
+ console.error("[dtc-mcp] warn: Both SHOPIFY_ACCESS_TOKEN and SHOPIFY_CLIENT_ID/SECRET are set. Using Client Credentials (recommended). Remove ACCESS_TOKEN to silence this warning.");
35
+ shopifyAuthMode = "client_credentials";
26
36
  }
27
- let shopifyAuthMode = null;
28
- if (hasClientCredentials) {
37
+ else if (hasClientCredentials) {
29
38
  shopifyAuthMode = "client_credentials";
30
39
  }
31
40
  else if (hasLegacy) {
32
41
  shopifyAuthMode = "legacy";
33
42
  }
34
- // Validate that store is set when any auth is configured
43
+ // Warn (don't throw) if store is missing when auth is configured
35
44
  if (shopifyAuthMode && !shopifyStore) {
36
- throw new Error("SHOPIFY_STORE is required when Shopify credentials are configured.");
45
+ console.error("[dtc-mcp] warn: Shopify credentials are set but SHOPIFY_STORE is missing. Shopify tools will be disabled.");
46
+ shopifyAuthMode = null;
37
47
  }
38
- // Validate partial client credentials
48
+ // Warn (don't throw) for partial client credentials
39
49
  if ((shopifyClientId && !shopifyClientSecret) ||
40
50
  (!shopifyClientId && shopifyClientSecret)) {
41
- throw new Error("Both SHOPIFY_CLIENT_ID and SHOPIFY_CLIENT_SECRET must be set together.");
51
+ console.error("[dtc-mcp] warn: Only one of SHOPIFY_CLIENT_ID/SHOPIFY_CLIENT_SECRET is set. Both are required. Shopify client credentials auth will be disabled.");
42
52
  }
43
- const logLevel = (process.env.LOG_LEVEL || "info");
44
- if (!["debug", "info", "warn", "error"].includes(logLevel)) {
45
- throw new Error(`Invalid LOG_LEVEL: ${logLevel}. Must be one of: debug, info, warn, error`);
53
+ const rawLogLevel = (process.env.LOG_LEVEL || "info");
54
+ const validLevels = ["debug", "info", "warn", "error"];
55
+ const logLevel = validLevels.includes(rawLogLevel) ? rawLogLevel : "info";
56
+ if (!validLevels.includes(rawLogLevel)) {
57
+ console.error(`[dtc-mcp] warn: Invalid LOG_LEVEL "${rawLogLevel}", defaulting to "info".`);
46
58
  }
47
59
  return Object.freeze({
48
60
  klaviyoApiKey,
49
61
  klaviyoRevision: "2026-01-15",
50
62
  shopifyStore,
51
- shopifyAccessToken,
52
- shopifyClientId,
53
- shopifyClientSecret,
63
+ shopifyAccessToken: shopifyAuthMode === "legacy" ? shopifyAccessToken : null,
64
+ shopifyClientId: shopifyAuthMode === "client_credentials" ? shopifyClientId : null,
65
+ shopifyClientSecret: shopifyAuthMode === "client_credentials" ? shopifyClientSecret : null,
54
66
  shopifyAuthMode,
55
67
  shopifyApiVersion: process.env.SHOPIFY_API_VERSION || "2026-01",
56
68
  klaviyoConversionMetricId: getOptional("KLAVIYO_CONVERSION_METRIC_ID"),