@shoppexio/mcp-commerce-server 0.6.0 → 0.8.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 +109 -39
- package/package.json +4 -2
- package/src/prompts.mjs +168 -0
- package/src/resources.mjs +112 -0
- package/src/server.mjs +127 -66
package/README.md
CHANGED
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Model Context Protocol server for Shoppex commerce operations.
|
|
4
4
|
|
|
5
|
-
Connect Claude Desktop, Claude Code, Cursor, Windsurf, or Codex to your Shoppex store
|
|
5
|
+
Connect Claude Desktop, Claude Code, Cursor, Windsurf, or Codex to your Shoppex store — manage products, orders, customers, coupons, payment links, subscriptions, tickets, licenses, blacklist, analytics, and more. 51 tools, 5 resources, 6 workflow prompts.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
+
The fastest way is the one-shot installer:
|
|
10
|
+
|
|
9
11
|
```bash
|
|
10
|
-
|
|
12
|
+
npx @shoppexio/mcp-shoppex install --api-key shx_your_dev_api_key
|
|
11
13
|
```
|
|
12
14
|
|
|
13
|
-
Or
|
|
15
|
+
That installs both commerce and theme MCPs. Or install commerce standalone:
|
|
14
16
|
|
|
15
17
|
```bash
|
|
16
|
-
|
|
18
|
+
npm install -g @shoppexio/mcp-commerce-server
|
|
17
19
|
```
|
|
18
20
|
|
|
19
21
|
## Claude Desktop
|
|
@@ -34,63 +36,131 @@ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS)
|
|
|
34
36
|
}
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
Restart Claude Desktop.
|
|
39
|
+
Restart Claude Desktop.
|
|
38
40
|
|
|
39
|
-
## Cursor / Windsurf /
|
|
41
|
+
## Cursor / Windsurf / Claude Code
|
|
40
42
|
|
|
41
|
-
Any MCP client with stdio transport works. Set the same env var and point to `shoppex-mcp-commerce-server` as the command
|
|
43
|
+
Any MCP client with stdio transport works. Set the same env var and point to `shoppex-mcp-commerce-server` as the command — or use the `@shoppexio/cli` helper: `shoppex mcp install --client cursor|windsurf|claude-code`.
|
|
42
44
|
|
|
43
|
-
## Tools (
|
|
45
|
+
## Tools (51)
|
|
44
46
|
|
|
45
|
-
###
|
|
47
|
+
### Products
|
|
48
|
+
- `products_list` — search or list products
|
|
49
|
+
- `products_get` — fetch one by id/uniqid
|
|
50
|
+
- `products_create` — create (service, serials, dynamic, file)
|
|
51
|
+
- `products_update` — price, title, gateways, stock, visibility
|
|
52
|
+
- `products_delete` — soft-delete
|
|
53
|
+
- `products_duplicate` — clone
|
|
54
|
+
- `products_upload_serials` — bulk-add license keys
|
|
46
55
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
| `orders.get` | Fetch one order with line items |
|
|
53
|
-
| `customers.get` | Fetch a customer by id or email |
|
|
54
|
-
| `coupons.list` | List active coupons |
|
|
55
|
-
| `analytics.revenue` | Revenue totals for a date range |
|
|
56
|
+
### Orders
|
|
57
|
+
- `orders_list` — list, filter by status/email
|
|
58
|
+
- `orders_get` — fetch one with line items
|
|
59
|
+
- `orders_update` — notes, internal notes
|
|
60
|
+
- `orders_fulfill` — mark delivered
|
|
56
61
|
|
|
57
|
-
###
|
|
62
|
+
### Customers
|
|
63
|
+
- `customers_list` — list with filter expressions
|
|
64
|
+
- `customers_get` — fetch by id or email
|
|
65
|
+
- `customers_wallet_credit` — credit wallet balance
|
|
58
66
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
| `products.update` | Update a product's price, title, gateways, stock, visibility |
|
|
63
|
-
| `coupons.create` | Create a coupon (percentage or fixed discount) |
|
|
64
|
-
| `coupons.update` | Update an existing coupon |
|
|
65
|
-
| `payment_links.create` | Create a payment link |
|
|
67
|
+
### Invoices
|
|
68
|
+
- `invoices_list` — filter by status, customer
|
|
69
|
+
- `invoices_get` — fetch one
|
|
66
70
|
|
|
67
|
-
|
|
71
|
+
### Coupons
|
|
72
|
+
- `coupons_list` — list active
|
|
73
|
+
- `coupons_get` — fetch one
|
|
74
|
+
- `coupons_create` — percentage or fixed
|
|
75
|
+
- `coupons_update` — update any field
|
|
76
|
+
- `coupons_delete` — remove
|
|
68
77
|
|
|
69
|
-
|
|
78
|
+
### Payment Links
|
|
79
|
+
- `payment_links_list` / `_get` / `_update` / `_delete` / `_toggle`
|
|
80
|
+
- `payment_links_create` — one-click checkout URLs
|
|
70
81
|
|
|
71
|
-
|
|
82
|
+
### Categories
|
|
83
|
+
- `categories_list` / `_create` / `_update`
|
|
72
84
|
|
|
73
|
-
|
|
85
|
+
### Webhooks
|
|
86
|
+
- `webhooks_list` / `_create`
|
|
74
87
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
88
|
+
### Tickets (support inbox)
|
|
89
|
+
- `tickets_list` / `_get` / `_create` / `_reply` / `_close`
|
|
90
|
+
|
|
91
|
+
### Disputes
|
|
92
|
+
- `disputes_list` / `_get`
|
|
93
|
+
|
|
94
|
+
### Licenses
|
|
95
|
+
- `licenses_list` / `_get` / `_update` (revoke/extend/reset HWID locks)
|
|
96
|
+
|
|
97
|
+
### Subscriptions
|
|
98
|
+
- `subscriptions_list` / `_cancel`
|
|
99
|
+
|
|
100
|
+
### Affiliates
|
|
101
|
+
- `affiliates_list`
|
|
102
|
+
|
|
103
|
+
### Blacklist
|
|
104
|
+
- `blacklist_list` / `_add` / `_remove`
|
|
105
|
+
|
|
106
|
+
### Analytics
|
|
107
|
+
- `analytics_revenue` — revenue totals
|
|
108
|
+
- `analytics_reports_list` — custom reports
|
|
109
|
+
- `analytics_report_generate` — re-generate a report
|
|
110
|
+
|
|
111
|
+
## Resources (5)
|
|
80
112
|
|
|
81
|
-
|
|
113
|
+
Claude Desktop surfaces these as attachable context documents. Drag them into any chat to ground the conversation in live shop data:
|
|
82
114
|
|
|
83
|
-
|
|
115
|
+
| URI | What |
|
|
116
|
+
|------|------|
|
|
117
|
+
| `shoppex://products` | Full product catalog with variants |
|
|
118
|
+
| `shoppex://orders/recent` | Last 50 orders |
|
|
119
|
+
| `shoppex://analytics/summary` | MTD revenue snapshot |
|
|
120
|
+
| `shoppex://categories` | Category tree |
|
|
121
|
+
| `shoppex://tickets/open` | Open support tickets |
|
|
122
|
+
|
|
123
|
+
## Prompts (6)
|
|
124
|
+
|
|
125
|
+
Pre-built workflows available as slash commands:
|
|
126
|
+
|
|
127
|
+
| Prompt | What it does |
|
|
128
|
+
|--------|--------------|
|
|
129
|
+
| `/shoppex_daily_brief` | Morning summary: revenue, orders, attention items |
|
|
130
|
+
| `/shoppex_launch_product` | End-to-end product launch (args: product_name, price) |
|
|
131
|
+
| `/shoppex_fraud_check` | Suspicious-orders scan + blacklist suggestions |
|
|
132
|
+
| `/shoppex_weekly_recap` | 7-day performance recap |
|
|
133
|
+
| `/shoppex_customer_care` | Full customer timeline + draft reply (args: email) |
|
|
134
|
+
| `/shoppex_theme_refresh` | Active-theme structural review |
|
|
135
|
+
|
|
136
|
+
## Example prompts
|
|
84
137
|
|
|
85
138
|
- "Create a Discord Nitro 3-month product for $14.99 paid in crypto."
|
|
86
139
|
- "Show me this month's top 10 customers by revenue."
|
|
87
140
|
- "Generate a 20% off coupon for my Discord members, valid until end of month."
|
|
88
141
|
- "Update the price of product `prd_abc123` to $12.99."
|
|
89
|
-
- "
|
|
142
|
+
- "Refund customer alice@example.com for invoice `inv_xyz`."
|
|
143
|
+
- "Are there any fraud patterns in the last 48h?"
|
|
144
|
+
- `/shoppex_daily_brief`
|
|
145
|
+
|
|
146
|
+
## Environment Variables
|
|
147
|
+
|
|
148
|
+
| Variable | Required | Default |
|
|
149
|
+
|----------|----------|---------|
|
|
150
|
+
| `SHOPPEX_SHOP_API_KEY` | yes (or passed per call) | — |
|
|
151
|
+
| `SHOPPEX_API_BASE_URL` | no | `https://api.shoppex.io` |
|
|
152
|
+
| `SHOPPEX_DEV_API_TIMEOUT_MS` | no | `10000` |
|
|
153
|
+
|
|
154
|
+
Create an API key at [dashboard.shoppex.io/developer/api](https://dashboard.shoppex.io/developer/api).
|
|
90
155
|
|
|
91
156
|
## Companion
|
|
92
157
|
|
|
93
|
-
Pair with [`@shoppexio/mcp-theme-server`](https://www.npmjs.com/package/@shoppexio/mcp-theme-server) for storefront theme operations.
|
|
158
|
+
Pair with [`@shoppexio/mcp-theme-server`](https://www.npmjs.com/package/@shoppexio/mcp-theme-server) for storefront theme operations including `theme_ai_apply` (natural-language theme editing).
|
|
159
|
+
|
|
160
|
+
## Agent Templates
|
|
161
|
+
|
|
162
|
+
Paste-ready agent personalities (customer care, product launch, fraud investigator, analyst, theme editor):
|
|
163
|
+
[`docs/agents/`](https://github.com/ShoppexIO/shoppex/tree/main/docs/agents)
|
|
94
164
|
|
|
95
165
|
## Docs
|
|
96
166
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shoppexio/mcp-commerce-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Shoppex MCP server for commerce operations over the Dev API",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"bin",
|
|
36
36
|
"src/server.mjs",
|
|
37
|
+
"src/prompts.mjs",
|
|
38
|
+
"src/resources.mjs",
|
|
37
39
|
"src/index.d.ts",
|
|
38
40
|
"README.md"
|
|
39
41
|
],
|
|
@@ -45,6 +47,6 @@
|
|
|
45
47
|
},
|
|
46
48
|
"dependencies": {
|
|
47
49
|
"@modelcontextprotocol/sdk": "^1.28.0",
|
|
48
|
-
"zod": "^4.
|
|
50
|
+
"zod": "^4.3.6"
|
|
49
51
|
}
|
|
50
52
|
}
|
package/src/prompts.mjs
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Prompts for Shoppex — curated multi-tool workflows that merchants
|
|
3
|
+
* invoke by name in their MCP client. Each prompt returns a user-message
|
|
4
|
+
* with a full instruction block that primes the LLM to run a pre-planned
|
|
5
|
+
* sequence of tool calls.
|
|
6
|
+
*
|
|
7
|
+
* Registration via `server.registerPrompt(name, {title, description, argsSchema}, cb)`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as z from 'zod/v4';
|
|
11
|
+
|
|
12
|
+
function text(body) {
|
|
13
|
+
return { messages: [{ role: 'user', content: { type: 'text', text: body } }] };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function registerShoppexPrompts(server) {
|
|
17
|
+
server.registerPrompt(
|
|
18
|
+
'shoppex_daily_brief',
|
|
19
|
+
{
|
|
20
|
+
title: 'Daily brief',
|
|
21
|
+
description:
|
|
22
|
+
'Summarise today\'s shop health: revenue, orders, failed payments, open tickets, new disputes. Produces an actionable morning brief.',
|
|
23
|
+
},
|
|
24
|
+
() => text(`Give me a concise morning brief for my Shoppex shop. Follow this order:
|
|
25
|
+
|
|
26
|
+
1. Call analytics_revenue for the current month.
|
|
27
|
+
2. Call orders_list with limit=10 to see the most recent orders.
|
|
28
|
+
3. Call invoices_list with status="pending", limit=5.
|
|
29
|
+
4. Call disputes_list with limit=5 (only flag if status=open).
|
|
30
|
+
5. Call tickets_list with status="open", limit=5.
|
|
31
|
+
|
|
32
|
+
Return a short markdown briefing with these sections:
|
|
33
|
+
- **Revenue today / MTD** (use currency from the analytics response)
|
|
34
|
+
- **Latest orders** (3 most recent, one line each)
|
|
35
|
+
- **Attention items** — pending invoices over 24h, open disputes, open tickets
|
|
36
|
+
- **Suggested next action** — one sentence, concrete
|
|
37
|
+
|
|
38
|
+
Keep it under 150 words. Do not invent numbers — if a tool returned 0 items, say so.`),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
server.registerPrompt(
|
|
42
|
+
'shoppex_launch_product',
|
|
43
|
+
{
|
|
44
|
+
title: 'Launch a product',
|
|
45
|
+
description:
|
|
46
|
+
'End-to-end product launch: create the product, generate a launch coupon, and return a ready-to-send Discord/Telegram announcement.',
|
|
47
|
+
argsSchema: {
|
|
48
|
+
product_name: z.string().min(1),
|
|
49
|
+
price: z.string().min(1),
|
|
50
|
+
coupon_percent: z.string().min(1).optional(),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
({ product_name, price, coupon_percent }) => text(`Launch a new product called "${product_name}" priced at $${price}. Work in this order:
|
|
54
|
+
|
|
55
|
+
1. Call products_create with { title: "${product_name}", price: ${price}, type: "SERVICE" }. Capture the returned uniqid.
|
|
56
|
+
2. ${coupon_percent ? `Call coupons_create with { code: "LAUNCH${Math.round(Math.random() * 999)}", discount_value: ${coupon_percent}, discount_type: "PERCENTAGE" }.` : 'Skip coupon unless the user explicitly asks for one.'}
|
|
57
|
+
3. Call payment_links_create for the new product so we have a single shareable checkout URL. Use name="${product_name} launch", type="PRODUCT", gateways=["STRIPE","CRYPTO"].
|
|
58
|
+
|
|
59
|
+
After the tools run, produce:
|
|
60
|
+
- A short confirmation in markdown ("Created …").
|
|
61
|
+
- A ready-to-post Discord launch blurb (2-3 sentences, casual tone, include the payment link and the coupon code if created).
|
|
62
|
+
|
|
63
|
+
Do NOT invent the uniqid or URL — use what the tools returned.`),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
server.registerPrompt(
|
|
67
|
+
'shoppex_fraud_check',
|
|
68
|
+
{
|
|
69
|
+
title: 'Fraud check',
|
|
70
|
+
description:
|
|
71
|
+
'Investigate suspicious orders: look at recent high-value orders, match them against the blacklist, and propose block/refund actions.',
|
|
72
|
+
},
|
|
73
|
+
() => text(`Run a fraud-risk scan on the shop. Work in this order:
|
|
74
|
+
|
|
75
|
+
1. Call orders_list with limit=30.
|
|
76
|
+
2. Call disputes_list with limit=20.
|
|
77
|
+
3. Call blacklist_list with limit=50.
|
|
78
|
+
|
|
79
|
+
Analyse the data and answer:
|
|
80
|
+
- Any order whose customer email or IP is already on the blacklist? (exact match)
|
|
81
|
+
- Any order with an unusually high value vs. the shop's typical order size?
|
|
82
|
+
- Any order with a recent dispute against the same customer email?
|
|
83
|
+
- Any order from a country/IP cluster that has disputes (>=2 in the last 30 days)?
|
|
84
|
+
|
|
85
|
+
Return a markdown report:
|
|
86
|
+
- **Risk score**: low / medium / high (overall)
|
|
87
|
+
- **Flagged orders** (table: order id, customer, reason, suggested action)
|
|
88
|
+
- **Recommended blacklist adds** (email/ip/country with reason)
|
|
89
|
+
|
|
90
|
+
Do not execute block/refund automatically. End with: "Want me to add these to the blacklist? Confirm one-by-one."`),
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
server.registerPrompt(
|
|
94
|
+
'shoppex_weekly_recap',
|
|
95
|
+
{
|
|
96
|
+
title: 'Weekly recap',
|
|
97
|
+
description:
|
|
98
|
+
'Seven-day performance recap: revenue, top products, customer additions, operational health. Designed for founder/team standups.',
|
|
99
|
+
},
|
|
100
|
+
() => text(`Give me a 7-day recap for the shop. Work in this order:
|
|
101
|
+
|
|
102
|
+
1. Call analytics_revenue for the last 7 days.
|
|
103
|
+
2. Call orders_list with limit=50 (to estimate order volume).
|
|
104
|
+
3. Call products_list with limit=20 to know the catalog.
|
|
105
|
+
4. Call customers_list with limit=20 (most recent signups).
|
|
106
|
+
5. Call invoices_list with status="paid", limit=20.
|
|
107
|
+
|
|
108
|
+
Return a markdown recap with:
|
|
109
|
+
- **Revenue (7d)** + YoY trend if data allows
|
|
110
|
+
- **Top 3 products by order count**
|
|
111
|
+
- **New customers this week** (count + 2-3 notable emails if VIP-like)
|
|
112
|
+
- **Operational signals** (pending invoices, failed payments if visible)
|
|
113
|
+
- **Focus for next week** — one concrete initiative
|
|
114
|
+
|
|
115
|
+
Tone: factual, founder-facing, under 250 words.`),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
server.registerPrompt(
|
|
119
|
+
'shoppex_customer_care',
|
|
120
|
+
{
|
|
121
|
+
title: 'Customer care',
|
|
122
|
+
description:
|
|
123
|
+
'Look up a customer end-to-end: orders, invoices, subscriptions, licenses, tickets. Returns a full timeline plus a draft reply.',
|
|
124
|
+
argsSchema: {
|
|
125
|
+
email: z.string().email(),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
({ email }) => text(`Look up the full history for customer "${email}". Work in this order:
|
|
129
|
+
|
|
130
|
+
1. Call customers_get with email="${email}".
|
|
131
|
+
2. Call orders_list with customer_email="${email}", limit=20.
|
|
132
|
+
3. Call invoices_list with customer_email="${email}", limit=10.
|
|
133
|
+
4. Call licenses_list (then filter locally by the customer's id).
|
|
134
|
+
5. Call subscriptions_list (then filter locally by the customer's id).
|
|
135
|
+
|
|
136
|
+
Return a markdown report:
|
|
137
|
+
- **Summary** (customer name/email, total spend, first-seen / last-seen)
|
|
138
|
+
- **Timeline** (orders + tickets chronologically)
|
|
139
|
+
- **Open issues** (active disputes, unresolved tickets, failed payments)
|
|
140
|
+
- **Draft reply** — a ready-to-send support message that addresses likely concerns, signed as the shop
|
|
141
|
+
|
|
142
|
+
Keep the draft reply polite, short, and actionable. Do not invent details the tools didn't return.`),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
server.registerPrompt(
|
|
146
|
+
'shoppex_theme_refresh',
|
|
147
|
+
{
|
|
148
|
+
title: 'Theme refresh',
|
|
149
|
+
description:
|
|
150
|
+
'Guided mini-review of the shop\'s active theme: structure, sections, recent changes, publish readiness.',
|
|
151
|
+
},
|
|
152
|
+
() => text(`Do a theme-refresh review. Work in this order:
|
|
153
|
+
|
|
154
|
+
1. Call theme_list to find the active theme for this shop (is_active=true).
|
|
155
|
+
2. Call theme_inspect on the active theme to get sections, settings, tokens.
|
|
156
|
+
3. Call theme_diff on the active theme to see draft vs. published state.
|
|
157
|
+
4. Call theme_latest_run on the active theme to see the last build health.
|
|
158
|
+
|
|
159
|
+
Return a markdown report:
|
|
160
|
+
- **Active theme** (name, scheme, version)
|
|
161
|
+
- **Section inventory** (list of sections + page count)
|
|
162
|
+
- **Draft status** (any pending changes? any build errors?)
|
|
163
|
+
- **Publish readiness** — safe to publish? what risks?
|
|
164
|
+
- **Suggested improvements** — 2-3 concrete section-level tweaks (e.g. "Add a testimonials section above footer"). Keep them specific.
|
|
165
|
+
|
|
166
|
+
Format: founder-facing, under 200 words.`),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Resources for Shoppex — cacheable read-only context documents the
|
|
3
|
+
* client can attach to a conversation (e.g. Claude Desktop "Attach" menu).
|
|
4
|
+
* Each resource fetches fresh data on read and returns JSON text.
|
|
5
|
+
*
|
|
6
|
+
* Registration via `server.registerResource(name, uri, { title, description, mimeType }, async () => ({ contents: [...] }))`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { ShoppexCommerceDevApiClient } from './server.mjs';
|
|
10
|
+
|
|
11
|
+
function resolveShopApiKey() {
|
|
12
|
+
const fromEnv = process.env.SHOPPEX_SHOP_API_KEY?.trim()
|
|
13
|
+
|| process.env.SHOPPEX_API_KEY?.trim()
|
|
14
|
+
|| process.env.SHOP_API_KEY?.trim();
|
|
15
|
+
if (!fromEnv) {
|
|
16
|
+
throw new Error('Shoppex resources require SHOPPEX_SHOP_API_KEY to be set.');
|
|
17
|
+
}
|
|
18
|
+
return fromEnv;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function buildClient() {
|
|
22
|
+
return new ShoppexCommerceDevApiClient({
|
|
23
|
+
shopApiKey: resolveShopApiKey(),
|
|
24
|
+
apiBaseUrl: process.env.SHOPPEX_API_BASE_URL?.trim()
|
|
25
|
+
|| process.env.API_URL?.trim()
|
|
26
|
+
|| 'https://api.shoppex.io',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function jsonResource(uri, value) {
|
|
31
|
+
return {
|
|
32
|
+
contents: [
|
|
33
|
+
{
|
|
34
|
+
uri,
|
|
35
|
+
mimeType: 'application/json',
|
|
36
|
+
text: JSON.stringify(value, null, 2),
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function registerShoppexResources(server) {
|
|
43
|
+
server.registerResource(
|
|
44
|
+
'shoppex_products_catalog',
|
|
45
|
+
'shoppex://products',
|
|
46
|
+
{
|
|
47
|
+
title: 'Product catalog',
|
|
48
|
+
description: 'Current product catalog for the authenticated shop (up to 50 products, includes variants).',
|
|
49
|
+
mimeType: 'application/json',
|
|
50
|
+
},
|
|
51
|
+
async (uri) => {
|
|
52
|
+
const data = await buildClient().productsList({ limit: 50 });
|
|
53
|
+
return jsonResource(uri.href, data ?? []);
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
server.registerResource(
|
|
58
|
+
'shoppex_recent_orders',
|
|
59
|
+
'shoppex://orders/recent',
|
|
60
|
+
{
|
|
61
|
+
title: 'Recent orders',
|
|
62
|
+
description: '50 most recent orders across all statuses.',
|
|
63
|
+
mimeType: 'application/json',
|
|
64
|
+
},
|
|
65
|
+
async (uri) => {
|
|
66
|
+
const data = await buildClient().ordersList({ limit: 50 });
|
|
67
|
+
return jsonResource(uri.href, data ?? []);
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
server.registerResource(
|
|
72
|
+
'shoppex_analytics_summary',
|
|
73
|
+
'shoppex://analytics/summary',
|
|
74
|
+
{
|
|
75
|
+
title: 'Analytics summary',
|
|
76
|
+
description: 'Month-to-date revenue totals and recent trends.',
|
|
77
|
+
mimeType: 'application/json',
|
|
78
|
+
},
|
|
79
|
+
async (uri) => {
|
|
80
|
+
const data = await buildClient().analyticsRevenue({ currency: 'USD' });
|
|
81
|
+
return jsonResource(uri.href, data ?? {});
|
|
82
|
+
},
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
server.registerResource(
|
|
86
|
+
'shoppex_categories',
|
|
87
|
+
'shoppex://categories',
|
|
88
|
+
{
|
|
89
|
+
title: 'Category tree',
|
|
90
|
+
description: 'Full product category list for the shop.',
|
|
91
|
+
mimeType: 'application/json',
|
|
92
|
+
},
|
|
93
|
+
async (uri) => {
|
|
94
|
+
const data = await buildClient().categoriesList();
|
|
95
|
+
return jsonResource(uri.href, data ?? []);
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
server.registerResource(
|
|
100
|
+
'shoppex_open_tickets',
|
|
101
|
+
'shoppex://tickets/open',
|
|
102
|
+
{
|
|
103
|
+
title: 'Open support tickets',
|
|
104
|
+
description: 'Tickets currently in status=open for the shop.',
|
|
105
|
+
mimeType: 'application/json',
|
|
106
|
+
},
|
|
107
|
+
async (uri) => {
|
|
108
|
+
const data = await buildClient().ticketsList({ limit: 50, status: 'open' });
|
|
109
|
+
return jsonResource(uri.href, data ?? []);
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
}
|
package/src/server.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import * as z from 'zod/v4';
|
|
4
|
+
import { registerShoppexPrompts } from './prompts.mjs';
|
|
5
|
+
import { registerShoppexResources } from './resources.mjs';
|
|
4
6
|
|
|
5
7
|
const DEV_API_TIMEOUT_MS = Number.parseInt(process.env.SHOPPEX_DEV_API_TIMEOUT_MS ?? '10000', 10);
|
|
6
8
|
|
|
@@ -433,9 +435,15 @@ export class ShoppexCommerceDevApiClient {
|
|
|
433
435
|
}
|
|
434
436
|
}
|
|
435
437
|
|
|
438
|
+
// SECURITY: `api_base_url` is intentionally NOT a tool argument.
|
|
439
|
+
// The Shoppex shop API bearer token is attached to every request, so the
|
|
440
|
+
// destination origin must never be chosen by an (untrusted) tool caller —
|
|
441
|
+
// that would allow exfiltrating the credential to an attacker-controlled host
|
|
442
|
+
// (SSRF / credential disclosure, CWE-918/CWE-200). The API base URL is resolved
|
|
443
|
+
// solely from server-side env (`SHOPPEX_API_BASE_URL`/`API_URL`) via
|
|
444
|
+
// `resolveBaseUrl`, which the operator controls, not the caller.
|
|
436
445
|
const BaseToolSchema = {
|
|
437
446
|
shop_api_key: z.string().trim().min(1).optional(),
|
|
438
|
-
api_base_url: z.string().trim().url().optional(),
|
|
439
447
|
};
|
|
440
448
|
|
|
441
449
|
function resolveShopApiKey(explicitValue) {
|
|
@@ -455,9 +463,10 @@ function resolveShopApiKey(explicitValue) {
|
|
|
455
463
|
}
|
|
456
464
|
|
|
457
465
|
function createClient(args) {
|
|
466
|
+
// Do not pass any caller-supplied base URL here. The destination origin is
|
|
467
|
+
// resolved from server env only (see BaseToolSchema security note).
|
|
458
468
|
return new ShoppexCommerceDevApiClient({
|
|
459
469
|
shopApiKey: resolveShopApiKey(args.shop_api_key),
|
|
460
|
-
apiBaseUrl: args.api_base_url,
|
|
461
470
|
});
|
|
462
471
|
}
|
|
463
472
|
|
|
@@ -759,39 +768,43 @@ export function createCommerceToolCatalog() {
|
|
|
759
768
|
},
|
|
760
769
|
{
|
|
761
770
|
name: 'categories_create',
|
|
762
|
-
description: 'Create a product category.',
|
|
771
|
+
description: 'Create a product category. Fields match POST /dev/v1/categories.',
|
|
763
772
|
inputSchema: {
|
|
764
773
|
...BaseToolSchema,
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
774
|
+
title: z.string().trim().min(1).max(64),
|
|
775
|
+
unlisted: z.boolean().optional(),
|
|
776
|
+
products_bound: z.array(z.string().trim().min(1)).optional(),
|
|
777
|
+
groups_bound: z.array(z.string().trim().min(1)).optional(),
|
|
778
|
+
sort_priority: z.number().int().optional(),
|
|
769
779
|
},
|
|
770
780
|
execute: async (args) => {
|
|
771
|
-
const body = {
|
|
772
|
-
if (args.
|
|
773
|
-
if (args.
|
|
774
|
-
if (args.
|
|
781
|
+
const body = { title: args.title };
|
|
782
|
+
if (args.unlisted !== undefined) body.unlisted = args.unlisted;
|
|
783
|
+
if (args.products_bound !== undefined) body.products_bound = args.products_bound;
|
|
784
|
+
if (args.groups_bound !== undefined) body.groups_bound = args.groups_bound;
|
|
785
|
+
if (args.sort_priority !== undefined) body.sort_priority = args.sort_priority;
|
|
775
786
|
return createClient(args).categoriesCreate(body);
|
|
776
787
|
},
|
|
777
788
|
},
|
|
778
789
|
{
|
|
779
790
|
name: 'categories_update',
|
|
780
|
-
description: 'Update a category by
|
|
791
|
+
description: 'Update a category by uniqid. Only provided fields change. Uses the same field shape as categories_create.',
|
|
781
792
|
inputSchema: {
|
|
782
793
|
...BaseToolSchema,
|
|
783
794
|
category_id: z.string().trim().min(1),
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
795
|
+
title: z.string().trim().min(1).max(64).optional(),
|
|
796
|
+
unlisted: z.boolean().optional(),
|
|
797
|
+
products_bound: z.array(z.string().trim().min(1)).optional(),
|
|
798
|
+
groups_bound: z.array(z.string().trim().min(1)).optional(),
|
|
799
|
+
sort_priority: z.number().int().optional(),
|
|
788
800
|
},
|
|
789
801
|
execute: async (args) => {
|
|
790
802
|
const body = {};
|
|
791
|
-
if (args.
|
|
792
|
-
if (args.
|
|
793
|
-
if (args.
|
|
794
|
-
if (args.
|
|
803
|
+
if (args.title !== undefined) body.title = args.title;
|
|
804
|
+
if (args.unlisted !== undefined) body.unlisted = args.unlisted;
|
|
805
|
+
if (args.products_bound !== undefined) body.products_bound = args.products_bound;
|
|
806
|
+
if (args.groups_bound !== undefined) body.groups_bound = args.groups_bound;
|
|
807
|
+
if (args.sort_priority !== undefined) body.sort_priority = args.sort_priority;
|
|
795
808
|
return createClient(args).categoriesUpdate(args.category_id, body);
|
|
796
809
|
},
|
|
797
810
|
},
|
|
@@ -897,18 +910,20 @@ export function createCommerceToolCatalog() {
|
|
|
897
910
|
},
|
|
898
911
|
},
|
|
899
912
|
{
|
|
900
|
-
name: '
|
|
901
|
-
description: '
|
|
913
|
+
name: 'orders_set_affiliate',
|
|
914
|
+
description: 'Attribute an order to an affiliate after the fact. Backed by PATCH /dev/v1/orders/:id which is the affiliate-attribution endpoint. Provide one of affiliate_code, referral_code, or affiliate_id (pass null to clear an existing attribution).',
|
|
902
915
|
inputSchema: {
|
|
903
916
|
...BaseToolSchema,
|
|
904
917
|
order_id: z.string().trim().min(1),
|
|
905
|
-
|
|
906
|
-
|
|
918
|
+
affiliate_code: z.string().trim().optional().nullable(),
|
|
919
|
+
referral_code: z.string().trim().optional().nullable(),
|
|
920
|
+
affiliate_id: z.string().trim().optional().nullable(),
|
|
907
921
|
},
|
|
908
922
|
execute: async (args) => {
|
|
909
923
|
const body = {};
|
|
910
|
-
if (args.
|
|
911
|
-
if (args.
|
|
924
|
+
if (args.affiliate_code !== undefined) body.affiliate_code = args.affiliate_code;
|
|
925
|
+
if (args.referral_code !== undefined) body.referral_code = args.referral_code;
|
|
926
|
+
if (args.affiliate_id !== undefined) body.affiliate_id = args.affiliate_id;
|
|
912
927
|
return createClient(args).ordersUpdate(args.order_id, body);
|
|
913
928
|
},
|
|
914
929
|
},
|
|
@@ -928,19 +943,22 @@ export function createCommerceToolCatalog() {
|
|
|
928
943
|
},
|
|
929
944
|
{
|
|
930
945
|
name: 'customers_wallet_credit',
|
|
931
|
-
description: 'Credit a customer wallet balance. Useful for refunds, goodwill credits, or promo grants.',
|
|
946
|
+
description: 'Credit a customer wallet balance. Useful for refunds, goodwill credits, or promo grants. Amount is a decimal string with up to 2 decimal places (e.g. "5.00"). Wallet currency is fixed to the shop default.',
|
|
932
947
|
inputSchema: {
|
|
933
948
|
...BaseToolSchema,
|
|
934
949
|
customer_id: z.string().trim().min(1),
|
|
935
|
-
amount: z.
|
|
936
|
-
|
|
937
|
-
|
|
950
|
+
amount: z.string().regex(/^\d+(\.\d{1,2})?$/, 'Amount must be a positive decimal string (e.g. "5.00")'),
|
|
951
|
+
type: z.enum(['CREDIT', 'PROMO', 'ADJUSTMENT']).optional(),
|
|
952
|
+
description: z.string().max(500).optional(),
|
|
953
|
+
expires_at: z.string().datetime().optional(),
|
|
954
|
+
},
|
|
955
|
+
execute: async (args) => {
|
|
956
|
+
const body = { amount: args.amount };
|
|
957
|
+
if (args.type !== undefined) body.type = args.type;
|
|
958
|
+
if (args.description !== undefined) body.description = args.description;
|
|
959
|
+
if (args.expires_at !== undefined) body.expires_at = args.expires_at;
|
|
960
|
+
return createClient(args).customersWalletCredit(args.customer_id, body);
|
|
938
961
|
},
|
|
939
|
-
execute: async (args) => createClient(args).customersWalletCredit(args.customer_id, {
|
|
940
|
-
amount: args.amount,
|
|
941
|
-
currency: args.currency,
|
|
942
|
-
reason: args.reason,
|
|
943
|
-
}),
|
|
944
962
|
},
|
|
945
963
|
{
|
|
946
964
|
name: 'coupons_get',
|
|
@@ -968,26 +986,65 @@ export function createCommerceToolCatalog() {
|
|
|
968
986
|
},
|
|
969
987
|
{
|
|
970
988
|
name: 'payment_links_update',
|
|
971
|
-
description: 'Update a payment link by uniqid.
|
|
989
|
+
description: 'Update a payment link by uniqid. The backend expects a full create-style payload, so this tool reads the existing link, merges your overrides, and sends the merged result. Pass only the fields you want to change — the rest are preserved from the current link.',
|
|
972
990
|
inputSchema: {
|
|
973
991
|
...BaseToolSchema,
|
|
974
992
|
link_id: z.string().trim().min(1),
|
|
975
993
|
name: z.string().trim().min(1).max(255).optional(),
|
|
994
|
+
type: z.enum(['PRODUCT', 'SUBSCRIPTION', 'SUBSCRIPTION_V2', 'LICENSE', 'PAY_WHAT_YOU_WANT', 'FIXED_PRICE']).optional(),
|
|
976
995
|
description: z.string().max(255).optional().nullable(),
|
|
996
|
+
active: z.boolean().optional(),
|
|
977
997
|
price: z.number().positive().optional(),
|
|
978
998
|
currency: z.string().trim().min(3).max(8).optional(),
|
|
979
999
|
gateways: z.array(z.string().trim().min(1)).optional(),
|
|
980
|
-
|
|
1000
|
+
product_ids: z.array(z.string().trim().min(1)).optional(),
|
|
1001
|
+
cta: z.enum(['PAY', 'BOOK', 'DONATE', 'SUBSCRIBE']).optional(),
|
|
1002
|
+
allow_discount_code: z.boolean().optional(),
|
|
1003
|
+
require_customer_address: z.boolean().optional(),
|
|
1004
|
+
require_customer_phone: z.boolean().optional(),
|
|
1005
|
+
show_confirmation_page: z.boolean().optional(),
|
|
1006
|
+
return_url: z.string().trim().url().optional().nullable(),
|
|
981
1007
|
},
|
|
982
1008
|
execute: async (args) => {
|
|
983
|
-
const
|
|
984
|
-
|
|
985
|
-
if (
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
1009
|
+
const client = createClient(args);
|
|
1010
|
+
const existing = await client.paymentLinksGet(args.link_id);
|
|
1011
|
+
if (!existing || typeof existing !== 'object') {
|
|
1012
|
+
throw new ShoppexCommerceDevApiError(
|
|
1013
|
+
`payment_links_update: payment link ${args.link_id} not found.`,
|
|
1014
|
+
{ code: 'not_found', retryable: false },
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
const cur = existing;
|
|
1018
|
+
const body = {
|
|
1019
|
+
uniqid: cur.uniqid ?? args.link_id,
|
|
1020
|
+
name: args.name ?? cur.name,
|
|
1021
|
+
type: args.type ?? cur.type,
|
|
1022
|
+
description: args.description !== undefined ? args.description : cur.description ?? null,
|
|
1023
|
+
status: args.active !== undefined ? args.active : cur.status,
|
|
1024
|
+
price: args.price !== undefined ? args.price : cur.price,
|
|
1025
|
+
currency: args.currency ?? cur.currency ?? 'USD',
|
|
1026
|
+
gateways: args.gateways !== undefined
|
|
1027
|
+
? args.gateways.join(',')
|
|
1028
|
+
: Array.isArray(cur.gateways) ? cur.gateways.join(',') : (cur.gateways ?? ''),
|
|
1029
|
+
products_ids: args.product_ids !== undefined
|
|
1030
|
+
? args.product_ids.join(',')
|
|
1031
|
+
: Array.isArray(cur.products_ids) ? cur.products_ids.join(',') : (cur.products_ids ?? ''),
|
|
1032
|
+
cta: args.cta ?? cur.cta ?? 'PAY',
|
|
1033
|
+
discount_code_allowed: args.allow_discount_code !== undefined
|
|
1034
|
+
? args.allow_discount_code
|
|
1035
|
+
: (cur.discount_code_allowed ?? false),
|
|
1036
|
+
customer_address_required: args.require_customer_address !== undefined
|
|
1037
|
+
? args.require_customer_address
|
|
1038
|
+
: (cur.customer_address_required ?? false),
|
|
1039
|
+
customer_phone_required: args.require_customer_phone !== undefined
|
|
1040
|
+
? args.require_customer_phone
|
|
1041
|
+
: (cur.customer_phone_required ?? false),
|
|
1042
|
+
show_confirmation_page: args.show_confirmation_page !== undefined
|
|
1043
|
+
? args.show_confirmation_page
|
|
1044
|
+
: (cur.show_confirmation_page ?? true),
|
|
1045
|
+
return_url: args.return_url !== undefined ? args.return_url : cur.return_url ?? null,
|
|
1046
|
+
};
|
|
1047
|
+
return client.paymentLinksUpdate(args.link_id, body);
|
|
991
1048
|
},
|
|
992
1049
|
},
|
|
993
1050
|
{
|
|
@@ -1027,22 +1084,21 @@ export function createCommerceToolCatalog() {
|
|
|
1027
1084
|
},
|
|
1028
1085
|
{
|
|
1029
1086
|
name: 'tickets_create',
|
|
1030
|
-
description: 'Create a new support ticket.
|
|
1087
|
+
description: 'Create a new support ticket. Email is the customer email; title is short (max 30 chars); message is the opening body. Optionally link an invoice.',
|
|
1031
1088
|
inputSchema: {
|
|
1032
1089
|
...BaseToolSchema,
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
customer_email: z.string().trim().email().optional(),
|
|
1090
|
+
email: z.string().trim().min(1),
|
|
1091
|
+
title: z.string().trim().min(2).max(30),
|
|
1092
|
+
message: z.string().trim().min(2).max(2000),
|
|
1037
1093
|
invoice_id: z.string().trim().min(1).optional(),
|
|
1038
|
-
order_id: z.string().trim().min(1).optional(),
|
|
1039
1094
|
},
|
|
1040
1095
|
execute: async (args) => {
|
|
1041
|
-
const body = {
|
|
1042
|
-
|
|
1043
|
-
|
|
1096
|
+
const body = {
|
|
1097
|
+
email: args.email,
|
|
1098
|
+
title: args.title,
|
|
1099
|
+
message: args.message,
|
|
1100
|
+
};
|
|
1044
1101
|
if (args.invoice_id !== undefined) body.invoice_id = args.invoice_id;
|
|
1045
|
-
if (args.order_id !== undefined) body.order_id = args.order_id;
|
|
1046
1102
|
return createClient(args).ticketsCreate(body);
|
|
1047
1103
|
},
|
|
1048
1104
|
},
|
|
@@ -1112,21 +1168,23 @@ export function createCommerceToolCatalog() {
|
|
|
1112
1168
|
},
|
|
1113
1169
|
{
|
|
1114
1170
|
name: 'licenses_update',
|
|
1115
|
-
description: 'Update a license (
|
|
1171
|
+
description: 'Update a license: set status (ACTIVE/SUSPENDED/REVOKED/EXPIRED), change the hardware-id binding, allowlist IPs, change activation limit, or update the expiry. Set status="REVOKED" to revoke access.',
|
|
1116
1172
|
inputSchema: {
|
|
1117
1173
|
...BaseToolSchema,
|
|
1118
1174
|
license_id: z.string().trim().min(1),
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1175
|
+
status: z.enum(['ACTIVE', 'SUSPENDED', 'REVOKED', 'EXPIRED']).optional(),
|
|
1176
|
+
hardware_id: z.string().trim().optional().nullable(),
|
|
1177
|
+
allowed_ips: z.array(z.string().trim().min(1)).optional().nullable(),
|
|
1178
|
+
max_uses: z.number().int().min(1).optional().nullable(),
|
|
1179
|
+
expires_at: z.string().trim().optional().nullable(),
|
|
1123
1180
|
},
|
|
1124
1181
|
execute: async (args) => {
|
|
1125
|
-
const body = {};
|
|
1126
|
-
if (args.
|
|
1127
|
-
if (args.
|
|
1128
|
-
if (args.
|
|
1129
|
-
if (args.
|
|
1182
|
+
const body = { id: args.license_id };
|
|
1183
|
+
if (args.status !== undefined) body.status = args.status;
|
|
1184
|
+
if (args.hardware_id !== undefined) body.hardware_id = args.hardware_id;
|
|
1185
|
+
if (args.allowed_ips !== undefined) body.allowed_ips = args.allowed_ips;
|
|
1186
|
+
if (args.max_uses !== undefined) body.max_uses = args.max_uses;
|
|
1187
|
+
if (args.expires_at !== undefined) body.expires_at = args.expires_at;
|
|
1130
1188
|
return createClient(args).licensesUpdate(args.license_id, body);
|
|
1131
1189
|
},
|
|
1132
1190
|
},
|
|
@@ -1162,7 +1220,7 @@ export async function executeCommerceTool(toolName, args) {
|
|
|
1162
1220
|
export function createCommerceMcpServer() {
|
|
1163
1221
|
const server = new McpServer({
|
|
1164
1222
|
name: 'shoppex-commerce',
|
|
1165
|
-
version: '0.
|
|
1223
|
+
version: '0.8.0',
|
|
1166
1224
|
});
|
|
1167
1225
|
|
|
1168
1226
|
for (const tool of createCommerceToolCatalog()) {
|
|
@@ -1175,6 +1233,9 @@ export function createCommerceMcpServer() {
|
|
|
1175
1233
|
});
|
|
1176
1234
|
}
|
|
1177
1235
|
|
|
1236
|
+
registerShoppexResources(server);
|
|
1237
|
+
registerShoppexPrompts(server);
|
|
1238
|
+
|
|
1178
1239
|
return server;
|
|
1179
1240
|
}
|
|
1180
1241
|
|