@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 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. Read products, orders, customers, and analytics, create payment links, manage coupons, and create or update products directly from chat.
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
- npm install -g @shoppexio/mcp-commerce-server
12
+ npx @shoppexio/mcp-shoppex install --api-key shx_your_dev_api_key
11
13
  ```
12
14
 
13
- Or run without installing:
15
+ That installs both commerce and theme MCPs. Or install commerce standalone:
14
16
 
15
17
  ```bash
16
- npx -y @shoppexio/mcp-commerce-server
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. Tools accept a `shop_api_key` argument on each call; the env var is available for clients that forward env to tool arguments.
39
+ Restart Claude Desktop.
38
40
 
39
- ## Cursor / Windsurf / Other
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 (12)
45
+ ## Tools (51)
44
46
 
45
- ### Read
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
- | Tool | Purpose |
48
- |------|---------|
49
- | `products.list` | Search or list products |
50
- | `products.get` | Fetch one product by id or uniqid |
51
- | `orders.list` | List recent orders, optionally filtered |
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
- ### Write
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
- | Tool | Purpose |
60
- |------|---------|
61
- | `products.create` | Create a product (service, serials, dynamic, file) |
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
- ## Required Scopes
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
- Your Shoppex Dev API key needs the scopes matching the tools you use (e.g. `products.read`, `products.write`, `orders.read`, `coupons.write`, `analytics.read`, `payment_links.write`).
78
+ ### Payment Links
79
+ - `payment_links_list` / `_get` / `_update` / `_delete` / `_toggle`
80
+ - `payment_links_create` — one-click checkout URLs
70
81
 
71
- Create one at [dashboard.shoppex.io/developer/api](https://dashboard.shoppex.io/developer/api).
82
+ ### Categories
83
+ - `categories_list` / `_create` / `_update`
72
84
 
73
- ## Environment Variables
85
+ ### Webhooks
86
+ - `webhooks_list` / `_create`
74
87
 
75
- | Variable | Required | Default |
76
- |----------|----------|---------|
77
- | `SHOPPEX_SHOP_API_KEY` | passed per call, or set here | — |
78
- | `SHOPPEX_API_BASE_URL` | no | `https://api.shoppex.io` |
79
- | `SHOPPEX_DEV_API_TIMEOUT_MS` | no | `10000` |
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
- ## Example Prompts
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
- Once connected in Claude Desktop:
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
- - "Make a payment link for a $50 USDT one-time purchase."
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.6.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.1.12"
50
+ "zod": "^4.3.6"
49
51
  }
50
52
  }
@@ -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
- name: z.string().trim().min(1).max(128),
766
- description: z.string().optional(),
767
- parent_id: z.string().trim().min(1).optional().nullable(),
768
- image_id: z.string().trim().min(1).optional().nullable(),
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 = { name: args.name };
772
- if (args.description !== undefined) body.description = args.description;
773
- if (args.parent_id !== undefined) body.parent_id = args.parent_id;
774
- if (args.image_id !== undefined) body.image_id = args.image_id;
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 id. Only provided fields change.',
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
- name: z.string().trim().min(1).max(128).optional(),
785
- description: z.string().optional(),
786
- parent_id: z.string().trim().min(1).optional().nullable(),
787
- image_id: z.string().trim().min(1).optional().nullable(),
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.name !== undefined) body.name = args.name;
792
- if (args.description !== undefined) body.description = args.description;
793
- if (args.parent_id !== undefined) body.parent_id = args.parent_id;
794
- if (args.image_id !== undefined) body.image_id = args.image_id;
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: 'orders_update',
901
- description: 'Update an order (notes, internal state). Only provided fields change.',
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
- notes: z.string().max(2000).optional().nullable(),
906
- internal_note: z.string().max(2000).optional().nullable(),
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.notes !== undefined) body.notes = args.notes;
911
- if (args.internal_note !== undefined) body.internal_note = args.internal_note;
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.number().positive(),
936
- currency: z.string().trim().min(3).max(8).default('USD'),
937
- reason: z.string().max(255).optional(),
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. Only provided fields change.',
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
- active: z.boolean().optional(),
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 body = {};
984
- if (args.name !== undefined) body.name = args.name;
985
- if (args.description !== undefined) body.description = args.description;
986
- if (args.price !== undefined) body.price = args.price;
987
- if (args.currency !== undefined) body.currency = args.currency;
988
- if (args.gateways !== undefined) body.gateways = args.gateways.join(',');
989
- if (args.active !== undefined) body.status = args.active;
990
- return createClient(args).paymentLinksUpdate(args.link_id, body);
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. Used when the merchant wants to proactively open a support conversation with a customer.',
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
- subject: z.string().trim().min(1).max(255),
1034
- message: z.string().trim().min(1),
1035
- customer_id: z.string().trim().min(1).optional(),
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 = { subject: args.subject, message: args.message };
1042
- if (args.customer_id !== undefined) body.customer_id = args.customer_id;
1043
- if (args.customer_email !== undefined) body.customer_email = args.customer_email;
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 (revoke, extend validity, reset HWID/IP locks). Set active=false to revoke access.',
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
- active: z.boolean().optional(),
1120
- valid_until: z.string().trim().optional().nullable(),
1121
- hwid_lock: z.string().trim().optional().nullable(),
1122
- ip_lock: z.string().trim().optional().nullable(),
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.active !== undefined) body.active = args.active;
1127
- if (args.valid_until !== undefined) body.valid_until = args.valid_until;
1128
- if (args.hwid_lock !== undefined) body.hwid_lock = args.hwid_lock;
1129
- if (args.ip_lock !== undefined) body.ip_lock = args.ip_lock;
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.1.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