epicmerch-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +182 -0
- package/manifest.json +17 -0
- package/package.json +64 -0
- package/skills/epicmerch-auth.md +78 -0
- package/skills/epicmerch-merchant-agent.md +74 -0
- package/skills/epicmerch-merchant.md +46 -0
- package/skills/epicmerch-migrate.md +62 -0
- package/skills/epicmerch-orders.md +153 -0
- package/skills/epicmerch-products.md +95 -0
- package/skills/epicmerch-stripe.md +64 -0
- package/skills/epicmerch.md +389 -0
- package/smithery.yaml +6 -0
- package/src/adapters/mcp.js +76 -0
- package/src/adapters/openapi.js +97 -0
- package/src/auth.js +118 -0
- package/src/client.js +81 -0
- package/src/index.js +67 -0
- package/src/install.js +269 -0
- package/src/login.js +149 -0
- package/src/logout.js +30 -0
- package/src/prompts/index.js +59 -0
- package/src/refresh.js +73 -0
- package/src/resources/examples/auth.md +31 -0
- package/src/resources/examples/orders.md +27 -0
- package/src/resources/examples/payments.md +23 -0
- package/src/resources/examples/products.md +22 -0
- package/src/resources/overview.md +26 -0
- package/src/resources/sdk-guide.md +341 -0
- package/src/status.js +22 -0
- package/src/token-store.js +52 -0
- package/src/tools/_examples.js +352 -0
- package/src/tools/developer.js +65 -0
- package/src/tools/merchant.js +618 -0
- package/src/tools/scaffold.js +278 -0
- package/src/tools/session.js +44 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
// src/tools/merchant.js
|
|
2
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { withExamples } from './_examples.js';
|
|
5
|
+
const _require = createRequire(import.meta.url);
|
|
6
|
+
|
|
7
|
+
const ok = (data) => ({ content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] });
|
|
8
|
+
const err = (msg) => ({ isError: true, content: [{ type: 'text', text: msg }] });
|
|
9
|
+
|
|
10
|
+
// In-memory session store for Shopify migration (scoped to MCP process lifetime)
|
|
11
|
+
const migrationSessions = new Map();
|
|
12
|
+
|
|
13
|
+
export function merchantTools(client, session) {
|
|
14
|
+
const requireAuth = () => {
|
|
15
|
+
// OAuth mode (sub-project A): access token travels via Authorization: Bearer
|
|
16
|
+
// header — no JWT needed locally. The Client class handles header selection
|
|
17
|
+
// based on session.getAuthMode(). The token's mere presence is enough proof
|
|
18
|
+
// of auth; the server-side bearerTokenMiddleware will reject it if invalid.
|
|
19
|
+
if (session.getAuthMode?.() === 'oauth' && session.getActiveTokenEntry?.()) return;
|
|
20
|
+
// Legacy JWT path is retained for tests/internal callers that pre-set a JWT via
|
|
21
|
+
// session.setJwt(...), but there is no longer a tool that collects credentials
|
|
22
|
+
// in chat — merchants must authenticate via the browser OAuth flow.
|
|
23
|
+
if (!session.getActiveJwt()) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
'Not authenticated. Tell the user to run `npx epicmerch-mcp login` in a ' +
|
|
26
|
+
'terminal — it opens a browser and saves a token locally (no credentials ' +
|
|
27
|
+
'pass through this chat). Once they confirm, retry the tool call.'
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
// --- PRODUCTS ---
|
|
34
|
+
async merchant_list_products({ page, limit, keyword } = {}) {
|
|
35
|
+
requireAuth();
|
|
36
|
+
const p = new URLSearchParams();
|
|
37
|
+
if (page) p.set('page', page);
|
|
38
|
+
if (limit) p.set('limit', limit);
|
|
39
|
+
if (keyword) p.set('keyword', keyword);
|
|
40
|
+
const qs = p.toString();
|
|
41
|
+
return ok(await client.authGet(`/products${qs ? `?${qs}` : ''}`));
|
|
42
|
+
},
|
|
43
|
+
async merchant_get_product({ id }) {
|
|
44
|
+
requireAuth();
|
|
45
|
+
return ok(await client.authGet(`/products/${id}`));
|
|
46
|
+
},
|
|
47
|
+
async merchant_create_product(body) {
|
|
48
|
+
requireAuth();
|
|
49
|
+
return ok(await client.authPost('/products', body));
|
|
50
|
+
},
|
|
51
|
+
async merchant_update_product({ id, ...body }) {
|
|
52
|
+
requireAuth();
|
|
53
|
+
return ok(await client.authPut(`/products/${id}`, body));
|
|
54
|
+
},
|
|
55
|
+
async merchant_delete_product({ id }) {
|
|
56
|
+
requireAuth();
|
|
57
|
+
return ok(await client.authDelete(`/products/${id}`));
|
|
58
|
+
},
|
|
59
|
+
async merchant_generate_product_description({ productId, name, category }) {
|
|
60
|
+
requireAuth();
|
|
61
|
+
return ok(await client.authPost('/ai/generate-description', { productId, name, category }));
|
|
62
|
+
},
|
|
63
|
+
async merchant_get_product_recommendations(_args) {
|
|
64
|
+
requireAuth();
|
|
65
|
+
return ok(await client.authGet('/ai/recommendations'));
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
// --- CATEGORIES ---
|
|
69
|
+
async merchant_list_categories(_args) {
|
|
70
|
+
requireAuth();
|
|
71
|
+
return ok(await client.authGet('/categories'));
|
|
72
|
+
},
|
|
73
|
+
async merchant_create_category({ name, visible }) {
|
|
74
|
+
requireAuth();
|
|
75
|
+
return ok(await client.authPost('/categories', { name, visible }));
|
|
76
|
+
},
|
|
77
|
+
async merchant_update_category({ id, ...body }) {
|
|
78
|
+
requireAuth();
|
|
79
|
+
return ok(await client.authPut(`/categories/${id}`, body));
|
|
80
|
+
},
|
|
81
|
+
async merchant_delete_category({ id }) {
|
|
82
|
+
requireAuth();
|
|
83
|
+
return ok(await client.authDelete(`/categories/${id}`));
|
|
84
|
+
},
|
|
85
|
+
async merchant_toggle_category_visibility({ id }) {
|
|
86
|
+
requireAuth();
|
|
87
|
+
return ok(await client.authPatch(`/categories/${id}/toggle`, {}));
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// --- ORDERS ---
|
|
91
|
+
async merchant_list_orders({ page, limit, status } = {}) {
|
|
92
|
+
requireAuth();
|
|
93
|
+
const p = new URLSearchParams();
|
|
94
|
+
if (page) p.set('page', page);
|
|
95
|
+
if (limit) p.set('limit', limit);
|
|
96
|
+
if (status) p.set('status', status);
|
|
97
|
+
const qs = p.toString();
|
|
98
|
+
return ok(await client.authGet(`/orders${qs ? `?${qs}` : ''}`));
|
|
99
|
+
},
|
|
100
|
+
async merchant_get_order({ id }) {
|
|
101
|
+
requireAuth();
|
|
102
|
+
return ok(await client.authGet(`/orders/${id}`));
|
|
103
|
+
},
|
|
104
|
+
async merchant_update_order_status({ id, status }) {
|
|
105
|
+
requireAuth();
|
|
106
|
+
return ok(await client.authPut(`/orders/${id}/status`, { status }));
|
|
107
|
+
},
|
|
108
|
+
async merchant_update_payment_status({ id, isPaid }) {
|
|
109
|
+
requireAuth();
|
|
110
|
+
return ok(await client.authPut(`/orders/${id}/paystatus`, { isPaid }));
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
// --- CUSTOMERS ---
|
|
114
|
+
async merchant_list_customers(_args) {
|
|
115
|
+
requireAuth();
|
|
116
|
+
return ok(await client.authGet('/users'));
|
|
117
|
+
},
|
|
118
|
+
async merchant_get_customer({ id }) {
|
|
119
|
+
requireAuth();
|
|
120
|
+
return ok(await client.authGet(`/users/${id}`));
|
|
121
|
+
},
|
|
122
|
+
async merchant_delete_customer({ id }) {
|
|
123
|
+
requireAuth();
|
|
124
|
+
return ok(await client.authDelete(`/users/${id}`));
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// --- ANALYTICS ---
|
|
128
|
+
async merchant_get_analytics_stats(_args) { requireAuth(); return ok(await client.authGet('/analytics/stats')); },
|
|
129
|
+
async merchant_get_sales_trend({ period } = {}) { requireAuth(); return ok(await client.authGet(`/analytics/sales-trend${period ? `?period=${period}` : ''}`)); },
|
|
130
|
+
async merchant_get_top_products(_args) { requireAuth(); return ok(await client.authGet('/analytics/top-products')); },
|
|
131
|
+
async merchant_get_traffic(_args) { requireAuth(); return ok(await client.authGet('/analytics/traffic')); },
|
|
132
|
+
async merchant_get_revenue_by_category(_args) { requireAuth(); return ok(await client.authGet('/analytics/revenue-by-category')); },
|
|
133
|
+
async merchant_get_order_status_distribution(_args) { requireAuth(); return ok(await client.authGet('/analytics/order-status')); },
|
|
134
|
+
async merchant_get_peak_hours(_args) { requireAuth(); return ok(await client.authGet('/analytics/peak-hours')); },
|
|
135
|
+
async merchant_get_geographic_distribution(_args) { requireAuth(); return ok(await client.authGet('/analytics/geographic')); },
|
|
136
|
+
async merchant_get_period_comparison({ period } = {}) { requireAuth(); return ok(await client.authGet(`/analytics/comparison${period ? `?period=${period}` : ''}`)); },
|
|
137
|
+
async merchant_get_cart_abandonment(_args) { requireAuth(); return ok(await client.authGet('/analytics/cart-abandonment')); },
|
|
138
|
+
async merchant_get_ai_insights(body) { requireAuth(); return ok(await client.authPost('/ai/analytics-insights', body)); },
|
|
139
|
+
|
|
140
|
+
// --- NOTIFICATIONS ---
|
|
141
|
+
async merchant_list_subscribers(_args) { requireAuth(); return ok(await client.authGet('/notifications/subscribers')); },
|
|
142
|
+
async merchant_send_notification(body) { requireAuth(); return ok(await client.authPost('/notifications/send', body)); },
|
|
143
|
+
async merchant_send_to_user(body) { requireAuth(); return ok(await client.authPost('/notifications/send-to-user', body)); },
|
|
144
|
+
async merchant_get_abandoned_carts({ hours } = {}) { requireAuth(); return ok(await client.authGet(`/notifications/abandoned-carts${hours ? `?hours=${hours}` : ''}`)); },
|
|
145
|
+
async merchant_get_notification_history(_args) { requireAuth(); return ok(await client.authGet('/notifications/history')); },
|
|
146
|
+
async merchant_get_notification_stats(_args) { requireAuth(); return ok(await client.authGet('/notifications/stats')); },
|
|
147
|
+
|
|
148
|
+
// --- SETTINGS ---
|
|
149
|
+
async merchant_get_settings(_args) { requireAuth(); return ok(await client.authGet('/users/settings')); },
|
|
150
|
+
async merchant_update_currency({ currency }) { requireAuth(); return ok(await client.authPut('/users/currency', { currency })); },
|
|
151
|
+
async merchant_get_email_settings(_args) { requireAuth(); return ok(await client.authGet('/users/email-settings')); },
|
|
152
|
+
async merchant_update_email_settings(body) { requireAuth(); return ok(await client.authPut('/users/email-settings', body)); },
|
|
153
|
+
async merchant_get_logistics_settings(_args) { requireAuth(); return ok(await client.authGet('/users/logistics-settings')); },
|
|
154
|
+
async merchant_update_logistics_settings(body) { requireAuth(); return ok(await client.authPut('/users/logistics-settings', body)); },
|
|
155
|
+
async merchant_get_checkout_settings(_args) { requireAuth(); return ok(await client.authGet('/users/checkout-settings')); },
|
|
156
|
+
async merchant_update_checkout_settings(body) { requireAuth(); return ok(await client.authPut('/users/checkout-settings', body)); },
|
|
157
|
+
async merchant_get_security_settings(_args) { requireAuth(); return ok(await client.authGet('/users/security-settings')); },
|
|
158
|
+
async merchant_add_allowed_domain({ domain }) { requireAuth(); return ok(await client.authPost('/users/security-settings/domains', { domain })); },
|
|
159
|
+
async merchant_remove_allowed_domain({ domain }) { requireAuth(); return ok(await client.authDelete(`/users/security-settings/domains/${domain}`)); },
|
|
160
|
+
|
|
161
|
+
// --- API KEYS ---
|
|
162
|
+
async merchant_list_api_keys(_args) { requireAuth(); return ok(await client.authGet('/keys')); },
|
|
163
|
+
async merchant_generate_api_key({ name }) { requireAuth(); return ok(await client.authPost('/keys', { name })); },
|
|
164
|
+
async merchant_delete_api_key({ id }) { requireAuth(); return ok(await client.authDelete(`/keys/${id}`)); },
|
|
165
|
+
|
|
166
|
+
// --- STOREFRONT GENERATION ---
|
|
167
|
+
async merchant_list_storefront_templates(_args) { requireAuth(); return ok(await client.authGet('/storefront/templates')); },
|
|
168
|
+
async merchant_generate_storefront_from_prompt(body) { requireAuth(); return ok(await client.authPost('/storefront/generate-from-prompt', body)); },
|
|
169
|
+
async merchant_get_deploy_options(_args) { requireAuth(); return ok(await client.authGet('/storefront/deploy-options')); },
|
|
170
|
+
|
|
171
|
+
// --- GITHUB ---
|
|
172
|
+
async merchant_get_github_status(_args) { requireAuth(); return ok(await client.authGet('/github/status')); },
|
|
173
|
+
async merchant_list_github_repos(_args) { requireAuth(); return ok(await client.authGet('/github/repos')); },
|
|
174
|
+
async merchant_push_to_github(body) { requireAuth(); return ok(await client.authPost('/github/push-code', body)); },
|
|
175
|
+
|
|
176
|
+
// --- LOGISTICS ---
|
|
177
|
+
async merchant_get_shipping_rates(body) { return ok(await client.post('/logistics/rates', body)); },
|
|
178
|
+
async merchant_create_shipment(body) { requireAuth(); return ok(await client.authPost('/logistics/shipment', body)); },
|
|
179
|
+
async merchant_track_shipment({ trackingId }) { return ok(await client.get(`/logistics/track/${trackingId}`)); },
|
|
180
|
+
|
|
181
|
+
// --- FINANCE ---
|
|
182
|
+
async merchant_get_consolidation_report(_args) { requireAuth(); return ok(await client.authGet('/consolidation/report')); },
|
|
183
|
+
|
|
184
|
+
// --- SHIPROCKET ---
|
|
185
|
+
async merchant_list_sr_orders(body) { requireAuth(); return ok(await client.authPost('/sr-checkout/order-list', body)); },
|
|
186
|
+
async merchant_initiate_refund({ orderId, amount }) { requireAuth(); return ok(await client.authPost('/sr-checkout/refund', { order_id: orderId, amount })); },
|
|
187
|
+
async merchant_sync_product_to_sr({ productId }) { requireAuth(); return ok(await client.authPost('/sr-checkout/catalog/sync-product', { productId })); },
|
|
188
|
+
|
|
189
|
+
// --- SHOPIFY MIGRATION ---
|
|
190
|
+
async merchant_shopify_migrate({ stage, shopifyStore, shopifyKey, sessionId }) {
|
|
191
|
+
requireAuth();
|
|
192
|
+
const epicmerchUrl = session.getApiUrl();
|
|
193
|
+
const epicmerchKey = session.getApiKey();
|
|
194
|
+
|
|
195
|
+
if (stage === 'extract') {
|
|
196
|
+
const { runExtract } = _require('../../epicmerch-migrate/src/stages/extract');
|
|
197
|
+
const { runTransform } = _require('../../epicmerch-migrate/src/stages/transform');
|
|
198
|
+
const { runPreview } = _require('../../epicmerch-migrate/src/stages/preview');
|
|
199
|
+
|
|
200
|
+
const raw = await runExtract({ source: 'api', credentials: { shopifyStore, shopifyKey } });
|
|
201
|
+
const transformed = runTransform(raw);
|
|
202
|
+
const diff = await runPreview({ transformed, epicmerchUrl, epicmerchKey });
|
|
203
|
+
|
|
204
|
+
const sid = uuidv4();
|
|
205
|
+
migrationSessions.set(sid, { diff, epicmerchUrl, epicmerchKey });
|
|
206
|
+
|
|
207
|
+
const summary = {
|
|
208
|
+
products: `${diff.products.new.length} new, ${diff.products.updated.length} updated, ${diff.products.unchanged.length} unchanged`,
|
|
209
|
+
customers: `${diff.customers.new.length} new`,
|
|
210
|
+
orders: `${diff.orders.new.length} new`,
|
|
211
|
+
categories: `${diff.categories.new.length} new`,
|
|
212
|
+
};
|
|
213
|
+
return { content: [{ type: 'text', text: `Preview ready (sessionId: ${sid}):\n${JSON.stringify(summary, null, 2)}\n\nCall merchant_shopify_migrate with stage="import" and sessionId="${sid}" to proceed.` }] };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (stage === 'import') {
|
|
217
|
+
if (!sessionId || !migrationSessions.has(sessionId)) {
|
|
218
|
+
return { content: [{ type: 'text', text: 'Invalid or expired sessionId. Run stage="extract" first.' }] };
|
|
219
|
+
}
|
|
220
|
+
const sess = migrationSessions.get(sessionId);
|
|
221
|
+
const { runImport } = _require('../../epicmerch-migrate/src/stages/import');
|
|
222
|
+
const { reuploadImages } = _require('../../epicmerch-migrate/src/stages/image-upload');
|
|
223
|
+
|
|
224
|
+
const result = await runImport({ diff: sess.diff, epicmerchUrl: sess.epicmerchUrl, epicmerchKey: sess.epicmerchKey });
|
|
225
|
+
reuploadImages({ pendingImages: result.pendingImages, epicmerchUrl: sess.epicmerchUrl, epicmerchKey: sess.epicmerchKey }).catch(() => {});
|
|
226
|
+
migrationSessions.delete(sessionId);
|
|
227
|
+
|
|
228
|
+
return { content: [{ type: 'text', text: `Migration complete!\nProducts: ${result.products.created} created, ${result.products.updated} updated\nCustomers: ${result.customers.created} created\nOrders: ${result.orders.created} created\n\nImages are re-uploading in the background.` }] };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { content: [{ type: 'text', text: 'Unknown stage. Use "extract" or "import".' }] };
|
|
232
|
+
},
|
|
233
|
+
|
|
234
|
+
// --- QUICK SETUP ---
|
|
235
|
+
async merchant_quick_setup({ email, password, storeName, currency, shopifyStore, shopifyKey }) {
|
|
236
|
+
// Best-effort pipeline: each step is independently caught. Steps after a failure
|
|
237
|
+
// continue running unless the failure is signup (which is the precondition for all
|
|
238
|
+
// subsequent steps).
|
|
239
|
+
const skipped = { status: 'skipped' };
|
|
240
|
+
const result = {
|
|
241
|
+
signup: skipped, currency: skipped, apiKey: skipped,
|
|
242
|
+
sampleProduct: skipped, sampleCategory: skipped,
|
|
243
|
+
paymentSetup: skipped, migration: skipped,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// --- Step 1: signup ---
|
|
247
|
+
try {
|
|
248
|
+
const data = await client.post('/users', {
|
|
249
|
+
name: storeName ?? email,
|
|
250
|
+
email,
|
|
251
|
+
password,
|
|
252
|
+
});
|
|
253
|
+
if (!data.token) throw new Error('Signup succeeded but no token returned');
|
|
254
|
+
session.setJwt(session.activeStore(), data.token);
|
|
255
|
+
result.signup = { status: 'ok', userId: data.user?._id ?? data.user?.id ?? null };
|
|
256
|
+
} catch (e) {
|
|
257
|
+
result.signup = { status: 'failed', reason: e.message || String(e) };
|
|
258
|
+
return ok(result); // every other step depends on having an auth token
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// --- Step 2: currency ---
|
|
262
|
+
try {
|
|
263
|
+
const value = currency ?? 'USD';
|
|
264
|
+
await client.authPut('/users/currency', { currency: value });
|
|
265
|
+
result.currency = { status: 'ok', value };
|
|
266
|
+
} catch (e) {
|
|
267
|
+
result.currency = { status: 'failed', reason: e.message || String(e) };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// --- Step 3: API key ---
|
|
271
|
+
try {
|
|
272
|
+
const data = await client.authPost('/keys', { name: 'default' });
|
|
273
|
+
result.apiKey = { status: 'ok', key: data.key ?? data.apiKey ?? null, id: data.id ?? data._id ?? null };
|
|
274
|
+
} catch (e) {
|
|
275
|
+
result.apiKey = { status: 'failed', reason: e.message || String(e) };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// --- Step 4: sample product ---
|
|
279
|
+
try {
|
|
280
|
+
const data = await client.authPost('/products', {
|
|
281
|
+
name: 'Welcome to EpicMerch',
|
|
282
|
+
price: 1.0,
|
|
283
|
+
description: 'A sample product to get you started. Delete or edit me anytime.',
|
|
284
|
+
type: 'physical',
|
|
285
|
+
});
|
|
286
|
+
result.sampleProduct = { status: 'ok', productId: data._id ?? data.id ?? null };
|
|
287
|
+
} catch (e) {
|
|
288
|
+
result.sampleProduct = { status: 'failed', reason: e.message || String(e) };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// --- Step 5: sample category ---
|
|
292
|
+
try {
|
|
293
|
+
const data = await client.authPost('/categories', { name: 'Featured', visible: true });
|
|
294
|
+
result.sampleCategory = { status: 'ok', categoryId: data._id ?? data.id ?? null };
|
|
295
|
+
} catch (e) {
|
|
296
|
+
result.sampleCategory = { status: 'failed', reason: e.message || String(e) };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// --- Step 6: payment setup status (read-only) ---
|
|
300
|
+
try {
|
|
301
|
+
const checkout = await client.authGet('/users/checkout-settings');
|
|
302
|
+
result.paymentSetup = {
|
|
303
|
+
status: 'pending',
|
|
304
|
+
currentType: checkout?.checkoutType ?? null,
|
|
305
|
+
link: '/settings/payment',
|
|
306
|
+
};
|
|
307
|
+
} catch (e) {
|
|
308
|
+
result.paymentSetup = { status: 'failed', reason: e.message || String(e) };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// --- Step 7: optional Shopify migration preview ---
|
|
312
|
+
if (shopifyStore && shopifyKey) {
|
|
313
|
+
try {
|
|
314
|
+
const epicmerchUrl = session.getApiUrl();
|
|
315
|
+
const epicmerchKey = result.apiKey?.key ?? session.getApiKey();
|
|
316
|
+
const { runExtract } = _require('../../epicmerch-migrate/src/stages/extract');
|
|
317
|
+
const { runTransform } = _require('../../epicmerch-migrate/src/stages/transform');
|
|
318
|
+
const { runPreview } = _require('../../epicmerch-migrate/src/stages/preview');
|
|
319
|
+
|
|
320
|
+
const raw = await runExtract({ source: 'api', credentials: { shopifyStore, shopifyKey } });
|
|
321
|
+
const transformed = runTransform(raw);
|
|
322
|
+
const diff = await runPreview({ transformed, epicmerchUrl, epicmerchKey });
|
|
323
|
+
|
|
324
|
+
const sid = uuidv4();
|
|
325
|
+
migrationSessions.set(sid, { diff, epicmerchUrl, epicmerchKey });
|
|
326
|
+
|
|
327
|
+
result.migration = {
|
|
328
|
+
status: 'preview_ready',
|
|
329
|
+
sessionId: sid,
|
|
330
|
+
diff: {
|
|
331
|
+
products: `${diff.products.new.length} new, ${diff.products.updated.length} updated, ${diff.products.unchanged.length} unchanged`,
|
|
332
|
+
customers: `${diff.customers.new.length} new`,
|
|
333
|
+
orders: `${diff.orders.new.length} new`,
|
|
334
|
+
categories: `${diff.categories.new.length} new`,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
} catch (e) {
|
|
338
|
+
result.migration = { status: 'failed', reason: e.message || String(e) };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return ok(result);
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
// --- STRIPE CONFIGURATION ---
|
|
346
|
+
async merchant_configure_stripe({ publishableKey, secretKey, webhookSecret }) {
|
|
347
|
+
requireAuth();
|
|
348
|
+
const body = {};
|
|
349
|
+
if (publishableKey !== undefined) body.stripePublishableKey = publishableKey;
|
|
350
|
+
if (secretKey !== undefined) body.stripeSecretKey = secretKey;
|
|
351
|
+
if (webhookSecret !== undefined) body.stripeWebhookSecret = webhookSecret;
|
|
352
|
+
return ok(await client.authPut('/users/checkout-settings', body));
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
async merchant_test_stripe_connection() {
|
|
356
|
+
requireAuth();
|
|
357
|
+
return ok(await client.authGet('/users/stripe/test'));
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
async merchant_set_checkout_type({ type }) {
|
|
361
|
+
requireAuth();
|
|
362
|
+
const valid = ['epicmerch', 'stripe', 'shiprocket'];
|
|
363
|
+
if (!valid.includes(type)) {
|
|
364
|
+
throw new Error(`checkoutType must be one of: ${valid.join(', ')}`);
|
|
365
|
+
}
|
|
366
|
+
return ok(await client.authPut('/users/checkout-settings', { checkoutType: type }));
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
// --- DIAGNOSE ---
|
|
370
|
+
async merchant_diagnose() {
|
|
371
|
+
requireAuth();
|
|
372
|
+
|
|
373
|
+
// Each section is fetched independently; one failure does not abort the others.
|
|
374
|
+
const safeGet = async (path) => {
|
|
375
|
+
try { return { ok: true, data: await client.authGet(path) }; }
|
|
376
|
+
catch (e) { return { ok: false, error: e.message || String(e) }; }
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
const [profileR, productsR, categoriesR, checkoutR, keysR] = await Promise.all([
|
|
380
|
+
safeGet('/users/profile'),
|
|
381
|
+
safeGet('/products'),
|
|
382
|
+
safeGet('/categories'),
|
|
383
|
+
safeGet('/users/checkout-settings'),
|
|
384
|
+
safeGet('/keys'),
|
|
385
|
+
]);
|
|
386
|
+
|
|
387
|
+
// Tolerate API shape drift: profile may be { user: {...} } or {...}; products may be
|
|
388
|
+
// an array or { products: [...] }.
|
|
389
|
+
const unwrap = (r, fallback) => (r.ok ? (r.data ?? fallback) : null);
|
|
390
|
+
const unwrapList = (r) => {
|
|
391
|
+
if (!r.ok) return null;
|
|
392
|
+
if (Array.isArray(r.data)) return r.data;
|
|
393
|
+
if (Array.isArray(r.data?.products)) return r.data.products;
|
|
394
|
+
if (Array.isArray(r.data?.categories)) return r.data.categories;
|
|
395
|
+
if (Array.isArray(r.data?.keys)) return r.data.keys;
|
|
396
|
+
return [];
|
|
397
|
+
};
|
|
398
|
+
const profile = unwrap(profileR, null);
|
|
399
|
+
const products = unwrapList(productsR);
|
|
400
|
+
const cats = unwrapList(categoriesR);
|
|
401
|
+
const checkout = unwrap(checkoutR, null);
|
|
402
|
+
const keys = unwrapList(keysR);
|
|
403
|
+
|
|
404
|
+
// Build each section, returning { error } if its fetch failed.
|
|
405
|
+
const store = profileR.ok ? {
|
|
406
|
+
name: profile?.name ?? profile?.user?.name ?? null,
|
|
407
|
+
currency: profile?.currency ?? profile?.user?.currency ?? null,
|
|
408
|
+
hasDomain: Boolean(profile?.domain ?? profile?.user?.domain),
|
|
409
|
+
hasLogo: Boolean(profile?.logo ?? profile?.user?.logo),
|
|
410
|
+
} : { error: profileR.error };
|
|
411
|
+
|
|
412
|
+
const catalog = productsR.ok && categoriesR.ok ? {
|
|
413
|
+
productCount: products.length,
|
|
414
|
+
categoryCount: cats.length,
|
|
415
|
+
outOfStockCount: products.filter((p) => (p.countInStock ?? 0) === 0).length,
|
|
416
|
+
} : { error: (productsR.error || categoriesR.error) };
|
|
417
|
+
|
|
418
|
+
const payment = checkoutR.ok ? {
|
|
419
|
+
razorpayConfigured: Boolean(checkout?.razorpayKeyId && checkout?.razorpayKeySecretIsSet),
|
|
420
|
+
stripeConfigured: Boolean(checkout?.stripeSecretKeyIsSet), // future-proofed for sub-project E
|
|
421
|
+
checkoutType: checkout?.checkoutType ?? null,
|
|
422
|
+
} : { error: checkoutR.error };
|
|
423
|
+
|
|
424
|
+
const logistics = checkoutR.ok ? {
|
|
425
|
+
shiprocketConfigured: Boolean(checkout?.shiprocketEmail && checkout?.shiprocketPasswordIsSet),
|
|
426
|
+
} : { error: checkoutR.error };
|
|
427
|
+
|
|
428
|
+
const apiKeys = keysR.ok ? {
|
|
429
|
+
count: keys.length,
|
|
430
|
+
lastUsedAt: keys
|
|
431
|
+
.map((k) => k.lastUsedAt)
|
|
432
|
+
.filter(Boolean)
|
|
433
|
+
.sort()
|
|
434
|
+
.reverse()[0] ?? null,
|
|
435
|
+
} : { error: keysR.error };
|
|
436
|
+
|
|
437
|
+
// --- Readiness score (max 100) ---
|
|
438
|
+
let score = 0;
|
|
439
|
+
const nextSteps = [];
|
|
440
|
+
|
|
441
|
+
if (!payment.error) {
|
|
442
|
+
if (payment.razorpayConfigured || payment.stripeConfigured) score += 40;
|
|
443
|
+
else nextSteps.push('Configure payment at /settings/payment (Razorpay or Stripe)');
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!catalog.error) {
|
|
447
|
+
const productPoints = Math.min(catalog.productCount, 3) * 8; // 8 pts/product, capped at 24
|
|
448
|
+
const productMax = catalog.productCount >= 3 ? 25 : productPoints;
|
|
449
|
+
score += productMax;
|
|
450
|
+
if (catalog.productCount < 3) nextSteps.push('Add at least 3 products to your catalog');
|
|
451
|
+
if (catalog.outOfStockCount > 0) nextSteps.push(`Restock ${catalog.outOfStockCount} out-of-stock product(s)`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (!logistics.error) {
|
|
455
|
+
if (logistics.shiprocketConfigured) score += 15;
|
|
456
|
+
else nextSteps.push('Connect Shiprocket at /settings/logistics');
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
if (!store.error) {
|
|
460
|
+
if (store.hasDomain) score += 10;
|
|
461
|
+
else nextSteps.push('Set a custom domain at /settings/profile');
|
|
462
|
+
if (store.hasLogo) score += 10;
|
|
463
|
+
else nextSteps.push('Add a store logo at /settings/profile');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return ok({
|
|
467
|
+
store,
|
|
468
|
+
catalog,
|
|
469
|
+
payment,
|
|
470
|
+
logistics,
|
|
471
|
+
apiKeys,
|
|
472
|
+
readinessScore: score,
|
|
473
|
+
nextSteps,
|
|
474
|
+
});
|
|
475
|
+
},
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const _baseMerchantToolDefs = [
|
|
480
|
+
// NOTE: merchant_login was deliberately removed in favour of the browser OAuth
|
|
481
|
+
// flow (`npx epicmerch-mcp login`). We do NOT expose a chat-callable tool that
|
|
482
|
+
// accepts credentials. See requireAuth() above for the error message merchants
|
|
483
|
+
// see when they call other tools without an active session.
|
|
484
|
+
{ name: 'merchant_list_products', description: 'List all products in the merchant dashboard.', schema: { page: { type: 'number' }, limit: { type: 'number' }, keyword: { type: 'string' } } },
|
|
485
|
+
{ name: 'merchant_get_product', description: 'Get a single product by ID.', schema: { id: { type: 'string' } } },
|
|
486
|
+
{
|
|
487
|
+
name: 'merchant_create_product',
|
|
488
|
+
description: 'Create a new product. Server currently creates with placeholder values then expects a follow-up merchant_update_product to set real data; pass all known fields here and a chained update may be needed.',
|
|
489
|
+
schema: {
|
|
490
|
+
name: { type: 'string' },
|
|
491
|
+
price: { type: 'number' },
|
|
492
|
+
salePrice: { type: 'number' },
|
|
493
|
+
discountPercent: { type: 'number' },
|
|
494
|
+
description: { type: 'string' },
|
|
495
|
+
type: { type: 'string' },
|
|
496
|
+
stock: { type: 'number' },
|
|
497
|
+
image: { type: 'string' },
|
|
498
|
+
},
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'merchant_update_product',
|
|
502
|
+
description: 'Update a product by ID. Pass any subset of fields to change.',
|
|
503
|
+
schema: {
|
|
504
|
+
id: { type: 'string' },
|
|
505
|
+
name: { type: 'string' },
|
|
506
|
+
price: { type: 'number' },
|
|
507
|
+
salePrice: { type: 'number' },
|
|
508
|
+
discountPercent: { type: 'number' },
|
|
509
|
+
description: { type: 'string' },
|
|
510
|
+
type: { type: 'string' },
|
|
511
|
+
stock: { type: 'number' },
|
|
512
|
+
image: { type: 'string' },
|
|
513
|
+
},
|
|
514
|
+
},
|
|
515
|
+
{ name: 'merchant_delete_product', description: 'Delete a product by ID.', schema: { id: { type: 'string' } } },
|
|
516
|
+
{ name: 'merchant_generate_product_description', description: 'Use AI to generate a product description.', schema: { productId: { type: 'string' }, name: { type: 'string' }, category: { type: 'string' } } },
|
|
517
|
+
{ name: 'merchant_get_product_recommendations', description: 'Get AI product recommendations.', schema: {} },
|
|
518
|
+
{ name: 'merchant_list_categories', description: 'List all categories.', schema: {} },
|
|
519
|
+
{ name: 'merchant_create_category', description: 'Create a category.', schema: { name: { type: 'string' }, visible: { type: 'boolean' } } },
|
|
520
|
+
{ name: 'merchant_update_category', description: 'Update a category.', schema: { id: { type: 'string' } } },
|
|
521
|
+
{ name: 'merchant_delete_category', description: 'Delete a category.', schema: { id: { type: 'string' } } },
|
|
522
|
+
{ name: 'merchant_toggle_category_visibility', description: 'Toggle a category visible/hidden.', schema: { id: { type: 'string' } } },
|
|
523
|
+
{ name: 'merchant_list_orders', description: 'List orders with optional filters.', schema: { page: { type: 'number' }, limit: { type: 'number' }, status: { type: 'string' } } },
|
|
524
|
+
{ name: 'merchant_get_order', description: 'Get order details by ID.', schema: { id: { type: 'string' } } },
|
|
525
|
+
{ name: 'merchant_update_order_status', description: 'Update order fulfilment status.', schema: { id: { type: 'string' }, status: { type: 'string' } } },
|
|
526
|
+
{ name: 'merchant_update_payment_status', description: 'Mark an order as paid/unpaid.', schema: { id: { type: 'string' }, isPaid: { type: 'boolean' } } },
|
|
527
|
+
{ name: 'merchant_list_customers', description: 'List all customers.', schema: {} },
|
|
528
|
+
{ name: 'merchant_get_customer', description: 'Get customer by ID.', schema: { id: { type: 'string' } } },
|
|
529
|
+
{ name: 'merchant_delete_customer', description: 'Delete a customer.', schema: { id: { type: 'string' } } },
|
|
530
|
+
{ name: 'merchant_get_analytics_stats', description: 'Get overall store stats (revenue, orders, customers).', schema: {} },
|
|
531
|
+
{ name: 'merchant_get_sales_trend', description: 'Get sales over time.', schema: { period: { type: 'string' } } },
|
|
532
|
+
{ name: 'merchant_get_top_products', description: 'Get best-selling products.', schema: {} },
|
|
533
|
+
{ name: 'merchant_get_traffic', description: 'Get storefront traffic stats.', schema: {} },
|
|
534
|
+
{ name: 'merchant_get_revenue_by_category', description: 'Revenue breakdown by product category.', schema: {} },
|
|
535
|
+
{ name: 'merchant_get_order_status_distribution', description: 'Order status breakdown.', schema: {} },
|
|
536
|
+
{ name: 'merchant_get_peak_hours', description: 'Peak order/traffic hours.', schema: {} },
|
|
537
|
+
{ name: 'merchant_get_geographic_distribution', description: 'Order geographic breakdown.', schema: {} },
|
|
538
|
+
{ name: 'merchant_get_period_comparison', description: 'Compare current vs previous period.', schema: { period: { type: 'string' } } },
|
|
539
|
+
{ name: 'merchant_get_cart_abandonment', description: 'Cart abandonment rate stats.', schema: {} },
|
|
540
|
+
{ name: 'merchant_get_ai_insights', description: 'Get AI-powered analytics insights.', schema: {} },
|
|
541
|
+
{ name: 'merchant_list_subscribers', description: 'List newsletter subscribers.', schema: {} },
|
|
542
|
+
{ name: 'merchant_send_notification', description: 'Send email or WhatsApp notification.', schema: { to: { type: 'string' }, subject: { type: 'string' }, message: { type: 'string' }, type: { type: 'string' } } },
|
|
543
|
+
{ name: 'merchant_send_to_user', description: 'Send notification to a specific user.', schema: { userId: { type: 'string' }, subject: { type: 'string' }, message: { type: 'string' } } },
|
|
544
|
+
{ name: 'merchant_get_abandoned_carts', description: 'List customers with abandoned carts.', schema: { hours: { type: 'number' } } },
|
|
545
|
+
{ name: 'merchant_get_notification_history', description: 'Get notification send history.', schema: {} },
|
|
546
|
+
{ name: 'merchant_get_notification_stats', description: 'Get notification open/click stats.', schema: {} },
|
|
547
|
+
{ name: 'merchant_get_settings', description: 'Get all merchant settings.', schema: {} },
|
|
548
|
+
{ name: 'merchant_update_currency', description: 'Update store currency.', schema: { currency: { type: 'string' } } },
|
|
549
|
+
{ name: 'merchant_get_email_settings', description: 'Get email settings.', schema: {} },
|
|
550
|
+
{ name: 'merchant_update_email_settings', description: 'Update email settings.', schema: {} },
|
|
551
|
+
{ name: 'merchant_get_logistics_settings', description: 'Get logistics/shipping settings.', schema: {} },
|
|
552
|
+
{ name: 'merchant_update_logistics_settings', description: 'Update logistics settings.', schema: {} },
|
|
553
|
+
{ name: 'merchant_get_checkout_settings', description: 'Get checkout settings.', schema: {} },
|
|
554
|
+
{ name: 'merchant_update_checkout_settings', description: 'Update checkout settings.', schema: {} },
|
|
555
|
+
{ name: 'merchant_get_security_settings', description: 'Get security settings and allowed domains.', schema: {} },
|
|
556
|
+
{ name: 'merchant_add_allowed_domain', description: 'Add an allowed CORS domain.', schema: { domain: { type: 'string' } } },
|
|
557
|
+
{ name: 'merchant_remove_allowed_domain', description: 'Remove an allowed CORS domain.', schema: { domain: { type: 'string' } } },
|
|
558
|
+
{ name: 'merchant_list_api_keys', description: 'List all API keys.', schema: {} },
|
|
559
|
+
{ name: 'merchant_generate_api_key', description: 'Generate a new API key.', schema: { name: { type: 'string' } } },
|
|
560
|
+
{ name: 'merchant_delete_api_key', description: 'Delete an API key.', schema: { id: { type: 'string' } } },
|
|
561
|
+
{ name: 'merchant_list_storefront_templates', description: 'List available storefront templates.', schema: {} },
|
|
562
|
+
{ name: 'merchant_generate_storefront_from_prompt', description: 'Generate a storefront from an AI prompt.', schema: { prompt: { type: 'string' } } },
|
|
563
|
+
{ name: 'merchant_get_deploy_options', description: 'Get available deployment options.', schema: {} },
|
|
564
|
+
{ name: 'merchant_get_github_status', description: 'Get GitHub connection status.', schema: {} },
|
|
565
|
+
{ name: 'merchant_list_github_repos', description: 'List connected GitHub repositories.', schema: {} },
|
|
566
|
+
{ name: 'merchant_push_to_github', description: 'Push storefront code to a GitHub repo.', schema: { repo: { type: 'string' }, message: { type: 'string' } } },
|
|
567
|
+
{ name: 'merchant_get_shipping_rates', description: 'Get shipping rates for an order.', schema: {} },
|
|
568
|
+
{ name: 'merchant_create_shipment', description: 'Create a shipment for an order.', schema: { orderId: { type: 'string' } } },
|
|
569
|
+
{ name: 'merchant_track_shipment', description: 'Track a shipment by tracking ID.', schema: { trackingId: { type: 'string' } } },
|
|
570
|
+
{ name: 'merchant_get_consolidation_report', description: 'Get financial consolidation report.', schema: {} },
|
|
571
|
+
{ name: 'merchant_list_sr_orders', description: 'List Shiprocket orders.', schema: {} },
|
|
572
|
+
{ name: 'merchant_initiate_refund', description: 'Initiate a Shiprocket refund.', schema: { orderId: { type: 'string' }, amount: { type: 'number' } } },
|
|
573
|
+
{ name: 'merchant_sync_product_to_sr', description: 'Sync a product to Shiprocket catalog.', schema: { productId: { type: 'string' } } },
|
|
574
|
+
{
|
|
575
|
+
name: 'merchant_quick_setup',
|
|
576
|
+
description: 'White-glove onboarding for a new merchant. Creates an account, sets currency, generates an API key, creates a sample product and category, reports payment configuration status, and (if shopifyStore + shopifyKey are provided) returns a Shopify migration preview. Each step is independently caught; the structured return reports per-step status.',
|
|
577
|
+
schema: {
|
|
578
|
+
email: { type: 'string' },
|
|
579
|
+
password: { type: 'string' },
|
|
580
|
+
storeName: { type: 'string' },
|
|
581
|
+
currency: { type: 'string' },
|
|
582
|
+
shopifyStore: { type: 'string' },
|
|
583
|
+
shopifyKey: { type: 'string' },
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
{ name: 'merchant_diagnose', description: 'Check overall store health: configuration status, product/category counts, payment + logistics setup, API keys, and a 0-100 readiness score with a prioritised list of next steps.', schema: {} },
|
|
587
|
+
{
|
|
588
|
+
name: 'merchant_configure_stripe',
|
|
589
|
+
description: 'Configure Stripe payment processing for the merchant: stores publishable key, secret key, and webhook signing secret. Pair with merchant_set_checkout_type({type:"stripe"}) to activate.',
|
|
590
|
+
schema: {
|
|
591
|
+
publishableKey: { type: 'string' },
|
|
592
|
+
secretKey: { type: 'string' },
|
|
593
|
+
webhookSecret: { type: 'string' },
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
{
|
|
597
|
+
name: 'merchant_test_stripe_connection',
|
|
598
|
+
description: 'Verify the merchant\'s Stripe keys work by fetching their Stripe account. Returns accountId, country, and charges-enabled status.',
|
|
599
|
+
schema: {},
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
name: 'merchant_set_checkout_type',
|
|
603
|
+
description: 'Set which checkout flow the merchant\'s storefront uses. Must be one of: "epicmerch" (Razorpay + Shiprocket), "stripe" (Stripe + Shiprocket), or "shiprocket" (full SR Checkout).',
|
|
604
|
+
schema: { type: { type: 'string' } },
|
|
605
|
+
},
|
|
606
|
+
{
|
|
607
|
+
name: 'merchant_shopify_migrate',
|
|
608
|
+
description: 'Migrate a Shopify store to EpicMerch. stage="extract" fetches Shopify data and returns a preview diff + sessionId. stage="import" runs the actual import for an approved sessionId.',
|
|
609
|
+
schema: {
|
|
610
|
+
stage: { type: 'string' },
|
|
611
|
+
shopifyStore: { type: 'string' },
|
|
612
|
+
shopifyKey: { type: 'string' },
|
|
613
|
+
sessionId: { type: 'string' },
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
export const merchantToolDefs = withExamples(_baseMerchantToolDefs);
|