epicmerch-mcp 1.3.21 → 1.3.22
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/.claude-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/skills/epicmerch-storefront.md +575 -90
- package/skills/epicmerch.md +4 -7
- package/src/adapters/mcp.js +2 -1
- package/src/adapters/openapi.js +2 -1
- package/src/version.js +13 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "epicmerch",
|
|
3
3
|
"description": "Run your EpicMerch store from chat — products, orders, analytics, Shopify migration, Stripe setup. Auto-installs the MCP + slash commands + browser OAuth.",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.3.22",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Aditya Patel",
|
|
7
7
|
"email": "aditya141312@gmail.com",
|
package/package.json
CHANGED
|
@@ -1,123 +1,608 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: epicmerch-storefront
|
|
3
|
-
description: EpicMerch storefront scaffold —
|
|
3
|
+
description: EpicMerch storefront scaffold — SDK + Login + ProductCard + ProductList + Cart + Checkout + OrderHistory + .env with auto-generated API key. The "do everything" route under /epicmerch, and also the home for single-slice integration (auth-only, products-only, or orders-only).
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# EpicMerch Storefront Scaffold
|
|
6
|
+
# EpicMerch Full Storefront Scaffold
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
`store_scaffold_full` tool** (instant — do NOT hand-write those files), apply a
|
|
10
|
-
**theme delta**, and only LLM-generate genuinely custom pages (in parallel).
|
|
8
|
+
You are integrating EpicMerch into this project. EpicMerch is a complete e-commerce backend that handles auth, inventory, orders, and payments. Follow every step below in order without asking for confirmation unless a step explicitly says to ask.
|
|
11
9
|
|
|
12
|
-
|
|
13
|
-
exists. If it does, SKIP it and tell the user "X already exists — keeping it".
|
|
14
|
-
Never overwrite a merchant's existing file.
|
|
10
|
+
## Scope — full storefront, or just one slice
|
|
15
11
|
|
|
16
|
-
|
|
12
|
+
This skill scaffolds the **full** storefront by default. It also absorbs the former `/epicmerch-auth`, `/epicmerch-products`, and `/epicmerch-orders` skills — if the merchant asked for only one slice, run just that subset. **Steps 0–3 always apply** (survey, detect framework, install SDK, write `src/lib/epicmerch.js`); then:
|
|
17
13
|
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- none / no `package.json` → tell the user "No frontend project detected here — run me from inside a React/Vite/Next project." STOP.
|
|
23
|
-
- List which scaffold targets already exist (so Phase 1 can skip them):
|
|
24
|
-
`src/lib/epicmerch.js`, `src/components/{Login,ProductCard,ProductList,Cart,Checkout,OrderHistory}.jsx`, `.env`, `.env.example`.
|
|
25
|
-
- Briefly tell the user which exist (will be kept) vs which will be created.
|
|
14
|
+
- **"add login / OTP / sign-in"** → also Step 4 (Login.jsx).
|
|
15
|
+
- **"add a product catalog / product list / search"** → also Step 5 (+5b) (ProductCard + ProductList, including the keep-existing-card adapter path).
|
|
16
|
+
- **"add a cart / checkout / order history"** → also Step 6 (Cart + Checkout + OrderHistory).
|
|
17
|
+
- **"set up everything" / unsure** → the whole checklist.
|
|
26
18
|
|
|
27
|
-
|
|
19
|
+
Step 7 (API key + `.env`) and Step 8 (next steps) run in every mode. The components and contracts are identical across modes — only which files you write changes.
|
|
28
20
|
|
|
29
|
-
|
|
21
|
+
**Idempotency rule (apply throughout):** Before writing any file in the scaffold steps below, check if it already exists. If it does, SKIP that file and tell the user "X already exists — keeping it". Never overwrite existing files. The merchant may already have a polished version of any of these components; preserving their work is more important than completing the scaffold.
|
|
30
22
|
|
|
23
|
+
## Checklist
|
|
24
|
+
|
|
25
|
+
- [ ] **Step 0: Survey the project**
|
|
26
|
+
|
|
27
|
+
List which of the scaffold target files already exist in this project:
|
|
28
|
+
- `src/lib/epicmerch.js`
|
|
29
|
+
- `src/components/Login.jsx`
|
|
30
|
+
- `src/components/ProductCard.jsx`
|
|
31
|
+
- `src/components/ProductList.jsx`
|
|
32
|
+
- `src/components/Cart.jsx`
|
|
33
|
+
- `src/components/Checkout.jsx`
|
|
34
|
+
- `src/components/OrderHistory.jsx`
|
|
35
|
+
- `.env.example`
|
|
36
|
+
- `.env`
|
|
37
|
+
|
|
38
|
+
Briefly tell the user which already exist (those will be skipped) and which will be created. Then proceed.
|
|
39
|
+
|
|
40
|
+
- [ ] **Step 1: Detect framework**
|
|
41
|
+
|
|
42
|
+
Read `package.json`. Determine the framework:
|
|
43
|
+
- If `"react"` or `"vite"` in dependencies → framework = `react`, envPrefix = `VITE_`
|
|
44
|
+
- If `"next"` in dependencies → framework = `react`, envPrefix = `NEXT_PUBLIC_`
|
|
45
|
+
- If `"vue"` in dependencies → framework = `react`, envPrefix = `VITE_`; tell the user: "Vue is not yet natively supported — installing React templates instead."
|
|
46
|
+
- Otherwise → framework = `react`, envPrefix = `VITE_`
|
|
47
|
+
|
|
48
|
+
Tell the user: "Detected [framework]. Installing EpicMerch SDK..."
|
|
49
|
+
|
|
50
|
+
- [ ] **Step 2: Install the SDK**
|
|
51
|
+
|
|
52
|
+
Check `package.json` first. If `"epicmerch"` is already in `dependencies`, skip the install and tell the user "epicmerch SDK already installed". Otherwise:
|
|
53
|
+
|
|
54
|
+
Run: `npm install epicmerch`
|
|
55
|
+
|
|
56
|
+
- [ ] **Step 3: Create SDK initializer**
|
|
57
|
+
|
|
58
|
+
**If `src/lib/epicmerch.js` already exists, skip this step entirely** and tell the user "src/lib/epicmerch.js already exists — keeping it". Otherwise, write `src/lib/epicmerch.js` using the `envPrefix` detected in Step 1 to substitute the env var names:
|
|
59
|
+
|
|
60
|
+
- For `VITE_` prefix (Vite / React):
|
|
61
|
+
```js
|
|
62
|
+
import EpicMerch from 'epicmerch';
|
|
63
|
+
|
|
64
|
+
export const store = new EpicMerch({
|
|
65
|
+
apiKey: import.meta.env.VITE_API_KEY,
|
|
66
|
+
...(import.meta.env.VITE_API_URL && { baseUrl: import.meta.env.VITE_API_URL }),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Restore the customer session on page load. Without this, OTP login
|
|
70
|
+
// appears to "work" (token gets saved to localStorage) but the SDK
|
|
71
|
+
// stays unauthenticated until something calls setCustomerToken — so
|
|
72
|
+
// the merchant logs in, the page reloads, and they're logged out again.
|
|
73
|
+
if (typeof window !== 'undefined') {
|
|
74
|
+
try {
|
|
75
|
+
const saved = localStorage.getItem('customerInfo');
|
|
76
|
+
if (saved) {
|
|
77
|
+
const parsed = JSON.parse(saved);
|
|
78
|
+
if (parsed?.token) store.setCustomerToken(parsed.token);
|
|
79
|
+
}
|
|
80
|
+
} catch (_) { /* localStorage unavailable — silent fallback */ }
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- For `NEXT_PUBLIC_` prefix (Next.js):
|
|
85
|
+
```js
|
|
86
|
+
import EpicMerch from 'epicmerch';
|
|
87
|
+
|
|
88
|
+
export const store = new EpicMerch({
|
|
89
|
+
apiKey: process.env.NEXT_PUBLIC_API_KEY,
|
|
90
|
+
...(process.env.NEXT_PUBLIC_API_URL && { baseUrl: process.env.NEXT_PUBLIC_API_URL }),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Restore customer session on page load (same as Vite — without this,
|
|
94
|
+
// OTP login looks like it works but doesn't survive a page reload).
|
|
95
|
+
// Guard the localStorage call so server-rendered pages don't crash.
|
|
96
|
+
if (typeof window !== 'undefined') {
|
|
97
|
+
try {
|
|
98
|
+
const saved = localStorage.getItem('customerInfo');
|
|
99
|
+
if (saved) {
|
|
100
|
+
const parsed = JSON.parse(saved);
|
|
101
|
+
if (parsed?.token) store.setCustomerToken(parsed.token);
|
|
102
|
+
}
|
|
103
|
+
} catch (_) { /* localStorage unavailable */ }
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- [ ] **Step 4: Scaffold auth**
|
|
108
|
+
|
|
109
|
+
**If `src/components/Login.jsx` already exists, skip this step** and tell the user "Login.jsx already exists — keeping it". Otherwise write `src/components/Login.jsx`:
|
|
110
|
+
|
|
111
|
+
```jsx
|
|
112
|
+
import { useState } from 'react';
|
|
113
|
+
import { store } from '../lib/epicmerch';
|
|
114
|
+
|
|
115
|
+
export default function Login({ onLogin }) {
|
|
116
|
+
const [phone, setPhone] = useState('');
|
|
117
|
+
const [otp, setOtp] = useState('');
|
|
118
|
+
const [step, setStep] = useState('phone');
|
|
119
|
+
const [error, setError] = useState('');
|
|
120
|
+
|
|
121
|
+
const sendOtp = async () => {
|
|
122
|
+
setError('');
|
|
123
|
+
try {
|
|
124
|
+
await store.auth.sendOtp(phone, 'phone');
|
|
125
|
+
setStep('otp');
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// e.g. invalid number, or OTP_RATE_LIMIT_EXCEEDED (5/hour).
|
|
128
|
+
setError(e?.message || 'Could not send OTP. Check the number and try again.');
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const verify = async () => {
|
|
133
|
+
setError('');
|
|
134
|
+
try {
|
|
135
|
+
const result = await store.auth.verifyOtp(phone, otp, {});
|
|
136
|
+
if (result.token) {
|
|
137
|
+
// SDK already sets the token internally during verifyOtp; mirror to
|
|
138
|
+
// localStorage so src/lib/epicmerch.js can restore on next page load.
|
|
139
|
+
store.setCustomerToken(result.token);
|
|
140
|
+
localStorage.setItem('customerInfo', JSON.stringify(result));
|
|
141
|
+
onLogin(result);
|
|
142
|
+
}
|
|
143
|
+
} catch (e) {
|
|
144
|
+
// e.g. "Invalid OTP" / "OTP expired" — let them retry.
|
|
145
|
+
setError(e?.message || 'Invalid or expired OTP. Try again.');
|
|
146
|
+
setOtp('');
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<div>
|
|
152
|
+
{step === 'phone' ? (
|
|
153
|
+
<>
|
|
154
|
+
<input
|
|
155
|
+
type="tel"
|
|
156
|
+
value={phone}
|
|
157
|
+
onChange={e => setPhone(e.target.value)}
|
|
158
|
+
placeholder="+919876543210"
|
|
159
|
+
autoComplete="tel"
|
|
160
|
+
/>
|
|
161
|
+
<button onClick={sendOtp}>Send OTP</button>
|
|
162
|
+
</>
|
|
163
|
+
) : (
|
|
164
|
+
<>
|
|
165
|
+
{/* EpicMerch OTPs are 4 digits. inputMode='numeric' pops the
|
|
166
|
+
numeric keypad on mobile; autoComplete='one-time-code' lets
|
|
167
|
+
iOS auto-fill from SMS without leaving the page. */}
|
|
168
|
+
<input
|
|
169
|
+
type="tel"
|
|
170
|
+
inputMode="numeric"
|
|
171
|
+
maxLength={4}
|
|
172
|
+
autoComplete="one-time-code"
|
|
173
|
+
value={otp}
|
|
174
|
+
onChange={e => setOtp(e.target.value.replace(/\D/g, '').slice(0, 4))}
|
|
175
|
+
placeholder="4-digit OTP"
|
|
176
|
+
autoFocus
|
|
177
|
+
/>
|
|
178
|
+
<button onClick={verify} disabled={otp.length !== 4}>Verify</button>
|
|
179
|
+
</>
|
|
180
|
+
)}
|
|
181
|
+
{error && <p style={{ color: 'crimson' }}>{error}</p>}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- [ ] **Step 5: Scaffold product catalog**
|
|
188
|
+
|
|
189
|
+
For each of the two files in this step, check existence individually. If a file already exists, skip writing it and tell the user "X already exists — keeping it". Otherwise write it.
|
|
190
|
+
|
|
191
|
+
**Special case — `ProductCard.jsx` already exists:** don't just skip it. The merchant has a card UI that's probably showing placeholder data. KEEP their card, but make sure it gets fed REAL products: (1) read the existing card to note the field names it expects (`name`/`title`, `price`/`cost`, `image`/`images[0]`/`thumbnail`, single `product` prop vs destructured); (2) when you write/keep `ProductList.jsx` below, have it call `store.products.list()` and map each API product onto the shape the existing card expects (an adapter — see the ProductList note below). Tell the user "ProductCard.jsx already exists — keeping your design and wiring it to live product data."
|
|
192
|
+
|
|
193
|
+
Write `src/components/ProductCard.jsx`:
|
|
194
|
+
|
|
195
|
+
```jsx
|
|
196
|
+
import { useState } from 'react';
|
|
197
|
+
|
|
198
|
+
export default function ProductCard({ product, onAddToCart }) {
|
|
199
|
+
// ProductVariant rows are { variant: String, stock: Int } — note the field
|
|
200
|
+
// is named `variant`, NOT `size`. The "variant" string is whatever the
|
|
201
|
+
// merchant configured (typically a size like 'S'/'M'/'L', but could be
|
|
202
|
+
// a color or material). Only show in-stock options — out-of-stock orders
|
|
203
|
+
// are rejected with "INSUFFICIENT_STOCK: <variant>: need N, have 0".
|
|
204
|
+
const availableVariants = (product.variants || []).filter(v => v.stock > 0);
|
|
205
|
+
const [variant, setVariant] = useState(availableVariants[0]?.variant);
|
|
206
|
+
|
|
207
|
+
// Products on sale return both `price` (original) and `salePrice` (current).
|
|
208
|
+
// Always display the current price the customer will actually pay.
|
|
209
|
+
const price = product.salePrice ?? product.price;
|
|
210
|
+
const image = product.images?.[0] || product.image;
|
|
211
|
+
const hasVariants = availableVariants.length > 0;
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<div className="product-card">
|
|
215
|
+
<img src={image} alt={product.name} />
|
|
216
|
+
<h3>{product.name}</h3>
|
|
217
|
+
<p>₹{price}</p>
|
|
218
|
+
{hasVariants && (
|
|
219
|
+
<div className="variant-picker">
|
|
220
|
+
{availableVariants.map(v => (
|
|
221
|
+
<button
|
|
222
|
+
key={v.variant}
|
|
223
|
+
type="button"
|
|
224
|
+
onClick={() => setVariant(v.variant)}
|
|
225
|
+
className={variant === v.variant ? 'selected' : ''}
|
|
226
|
+
>
|
|
227
|
+
{v.variant}
|
|
228
|
+
</button>
|
|
229
|
+
))}
|
|
230
|
+
</div>
|
|
231
|
+
)}
|
|
232
|
+
<button
|
|
233
|
+
onClick={() => onAddToCart(product._id, variant)}
|
|
234
|
+
disabled={hasVariants && !variant}
|
|
235
|
+
>
|
|
236
|
+
Add to Cart
|
|
237
|
+
</button>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Write `src/components/ProductList.jsx`:
|
|
244
|
+
|
|
245
|
+
```jsx
|
|
246
|
+
import { useEffect, useState } from 'react';
|
|
247
|
+
import { store } from '../lib/epicmerch';
|
|
248
|
+
import ProductCard from './ProductCard';
|
|
249
|
+
|
|
250
|
+
export default function ProductList() {
|
|
251
|
+
const [products, setProducts] = useState([]);
|
|
252
|
+
const [categories, setCategories] = useState([]);
|
|
253
|
+
const [category, setCategory] = useState(undefined);
|
|
254
|
+
const [query, setQuery] = useState('');
|
|
255
|
+
// products.list/search return { products, page, pages, total } — track
|
|
256
|
+
// page + pages for "Load more" so a catalog (or result set) larger than
|
|
257
|
+
// `limit` isn't silently truncated to the first page.
|
|
258
|
+
const [page, setPage] = useState(1);
|
|
259
|
+
const [pages, setPages] = useState(1);
|
|
260
|
+
|
|
261
|
+
useEffect(() => { store.categories.list().then(setCategories); }, []);
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
const req = query.trim()
|
|
265
|
+
? store.products.search(query, { page, limit: 12 })
|
|
266
|
+
: store.products.list({ type: category, page, limit: 12 });
|
|
267
|
+
req.then(d => {
|
|
268
|
+
setPages(d.pages ?? 1);
|
|
269
|
+
// Page 1 replaces (new filter); later pages append (Load more).
|
|
270
|
+
setProducts(prev => (page === 1 ? (d.products ?? d) : [...prev, ...(d.products ?? d)]));
|
|
271
|
+
});
|
|
272
|
+
}, [query, category, page]);
|
|
273
|
+
|
|
274
|
+
// Reset to page 1 whenever the filter changes.
|
|
275
|
+
const changeQuery = (q) => { setPage(1); setQuery(q); };
|
|
276
|
+
const changeCategory = (c) => { setPage(1); setQuery(''); setCategory(c === 'All' ? undefined : c); };
|
|
277
|
+
|
|
278
|
+
// `variant` is the size STRING (e.g. 'L'), per the SDK signature
|
|
279
|
+
// store.cart.add(productId, qty, variant). Products without variants
|
|
280
|
+
// pass undefined and the SDK accepts that.
|
|
281
|
+
const addToCart = (productId, variant) => store.cart.add(productId, 1, variant);
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div>
|
|
285
|
+
<input
|
|
286
|
+
placeholder="Search products..."
|
|
287
|
+
value={query}
|
|
288
|
+
onChange={e => changeQuery(e.target.value)}
|
|
289
|
+
/>
|
|
290
|
+
<div>
|
|
291
|
+
{categories.map(c => (
|
|
292
|
+
<button key={c} onClick={() => changeCategory(c)}>{c}</button>
|
|
293
|
+
))}
|
|
294
|
+
</div>
|
|
295
|
+
<div className="product-grid">
|
|
296
|
+
{products.map(p => <ProductCard key={p._id} product={p} onAddToCart={addToCart} />)}
|
|
297
|
+
</div>
|
|
298
|
+
{page < pages && (
|
|
299
|
+
<button onClick={() => setPage(p => p + 1)}>Load more</button>
|
|
300
|
+
)}
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**If you KEPT an existing `ProductCard` (it already existed):** the API returns each product as `{ _id, name, price, salePrice, image, images, variants, ... }`. If the merchant's card reads different field names, don't change the card — adapt the data in the `.map()` so live products populate their existing design:
|
|
307
|
+
|
|
308
|
+
```jsx
|
|
309
|
+
// e.g. existing card expects { title, cost, thumbnail }:
|
|
310
|
+
{products.map(p => (
|
|
311
|
+
<ProductCard
|
|
312
|
+
key={p._id}
|
|
313
|
+
product={{ ...p, title: p.name, cost: p.salePrice ?? p.price, thumbnail: p.images?.[0] || p.image }}
|
|
314
|
+
onAddToCart={addToCart}
|
|
315
|
+
/>
|
|
316
|
+
))}
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
Match the adapter keys to whatever the existing card actually reads (you noted that when you decided to keep it). The cards keep the merchant's look but show **real products fetched via `store.products.list()`**.
|
|
320
|
+
|
|
321
|
+
- [ ] **Step 5b: Wire up an existing search bar (only if `ProductList.jsx` was skipped)**
|
|
322
|
+
|
|
323
|
+
If you wrote a fresh `ProductList.jsx` in Step 5, skip this step — the canonical scaffold already has search wired.
|
|
324
|
+
|
|
325
|
+
If `ProductList.jsx` was SKIPPED because it already existed, the merchant has their own UI. They may have a search bar somewhere — in a `Navbar`, a `Header`, a dedicated `SearchBar.jsx`, a search page, etc. — that isn't currently calling EpicMerch's search API. Detect and offer to wire it up.
|
|
326
|
+
|
|
327
|
+
**Detection.** Look through `src/` for files containing any of these signals (case-insensitive):
|
|
328
|
+
- `<input` whose `placeholder` mentions `search`
|
|
329
|
+
- A state variable named `query`, `search`, `searchTerm`, `searchQuery`, or `keyword`
|
|
330
|
+
- A handler named `onSearch`, `handleSearch`, `onQueryChange`, or similar
|
|
331
|
+
|
|
332
|
+
For each match, check whether the file already calls `store.products.search(`. If yes, it's wired — skip silently.
|
|
333
|
+
|
|
334
|
+
**For each unwired match:** show the merchant the file path and the snippet you found, then say:
|
|
335
|
+
|
|
336
|
+
> "I noticed a search input in `<path>` that doesn't call EpicMerch's search yet. The one-line wire-up is:
|
|
337
|
+
>
|
|
338
|
+
> ```js
|
|
339
|
+
> import { store } from '../lib/epicmerch'; // add at top if missing
|
|
340
|
+
>
|
|
341
|
+
> // inside the search handler / useEffect:
|
|
342
|
+
> store.products.search(query).then(d => setProducts(d.products ?? d));
|
|
343
|
+
> ```
|
|
344
|
+
>
|
|
345
|
+
> Want me to apply this to your component? (yes / no — I'll leave it alone if no.)"
|
|
346
|
+
|
|
347
|
+
**WAIT for an explicit yes before editing.** If the merchant declines, move on without changing the file.
|
|
348
|
+
|
|
349
|
+
If you find no search bar anywhere in `src/`, skip this step silently — don't tell the merchant "no search bar found", just continue to Step 6.
|
|
350
|
+
|
|
351
|
+
- [ ] **Step 6: Scaffold cart + orders**
|
|
352
|
+
|
|
353
|
+
For each of the three files in this step (Cart.jsx, Checkout.jsx, OrderHistory.jsx), check existence individually. If a file already exists, skip writing it and tell the user "X already exists — keeping it". Otherwise write it.
|
|
354
|
+
|
|
355
|
+
Write `src/components/Cart.jsx`:
|
|
356
|
+
|
|
357
|
+
```jsx
|
|
358
|
+
import { useEffect, useState } from 'react';
|
|
359
|
+
import { store } from '../lib/epicmerch';
|
|
360
|
+
|
|
361
|
+
export default function Cart({ onCheckout }) {
|
|
362
|
+
// store.cart.get() returns { cart: [...] } — JUST one field. The SDK
|
|
363
|
+
// README mentions cartCount/cartTotal but those aren't actually sent by
|
|
364
|
+
// the server; compute them locally. Each line item is shaped
|
|
365
|
+
// { product: { _id, name, price, originalPrice, salePrice, image, type },
|
|
366
|
+
// qty, variant }. Note `product.price` on cart items is ALREADY the
|
|
367
|
+
// effective price (sale if on sale, else regular).
|
|
368
|
+
const [cart, setCart] = useState({ cart: [] });
|
|
369
|
+
|
|
370
|
+
useEffect(() => { store.cart.get().then(setCart); }, []);
|
|
371
|
+
|
|
372
|
+
const remove = async (productId, variant) => {
|
|
373
|
+
await store.cart.remove(productId, variant);
|
|
374
|
+
// Optimistic local update — match on BOTH productId AND variant so we
|
|
375
|
+
// don't accidentally remove a different size of the same product.
|
|
376
|
+
setCart(c => ({
|
|
377
|
+
...c,
|
|
378
|
+
cart: (c.cart || []).filter(
|
|
379
|
+
i => !(i.product._id === productId && i.variant === variant),
|
|
380
|
+
),
|
|
381
|
+
}));
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const items = cart.cart || [];
|
|
385
|
+
const total = items.reduce((sum, i) => {
|
|
386
|
+
const price = i.product.salePrice ?? i.product.price;
|
|
387
|
+
return sum + price * i.qty;
|
|
388
|
+
}, 0);
|
|
389
|
+
|
|
390
|
+
return (
|
|
391
|
+
<div>
|
|
392
|
+
{items.map((item, idx) => {
|
|
393
|
+
const price = item.product.salePrice ?? item.product.price;
|
|
394
|
+
return (
|
|
395
|
+
<div key={`${item.product._id}-${item.variant || 'novariant'}-${idx}`}>
|
|
396
|
+
<span>
|
|
397
|
+
{item.product.name} x{item.qty}
|
|
398
|
+
{item.variant ? ` (${item.variant})` : ''}
|
|
399
|
+
</span>
|
|
400
|
+
<span>₹{price * item.qty}</span>
|
|
401
|
+
<button onClick={() => remove(item.product._id, item.variant)}>Remove</button>
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
})}
|
|
405
|
+
<p>Total: ₹{total}</p>
|
|
406
|
+
<button onClick={onCheckout}>Checkout</button>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Write `src/components/Checkout.jsx`:
|
|
413
|
+
|
|
414
|
+
```jsx
|
|
415
|
+
import { useState } from 'react';
|
|
416
|
+
import { store } from '../lib/epicmerch';
|
|
417
|
+
|
|
418
|
+
// Razorpay's checkout.js is heavy — load it lazily on the first Place Order
|
|
419
|
+
// click instead of blocking initial page render.
|
|
420
|
+
const loadRazorpay = () =>
|
|
421
|
+
new Promise((resolve) => {
|
|
422
|
+
if (window.Razorpay) return resolve(true);
|
|
423
|
+
const script = document.createElement('script');
|
|
424
|
+
script.src = 'https://checkout.razorpay.com/v1/checkout.js';
|
|
425
|
+
script.onload = () => resolve(true);
|
|
426
|
+
script.onerror = () => resolve(false);
|
|
427
|
+
document.body.appendChild(script);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
export default function Checkout({ cart, onSuccess }) {
|
|
431
|
+
const [address, setAddress] = useState({ street: '', city: '', postalCode: '', country: 'India' });
|
|
432
|
+
const [error, setError] = useState('');
|
|
433
|
+
|
|
434
|
+
const placeOrder = async () => {
|
|
435
|
+
setError('');
|
|
436
|
+
await loadRazorpay();
|
|
437
|
+
|
|
438
|
+
// Map cart line items to the orderItems shape the API expects.
|
|
439
|
+
// Critical fields per the SDK contract:
|
|
440
|
+
// - `productId` — the product _id (same field name as cart.add,
|
|
441
|
+
// magic-checkout-init, analytics.track; consistent
|
|
442
|
+
// across every endpoint). Server still accepts the
|
|
443
|
+
// legacy `product` field but `productId` is canonical.
|
|
444
|
+
// - `name`, `image` — required, the server doesn't re-fetch them
|
|
445
|
+
// - `variant` — the size STRING, must match what was added to cart
|
|
446
|
+
// or the variant lookup returns undefined and you'll
|
|
447
|
+
// see "INSUFFICIENT_STOCK: undefined: need 1, have 0"
|
|
448
|
+
const items = cart.cart || [];
|
|
449
|
+
const orderItems = items.map(i => ({
|
|
450
|
+
productId: i.product._id,
|
|
451
|
+
name: i.product.name,
|
|
452
|
+
image: i.product.images?.[0] || i.product.image,
|
|
453
|
+
qty: i.qty,
|
|
454
|
+
price: i.product.salePrice ?? i.product.price,
|
|
455
|
+
variant: i.variant,
|
|
456
|
+
}));
|
|
457
|
+
const total = orderItems.reduce((s, i) => s + i.price * i.qty, 0);
|
|
458
|
+
|
|
459
|
+
// ONE call to /customer/orders is enough — the server creates the
|
|
460
|
+
// EpicMerch order AND the Razorpay order in a single transaction and
|
|
461
|
+
// returns both back. Do NOT separately call store.payment.getConfig()
|
|
462
|
+
// or store.payment.createOrder() — orderResult already includes
|
|
463
|
+
// razorpayKeyId, razorpayOrderId, amount, currency, merchantName.
|
|
464
|
+
// Calling them again creates an orphan second Razorpay order AND can
|
|
465
|
+
// trigger an idempotency-key collision against /customer/orders.
|
|
466
|
+
let orderResult;
|
|
467
|
+
try {
|
|
468
|
+
orderResult = await store.orders.create({
|
|
469
|
+
orderItems,
|
|
470
|
+
shippingAddress: address,
|
|
471
|
+
paymentMethod: 'Razorpay',
|
|
472
|
+
totalPrice: total,
|
|
473
|
+
});
|
|
474
|
+
} catch (e) {
|
|
475
|
+
// The API returns a structured error and the SDK throws it:
|
|
476
|
+
// INSUFFICIENT_STOCK ("Halden 3-Seat Sofa: need 1, have 0"),
|
|
477
|
+
// INVALID_ORDER_ITEM, PAYMENT_NOT_CONFIGURED, etc. Surface the message.
|
|
478
|
+
setError(e?.message || 'Could not place the order. Please try again.');
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// COD / fully-discounted orders return NO Razorpay session
|
|
483
|
+
// (razorpayOrderId is null) — the order is already placed, nothing to pay.
|
|
484
|
+
if (!orderResult.razorpayOrderId) {
|
|
485
|
+
await store.cart.clear();
|
|
486
|
+
onSuccess(orderResult.orderId);
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const rzp = new window.Razorpay({
|
|
491
|
+
key: orderResult.razorpayKeyId,
|
|
492
|
+
order_id: orderResult.razorpayOrderId,
|
|
493
|
+
amount: orderResult.amount, // already in paise
|
|
494
|
+
currency: orderResult.currency,
|
|
495
|
+
name: orderResult.merchantName,
|
|
496
|
+
handler: async (response) => {
|
|
497
|
+
await store.payment.verify({ ...response, orderId: orderResult.orderId });
|
|
498
|
+
await store.cart.clear();
|
|
499
|
+
onSuccess(orderResult.orderId);
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
rzp.open();
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
return (
|
|
506
|
+
<div>
|
|
507
|
+
<input placeholder="Street" onChange={e => setAddress(a => ({ ...a, street: e.target.value }))} />
|
|
508
|
+
<input placeholder="City" onChange={e => setAddress(a => ({ ...a, city: e.target.value }))} />
|
|
509
|
+
<input placeholder="Postal Code" onChange={e => setAddress(a => ({ ...a, postalCode: e.target.value }))} />
|
|
510
|
+
{error && <p style={{ color: 'crimson' }}>{error}</p>}
|
|
511
|
+
<button onClick={placeOrder}>Place Order</button>
|
|
512
|
+
</div>
|
|
513
|
+
);
|
|
514
|
+
}
|
|
31
515
|
```
|
|
32
|
-
|
|
516
|
+
|
|
517
|
+
**If the merchant has a top-level `App.jsx` (or similar) that maintains a `refreshCartCount` or `handleAddToCart`, also update it:** `cart.get()` returns `{ cart: [...] }`, so `refreshCartCount` should read `c?.cart?.length` (NOT `c?.items?.length` and NOT `c?.cartCount` — that field doesn't come back from the server, despite what the SDK README says); and `handleAddToCart` should accept a `variant` arg and forward it: `store.cart.add(productId, 1, variant)`. Bonus: `store.cart.add` itself returns `{ message, cartCount }`, so you can update the badge from THAT response without a separate `cart.get()` round-trip.
|
|
518
|
+
|
|
519
|
+
Do NOT add any Razorpay script tag to `index.html` — the script loads dynamically only when the user clicks Place Order.
|
|
520
|
+
|
|
521
|
+
Write `src/components/OrderHistory.jsx`:
|
|
522
|
+
|
|
523
|
+
```jsx
|
|
524
|
+
import { useEffect, useState } from 'react';
|
|
525
|
+
import { store } from '../lib/epicmerch';
|
|
526
|
+
|
|
527
|
+
export default function OrderHistory() {
|
|
528
|
+
const [orders, setOrders] = useState([]);
|
|
529
|
+
|
|
530
|
+
useEffect(() => { store.orders.list().then(setOrders); }, []);
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
<div>
|
|
534
|
+
<h2>Your Orders</h2>
|
|
535
|
+
{orders.map(order => (
|
|
536
|
+
<div key={order._id}>
|
|
537
|
+
<span>Order #{order._id.slice(-6)}</span>
|
|
538
|
+
<span> — ₹{order.totalPrice}</span>
|
|
539
|
+
<span> — {order.status}</span>
|
|
540
|
+
</div>
|
|
541
|
+
))}
|
|
542
|
+
</div>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
33
545
|
```
|
|
34
546
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
547
|
+
- [ ] **Step 7: Generate API key via MCP + write `.env`**
|
|
548
|
+
|
|
549
|
+
This step has two parts: write `.env.example` (the template), then generate a real API key and write a working `.env`.
|
|
550
|
+
|
|
551
|
+
**Part A — `.env.example`:** If `.env.example` already exists, skip writing it and tell the user ".env.example already exists — keeping it". Otherwise write `.env.example`:
|
|
38
552
|
|
|
39
|
-
|
|
40
|
-
|
|
553
|
+
```
|
|
554
|
+
VITE_API_KEY=your_epicmerch_api_key_here
|
|
555
|
+
# VITE_API_URL is OPTIONAL — only set it for local development.
|
|
556
|
+
# Production: leave it unset; the SDK defaults to https://api.epicmerch.in/api
|
|
557
|
+
# Local dev: VITE_API_URL=http://localhost:5002/api
|
|
558
|
+
```
|
|
41
559
|
|
|
42
|
-
|
|
43
|
-
session-restore, COD guard, pagination) — they are correct by construction, which
|
|
44
|
-
is why we use the tool instead of writing them by hand.
|
|
560
|
+
Check `.gitignore` — if `.env` is not listed, append `.env` to `.gitignore` before any `.env` file is written.
|
|
45
561
|
|
|
46
|
-
|
|
47
|
-
> call the narrower tool instead: `store_scaffold_auth`, `store_scaffold_products`,
|
|
48
|
-
> or `store_scaffold_orders` (same `{framework}` arg, same `[{path,content}]`
|
|
49
|
-
> return). Phases 0, 2, 3, 5 still apply.
|
|
562
|
+
**Part B — generate a real API key + write `.env`:**
|
|
50
563
|
|
|
51
|
-
|
|
564
|
+
First, read `.env` if it exists. If it already contains a `VITE_API_KEY=` line whose value is NOT the placeholder string `your_epicmerch_api_key_here` (i.e. there's already a real key), skip Part B entirely and tell the user "Existing API key in .env — keeping it".
|
|
52
565
|
|
|
53
|
-
|
|
566
|
+
Otherwise, the merchant is authenticated via the EpicMerch MCP (this skill assumes the MCP is connected — that's how it was invoked). Generate a key by calling the MCP tool:
|
|
54
567
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
returned key, and write `.env`:
|
|
59
|
-
- Production (default): just `VITE_API_KEY=<key>` (the SDK defaults to
|
|
60
|
-
`https://api.epicmerch.in/api`).
|
|
61
|
-
- Local dev (only if the merchant is clearly on a local server): also add
|
|
62
|
-
`VITE_API_URL=http://localhost:5001/api`.
|
|
63
|
-
- For Next.js, use `NEXT_PUBLIC_API_KEY` / `NEXT_PUBLIC_API_URL`.
|
|
64
|
-
3. Ensure `.env` is gitignored — if `.gitignore` lacks `.env`, append it BEFORE
|
|
65
|
-
writing `.env`.
|
|
66
|
-
4. Fallback: if `merchant_generate_api_key` is unavailable, tell the user to grab a
|
|
67
|
-
key from `https://epicmerch.in/dashboard → API Keys` and add it to `.env`. Do NOT
|
|
68
|
-
abort — continue to Phase 3.
|
|
568
|
+
```
|
|
569
|
+
merchant_generate_api_key({ name: "Storefront" })
|
|
570
|
+
```
|
|
69
571
|
|
|
70
|
-
|
|
572
|
+
The response will include the new key. Extract it (the field is typically `key` or `apiKey` — check the response shape). Then write/update `.env`.
|
|
71
573
|
|
|
72
|
-
|
|
574
|
+
**Production (default).** Write only the key — the SDK already knows the prod URL:
|
|
73
575
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
> 3) Vibrant Pop (colourful, rounded corners, playful)
|
|
78
|
-
> 4) Skip — functional unstyled scaffolds, I'll polish later"
|
|
576
|
+
```
|
|
577
|
+
VITE_API_KEY=<the generated key from the MCP response>
|
|
578
|
+
```
|
|
79
579
|
|
|
80
|
-
|
|
81
|
-
per scaffolded component (typography, spacing, colour) matching the vibe. This is
|
|
82
|
-
a class edit, NOT a rewrite — do not regenerate the components.
|
|
83
|
-
- Option 4: skip the pass.
|
|
84
|
-
- Either way, confirm `src/lib/epicmerch.js` contains the **session-restore block**
|
|
85
|
-
(`localStorage.getItem('customerInfo') → setCustomerToken`). The tool includes it
|
|
86
|
-
by default; if a pre-existing file was kept and lacks it, add it manually — without
|
|
87
|
-
it, OTP login silently doesn't survive a page reload (the #1 storefront bug).
|
|
88
|
-
- Recap `✓ Storefront scaffolded + styled (<vibe>)` or `✓ ... (unstyled)`.
|
|
580
|
+
**Local development.** Only if the merchant is clearly running against a local EpicMerch server (e.g. they explicitly said so, or their existing `.env` already had a `localhost` URL, or the MCP was wired with `EPICMERCH_API_URL=http://localhost:...`), ALSO write the override:
|
|
89
581
|
|
|
90
|
-
|
|
582
|
+
```
|
|
583
|
+
VITE_API_KEY=<the generated key from the MCP response>
|
|
584
|
+
VITE_API_URL=http://localhost:5002/api
|
|
585
|
+
```
|
|
91
586
|
|
|
92
|
-
|
|
93
|
-
lookbook, a size guide, a blog)? Name them, or say 'no' to finish."*
|
|
587
|
+
When in doubt, default to the production form (no `VITE_API_URL` line). Don't ask the merchant — guess from context.
|
|
94
588
|
|
|
95
|
-
|
|
96
|
-
- Otherwise build a manifest: one entry per custom file (path + a one-line spec).
|
|
97
|
-
Then generate them — **in parallel if your client supports subagents** (e.g.
|
|
98
|
-
Claude Code's Task/Agent tool: dispatch one agent per file in a single batch);
|
|
99
|
-
otherwise generate them **sequentially** (still far faster than the old flow,
|
|
100
|
-
since the whole skeleton was free). Each generation gets: the SDK usage (import
|
|
101
|
-
`store` from `src/lib/epicmerch.js`; products via `store.products.list()`; the
|
|
102
|
-
cart/checkout contracts), the chosen vibe, and that one file's spec.
|
|
103
|
-
- After the custom files exist, do ONE small assembly pass: wire routes/nav so the
|
|
104
|
-
new pages are reachable. Preserve existing routing.
|
|
589
|
+
**Fallback if MCP isn't available:** If the `merchant_generate_api_key` tool fails or isn't connected, tell the user: "MCP tool unavailable. Generate a key manually at https://epicmerch.in/dashboard → API Keys and add it to .env." Do NOT fail the whole flow — continue to Step 8.
|
|
105
590
|
|
|
106
|
-
|
|
591
|
+
- [ ] **Step 8: Tell the user what to do next**
|
|
107
592
|
|
|
108
|
-
|
|
109
|
-
the live API; on pass it stamps server-side verification. Do not tell the merchant
|
|
110
|
-
"your store is set up" until this is green. If it fails, route to `/epicmerch-debug`,
|
|
111
|
-
fix, and re-run.
|
|
593
|
+
Print a summary using the actual state of the project:
|
|
112
594
|
|
|
113
|
-
|
|
595
|
+
- List the files you CREATED in this run (skip the ones that were already present).
|
|
596
|
+
- If you skipped any, briefly mention them as "kept as-is".
|
|
597
|
+
- If Step 5b wired up a search bar in an existing component, mention which file you edited and that it now calls `store.products.search()`.
|
|
598
|
+
- If the API key was generated automatically, mention that ".env now has a working VITE_API_KEY — your storefront should load real product data on next refresh."
|
|
599
|
+
- Then output the next-steps block exactly:
|
|
114
600
|
|
|
115
|
-
|
|
116
|
-
|
|
601
|
+
```
|
|
602
|
+
Next steps:
|
|
603
|
+
1. (API key auto-generated and saved to .env — if not, get one at https://epicmerch.in/dashboard → API Keys)
|
|
604
|
+
2. Restart your dev server (npm run dev) to pick up the .env changes
|
|
605
|
+
3. Import the new components into your App.jsx where you need them
|
|
117
606
|
|
|
118
|
-
|
|
119
|
-
Next:
|
|
120
|
-
1. Restart your dev server (npm run dev) to pick up .env.
|
|
121
|
-
2. Import the components where you need them.
|
|
122
|
-
Need payments, analytics, or a Buy Now button? Just ask.
|
|
607
|
+
Need help with payments, analytics, or notifications? Ask me!
|
|
123
608
|
```
|
package/skills/epicmerch.md
CHANGED
|
@@ -120,13 +120,10 @@ There is no hard "skip" here: `complete` requires a non-empty catalog. If the me
|
|
|
120
120
|
|
|
121
121
|
Remember the answer as `visualStyle`.
|
|
122
122
|
|
|
123
|
-
**4b — Scaffold + style.** Run `/epicmerch-storefront
|
|
124
|
-
`visualStyle
|
|
125
|
-
`
|
|
126
|
-
|
|
127
|
-
ends on the `/epicmerch-verify` gate. You don't style files here — the leaf skill owns
|
|
128
|
-
the theme pass. Recap what it reports (`✓ Storefront scaffolded + styled (<visualStyle>)`
|
|
129
|
-
or `unstyled`).
|
|
123
|
+
**4b — Scaffold + style.** Run `/epicmerch-storefront`. After it finishes:
|
|
124
|
+
- If `visualStyle` ≠ skip, do a quick styling pass: 5-10 well-chosen Tailwind utility classes per component (typography, spacing, colour) matching `visualStyle`. Don't overdo it.
|
|
125
|
+
- Confirm `src/lib/epicmerch.js` contains the **session-restore block** (the `localStorage.getItem('customerInfo') → setCustomerToken` snippet). Without it, OTP login appears to work but doesn't survive a page reload — the #1 silent storefront bug. The 1.3.4+ scaffold includes it by default; add it manually if missing.
|
|
126
|
+
- Recap `✓ Storefront scaffolded + styled (<visualStyle>)` (or `unstyled`).
|
|
130
127
|
|
|
131
128
|
**Optional add-on (not gated):** once the storefront exists, you may offer a 1-click Buy Now button — *"Want a 1-click 'Buy Now' button? Razorpay's Magic Checkout lets customers buy without a cart screen."* → `/epicmerch-magic-checkout` (it verifies Razorpay first). This is optional and does NOT affect `complete`; the merchant can add it anytime by saying *"add a Buy Now button"*. Then re-loop.
|
|
132
129
|
|
package/src/adapters/mcp.js
CHANGED
|
@@ -11,6 +11,7 @@ import { developerTools, developerToolDefs } from '../tools/developer.js';
|
|
|
11
11
|
import { scaffoldTools, scaffoldToolDefs } from '../tools/scaffold.js';
|
|
12
12
|
import { merchantTools, merchantToolDefs } from '../tools/merchant.js';
|
|
13
13
|
import { prompts } from '../prompts/index.js';
|
|
14
|
+
import { VERSION } from '../version.js';
|
|
14
15
|
|
|
15
16
|
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
const readResource = (rel) => readFileSync(join(__dir, '../resources', rel), 'utf-8');
|
|
@@ -27,7 +28,7 @@ function buildZodSchema(schemaDef) {
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export async function startMcpAdapter(session, client) {
|
|
30
|
-
const server = new McpServer({ name: 'epicmerch', version:
|
|
31
|
+
const server = new McpServer({ name: 'epicmerch', version: VERSION });
|
|
31
32
|
|
|
32
33
|
const allToolDefs = [...sessionToolDefs, ...developerToolDefs, ...scaffoldToolDefs, ...merchantToolDefs];
|
|
33
34
|
const allHandlers = {
|
package/src/adapters/openapi.js
CHANGED
|
@@ -6,6 +6,7 @@ import { sessionTools, sessionToolDefs } from '../tools/session.js';
|
|
|
6
6
|
import { developerTools, developerToolDefs } from '../tools/developer.js';
|
|
7
7
|
import { scaffoldTools, scaffoldToolDefs } from '../tools/scaffold.js';
|
|
8
8
|
import { merchantTools, merchantToolDefs } from '../tools/merchant.js';
|
|
9
|
+
import { VERSION } from '../version.js';
|
|
9
10
|
|
|
10
11
|
const ALL_TOOL_DEFS = [...sessionToolDefs, ...developerToolDefs, ...scaffoldToolDefs, ...merchantToolDefs];
|
|
11
12
|
|
|
@@ -46,7 +47,7 @@ function buildOpenApiSpec(serverUrl) {
|
|
|
46
47
|
|
|
47
48
|
return {
|
|
48
49
|
openapi: '3.1.0',
|
|
49
|
-
info: { title: 'EpicMerch MCP Tools', version:
|
|
50
|
+
info: { title: 'EpicMerch MCP Tools', version: VERSION, description: 'EpicMerch tools for Claude and ChatGPT' },
|
|
50
51
|
servers: [{ url: serverUrl || 'https://mcp.epicmerch.in' }],
|
|
51
52
|
paths,
|
|
52
53
|
components: {
|
package/src/version.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/version.js
|
|
2
|
+
// Single source of the package version, read from package.json at runtime — so
|
|
3
|
+
// the MCP serverInfo handshake, the OpenAPI spec, etc. always match the published
|
|
4
|
+
// version instead of drifting from a hardcoded literal. Works in local dev, the
|
|
5
|
+
// npm tarball, and the staged .dxt (package.json sits at ../ from src/ in all three).
|
|
6
|
+
import { readFileSync } from 'fs';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
import { dirname, join } from 'path';
|
|
9
|
+
|
|
10
|
+
const __dir = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
export const VERSION = JSON.parse(
|
|
12
|
+
readFileSync(join(__dir, '..', 'package.json'), 'utf-8')
|
|
13
|
+
).version;
|