@shoppexio/mcp-commerce-server 0.2.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 ADDED
@@ -0,0 +1,103 @@
1
+ # @shoppexio/mcp-commerce-server
2
+
3
+ Model Context Protocol server for Shoppex commerce operations.
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.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install -g @shoppexio/mcp-commerce-server
11
+ ```
12
+
13
+ Or run without installing:
14
+
15
+ ```bash
16
+ npx -y @shoppexio/mcp-commerce-server
17
+ ```
18
+
19
+ ## Claude Desktop
20
+
21
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
22
+
23
+ ```json
24
+ {
25
+ "mcpServers": {
26
+ "shoppex-commerce": {
27
+ "command": "npx",
28
+ "args": ["-y", "@shoppexio/mcp-commerce-server"],
29
+ "env": {
30
+ "SHOPPEX_SHOP_API_KEY": "shx_your_dev_api_key"
31
+ }
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
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.
38
+
39
+ ## Cursor / Windsurf / Other
40
+
41
+ Any MCP client with stdio transport works. Set the same env var and point to `shoppex-mcp-commerce-server` as the command.
42
+
43
+ ## Tools (12)
44
+
45
+ ### Read
46
+
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
+
57
+ ### Write
58
+
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 |
66
+
67
+ ## Required Scopes
68
+
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`).
70
+
71
+ Create one at [dashboard.shoppex.io/developer/api](https://dashboard.shoppex.io/developer/api).
72
+
73
+ ## Environment Variables
74
+
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` |
80
+
81
+ ## Example Prompts
82
+
83
+ Once connected in Claude Desktop:
84
+
85
+ - "Create a Discord Nitro 3-month product for $14.99 paid in crypto."
86
+ - "Show me this month's top 10 customers by revenue."
87
+ - "Generate a 20% off coupon for my Discord members, valid until end of month."
88
+ - "Update the price of product `prd_abc123` to $12.99."
89
+ - "Make a payment link for a $50 USDT one-time purchase."
90
+
91
+ ## Companion
92
+
93
+ Pair with [`@shoppexio/mcp-theme-server`](https://www.npmjs.com/package/@shoppexio/mcp-theme-server) for storefront theme operations.
94
+
95
+ ## Docs
96
+
97
+ - [Developer API Reference](https://docs.shoppex.io/api-reference/introduction)
98
+ - [Quickstart](https://docs.shoppex.io/api-reference/quickstart)
99
+ - [Authentication](https://docs.shoppex.io/api-reference/authentication)
100
+
101
+ ## License
102
+
103
+ Proprietary. © Shoppex.
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { startCommerceMcpServer } from '../src/server.mjs';
4
+
5
+ await startCommerceMcpServer().catch((error) => {
6
+ console.error(error instanceof Error ? error.message : 'Failed to start Shoppex commerce MCP server.');
7
+ process.exit(1);
8
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@shoppexio/mcp-commerce-server",
3
+ "version": "0.2.0",
4
+ "description": "Shoppex MCP server for commerce operations over the Dev API",
5
+ "type": "module",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/ShoppexIO/shoppex.git",
9
+ "directory": "packages/mcp-commerce-server"
10
+ },
11
+ "homepage": "https://docs.shoppex.io/developer-api",
12
+ "bugs": {
13
+ "url": "https://github.com/ShoppexIO/shoppex/issues"
14
+ },
15
+ "publishConfig": {
16
+ "access": "public"
17
+ },
18
+ "exports": {
19
+ ".": {
20
+ "import": "./src/server.mjs",
21
+ "types": "./src/index.d.ts"
22
+ }
23
+ },
24
+ "keywords": [
25
+ "shoppex",
26
+ "mcp",
27
+ "commerce",
28
+ "server",
29
+ "telegram"
30
+ ],
31
+ "bin": {
32
+ "shoppex-mcp-commerce-server": "./bin/shoppex-mcp-commerce-server.mjs"
33
+ },
34
+ "files": [
35
+ "bin",
36
+ "src/server.mjs",
37
+ "src/index.d.ts",
38
+ "README.md"
39
+ ],
40
+ "scripts": {
41
+ "test": "bun test"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
45
+ },
46
+ "dependencies": {
47
+ "@modelcontextprotocol/sdk": "^1.28.0",
48
+ "zod": "^4.1.12"
49
+ }
50
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export type CommerceToolDefinition = {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: unknown;
5
+ execute: (args: Record<string, unknown>) => Promise<unknown>;
6
+ };
7
+
8
+ export function createCommerceToolCatalog(): CommerceToolDefinition[];
9
+ export function executeCommerceTool(toolName: string, args: Record<string, unknown>): Promise<unknown>;
10
+ export function createCommerceMcpServer(): unknown;
11
+ export function startCommerceMcpServer(): Promise<void>;
12
+ export class ShoppexCommerceDevApiClient {
13
+ constructor(input: { shopApiKey: string; apiBaseUrl?: string; fetchImpl?: typeof fetch });
14
+ request(method: string, path: string, options?: { query?: Record<string, unknown>; body?: unknown }): Promise<unknown>;
15
+ }
16
+ export class ShoppexCommerceDevApiError extends Error {
17
+ status: number | null;
18
+ code: string;
19
+ retryable: boolean;
20
+ }
package/src/server.mjs ADDED
@@ -0,0 +1,557 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import * as z from 'zod/v4';
4
+
5
+ const DEV_API_TIMEOUT_MS = Number.parseInt(process.env.SHOPPEX_DEV_API_TIMEOUT_MS ?? '10000', 10);
6
+
7
+ function resolveBaseUrl(explicitValue) {
8
+ const rawValue = typeof explicitValue === 'string' && explicitValue.trim()
9
+ ? explicitValue.trim()
10
+ : (process.env.SHOPPEX_API_BASE_URL?.trim() || process.env.API_URL?.trim() || 'https://api.shoppex.io');
11
+ return rawValue.replace(/\/+$/, '');
12
+ }
13
+
14
+ function jsonContent(value) {
15
+ return {
16
+ content: [
17
+ {
18
+ type: 'text',
19
+ text: JSON.stringify(value, null, 2),
20
+ },
21
+ ],
22
+ structuredContent: value,
23
+ };
24
+ }
25
+
26
+ export class ShoppexCommerceDevApiError extends Error {
27
+ constructor(
28
+ message,
29
+ {
30
+ status = null,
31
+ code = 'unknown_error',
32
+ retryable = false,
33
+ cause = undefined,
34
+ } = {},
35
+ ) {
36
+ super(message, cause ? { cause } : undefined);
37
+ this.name = 'ShoppexCommerceDevApiError';
38
+ this.status = status;
39
+ this.code = code;
40
+ this.retryable = retryable;
41
+ }
42
+ }
43
+
44
+ function classifyHttpError(status) {
45
+ if (status === 401 || status === 403) {
46
+ return { code: 'auth_error', retryable: false };
47
+ }
48
+
49
+ if (status === 404) {
50
+ return { code: 'not_found', retryable: false };
51
+ }
52
+
53
+ if (status === 408 || status === 429) {
54
+ return { code: 'rate_limited', retryable: true };
55
+ }
56
+
57
+ if (status >= 500) {
58
+ return { code: 'upstream_error', retryable: true };
59
+ }
60
+
61
+ return { code: 'bad_request', retryable: false };
62
+ }
63
+
64
+ export class ShoppexCommerceDevApiClient {
65
+ constructor({ shopApiKey, apiBaseUrl, fetchImpl = fetch }) {
66
+ this.shopApiKey = shopApiKey.trim();
67
+ this.apiBaseUrl = resolveBaseUrl(apiBaseUrl);
68
+ this.fetchImpl = fetchImpl;
69
+ }
70
+
71
+ async request(method, path, { query, body } = {}) {
72
+ const url = new URL(`${this.apiBaseUrl}${path}`);
73
+ if (query && typeof query === 'object') {
74
+ for (const [key, value] of Object.entries(query)) {
75
+ if (value === null || value === undefined || value === '') {
76
+ continue;
77
+ }
78
+ url.searchParams.set(key, String(value));
79
+ }
80
+ }
81
+
82
+ const controller = new AbortController();
83
+ const timeout = setTimeout(() => {
84
+ controller.abort(new Error(`Shoppex Dev API request timed out after ${DEV_API_TIMEOUT_MS}ms.`));
85
+ }, DEV_API_TIMEOUT_MS);
86
+
87
+ try {
88
+ const response = await this.fetchImpl(url, {
89
+ method,
90
+ headers: {
91
+ Authorization: `Bearer ${this.shopApiKey}`,
92
+ 'Content-Type': 'application/json',
93
+ },
94
+ body: body === undefined ? undefined : JSON.stringify(body),
95
+ signal: controller.signal,
96
+ });
97
+
98
+ const rawPayload = await response.text();
99
+ const payload = rawPayload.length > 0
100
+ ? JSON.parse(rawPayload)
101
+ : null;
102
+
103
+ if (!response.ok) {
104
+ const classification = classifyHttpError(response.status);
105
+ const message = payload?.error?.message ?? payload?.message ?? `Shoppex Dev API request failed (${response.status}).`;
106
+ throw new ShoppexCommerceDevApiError(message, {
107
+ status: response.status,
108
+ code: classification.code,
109
+ retryable: classification.retryable,
110
+ });
111
+ }
112
+
113
+ if (rawPayload.length > 0 && payload === null) {
114
+ throw new ShoppexCommerceDevApiError('Shoppex Dev API returned an empty payload.', {
115
+ status: response.status,
116
+ code: 'invalid_response',
117
+ retryable: true,
118
+ });
119
+ }
120
+
121
+ return payload?.data ?? payload ?? null;
122
+ } catch (error) {
123
+ if (error instanceof ShoppexCommerceDevApiError) {
124
+ throw error;
125
+ }
126
+
127
+ if (error instanceof SyntaxError) {
128
+ throw new ShoppexCommerceDevApiError('Shoppex Dev API returned invalid JSON.', {
129
+ code: 'invalid_response',
130
+ retryable: true,
131
+ cause: error,
132
+ });
133
+ }
134
+
135
+ const message = error instanceof Error ? error.message : 'Shoppex Dev API request failed.';
136
+ const isTimeout = message.toLowerCase().includes('timed out') || message.toLowerCase().includes('aborted');
137
+ throw new ShoppexCommerceDevApiError(message, {
138
+ code: isTimeout ? 'timeout' : 'network_error',
139
+ retryable: true,
140
+ cause: error instanceof Error ? error : undefined,
141
+ });
142
+ } finally {
143
+ clearTimeout(timeout);
144
+ }
145
+ }
146
+
147
+ productsList(input = {}) {
148
+ const search = typeof input.search === 'string' ? input.search.trim() : '';
149
+ if (search) {
150
+ return this.request('GET', '/dev/v1/products/search', {
151
+ query: {
152
+ q: search,
153
+ limit: input.limit,
154
+ include_variants: true,
155
+ },
156
+ });
157
+ }
158
+
159
+ return this.request('GET', '/dev/v1/products', {
160
+ query: {
161
+ limit: input.limit,
162
+ status: input.status,
163
+ include_variants: true,
164
+ },
165
+ });
166
+ }
167
+
168
+ productsGet(productId) {
169
+ return this.request('GET', `/dev/v1/products/${encodeURIComponent(productId)}`, {
170
+ query: {
171
+ include_variants: true,
172
+ },
173
+ });
174
+ }
175
+
176
+ ordersList(input = {}) {
177
+ return this.request('GET', '/dev/v1/orders', {
178
+ query: {
179
+ limit: input.limit,
180
+ status: input.status,
181
+ customerEmail: input.customerEmail,
182
+ },
183
+ });
184
+ }
185
+
186
+ ordersGet(orderId) {
187
+ return this.request('GET', `/dev/v1/orders/${encodeURIComponent(orderId)}`);
188
+ }
189
+
190
+ customersGet(input) {
191
+ if (input.customerId) {
192
+ return this.request('GET', `/dev/v1/customers/${encodeURIComponent(input.customerId)}`);
193
+ }
194
+
195
+ if (!input.email) {
196
+ throw new ShoppexCommerceDevApiError('customers.get requires customer_id or email.', {
197
+ code: 'bad_request',
198
+ retryable: false,
199
+ });
200
+ }
201
+
202
+ return this.request('GET', '/dev/v1/customers', {
203
+ query: {
204
+ limit: 1,
205
+ filters: `email:${input.email}`,
206
+ },
207
+ }).then((data) => Array.isArray(data) ? (data[0] ?? null) : data);
208
+ }
209
+
210
+ paymentLinksCreate(input) {
211
+ return this.request('POST', '/dev/v1/payment-links', {
212
+ body: input,
213
+ });
214
+ }
215
+
216
+ analyticsRevenue(input = {}) {
217
+ return this.request('GET', '/dev/v1/analytics/revenue', {
218
+ query: {
219
+ from: input.from,
220
+ to: input.to,
221
+ currency: input.currency,
222
+ },
223
+ });
224
+ }
225
+
226
+ couponsList(input = {}) {
227
+ return this.request('GET', '/dev/v1/coupons', {
228
+ query: {
229
+ limit: input.limit,
230
+ },
231
+ });
232
+ }
233
+
234
+ couponsCreate(input) {
235
+ return this.request('POST', '/dev/v1/coupons', {
236
+ body: input,
237
+ });
238
+ }
239
+
240
+ couponsUpdate(couponId, input) {
241
+ return this.request('PATCH', `/dev/v1/coupons/${encodeURIComponent(couponId)}`, {
242
+ body: input,
243
+ });
244
+ }
245
+
246
+ productsCreate(input) {
247
+ return this.request('POST', '/dev/v1/products', {
248
+ body: input,
249
+ });
250
+ }
251
+
252
+ productsUpdate(productId, input) {
253
+ return this.request('PATCH', `/dev/v1/products/${encodeURIComponent(productId)}`, {
254
+ body: input,
255
+ });
256
+ }
257
+ }
258
+
259
+ const BaseToolSchema = {
260
+ shop_api_key: z.string().trim().min(1),
261
+ api_base_url: z.string().trim().url().optional(),
262
+ };
263
+
264
+ function createClient(args) {
265
+ return new ShoppexCommerceDevApiClient({
266
+ shopApiKey: args.shop_api_key,
267
+ apiBaseUrl: args.api_base_url,
268
+ });
269
+ }
270
+
271
+ export function createCommerceToolCatalog() {
272
+ return [
273
+ {
274
+ name: 'products.list',
275
+ description: 'Search or list products for one Shoppex shop.',
276
+ inputSchema: {
277
+ ...BaseToolSchema,
278
+ search: z.string().trim().min(1).optional(),
279
+ limit: z.number().int().min(1).max(50).optional(),
280
+ status: z.enum(['active', 'private', 'unlisted', 'all']).optional(),
281
+ },
282
+ execute: async (args) => createClient(args).productsList({
283
+ search: args.search,
284
+ limit: args.limit,
285
+ status: args.status,
286
+ }),
287
+ },
288
+ {
289
+ name: 'products.get',
290
+ description: 'Fetch one product by Shoppex product id or uniqid.',
291
+ inputSchema: {
292
+ ...BaseToolSchema,
293
+ product_id: z.string().trim().min(1),
294
+ },
295
+ execute: async (args) => createClient(args).productsGet(args.product_id),
296
+ },
297
+ {
298
+ name: 'orders.list',
299
+ description: 'List recent orders. Optionally filter by status or customer email.',
300
+ inputSchema: {
301
+ ...BaseToolSchema,
302
+ limit: z.number().int().min(1).max(50).optional(),
303
+ status: z.string().trim().min(1).optional(),
304
+ customer_email: z.string().trim().email().optional(),
305
+ },
306
+ execute: async (args) => createClient(args).ordersList({
307
+ limit: args.limit,
308
+ status: args.status,
309
+ customerEmail: args.customer_email,
310
+ }),
311
+ },
312
+ {
313
+ name: 'orders.get',
314
+ description: 'Fetch one order with line items by id or uniqid.',
315
+ inputSchema: {
316
+ ...BaseToolSchema,
317
+ order_id: z.string().trim().min(1),
318
+ },
319
+ execute: async (args) => createClient(args).ordersGet(args.order_id),
320
+ },
321
+ {
322
+ name: 'customers.get',
323
+ description: 'Fetch one customer by Shoppex customer id or email.',
324
+ inputSchema: {
325
+ ...BaseToolSchema,
326
+ customer_id: z.string().trim().min(1).optional(),
327
+ email: z.string().trim().email().optional(),
328
+ },
329
+ execute: async (args) => createClient(args).customersGet({
330
+ customerId: args.customer_id,
331
+ email: args.email,
332
+ }),
333
+ },
334
+ {
335
+ name: 'payment_links.create',
336
+ description: 'Create a Shoppex payment link.',
337
+ inputSchema: {
338
+ ...BaseToolSchema,
339
+ name: z.string().trim().min(1).max(255),
340
+ type: z.enum(['PRODUCT', 'SUBSCRIPTION', 'SUBSCRIPTION_V2', 'LICENSE', 'PAY_WHAT_YOU_WANT', 'FIXED_PRICE']),
341
+ description: z.string().max(255).optional().nullable(),
342
+ active: z.boolean().default(true),
343
+ price: z.number().positive(),
344
+ currency: z.string().trim().min(3).max(8).default('USD'),
345
+ gateways: z.array(z.string().trim().min(1)).min(1),
346
+ product_ids: z.array(z.string().trim().min(1)).optional(),
347
+ cta: z.enum(['PAY', 'BOOK', 'DONATE', 'SUBSCRIBE']).default('PAY'),
348
+ allow_discount_code: z.boolean().default(false),
349
+ require_customer_address: z.boolean().default(false),
350
+ require_customer_phone: z.boolean().default(false),
351
+ show_confirmation_page: z.boolean().default(true),
352
+ return_url: z.string().trim().url().optional().nullable(),
353
+ },
354
+ execute: async (args) => createClient(args).paymentLinksCreate({
355
+ name: args.name,
356
+ type: args.type,
357
+ description: args.description ?? null,
358
+ status: args.active,
359
+ price: args.price,
360
+ currency: args.currency,
361
+ gateways: args.gateways.join(','),
362
+ products_ids: (args.product_ids ?? []).join(','),
363
+ discount_code_allowed: args.allow_discount_code,
364
+ customer_address_required: args.require_customer_address,
365
+ customer_phone_required: args.require_customer_phone,
366
+ cta: args.cta,
367
+ show_confirmation_page: args.show_confirmation_page,
368
+ return_url: args.return_url ?? null,
369
+ }),
370
+ },
371
+ {
372
+ name: 'analytics.revenue',
373
+ description: 'Fetch revenue totals for a date range.',
374
+ inputSchema: {
375
+ ...BaseToolSchema,
376
+ from: z.string().trim().min(1).optional(),
377
+ to: z.string().trim().min(1).optional(),
378
+ currency: z.string().trim().min(3).max(8).default('USD'),
379
+ },
380
+ execute: async (args) => createClient(args).analyticsRevenue({
381
+ from: args.from,
382
+ to: args.to,
383
+ currency: args.currency,
384
+ }),
385
+ },
386
+ {
387
+ name: 'coupons.list',
388
+ description: 'List active coupons for a shop.',
389
+ inputSchema: {
390
+ ...BaseToolSchema,
391
+ limit: z.number().int().min(1).max(50).optional(),
392
+ },
393
+ execute: async (args) => createClient(args).couponsList({
394
+ limit: args.limit,
395
+ }),
396
+ },
397
+ {
398
+ name: 'coupons.create',
399
+ description: 'Create a coupon in Shoppex.',
400
+ inputSchema: {
401
+ ...BaseToolSchema,
402
+ code: z.string().trim().min(1).max(64),
403
+ discount_value: z.number().positive(),
404
+ discount_type: z.enum(['PERCENTAGE', 'FIXED']).default('PERCENTAGE'),
405
+ max_uses: z.number().int().positive().optional().nullable(),
406
+ product_ids: z.array(z.string().trim().min(1)).optional(),
407
+ min_order_amount: z.number().nonnegative().optional().nullable(),
408
+ max_discount: z.number().nonnegative().optional().nullable(),
409
+ valid_from: z.string().trim().optional().nullable(),
410
+ valid_until: z.string().trim().optional().nullable(),
411
+ },
412
+ execute: async (args) => createClient(args).couponsCreate({
413
+ code: args.code,
414
+ discount_value: args.discount_value,
415
+ discount_type: args.discount_type,
416
+ max_uses: args.max_uses ?? null,
417
+ products_bound: args.product_ids ?? [],
418
+ min_order_amount: args.min_order_amount ?? null,
419
+ max_discount: args.max_discount ?? null,
420
+ valid_from: args.valid_from ?? null,
421
+ valid_until: args.valid_until ?? null,
422
+ }),
423
+ },
424
+ {
425
+ name: 'coupons.update',
426
+ description: 'Update an existing coupon by uniqid. Only provided fields are changed.',
427
+ inputSchema: {
428
+ ...BaseToolSchema,
429
+ coupon_id: z.string().trim().min(1),
430
+ code: z.string().trim().min(1).max(64).optional(),
431
+ discount_value: z.number().positive().optional(),
432
+ discount_type: z.enum(['PERCENTAGE', 'FIXED']).optional(),
433
+ max_uses: z.number().int().positive().optional().nullable(),
434
+ min_order_amount: z.number().nonnegative().optional().nullable(),
435
+ max_discount: z.number().nonnegative().optional().nullable(),
436
+ valid_from: z.string().trim().optional().nullable(),
437
+ valid_until: z.string().trim().optional().nullable(),
438
+ },
439
+ execute: async (args) => {
440
+ const body = {};
441
+ if (args.code !== undefined) body.code = args.code;
442
+ if (args.discount_value !== undefined) body.discount_value = args.discount_value;
443
+ if (args.discount_type !== undefined) body.discount_type = args.discount_type;
444
+ if (args.max_uses !== undefined) body.max_uses = args.max_uses;
445
+ if (args.min_order_amount !== undefined) body.min_order_amount = args.min_order_amount;
446
+ if (args.max_discount !== undefined) body.max_discount = args.max_discount;
447
+ if (args.valid_from !== undefined) body.valid_from = args.valid_from;
448
+ if (args.valid_until !== undefined) body.valid_until = args.valid_until;
449
+ return createClient(args).couponsUpdate(args.coupon_id, body);
450
+ },
451
+ },
452
+ {
453
+ name: 'products.create',
454
+ description: 'Create a product. Defaults to a simple digital/service product. For key-based products pass type="SERIALS" and provide the serials array.',
455
+ inputSchema: {
456
+ ...BaseToolSchema,
457
+ title: z.string().trim().min(1).max(128),
458
+ price: z.number().min(0),
459
+ description: z.string().optional(),
460
+ currency: z.string().trim().min(3).max(8).optional(),
461
+ type: z.enum(['SERVICE', 'SERIALS', 'DYNAMIC', 'FILE']).default('SERVICE'),
462
+ gateways: z.array(z.string().trim().min(1)).optional(),
463
+ serials: z.array(z.string().trim().min(1)).optional(),
464
+ service_text: z.string().optional(),
465
+ delivery_text: z.string().optional(),
466
+ stock: z.number().int().min(-1).optional(),
467
+ category_id: z.string().trim().min(1).optional().nullable(),
468
+ unlisted: z.boolean().optional(),
469
+ private: z.boolean().optional(),
470
+ },
471
+ execute: async (args) => {
472
+ const body = {
473
+ title: args.title,
474
+ price: args.price,
475
+ type: args.type,
476
+ };
477
+ if (args.description !== undefined) body.description = args.description;
478
+ if (args.currency !== undefined) body.currency = args.currency;
479
+ if (args.gateways !== undefined) body.gateways = args.gateways;
480
+ if (args.serials !== undefined) body.serials = args.serials;
481
+ if (args.service_text !== undefined) body.service_text = args.service_text;
482
+ if (args.delivery_text !== undefined) body.delivery_text = args.delivery_text;
483
+ if (args.stock !== undefined) body.stock = args.stock;
484
+ if (args.category_id !== undefined) body.category_id = args.category_id;
485
+ if (args.unlisted !== undefined) body.unlisted = args.unlisted;
486
+ if (args.private !== undefined) body.private = args.private;
487
+ return createClient(args).productsCreate(body);
488
+ },
489
+ },
490
+ {
491
+ name: 'products.update',
492
+ description: 'Update an existing product by uniqid. Only provided fields are changed.',
493
+ inputSchema: {
494
+ ...BaseToolSchema,
495
+ product_id: z.string().trim().min(1),
496
+ title: z.string().trim().min(1).max(128).optional(),
497
+ price: z.number().min(0).optional(),
498
+ description: z.string().optional(),
499
+ currency: z.string().trim().min(3).max(8).optional(),
500
+ gateways: z.array(z.string().trim().min(1)).optional(),
501
+ stock: z.number().int().min(-1).optional(),
502
+ unlisted: z.boolean().optional(),
503
+ private: z.boolean().optional(),
504
+ on_hold: z.boolean().optional(),
505
+ category_id: z.string().trim().min(1).optional().nullable(),
506
+ },
507
+ execute: async (args) => {
508
+ const body = {};
509
+ if (args.title !== undefined) body.title = args.title;
510
+ if (args.price !== undefined) body.price = args.price;
511
+ if (args.description !== undefined) body.description = args.description;
512
+ if (args.currency !== undefined) body.currency = args.currency;
513
+ if (args.gateways !== undefined) body.gateways = args.gateways;
514
+ if (args.stock !== undefined) body.stock = args.stock;
515
+ if (args.unlisted !== undefined) body.unlisted = args.unlisted;
516
+ if (args.private !== undefined) body.private = args.private;
517
+ if (args.on_hold !== undefined) body.on_hold = args.on_hold;
518
+ if (args.category_id !== undefined) body.category_id = args.category_id;
519
+ return createClient(args).productsUpdate(args.product_id, body);
520
+ },
521
+ },
522
+ ];
523
+ }
524
+
525
+ export async function executeCommerceTool(toolName, args) {
526
+ const tool = createCommerceToolCatalog().find((entry) => entry.name === toolName);
527
+ if (!tool) {
528
+ throw new Error(`Unknown tool: ${toolName}`);
529
+ }
530
+
531
+ return tool.execute(args);
532
+ }
533
+
534
+ export function createCommerceMcpServer() {
535
+ const server = new McpServer({
536
+ name: 'shoppex-commerce',
537
+ version: '0.1.0',
538
+ });
539
+
540
+ for (const tool of createCommerceToolCatalog()) {
541
+ server.registerTool(tool.name, {
542
+ description: tool.description,
543
+ inputSchema: tool.inputSchema,
544
+ }, async (args) => {
545
+ const result = await tool.execute(args);
546
+ return jsonContent(result);
547
+ });
548
+ }
549
+
550
+ return server;
551
+ }
552
+
553
+ export async function startCommerceMcpServer() {
554
+ const server = createCommerceMcpServer();
555
+ const transport = new StdioServerTransport();
556
+ await server.connect(transport);
557
+ }