epicmerch-mcp 1.3.6 → 1.3.12
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 +22 -0
- package/package.json +1 -1
- package/skills/epicmerch-debug.md +11 -0
- package/skills/epicmerch-e2e.md +203 -0
- package/skills/epicmerch-verify.md +53 -0
- package/skills/epicmerch.md +1 -0
- package/src/prompts/index.js +13 -0
- package/src/tools/developer.js +127 -0
- package/src/tools/merchant.js +15 -1
package/README.md
CHANGED
|
@@ -156,6 +156,28 @@ merchant_diagnose({})
|
|
|
156
156
|
|
|
157
157
|
Returns store name + currency, product/category/out-of-stock counts, payment + logistics configuration status, API key info, a 0-100 readiness score, and a prioritised list of next steps. Use this to drive "what's still missing?" conversations.
|
|
158
158
|
|
|
159
|
+
## Debugging a broken storefront
|
|
160
|
+
|
|
161
|
+
Three layers, from quickest to deepest:
|
|
162
|
+
|
|
163
|
+
| Tool | What it does | When to use |
|
|
164
|
+
|------|--------------|-------------|
|
|
165
|
+
| `merchant_diagnose({})` | Server-side health snapshot (above) | "Is my store set up correctly?" |
|
|
166
|
+
| `/epicmerch-verify` skill | Smoke-tests every read path + validates the checkout payload shape against the live API, WITHOUT sending OTP or charging a card. Ends with a manual Razorpay-test-card recipe. | "Does my site actually load products / will checkout work?" — run after scaffolding |
|
|
167
|
+
| `/epicmerch-debug` skill | Four-check playbook for the most common failures: (1) OTP login that doesn't persist across reloads, (2) products not loading, (3) `INSUFFICIENT_STOCK: undefined` on checkout, (4) cart returning 401. Diagnoses + applies the fix. | "Something's broken and I don't know why" |
|
|
168
|
+
|
|
169
|
+
The wizard (`/epicmerch`) auto-runs `/epicmerch-verify` as its final step and auto-routes to `/epicmerch-debug` if any check fails — so most issues surface and get fixed before you ever see them.
|
|
170
|
+
|
|
171
|
+
### Error responses are structured
|
|
172
|
+
|
|
173
|
+
Every API error returns `{ success: false, code, message, hint?, items? }`.
|
|
174
|
+
Branch on `code` (stable) rather than substring-matching `message` (human-readable, may change). The SDK README has the full code table + rate-limit table. The most useful for debugging:
|
|
175
|
+
|
|
176
|
+
- `INVALID_ORDER_ITEM` — an `orderItems[]` entry is missing `productId`; the response's `received` field shows exactly what you sent.
|
|
177
|
+
- `INSUFFICIENT_STOCK` — `items[]` lists each shortfall as `{ product, variant, requested, available }`.
|
|
178
|
+
- `PRODUCT_NOT_FOUND` — the `productId` doesn't match any product in this store.
|
|
179
|
+
- `PAYMENT_NOT_CONFIGURED` — no Razorpay keys yet (run `/epicmerch-razorpay`).
|
|
180
|
+
|
|
159
181
|
## Shopify migration (MCP tool)
|
|
160
182
|
|
|
161
183
|
Migrate a Shopify store to EpicMerch in two steps:
|
package/package.json
CHANGED
|
@@ -175,6 +175,17 @@ clicking Add to Cart fails. Network shows 401 or 403 on POST `/customer/cart`.
|
|
|
175
175
|
that subsequent API calls include the Authorization header.
|
|
176
176
|
3. Call `store.auth.getSession()` — if it returns null, the token has
|
|
177
177
|
expired or wasn't restored. `store.auth.refreshToken()` can extend it.
|
|
178
|
+
4. **Confirm against the live API.** Grab the token from
|
|
179
|
+
`localStorage.customerInfo.token` and the store's API key, then:
|
|
180
|
+
```
|
|
181
|
+
store_cart_get({ apiKey: "<KEY>", customerToken: "<TOKEN>" })
|
|
182
|
+
```
|
|
183
|
+
- `httpStatus: 200` → the token IS valid; the bug is purely client-side
|
|
184
|
+
(the SDK isn't restoring it — apply Check 1's fix to src/lib/epicmerch.js).
|
|
185
|
+
- `httpStatus: 401` → the token is expired or invalid; the customer
|
|
186
|
+
needs to log in again, or refreshToken() needs wiring.
|
|
187
|
+
This pins whether the problem is "token bad" vs "SDK not using a good
|
|
188
|
+
token" — the two have completely different fixes.
|
|
178
189
|
|
|
179
190
|
**Fix.**
|
|
180
191
|
- If localStorage is empty: merchant needs to log in again.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: epicmerch-e2e
|
|
3
|
+
description: Full end-to-end smoke test against the LIVE EpicMerch API — creates a throwaway test product, adds it to a cart, creates an order (exercising stock reservation + Razorpay order creation), verifies every response shape, then cleans everything up. Proves the entire merchant→customer pipeline works without spending money or leaving junk data. The deepest verification available.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# EpicMerch End-to-End Smoke Test
|
|
7
|
+
|
|
8
|
+
This runs the **complete pipeline** against the live API with real data:
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
create product → add to cart → get cart → create order → cancel order → delete product
|
|
12
|
+
(merchant) (customer) (customer) (customer) (customer) (merchant)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
If every step returns a good response, the whole store genuinely works —
|
|
16
|
+
not just the read paths. Nothing is charged (the order is created UNPAID
|
|
17
|
+
and cancelled), and nothing is left behind (the test product is deleted).
|
|
18
|
+
|
|
19
|
+
Use this when a merchant wants real confidence ("does checkout actually
|
|
20
|
+
work?"), or after any change to the order/cart/product APIs. It's heavier
|
|
21
|
+
than `/epicmerch-verify` (it writes + deletes data), so it's opt-in.
|
|
22
|
+
|
|
23
|
+
**Fully automated — no real SMS.** The customer session comes from the
|
|
24
|
+
genuine OTP login flow against the server's configured test phone
|
|
25
|
+
(`E2E_TEST_PHONE`), which skips the actual SMS and seeds a secret code
|
|
26
|
+
(`E2E_TEST_OTP`). So the test exercises the REAL `/auth/otp/send` +
|
|
27
|
+
`/auth/otp/verify` endpoints with zero SMS cost.
|
|
28
|
+
|
|
29
|
+
## Prerequisites
|
|
30
|
+
|
|
31
|
+
1. **Merchant MCP session** — you call `merchant_create_product` /
|
|
32
|
+
`merchant_delete_product` with the merchant's OAuth token (already
|
|
33
|
+
present if the MCP is connected).
|
|
34
|
+
2. **A store API key** — `merchant_list_api_keys` (or generate a temp one
|
|
35
|
+
with `merchant_generate_api_key({ name: 'E2E test' })` and delete it
|
|
36
|
+
after).
|
|
37
|
+
3. **A customer token** — obtained via the real OTP login flow against
|
|
38
|
+
the configured test phone (the server seeds the code, no SMS sent):
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
store_auth_send_otp({ apiKey: "<KEY>", phone: "<E2E_TEST_PHONE>" })
|
|
42
|
+
store_auth_verify_otp({ apiKey: "<KEY>", phone: "<E2E_TEST_PHONE>", otp: "<E2E_TEST_OTP>" })
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`store_auth_verify_otp`'s `response.token` is your customer token —
|
|
46
|
+
call it `<CT>`. This exercises the genuine `/auth/otp/send` +
|
|
47
|
+
`/auth/otp/verify` endpoints, so it ALSO verifies that login works,
|
|
48
|
+
not just cart/order. `<E2E_TEST_PHONE>` and `<E2E_TEST_OTP>` are the
|
|
49
|
+
values the server admin configured in its environment.
|
|
50
|
+
|
|
51
|
+
If `store_auth_send_otp` returns a normal SMS channel (not the
|
|
52
|
+
`e2e-test` channel) it means this server doesn't have the test phone
|
|
53
|
+
configured — ask the operator to set `E2E_TEST_PHONE` / `E2E_TEST_OTP`,
|
|
54
|
+
or run `/epicmerch-verify` instead (no token needed).
|
|
55
|
+
|
|
56
|
+
Call the customer token `<CT>` and the API key `<KEY>` below.
|
|
57
|
+
|
|
58
|
+
Confirm payments are configured first: `merchant_get_checkout_settings`.
|
|
59
|
+
If Razorpay isn't set up, the order step will return
|
|
60
|
+
`PAYMENT_NOT_CONFIGURED` — that's still a useful result (route to
|
|
61
|
+
`/epicmerch-razorpay`), but tell the merchant up front.
|
|
62
|
+
|
|
63
|
+
## The test
|
|
64
|
+
|
|
65
|
+
Run these steps in order. STOP and report at the first failure — a later
|
|
66
|
+
step depends on the earlier ones. Always run the cleanup steps (5, 6)
|
|
67
|
+
even if an earlier step failed, so you don't leave a test product or a
|
|
68
|
+
stuck reservation behind.
|
|
69
|
+
|
|
70
|
+
### Step 1 — Create a throwaway test product
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
merchant_create_product({
|
|
74
|
+
name: "__E2E_TEST__ (safe to delete)",
|
|
75
|
+
type: "Test",
|
|
76
|
+
description: "Automated end-to-end test product. Delete if you see it.",
|
|
77
|
+
price: 1,
|
|
78
|
+
stock: 5,
|
|
79
|
+
variants: [{ variant: "TEST", stock: 5 }],
|
|
80
|
+
images: ["https://via.placeholder.com/300"]
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Grab the new product's `_id` (call it `<PID>`). Price ₹1 + a single
|
|
85
|
+
`TEST` variant keeps it cheap and unambiguous.
|
|
86
|
+
|
|
87
|
+
> ✓ Step 1 — test product created (`<PID>`)
|
|
88
|
+
|
|
89
|
+
If the server only creates a placeholder and needs a follow-up update
|
|
90
|
+
(some versions do), chain `merchant_update_product({ id: <PID>, ... })`
|
|
91
|
+
with the same fields, then re-fetch with `merchant_get_product({ id: <PID> })`
|
|
92
|
+
to confirm the variant + stock landed.
|
|
93
|
+
|
|
94
|
+
### Step 2 — Add it to the cart
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
store_cart_add({ apiKey: "<KEY>", customerToken: "<CT>", productId: "<PID>", qty: 1, variant: "TEST" })
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Expect `httpStatus: 200`, body `{ message: 'Added to cart', cartCount: N }`.
|
|
101
|
+
|
|
102
|
+
> ✓ Step 2 — added to cart
|
|
103
|
+
|
|
104
|
+
### Step 3 — Fetch the cart and verify the shape
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
store_cart_get({ apiKey: "<KEY>", customerToken: "<CT>" })
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Confirm `response.cart` contains the test item shaped
|
|
111
|
+
`{ product: { _id: "<PID>", name, price, image, ... }, qty: 1, variant: "TEST" }`.
|
|
112
|
+
This is the authoritative cart shape — if the scaffolded `Cart.jsx`
|
|
113
|
+
reads anything different, FIX the scaffold to match.
|
|
114
|
+
|
|
115
|
+
> ✓ Step 3 — cart returns the item with the expected shape
|
|
116
|
+
|
|
117
|
+
### Step 4 — Create the order (the real checkout pipeline)
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
store_order_create({
|
|
121
|
+
apiKey: "<KEY>",
|
|
122
|
+
customerToken: "<CT>",
|
|
123
|
+
orderItems: [{ productId: "<PID>", name: "__E2E_TEST__ (safe to delete)", image: "https://via.placeholder.com/300", qty: 1, price: 1, variant: "TEST" }],
|
|
124
|
+
shippingAddress: { fullName: "E2E Test", address: "1 Test St", city: "Mumbai", state: "MH", postalCode: "400001", country: "India", phone: "+919999999999" },
|
|
125
|
+
paymentMethod: "Razorpay",
|
|
126
|
+
totalPrice: 1
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
This exercises the FULL pipeline: stock reservation (SELECT FOR UPDATE),
|
|
131
|
+
order row creation, and Razorpay order creation. **It does not charge** —
|
|
132
|
+
no payment is captured. Confirm `response` contains:
|
|
133
|
+
- `orderId` (the EpicMerch order)
|
|
134
|
+
- `razorpayOrderId` + `razorpayKeyId` + `amount` + `currency` + `merchantName`
|
|
135
|
+
(if Razorpay is configured)
|
|
136
|
+
|
|
137
|
+
Grab `response.orderId` as `<OID>`.
|
|
138
|
+
|
|
139
|
+
If you get `code: INSUFFICIENT_STOCK` here, the variant string in
|
|
140
|
+
orderItems didn't match the cart/product variant — compare against
|
|
141
|
+
Step 3's response. If `code: INVALID_ORDER_ITEM`, the orderItems shape
|
|
142
|
+
is wrong (missing productId). If `code: PAYMENT_NOT_CONFIGURED`, route
|
|
143
|
+
to `/epicmerch-razorpay`.
|
|
144
|
+
|
|
145
|
+
> ✓ Step 4 — order created (`<OID>`), Razorpay order + key returned
|
|
146
|
+
|
|
147
|
+
### Step 5 — Cancel the order (cleanup, releases the reservation)
|
|
148
|
+
|
|
149
|
+
```
|
|
150
|
+
store_order_cancel({ apiKey: "<KEY>", customerToken: "<CT>", orderId: "<OID>" })
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
This frees the stock reservation immediately (otherwise it'd auto-expire
|
|
154
|
+
in ~15 min).
|
|
155
|
+
|
|
156
|
+
> ✓ Step 5 — test order cancelled, stock released
|
|
157
|
+
|
|
158
|
+
### Step 6 — Delete the test product
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
merchant_delete_product({ id: "<PID>" })
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Also remove it from the cart if it lingers:
|
|
165
|
+
`store_cart_remove({ apiKey, customerToken, productId: "<PID>", variant: "TEST" })`.
|
|
166
|
+
|
|
167
|
+
If you generated a temp API key in prerequisites, delete it too:
|
|
168
|
+
`merchant_delete_api_key({ id: "<temp_key_id>" })`.
|
|
169
|
+
|
|
170
|
+
> ✓ Step 6 — test product + temp key deleted
|
|
171
|
+
|
|
172
|
+
## Final report
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
EpicMerch End-to-End Test — <merchantEmail>
|
|
176
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
177
|
+
✓ 1. Create product __E2E_TEST__ (₹1, variant TEST)
|
|
178
|
+
✓ 2. Add to cart cartCount: 1
|
|
179
|
+
✓ 3. Get cart shape matches Cart.jsx contract
|
|
180
|
+
✓ 4. Create order orderId + razorpayOrderId returned (UNPAID)
|
|
181
|
+
✓ 5. Cancel order reservation released
|
|
182
|
+
✓ 6. Cleanup product + temp key deleted
|
|
183
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
184
|
+
Full pipeline verified. The only thing not exercised is the actual
|
|
185
|
+
Razorpay card capture — test that once in a browser with test card
|
|
186
|
+
4111 1111 1111 1111. Everything up to the charge is confirmed working.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
If any step failed, the report should show ✗ on that step with the exact
|
|
190
|
+
error code/message, and the cleanup steps should still have run. Never
|
|
191
|
+
leave a `__E2E_TEST__` product in the merchant's live catalog.
|
|
192
|
+
|
|
193
|
+
## Safety notes
|
|
194
|
+
|
|
195
|
+
- The test product is named `__E2E_TEST__ (safe to delete)` so it's
|
|
196
|
+
obvious in the dashboard if cleanup ever fails.
|
|
197
|
+
- Price ₹1 + stock 5 keeps it harmless even if a real customer somehow
|
|
198
|
+
saw it during the brief window it exists.
|
|
199
|
+
- The order is never paid, so no money moves and no real fulfilment is
|
|
200
|
+
triggered.
|
|
201
|
+
- Always cancel the order AND delete the product, even on partial
|
|
202
|
+
failure — orphaned reservations hold stock, orphaned test products
|
|
203
|
+
clutter the catalog.
|
|
@@ -105,6 +105,59 @@ merchant_delete_api_key({ id: "<temp_key_id>" })
|
|
|
105
105
|
|
|
106
106
|
---
|
|
107
107
|
|
|
108
|
+
## Phase 2.5 — Live cart round-trip (the authoritative cart-shape check)
|
|
109
|
+
|
|
110
|
+
**This is the most reliable way to get the cart contract right.** Instead
|
|
111
|
+
of trusting docs or controller reads, actually add an item to a cart and
|
|
112
|
+
fetch it back through the MCP, then read the REAL response shape. The
|
|
113
|
+
scaffolded `Cart.jsx` must parse exactly what comes back here.
|
|
114
|
+
|
|
115
|
+
This needs a **customer token** (a JWT from a storefront login), which
|
|
116
|
+
costs one OTP. If the merchant can provide one, do this — it's worth it.
|
|
117
|
+
If not, skip to Phase 3 (the structural check) and note the cart shape
|
|
118
|
+
wasn't verified against a live session.
|
|
119
|
+
|
|
120
|
+
Ask:
|
|
121
|
+
|
|
122
|
+
> "To verify the cart end-to-end I need a customer token. Open your
|
|
123
|
+
> storefront, log in once, then in DevTools → Application → Local Storage
|
|
124
|
+
> copy the `token` value from `customerInfo`. Paste it here — or say
|
|
125
|
+
> 'skip' and I'll validate the shape structurally instead."
|
|
126
|
+
|
|
127
|
+
If they provide a token (call it `<CUSTOMER_TOKEN>`) and you have an API
|
|
128
|
+
key from Phase 2 (`<KEY>`) plus a product id (`<FIRST_PRODUCT_ID>`) with
|
|
129
|
+
an in-stock variant (`<VARIANT>`):
|
|
130
|
+
|
|
131
|
+
1. **Add to cart:**
|
|
132
|
+
```
|
|
133
|
+
store_cart_add({ apiKey: "<KEY>", customerToken: "<CUSTOMER_TOKEN>", productId: "<FIRST_PRODUCT_ID>", qty: 1, variant: "<VARIANT>" })
|
|
134
|
+
```
|
|
135
|
+
Expect `httpStatus: 200` and a body like `{ message: 'Added to cart', cartCount: N }`. If you get 401 → the token is expired/wrong. If 404 → the productId is wrong.
|
|
136
|
+
|
|
137
|
+
2. **Fetch the cart:**
|
|
138
|
+
```
|
|
139
|
+
store_cart_get({ apiKey: "<KEY>", customerToken: "<CUSTOMER_TOKEN>" })
|
|
140
|
+
```
|
|
141
|
+
Read `response.cart`. This is the AUTHORITATIVE shape. Confirm each line item is `{ product: { _id, name, price, originalPrice, salePrice, image, type }, qty, variant }`.
|
|
142
|
+
|
|
143
|
+
3. **Cross-check against the scaffold.** Open the project's
|
|
144
|
+
`src/components/Cart.jsx` and confirm it reads:
|
|
145
|
+
- `cart.cart` (the array) — NOT `cart.items`
|
|
146
|
+
- `item.product._id`, `item.product.name`, `item.product.image`
|
|
147
|
+
- `item.product.salePrice ?? item.product.price`
|
|
148
|
+
- `item.variant`
|
|
149
|
+
If the scaffold reads anything the live response doesn't have, FIX the
|
|
150
|
+
scaffold to match the live shape — the live response wins, always.
|
|
151
|
+
|
|
152
|
+
4. **Clean up** so you don't leave a junk item in the cart:
|
|
153
|
+
```
|
|
154
|
+
store_cart_remove({ apiKey: "<KEY>", customerToken: "<CUSTOMER_TOKEN>", productId: "<FIRST_PRODUCT_ID>", variant: "<VARIANT>" })
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Report: `✓ Live cart round-trip — add → get → shape matches Cart.jsx → cleaned up`.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
108
161
|
## Phase 3 — Checkout payload shape check (no actual order created)
|
|
109
162
|
|
|
110
163
|
This catches the contract bugs that have historically caused live `INSUFFICIENT_STOCK: undefined` or `IDEMPOTENCY_KEY_MISMATCH` failures. We BUILD the orderItems payload that the scaffolded `Checkout.jsx` would build, then validate its shape against the API contract — without actually calling `orders.create`.
|
package/skills/epicmerch.md
CHANGED
|
@@ -197,6 +197,7 @@ The merchant said something specific. Match against the table below, pick the cl
|
|
|
197
197
|
| "show me my store / products / orders / sales", "manage my store from chat" | `/epicmerch-merchant` (first-time) or `/epicmerch-merchant-agent` (ongoing) |
|
|
198
198
|
| "migrate from Shopify", "import my Shopify catalog" | `/epicmerch-migrate` |
|
|
199
199
|
| "my login isn't sticking", "products aren't loading", "checkout fails", "insufficient stock undefined", "something's broken" | `/epicmerch-debug` |
|
|
200
|
+
| "test everything", "does checkout actually work", "full end-to-end test", "run a real order test" | `/epicmerch-e2e` |
|
|
200
201
|
|
|
201
202
|
### Hand-off (or inline)
|
|
202
203
|
|
package/src/prompts/index.js
CHANGED
|
@@ -218,6 +218,19 @@ export const prompts = [
|
|
|
218
218
|
},
|
|
219
219
|
|
|
220
220
|
// ─── Verification ──────────────────────────────────────────────────────
|
|
221
|
+
{
|
|
222
|
+
name: 'epicmerch-e2e',
|
|
223
|
+
description: 'Full end-to-end smoke test against the LIVE API — creates a throwaway test product, adds it to a cart, creates an order (exercises stock reservation + Razorpay order creation), verifies every response, then deletes the product + cancels the order. Proves the entire merchant→customer pipeline works without charging money or leaving junk data. Deeper than /epicmerch-verify; needs a customer token (one OTP).',
|
|
224
|
+
handler: async () => ({
|
|
225
|
+
messages: [{
|
|
226
|
+
role: 'user',
|
|
227
|
+
content: {
|
|
228
|
+
type: 'text',
|
|
229
|
+
text: `Run the full EpicMerch end-to-end smoke test. Fetch https://api.epicmerch.in/api/skills/epicmerch-e2e.md if needed and follow every step: get a customer token via the real OTP login flow against the configured test phone (store_auth_send_otp + store_auth_verify_otp with E2E_TEST_PHONE / E2E_TEST_OTP — no SMS sent), create a throwaway test product (merchant_create_product), add it to cart (store_cart_add), get the cart and verify the shape (store_cart_get), create the order (store_order_create — exercises stock reservation + Razorpay order, does NOT charge), then ALWAYS clean up — cancel the order (store_order_cancel) and delete the test product (merchant_delete_product). Print the consolidated checklist at the end.`,
|
|
230
|
+
},
|
|
231
|
+
}],
|
|
232
|
+
}),
|
|
233
|
+
},
|
|
221
234
|
{
|
|
222
235
|
name: 'epicmerch-verify',
|
|
223
236
|
description: 'Smoke-test every storefront API the merchant\'s site loads — confirms products / categories / search / single product / checkout payload shape all work against the live server, BEFORE a real customer hits a 422 at checkout. Skips OTP (costs money) and real Razorpay charges (covers with a manual test-card recipe).',
|
package/src/tools/developer.js
CHANGED
|
@@ -7,6 +7,34 @@ const __dir = dirname(fileURLToPath(import.meta.url));
|
|
|
7
7
|
const readExample = (name) =>
|
|
8
8
|
readFileSync(join(__dir, '../resources/examples', `${name}.md`), 'utf-8');
|
|
9
9
|
|
|
10
|
+
// Base URL for direct customer-scoped calls (cart/order). These endpoints
|
|
11
|
+
// need BOTH the store's x-api-key (identifies the merchant/tenant) AND a
|
|
12
|
+
// customer JWT (identifies the shopper) — a different auth combo than the
|
|
13
|
+
// MCP merchant session carries — so the cart tools below take those
|
|
14
|
+
// explicitly and make a raw fetch. This hits the live endpoint EXACTLY as
|
|
15
|
+
// the storefront does, so the response shape they return is the real,
|
|
16
|
+
// authoritative one the scaffolded Cart.jsx / Checkout.jsx must match.
|
|
17
|
+
const API_BASE = (process.env.EPICMERCH_API_URL || 'https://api.epicmerch.in/api').replace(/\/$/, '');
|
|
18
|
+
|
|
19
|
+
async function customerCartFetch(path, { apiKey, customerToken, method = 'GET', body } = {}) {
|
|
20
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
21
|
+
if (apiKey) headers['x-api-key'] = apiKey;
|
|
22
|
+
if (customerToken) headers['Authorization'] = `Bearer ${customerToken}`;
|
|
23
|
+
const res = await fetch(`${API_BASE}${path}`, {
|
|
24
|
+
method,
|
|
25
|
+
headers,
|
|
26
|
+
...(body ? { body: JSON.stringify(body) } : {}),
|
|
27
|
+
});
|
|
28
|
+
let parsed = null;
|
|
29
|
+
try { parsed = await res.json(); } catch (_) { /* non-JSON body */ }
|
|
30
|
+
// Return BOTH the HTTP status and the parsed body so the assistant can
|
|
31
|
+
// see exactly what the storefront would receive — status code AND shape.
|
|
32
|
+
return { httpStatus: res.status, ok: res.ok, response: parsed };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const CUSTOMER_TOKEN_HELP =
|
|
36
|
+
'Get a customer token from your storefront: log in once (one OTP), then in DevTools → Application → Local Storage read `customerInfo` and copy its `.token` value.';
|
|
37
|
+
|
|
10
38
|
export function developerTools(client) {
|
|
11
39
|
return {
|
|
12
40
|
async store_get_config(_args) {
|
|
@@ -14,6 +42,97 @@ export function developerTools(client) {
|
|
|
14
42
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
15
43
|
},
|
|
16
44
|
|
|
45
|
+
// ── Live cart verification ──────────────────────────────────────────
|
|
46
|
+
// Use these to STOP GUESSING the cart contract. Add an item, fetch the
|
|
47
|
+
// cart, and read the real `response.cart` shape — then write Cart.jsx /
|
|
48
|
+
// Checkout.jsx to match it exactly. Returns { httpStatus, ok, response }.
|
|
49
|
+
|
|
50
|
+
async store_cart_add({ apiKey, customerToken, productId, qty = 1, variant } = {}) {
|
|
51
|
+
if (!apiKey || !customerToken || !productId) {
|
|
52
|
+
return { isError: true, content: [{ type: 'text', text: `store_cart_add needs apiKey + customerToken + productId. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
53
|
+
}
|
|
54
|
+
const body = { productId, qty };
|
|
55
|
+
if (variant) body.variant = variant;
|
|
56
|
+
const result = await customerCartFetch('/customer/cart', { apiKey, customerToken, method: 'POST', body });
|
|
57
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
async store_cart_get({ apiKey, customerToken } = {}) {
|
|
61
|
+
if (!apiKey || !customerToken) {
|
|
62
|
+
return { isError: true, content: [{ type: 'text', text: `store_cart_get needs apiKey + customerToken. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
63
|
+
}
|
|
64
|
+
const result = await customerCartFetch('/customer/cart', { apiKey, customerToken });
|
|
65
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
async store_cart_remove({ apiKey, customerToken, productId, variant } = {}) {
|
|
69
|
+
if (!apiKey || !customerToken || !productId) {
|
|
70
|
+
return { isError: true, content: [{ type: 'text', text: `store_cart_remove needs apiKey + customerToken + productId. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
71
|
+
}
|
|
72
|
+
const qs = variant ? `?variant=${encodeURIComponent(variant)}` : '';
|
|
73
|
+
const result = await customerCartFetch(`/customer/cart/${productId}${qs}`, { apiKey, customerToken, method: 'DELETE' });
|
|
74
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// ── Live OTP login (for exercising the REAL auth flow) ──────────────
|
|
78
|
+
// These hit /auth/otp/send + /auth/otp/verify exactly as the storefront
|
|
79
|
+
// Login.jsx does. Pair with the server's env-gated E2E_TEST_PHONE bypass
|
|
80
|
+
// to get a customer token through the genuine login path (no SMS cost)
|
|
81
|
+
// — catches login-flow bugs that merchant_create_test_session skips.
|
|
82
|
+
|
|
83
|
+
async store_auth_send_otp({ apiKey, phone } = {}) {
|
|
84
|
+
if (!apiKey || !phone) {
|
|
85
|
+
return { isError: true, content: [{ type: 'text', text: 'store_auth_send_otp needs apiKey + phone. For automated tests, use the server-configured E2E_TEST_PHONE so no real SMS is sent.' }] };
|
|
86
|
+
}
|
|
87
|
+
const result = await customerCartFetch('/auth/otp/send', { apiKey, method: 'POST', body: { phone } });
|
|
88
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
async store_auth_verify_otp({ apiKey, phone, otp, profile = {} } = {}) {
|
|
92
|
+
if (!apiKey || !phone || !otp) {
|
|
93
|
+
return { isError: true, content: [{ type: 'text', text: 'store_auth_verify_otp needs apiKey + phone + otp. Returns { token, user } on success — that token is the customer JWT for cart/order calls.' }] };
|
|
94
|
+
}
|
|
95
|
+
const result = await customerCartFetch('/auth/otp/verify', { apiKey, method: 'POST', body: { phone, otp, profile } });
|
|
96
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// ── Live order verification ─────────────────────────────────────────
|
|
100
|
+
// store_order_create exercises the FULL checkout pipeline against the
|
|
101
|
+
// live API: stock reservation + order row + Razorpay order creation.
|
|
102
|
+
// It does NOT charge — the Razorpay order is created but no payment is
|
|
103
|
+
// captured (that needs the hosted widget + a real card). So the order
|
|
104
|
+
// sits UNPAID; its stock reservation expires automatically, or you can
|
|
105
|
+
// release it immediately with store_order_cancel. Use this to confirm
|
|
106
|
+
// the orders.create response shape (razorpayOrderId/razorpayKeyId/
|
|
107
|
+
// amount/currency/merchantName) end-to-end without spending a rupee.
|
|
108
|
+
|
|
109
|
+
async store_order_create({ apiKey, customerToken, orderItems, shippingAddress, paymentMethod = 'Razorpay', totalPrice } = {}) {
|
|
110
|
+
if (!apiKey || !customerToken || !Array.isArray(orderItems) || orderItems.length === 0) {
|
|
111
|
+
return { isError: true, content: [{ type: 'text', text: `store_order_create needs apiKey + customerToken + a non-empty orderItems array. Each item: { productId, name, image, qty, price, variant }. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
112
|
+
}
|
|
113
|
+
const body = { orderItems, shippingAddress, paymentMethod, totalPrice };
|
|
114
|
+
const result = await customerCartFetch('/customer/orders', { apiKey, customerToken, method: 'POST', body });
|
|
115
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async store_order_get({ apiKey, customerToken, orderId } = {}) {
|
|
119
|
+
if (!apiKey || !customerToken || !orderId) {
|
|
120
|
+
return { isError: true, content: [{ type: 'text', text: `store_order_get needs apiKey + customerToken + orderId. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
121
|
+
}
|
|
122
|
+
const result = await customerCartFetch(`/customer/orders/${orderId}`, { apiKey, customerToken });
|
|
123
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async store_order_cancel({ apiKey, customerToken, orderId } = {}) {
|
|
127
|
+
if (!apiKey || !customerToken || !orderId) {
|
|
128
|
+
return { isError: true, content: [{ type: 'text', text: `store_order_cancel needs apiKey + customerToken + orderId. ${CUSTOMER_TOKEN_HELP}` }] };
|
|
129
|
+
}
|
|
130
|
+
// PUT /customer/orders/:id/cancel — releases the stock reservation
|
|
131
|
+
// on an unpaid order. Use this to clean up after an E2E test order.
|
|
132
|
+
const result = await customerCartFetch(`/customer/orders/${orderId}/cancel`, { apiKey, customerToken, method: 'PUT' });
|
|
133
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
134
|
+
},
|
|
135
|
+
|
|
17
136
|
async store_list_products({ category, keyword, sort, page, limit } = {}) {
|
|
18
137
|
const p = new URLSearchParams();
|
|
19
138
|
if (category) p.set('type', category);
|
|
@@ -62,4 +181,12 @@ export const developerToolDefs = [
|
|
|
62
181
|
{ name: 'store_search_products', description: 'Search products by keyword.', schema: { query: { type: 'string' }, category: { type: 'string' }, page: { type: 'number' }, limit: { type: 'number' } } },
|
|
63
182
|
{ name: 'store_list_categories', description: 'List all visible product categories.', schema: {} },
|
|
64
183
|
{ name: 'store_get_integration_guide', description: 'Get code examples for integrating a specific EpicMerch module. module: auth | products | orders | payments', schema: { module: { type: 'string' } } },
|
|
184
|
+
{ name: 'store_cart_add', description: 'Add an item to a customer cart on the LIVE store, to verify the real cart contract before writing Cart.jsx/Checkout.jsx. Returns { httpStatus, ok, response }. Requires apiKey (store public key) + customerToken (a customer JWT from a storefront login — localStorage.customerInfo.token) + productId (+ optional qty, variant). Pair with store_cart_get to see how the added item comes back.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, productId: { type: 'string' }, qty: { type: 'number' }, variant: { type: 'string' } } },
|
|
185
|
+
{ name: 'store_cart_get', description: 'Fetch a customer cart from the LIVE store. Returns { httpStatus, ok, response }. Inspect response.cart to read the EXACT line-item shape your Cart.jsx must parse — { product: { _id, name, price, originalPrice, salePrice, image, type }, qty, variant }. Stops you guessing the shape from docs. Requires apiKey + customerToken.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' } } },
|
|
186
|
+
{ name: 'store_cart_remove', description: 'Remove an item from a customer cart on the LIVE store (clean up after a verification add). Requires apiKey + customerToken + productId (+ optional variant to disambiguate sizes).', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, productId: { type: 'string' }, variant: { type: 'string' } } },
|
|
187
|
+
{ name: 'store_auth_send_otp', description: 'Send an OTP via the LIVE store auth flow (POST /auth/otp/send) — exercises the REAL login path. For automated tests, use a phone configured as the server\'s E2E_TEST_PHONE so no real SMS is sent (the server seeds the test code instead). Requires apiKey + phone.', schema: { apiKey: { type: 'string' }, phone: { type: 'string' } } },
|
|
188
|
+
{ name: 'store_auth_verify_otp', description: 'Verify an OTP via the LIVE store auth flow (POST /auth/otp/verify) and get a customer token. Returns { httpStatus, ok, response } where response.token is the customer JWT to use with store_cart_* / store_order_*. With the E2E_TEST_PHONE bypass, pass the configured E2E_TEST_OTP. Requires apiKey + phone + otp.', schema: { apiKey: { type: 'string' }, phone: { type: 'string' }, otp: { type: 'string' }, profile: { type: 'object' } } },
|
|
189
|
+
{ name: 'store_order_create', description: 'Create an order on the LIVE store to verify the full checkout pipeline (stock reservation + order + Razorpay order creation). Does NOT charge — the order is UNPAID and its reservation auto-expires (or cancel it with store_order_cancel). Returns { httpStatus, ok, response } — confirm response has orderId, razorpayOrderId, razorpayKeyId, amount, currency, merchantName. Requires apiKey + customerToken + orderItems [{ productId, name, image, qty, price, variant }] + shippingAddress + totalPrice.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, orderItems: { type: 'array' }, shippingAddress: { type: 'object' }, paymentMethod: { type: 'string' }, totalPrice: { type: 'number' } } },
|
|
190
|
+
{ name: 'store_order_get', description: 'Fetch a customer order by id from the LIVE store. Returns { httpStatus, ok, response }. Requires apiKey + customerToken + orderId.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, orderId: { type: 'string' } } },
|
|
191
|
+
{ name: 'store_order_cancel', description: 'Cancel an unpaid order on the LIVE store — releases its stock reservation. Use to clean up after an E2E test order. Requires apiKey + customerToken + orderId.', schema: { apiKey: { type: 'string' }, customerToken: { type: 'string' }, orderId: { type: 'string' } } },
|
|
65
192
|
];
|
package/src/tools/merchant.js
CHANGED
|
@@ -426,10 +426,24 @@ export function merchantTools(client, session) {
|
|
|
426
426
|
hasLogo: Boolean(profile?.logo ?? profile?.user?.logo),
|
|
427
427
|
} : { error: profileR.error };
|
|
428
428
|
|
|
429
|
+
// Total available stock for a product. The Prisma API returns `stock`
|
|
430
|
+
// (NOT the legacy Mongo field `countInStock` — reading that made EVERY
|
|
431
|
+
// product look out-of-stock). For products with variants, the true
|
|
432
|
+
// availability is the sum of per-variant stock; the server derives
|
|
433
|
+
// `stock` from that on write, but we sum defensively in case the
|
|
434
|
+
// top-level value is stale. Falls back to countInStock only for very
|
|
435
|
+
// old API shapes.
|
|
436
|
+
const productStock = (p) => {
|
|
437
|
+
if (Array.isArray(p.variants) && p.variants.length > 0) {
|
|
438
|
+
return p.variants.reduce((sum, v) => sum + (Number(v.stock) || 0), 0);
|
|
439
|
+
}
|
|
440
|
+
return Number(p.stock ?? p.countInStock ?? 0) || 0;
|
|
441
|
+
};
|
|
442
|
+
|
|
429
443
|
const catalog = productsR.ok && categoriesR.ok ? {
|
|
430
444
|
productCount: products.length,
|
|
431
445
|
categoryCount: cats.length,
|
|
432
|
-
outOfStockCount: products.filter((p) => (p
|
|
446
|
+
outOfStockCount: products.filter((p) => productStock(p) === 0).length,
|
|
433
447
|
} : { error: (productsR.error || categoriesR.error) };
|
|
434
448
|
|
|
435
449
|
const payment = checkoutR.ok ? {
|