@swatbloc/mcp-server 0.1.1 → 0.1.2

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.
@@ -0,0 +1,30 @@
1
+ import { SwatBloc } from "@swatbloc/sdk";
2
+ export function getSdkClient(args) {
3
+ const apiKey = args.api_key || process.env.SWATBLOC_API_KEY;
4
+ const baseUrl = args.base_url || process.env.SWATBLOC_BASE_URL;
5
+ if (!apiKey) {
6
+ throw new Error("Missing API key. Provide api_key in tool input or set SWATBLOC_API_KEY.");
7
+ }
8
+ return new SwatBloc(apiKey, baseUrl ? { baseUrl } : {});
9
+ }
10
+ /**
11
+ * Generate Cartesian product from options
12
+ */
13
+ export function generateCartesianProduct(options) {
14
+ if (options.length === 0)
15
+ return [{}];
16
+ const result = [];
17
+ function recurse(index, current) {
18
+ if (index === options.length) {
19
+ result.push({ ...current });
20
+ return;
21
+ }
22
+ const option = options[index];
23
+ for (const value of option.values) {
24
+ current[option.name] = value;
25
+ recurse(index + 1, current);
26
+ }
27
+ }
28
+ recurse(0, {});
29
+ return result;
30
+ }
@@ -1,387 +1,3 @@
1
- import { z } from "zod";
2
- import { getSupabaseClient } from "../common/supabase.js";
3
- /**
4
- * Generate Cartesian product from options
5
- */
6
- function generateCartesianProduct(options) {
7
- if (options.length === 0)
8
- return [{}];
9
- const result = [];
10
- function recurse(index, current) {
11
- if (index === options.length) {
12
- result.push({ ...current });
13
- return;
14
- }
15
- const option = options[index];
16
- for (const value of option.values) {
17
- current[option.name] = value;
18
- recurse(index + 1, current);
19
- }
20
- }
21
- recurse(0, {});
22
- return result;
23
- }
24
- export const tools = [
25
- {
26
- name: "create_database",
27
- description: "Create a new custom database (content model) for the store. Use this to store structured data like reviews, preferences, logs, etc. This essentially upgrades the platform.",
28
- schema: {
29
- input: z.object({
30
- store_id: z.string().describe("The ID of the store to create the database for."),
31
- name: z.string().describe("Human readable name, e.g. 'Liability Photos'"),
32
- slug: z.string().describe("URL friendly identifier, e.g. 'liability-photos'"),
33
- fields: z.array(z.object({
34
- key: z.string(),
35
- type: z.enum(['text', 'number', 'boolean', 'image', 'date', 'json', 'reference']),
36
- label: z.string().optional(),
37
- required: z.boolean().optional()
38
- })).describe("The schema definition for the database.")
39
- })
40
- },
41
- execute: async (args) => {
42
- const supabase = getSupabaseClient();
43
- const { store_id, name, slug, fields } = args;
44
- // Check if exists (Idempotency)
45
- const { data: existing } = await supabase
46
- .from('content_models')
47
- .select('*')
48
- .eq('store_id', store_id)
49
- .eq('slug', slug)
50
- .single();
51
- if (existing) {
52
- return {
53
- message: "Database already exists",
54
- model: existing
55
- };
56
- }
57
- // Rate Limit / Safety Check: Max 50 tables per store
58
- const { count } = await supabase
59
- .from('content_models')
60
- .select('*', { count: 'exact', head: true })
61
- .eq('store_id', store_id);
62
- if (count && count >= 50) {
63
- return {
64
- message: "Error: Maximum number of databases (50) reached for this store. Please delete some before creating more.",
65
- error: true
66
- };
67
- }
68
- const { data: newModel, error } = await supabase
69
- .from('content_models')
70
- .insert({
71
- store_id,
72
- name,
73
- slug,
74
- schema: { fields }
75
- })
76
- .select()
77
- .single();
78
- if (error)
79
- throw new Error(error.message);
80
- return {
81
- message: "Database created successfully",
82
- model: newModel
83
- };
84
- }
85
- },
86
- // ==================== PRODUCT TOOLS ====================
87
- {
88
- name: "list_products",
89
- description: "List all products for a store. Returns product titles, prices, and IDs.",
90
- schema: {
91
- input: z.object({
92
- store_id: z.string().describe("The ID of the store to list products for."),
93
- limit: z.number().optional().describe("Maximum number of products to return (default: 50)"),
94
- published_only: z.boolean().optional().describe("Only return published products (default: false)")
95
- })
96
- },
97
- execute: async (args) => {
98
- const supabase = getSupabaseClient();
99
- const { store_id, limit = 50, published_only = false } = args;
100
- let query = supabase
101
- .from('products')
102
- .select('id, title, slug, price, published, options')
103
- .eq('store_id', store_id)
104
- .order('created_at', { ascending: false })
105
- .limit(limit);
106
- if (published_only) {
107
- query = query.eq('published', true);
108
- }
109
- const { data: products, error } = await query;
110
- if (error)
111
- throw new Error(error.message);
112
- return {
113
- message: `Found ${products?.length || 0} products`,
114
- products: products || []
115
- };
116
- }
117
- },
118
- {
119
- name: "manage_product",
120
- description: "Create, update, or delete a product. For creating products with variants, first create the product with options, then use manage_variant with action 'generate' to auto-create variants.",
121
- schema: {
122
- input: z.object({
123
- store_id: z.string().describe("The ID of the store."),
124
- action: z.enum(['create', 'update', 'delete']).describe("The action to perform."),
125
- product_id: z.string().optional().describe("Required for update/delete actions."),
126
- data: z.object({
127
- title: z.string().optional().describe("Product title"),
128
- description: z.string().optional().describe("Product description"),
129
- price: z.number().optional().describe("Product price in cents (e.g., 1999 = $19.99)"),
130
- compare_at_price: z.number().optional().describe("Compare at price in cents"),
131
- options: z.array(z.object({
132
- name: z.string().describe("Option name, e.g., 'Size' or 'Color'"),
133
- values: z.array(z.string()).describe("Option values, e.g., ['S', 'M', 'L']")
134
- })).optional().describe("Product options for variant generation"),
135
- published: z.boolean().optional().describe("Whether the product is published"),
136
- category: z.string().optional().describe("Product category"),
137
- sku: z.string().optional().describe("Product SKU"),
138
- images: z.array(z.string()).optional().describe("Array of image URLs")
139
- }).optional().describe("Product data for create/update")
140
- })
141
- },
142
- execute: async (args) => {
143
- const supabase = getSupabaseClient();
144
- const { store_id, action, product_id, data } = args;
145
- if (action === 'create') {
146
- if (!data?.title) {
147
- return { error: true, message: "Title is required to create a product" };
148
- }
149
- const slug = data.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') + '-' + Math.random().toString(36).substring(2, 7);
150
- const productData = {
151
- store_id,
152
- title: data.title,
153
- slug,
154
- description: data.description || '',
155
- price: data.price || 0,
156
- compare_at_price: data.compare_at_price || null,
157
- options: data.options || [],
158
- published: data.published ?? false,
159
- category: data.category || null,
160
- sku: data.sku || null,
161
- images: data.images || []
162
- };
163
- const { data: product, error } = await supabase
164
- .from('products')
165
- .insert(productData)
166
- .select()
167
- .single();
168
- if (error)
169
- throw new Error(error.message);
170
- return {
171
- message: "Product created successfully",
172
- product
173
- };
174
- }
175
- if (action === 'update') {
176
- if (!product_id) {
177
- return { error: true, message: "product_id is required for update" };
178
- }
179
- const updateData = {};
180
- if (data?.title)
181
- updateData.title = data.title;
182
- if (data?.description !== undefined)
183
- updateData.description = data.description;
184
- if (data?.price !== undefined)
185
- updateData.price = data.price;
186
- if (data?.compare_at_price !== undefined)
187
- updateData.compare_at_price = data.compare_at_price;
188
- if (data?.options)
189
- updateData.options = data.options;
190
- if (data?.published !== undefined)
191
- updateData.published = data.published;
192
- if (data?.category !== undefined)
193
- updateData.category = data.category;
194
- if (data?.sku !== undefined)
195
- updateData.sku = data.sku;
196
- if (data?.images)
197
- updateData.images = data.images;
198
- const { data: product, error } = await supabase
199
- .from('products')
200
- .update(updateData)
201
- .eq('id', product_id)
202
- .eq('store_id', store_id)
203
- .select()
204
- .single();
205
- if (error)
206
- throw new Error(error.message);
207
- return {
208
- message: "Product updated successfully",
209
- product
210
- };
211
- }
212
- if (action === 'delete') {
213
- if (!product_id) {
214
- return { error: true, message: "product_id is required for delete" };
215
- }
216
- const { error } = await supabase
217
- .from('products')
218
- .delete()
219
- .eq('id', product_id)
220
- .eq('store_id', store_id);
221
- if (error)
222
- throw new Error(error.message);
223
- return {
224
- message: "Product deleted successfully"
225
- };
226
- }
227
- return { error: true, message: "Invalid action" };
228
- }
229
- },
230
- {
231
- name: "manage_variant",
232
- description: "Create, update, delete, or auto-generate variants for a product. Use 'generate' action to automatically create variants from product options (Cartesian product).",
233
- schema: {
234
- input: z.object({
235
- product_id: z.string().describe("The ID of the product."),
236
- action: z.enum(['create', 'update', 'delete', 'generate', 'list']).describe("The action to perform."),
237
- variant_id: z.string().optional().describe("Required for update/delete actions."),
238
- data: z.object({
239
- title: z.string().optional().describe("Variant title (e.g., 'S / Red')"),
240
- sku: z.string().optional().describe("Variant SKU"),
241
- price: z.number().optional().describe("Variant price in cents"),
242
- inventory_quantity: z.number().optional().describe("Inventory count"),
243
- options: z.record(z.string(), z.string()).optional().describe("Option values, e.g., { 'Size': 'S', 'Color': 'Red' }")
244
- }).optional().describe("Variant data for create/update"),
245
- replace_existing: z.boolean().optional().describe("For 'generate': replace existing variants (default: true)")
246
- })
247
- },
248
- execute: async (args) => {
249
- const supabase = getSupabaseClient();
250
- const { product_id, action, variant_id, data, replace_existing = true } = args;
251
- // Verify product exists
252
- const { data: product, error: productError } = await supabase
253
- .from('products')
254
- .select('id, price, options')
255
- .eq('id', product_id)
256
- .single();
257
- if (productError || !product) {
258
- return { error: true, message: "Product not found" };
259
- }
260
- if (action === 'list') {
261
- const { data: variants, error } = await supabase
262
- .from('product_variants')
263
- .select('id, title, sku, price, inventory_quantity, options')
264
- .eq('product_id', product_id)
265
- .order('title');
266
- if (error)
267
- throw new Error(error.message);
268
- return {
269
- message: `Found ${variants?.length || 0} variants`,
270
- variants: variants || []
271
- };
272
- }
273
- if (action === 'generate') {
274
- const options = product.options || [];
275
- if (!options.length || !options.every((o) => o.values?.length > 0)) {
276
- return {
277
- error: true,
278
- message: "Product must have at least one option with values defined. Update the product options first."
279
- };
280
- }
281
- const combinations = generateCartesianProduct(options);
282
- if (combinations.length > 100) {
283
- return {
284
- error: true,
285
- message: `Too many variants (${combinations.length}). Maximum is 100. Reduce option values.`
286
- };
287
- }
288
- if (replace_existing) {
289
- await supabase
290
- .from('product_variants')
291
- .delete()
292
- .eq('product_id', product_id);
293
- }
294
- const variantsToInsert = combinations.map(combo => ({
295
- product_id,
296
- title: Object.values(combo).join(' / '),
297
- sku: null,
298
- price: product.price,
299
- inventory_quantity: 0,
300
- options: combo,
301
- description: null,
302
- image_url: null,
303
- images: []
304
- }));
305
- const { data: variants, error } = await supabase
306
- .from('product_variants')
307
- .insert(variantsToInsert)
308
- .select();
309
- if (error)
310
- throw new Error(error.message);
311
- return {
312
- message: `Generated ${variants?.length || 0} variants from options`,
313
- variants: variants || []
314
- };
315
- }
316
- if (action === 'create') {
317
- const variantData = {
318
- product_id,
319
- title: data?.title || 'New Variant',
320
- sku: data?.sku || null,
321
- price: data?.price ?? product.price,
322
- inventory_quantity: data?.inventory_quantity || 0,
323
- options: data?.options || {},
324
- description: null,
325
- image_url: null,
326
- images: []
327
- };
328
- const { data: variant, error } = await supabase
329
- .from('product_variants')
330
- .insert(variantData)
331
- .select()
332
- .single();
333
- if (error)
334
- throw new Error(error.message);
335
- return {
336
- message: "Variant created successfully",
337
- variant
338
- };
339
- }
340
- if (action === 'update') {
341
- if (!variant_id) {
342
- return { error: true, message: "variant_id is required for update" };
343
- }
344
- const updateData = {};
345
- if (data?.title)
346
- updateData.title = data.title;
347
- if (data?.sku !== undefined)
348
- updateData.sku = data.sku;
349
- if (data?.price !== undefined)
350
- updateData.price = data.price;
351
- if (data?.inventory_quantity !== undefined)
352
- updateData.inventory_quantity = data.inventory_quantity;
353
- if (data?.options)
354
- updateData.options = data.options;
355
- const { data: variant, error } = await supabase
356
- .from('product_variants')
357
- .update(updateData)
358
- .eq('id', variant_id)
359
- .eq('product_id', product_id)
360
- .select()
361
- .single();
362
- if (error)
363
- throw new Error(error.message);
364
- return {
365
- message: "Variant updated successfully",
366
- variant
367
- };
368
- }
369
- if (action === 'delete') {
370
- if (!variant_id) {
371
- return { error: true, message: "variant_id is required for delete" };
372
- }
373
- const { error } = await supabase
374
- .from('product_variants')
375
- .delete()
376
- .eq('id', variant_id)
377
- .eq('product_id', product_id);
378
- if (error)
379
- throw new Error(error.message);
380
- return {
381
- message: "Variant deleted successfully"
382
- };
383
- }
384
- return { error: true, message: "Invalid action" };
385
- }
386
- }
387
- ];
1
+ import { legacyTools } from "./legacy-tools.js";
2
+ import { sdkTools } from "./sdk-tools.js";
3
+ export const tools = [...legacyTools, ...sdkTools];
@@ -0,0 +1,363 @@
1
+ import { z } from "zod";
2
+ import { getSupabaseClient } from "../common/supabase.js";
3
+ import { generateCartesianProduct } from "./helpers.js";
4
+ export const legacyTools = [
5
+ {
6
+ name: "create_database",
7
+ description: "Create a new custom database (content model) for the store. Use this to store structured data like reviews, preferences, logs, etc. This essentially upgrades the platform.",
8
+ schema: {
9
+ input: z.object({
10
+ store_id: z.string().describe("The ID of the store to create the database for."),
11
+ name: z.string().describe("Human readable name, e.g. 'Liability Photos'"),
12
+ slug: z.string().describe("URL friendly identifier, e.g. 'liability-photos'"),
13
+ fields: z.array(z.object({
14
+ key: z.string(),
15
+ type: z.enum(["text", "number", "boolean", "image", "date", "json", "reference"]),
16
+ label: z.string().optional(),
17
+ required: z.boolean().optional()
18
+ })).describe("The schema definition for the database.")
19
+ })
20
+ },
21
+ execute: async (args) => {
22
+ const supabase = getSupabaseClient();
23
+ const { store_id, name, slug, fields } = args;
24
+ const { data: existing } = await supabase
25
+ .from("content_models")
26
+ .select("*")
27
+ .eq("store_id", store_id)
28
+ .eq("slug", slug)
29
+ .single();
30
+ if (existing) {
31
+ return {
32
+ message: "Database already exists",
33
+ model: existing
34
+ };
35
+ }
36
+ const { count } = await supabase
37
+ .from("content_models")
38
+ .select("*", { count: "exact", head: true })
39
+ .eq("store_id", store_id);
40
+ if (count && count >= 50) {
41
+ return {
42
+ message: "Error: Maximum number of databases (50) reached for this store. Please delete some before creating more.",
43
+ error: true
44
+ };
45
+ }
46
+ const { data: newModel, error } = await supabase
47
+ .from("content_models")
48
+ .insert({
49
+ store_id,
50
+ name,
51
+ slug,
52
+ schema: { fields }
53
+ })
54
+ .select()
55
+ .single();
56
+ if (error)
57
+ throw new Error(error.message);
58
+ return {
59
+ message: "Database created successfully",
60
+ model: newModel
61
+ };
62
+ }
63
+ },
64
+ {
65
+ name: "list_products",
66
+ description: "List all products for a store. Returns product titles, prices, and IDs.",
67
+ schema: {
68
+ input: z.object({
69
+ store_id: z.string().describe("The ID of the store to list products for."),
70
+ limit: z.number().optional().describe("Maximum number of products to return (default: 50)"),
71
+ published_only: z.boolean().optional().describe("Only return published products (default: false)")
72
+ })
73
+ },
74
+ execute: async (args) => {
75
+ const supabase = getSupabaseClient();
76
+ const { store_id, limit = 50, published_only = false } = args;
77
+ let query = supabase
78
+ .from("products")
79
+ .select("id, title, slug, price, published, options")
80
+ .eq("store_id", store_id)
81
+ .order("created_at", { ascending: false })
82
+ .limit(limit);
83
+ if (published_only) {
84
+ query = query.eq("published", true);
85
+ }
86
+ const { data: products, error } = await query;
87
+ if (error)
88
+ throw new Error(error.message);
89
+ return {
90
+ message: `Found ${products?.length || 0} products`,
91
+ products: products || []
92
+ };
93
+ }
94
+ },
95
+ {
96
+ name: "manage_product",
97
+ description: "Create, update, or delete a product. For creating products with variants, first create the product with options, then use manage_variant with action 'generate' to auto-create variants.",
98
+ schema: {
99
+ input: z.object({
100
+ store_id: z.string().describe("The ID of the store."),
101
+ action: z.enum(["create", "update", "delete"]).describe("The action to perform."),
102
+ product_id: z.string().optional().describe("Required for update/delete actions."),
103
+ data: z.object({
104
+ title: z.string().optional().describe("Product title"),
105
+ description: z.string().optional().describe("Product description"),
106
+ price: z.number().optional().describe("Product price in cents (e.g., 1999 = $19.99)"),
107
+ compare_at_price: z.number().optional().describe("Compare at price in cents"),
108
+ options: z.array(z.object({
109
+ name: z.string().describe("Option name, e.g., 'Size' or 'Color'"),
110
+ values: z.array(z.string()).describe("Option values, e.g., ['S', 'M', 'L']")
111
+ })).optional().describe("Product options for variant generation"),
112
+ published: z.boolean().optional().describe("Whether the product is published"),
113
+ category: z.string().optional().describe("Product category"),
114
+ sku: z.string().optional().describe("Product SKU"),
115
+ images: z.array(z.string()).optional().describe("Array of image URLs")
116
+ }).optional().describe("Product data for create/update")
117
+ })
118
+ },
119
+ execute: async (args) => {
120
+ const supabase = getSupabaseClient();
121
+ const { store_id, action, product_id, data } = args;
122
+ if (action === "create") {
123
+ if (!data?.title) {
124
+ return { error: true, message: "Title is required to create a product" };
125
+ }
126
+ const slug = data.title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") + "-" + Math.random().toString(36).substring(2, 7);
127
+ const productData = {
128
+ store_id,
129
+ title: data.title,
130
+ slug,
131
+ description: data.description || "",
132
+ price: data.price || 0,
133
+ compare_at_price: data.compare_at_price || null,
134
+ options: data.options || [],
135
+ published: data.published ?? false,
136
+ category: data.category || null,
137
+ sku: data.sku || null,
138
+ images: data.images || []
139
+ };
140
+ const { data: product, error } = await supabase
141
+ .from("products")
142
+ .insert(productData)
143
+ .select()
144
+ .single();
145
+ if (error)
146
+ throw new Error(error.message);
147
+ return {
148
+ message: "Product created successfully",
149
+ product
150
+ };
151
+ }
152
+ if (action === "update") {
153
+ if (!product_id) {
154
+ return { error: true, message: "product_id is required for update" };
155
+ }
156
+ const updateData = {};
157
+ if (data?.title)
158
+ updateData.title = data.title;
159
+ if (data?.description !== undefined)
160
+ updateData.description = data.description;
161
+ if (data?.price !== undefined)
162
+ updateData.price = data.price;
163
+ if (data?.compare_at_price !== undefined)
164
+ updateData.compare_at_price = data.compare_at_price;
165
+ if (data?.options)
166
+ updateData.options = data.options;
167
+ if (data?.published !== undefined)
168
+ updateData.published = data.published;
169
+ if (data?.category !== undefined)
170
+ updateData.category = data.category;
171
+ if (data?.sku !== undefined)
172
+ updateData.sku = data.sku;
173
+ if (data?.images)
174
+ updateData.images = data.images;
175
+ const { data: product, error } = await supabase
176
+ .from("products")
177
+ .update(updateData)
178
+ .eq("id", product_id)
179
+ .eq("store_id", store_id)
180
+ .select()
181
+ .single();
182
+ if (error)
183
+ throw new Error(error.message);
184
+ return {
185
+ message: "Product updated successfully",
186
+ product
187
+ };
188
+ }
189
+ if (action === "delete") {
190
+ if (!product_id) {
191
+ return { error: true, message: "product_id is required for delete" };
192
+ }
193
+ const { error } = await supabase
194
+ .from("products")
195
+ .delete()
196
+ .eq("id", product_id)
197
+ .eq("store_id", store_id);
198
+ if (error)
199
+ throw new Error(error.message);
200
+ return {
201
+ message: "Product deleted successfully"
202
+ };
203
+ }
204
+ return { error: true, message: "Invalid action" };
205
+ }
206
+ },
207
+ {
208
+ name: "manage_variant",
209
+ description: "Create, update, delete, or auto-generate variants for a product. Use 'generate' action to automatically create variants from product options (Cartesian product).",
210
+ schema: {
211
+ input: z.object({
212
+ product_id: z.string().describe("The ID of the product."),
213
+ action: z.enum(["create", "update", "delete", "generate", "list"]).describe("The action to perform."),
214
+ variant_id: z.string().optional().describe("Required for update/delete actions."),
215
+ data: z.object({
216
+ title: z.string().optional().describe("Variant title (e.g., 'S / Red')"),
217
+ sku: z.string().optional().describe("Variant SKU"),
218
+ price: z.number().optional().describe("Variant price in cents"),
219
+ inventory_quantity: z.number().optional().describe("Inventory count"),
220
+ options: z.record(z.string(), z.string()).optional().describe("Option values, e.g., { 'Size': 'S', 'Color': 'Red' }")
221
+ }).optional().describe("Variant data for create/update"),
222
+ replace_existing: z.boolean().optional().describe("For 'generate': replace existing variants (default: true)")
223
+ })
224
+ },
225
+ execute: async (args) => {
226
+ const supabase = getSupabaseClient();
227
+ const { product_id, action, variant_id, data, replace_existing = true } = args;
228
+ const { data: product, error: productError } = await supabase
229
+ .from("products")
230
+ .select("id, price, options")
231
+ .eq("id", product_id)
232
+ .single();
233
+ if (productError || !product) {
234
+ return { error: true, message: "Product not found" };
235
+ }
236
+ if (action === "list") {
237
+ const { data: variants, error } = await supabase
238
+ .from("product_variants")
239
+ .select("id, title, sku, price, inventory_quantity, options")
240
+ .eq("product_id", product_id)
241
+ .order("title");
242
+ if (error)
243
+ throw new Error(error.message);
244
+ return {
245
+ message: `Found ${variants?.length || 0} variants`,
246
+ variants: variants || []
247
+ };
248
+ }
249
+ if (action === "generate") {
250
+ const options = product.options || [];
251
+ if (!options.length || !options.every((o) => o.values?.length > 0)) {
252
+ return {
253
+ error: true,
254
+ message: "Product must have at least one option with values defined. Update the product options first."
255
+ };
256
+ }
257
+ const combinations = generateCartesianProduct(options);
258
+ if (combinations.length > 100) {
259
+ return {
260
+ error: true,
261
+ message: `Too many variants (${combinations.length}). Maximum is 100. Reduce option values.`
262
+ };
263
+ }
264
+ if (replace_existing) {
265
+ await supabase
266
+ .from("product_variants")
267
+ .delete()
268
+ .eq("product_id", product_id);
269
+ }
270
+ const variantsToInsert = combinations.map(combo => ({
271
+ product_id,
272
+ title: Object.values(combo).join(" / "),
273
+ sku: null,
274
+ price: product.price,
275
+ inventory_quantity: 0,
276
+ options: combo,
277
+ description: null,
278
+ image_url: null,
279
+ images: []
280
+ }));
281
+ const { data: variants, error } = await supabase
282
+ .from("product_variants")
283
+ .insert(variantsToInsert)
284
+ .select();
285
+ if (error)
286
+ throw new Error(error.message);
287
+ return {
288
+ message: `Generated ${variants?.length || 0} variants from options`,
289
+ variants: variants || []
290
+ };
291
+ }
292
+ if (action === "create") {
293
+ const variantData = {
294
+ product_id,
295
+ title: data?.title || "New Variant",
296
+ sku: data?.sku || null,
297
+ price: data?.price ?? product.price,
298
+ inventory_quantity: data?.inventory_quantity || 0,
299
+ options: data?.options || {},
300
+ description: null,
301
+ image_url: null,
302
+ images: []
303
+ };
304
+ const { data: variant, error } = await supabase
305
+ .from("product_variants")
306
+ .insert(variantData)
307
+ .select()
308
+ .single();
309
+ if (error)
310
+ throw new Error(error.message);
311
+ return {
312
+ message: "Variant created successfully",
313
+ variant
314
+ };
315
+ }
316
+ if (action === "update") {
317
+ if (!variant_id) {
318
+ return { error: true, message: "variant_id is required for update" };
319
+ }
320
+ const updateData = {};
321
+ if (data?.title)
322
+ updateData.title = data.title;
323
+ if (data?.sku !== undefined)
324
+ updateData.sku = data.sku;
325
+ if (data?.price !== undefined)
326
+ updateData.price = data.price;
327
+ if (data?.inventory_quantity !== undefined)
328
+ updateData.inventory_quantity = data.inventory_quantity;
329
+ if (data?.options)
330
+ updateData.options = data.options;
331
+ const { data: variant, error } = await supabase
332
+ .from("product_variants")
333
+ .update(updateData)
334
+ .eq("id", variant_id)
335
+ .eq("product_id", product_id)
336
+ .select()
337
+ .single();
338
+ if (error)
339
+ throw new Error(error.message);
340
+ return {
341
+ message: "Variant updated successfully",
342
+ variant
343
+ };
344
+ }
345
+ if (action === "delete") {
346
+ if (!variant_id) {
347
+ return { error: true, message: "variant_id is required for delete" };
348
+ }
349
+ const { error } = await supabase
350
+ .from("product_variants")
351
+ .delete()
352
+ .eq("id", variant_id)
353
+ .eq("product_id", product_id);
354
+ if (error)
355
+ throw new Error(error.message);
356
+ return {
357
+ message: "Variant deleted successfully"
358
+ };
359
+ }
360
+ return { error: true, message: "Invalid action" };
361
+ }
362
+ }
363
+ ];
@@ -0,0 +1,367 @@
1
+ import { z } from "zod";
2
+ import { getSdkClient } from "./helpers.js";
3
+ export const sdkTools = [
4
+ {
5
+ name: "sdk_products",
6
+ description: "Full Products SDK tool: list, get, create, update, delete, by_category, search, update_pipeline.",
7
+ schema: {
8
+ input: z.object({
9
+ api_key: z.string().optional().describe("SwatBloc API key (pk_... or sk_...). Falls back to SWATBLOC_API_KEY env."),
10
+ base_url: z.string().url().optional().describe("Optional API base URL. Falls back to SWATBLOC_BASE_URL env."),
11
+ action: z.enum(["list", "get", "create", "update", "delete", "by_category", "search", "update_pipeline"]),
12
+ id: z.string().optional(),
13
+ id_or_slug: z.string().optional(),
14
+ data: z.record(z.string(), z.any()).optional(),
15
+ category: z.string().optional(),
16
+ query: z.string().optional(),
17
+ options: z.object({
18
+ limit: z.number().optional(),
19
+ offset: z.number().optional(),
20
+ category: z.string().optional(),
21
+ search: z.string().optional()
22
+ }).optional(),
23
+ pipeline_steps: z.array(z.object({
24
+ id: z.string(),
25
+ label: z.string(),
26
+ required_metadata: z.array(z.string()).default([])
27
+ })).optional()
28
+ })
29
+ },
30
+ execute: async (args) => {
31
+ const sdk = getSdkClient(args);
32
+ switch (args.action) {
33
+ case "list":
34
+ return sdk.products.list(args.options || {});
35
+ case "get":
36
+ if (!args.id_or_slug)
37
+ throw new Error("id_or_slug is required for get");
38
+ return sdk.products.get(args.id_or_slug);
39
+ case "create":
40
+ if (!args.data)
41
+ throw new Error("data is required for create");
42
+ return sdk.products.create(args.data);
43
+ case "update":
44
+ if (!args.id)
45
+ throw new Error("id is required for update");
46
+ if (!args.data)
47
+ throw new Error("data is required for update");
48
+ return sdk.products.update(args.id, args.data);
49
+ case "delete":
50
+ if (!args.id)
51
+ throw new Error("id is required for delete");
52
+ await sdk.products.delete(args.id);
53
+ return { message: "Product deleted" };
54
+ case "by_category":
55
+ if (!args.category)
56
+ throw new Error("category is required for by_category");
57
+ return sdk.products.byCategory(args.category, args.options || {});
58
+ case "search":
59
+ if (!args.query)
60
+ throw new Error("query is required for search");
61
+ return sdk.products.search(args.query, args.options || {});
62
+ case "update_pipeline":
63
+ if (!args.id)
64
+ throw new Error("id is required for update_pipeline");
65
+ if (!args.pipeline_steps)
66
+ throw new Error("pipeline_steps is required for update_pipeline");
67
+ if (typeof sdk.products.updatePipeline === "function") {
68
+ return sdk.products.updatePipeline(args.id, args.pipeline_steps);
69
+ }
70
+ return sdk.products.update(args.id, { fulfillment_pipeline: args.pipeline_steps });
71
+ default:
72
+ return { error: true, message: "Invalid action" };
73
+ }
74
+ }
75
+ },
76
+ {
77
+ name: "sdk_variants",
78
+ description: "Full Variants SDK tool: list, get, create, update, delete, generate.",
79
+ schema: {
80
+ input: z.object({
81
+ api_key: z.string().optional(),
82
+ base_url: z.string().url().optional(),
83
+ action: z.enum(["list", "get", "create", "update", "delete", "generate"]),
84
+ product_id: z.string(),
85
+ variant_id: z.string().optional(),
86
+ data: z.record(z.string(), z.any()).optional(),
87
+ replace_existing: z.boolean().optional()
88
+ })
89
+ },
90
+ execute: async (args) => {
91
+ const sdk = getSdkClient(args);
92
+ switch (args.action) {
93
+ case "list":
94
+ return sdk.variants.list(args.product_id);
95
+ case "get":
96
+ if (!args.variant_id)
97
+ throw new Error("variant_id is required for get");
98
+ return sdk.variants.get(args.product_id, args.variant_id);
99
+ case "create":
100
+ if (!args.data)
101
+ throw new Error("data is required for create");
102
+ return sdk.variants.create(args.product_id, args.data);
103
+ case "update":
104
+ if (!args.variant_id)
105
+ throw new Error("variant_id is required for update");
106
+ if (!args.data)
107
+ throw new Error("data is required for update");
108
+ return sdk.variants.update(args.product_id, args.variant_id, args.data);
109
+ case "delete":
110
+ if (!args.variant_id)
111
+ throw new Error("variant_id is required for delete");
112
+ await sdk.variants.delete(args.product_id, args.variant_id);
113
+ return { message: "Variant deleted" };
114
+ case "generate":
115
+ return sdk.variants.generateFromOptions(args.product_id, args.replace_existing ?? true);
116
+ default:
117
+ return { error: true, message: "Invalid action" };
118
+ }
119
+ }
120
+ },
121
+ {
122
+ name: "sdk_cart",
123
+ description: "Full Cart SDK tool: create, get, add_items, update_item, remove_item.",
124
+ schema: {
125
+ input: z.object({
126
+ api_key: z.string().optional(),
127
+ base_url: z.string().url().optional(),
128
+ action: z.enum(["create", "get", "add_items", "update_item", "remove_item"]),
129
+ cart_id: z.string().optional(),
130
+ items: z.array(z.object({
131
+ productId: z.string(),
132
+ quantity: z.number(),
133
+ variantId: z.string().optional(),
134
+ metadata: z.record(z.string(), z.any()).optional()
135
+ })).optional(),
136
+ product_id: z.string().optional(),
137
+ quantity: z.number().optional()
138
+ })
139
+ },
140
+ execute: async (args) => {
141
+ const sdk = getSdkClient(args);
142
+ switch (args.action) {
143
+ case "create":
144
+ if (!args.items)
145
+ throw new Error("items is required for create");
146
+ return sdk.cart.create(args.items);
147
+ case "get":
148
+ if (!args.cart_id)
149
+ throw new Error("cart_id is required for get");
150
+ return sdk.cart.get(args.cart_id);
151
+ case "add_items":
152
+ if (!args.cart_id)
153
+ throw new Error("cart_id is required for add_items");
154
+ if (!args.items)
155
+ throw new Error("items is required for add_items");
156
+ return sdk.cart.addItems(args.cart_id, args.items);
157
+ case "update_item":
158
+ if (!args.cart_id)
159
+ throw new Error("cart_id is required for update_item");
160
+ if (!args.product_id)
161
+ throw new Error("product_id is required for update_item");
162
+ if (args.quantity === undefined)
163
+ throw new Error("quantity is required for update_item");
164
+ return sdk.cart.updateItem(args.cart_id, args.product_id, args.quantity);
165
+ case "remove_item":
166
+ if (!args.cart_id)
167
+ throw new Error("cart_id is required for remove_item");
168
+ if (!args.product_id)
169
+ throw new Error("product_id is required for remove_item");
170
+ return sdk.cart.removeItem(args.cart_id, args.product_id);
171
+ default:
172
+ return { error: true, message: "Invalid action" };
173
+ }
174
+ }
175
+ },
176
+ {
177
+ name: "sdk_checkout",
178
+ description: "Checkout SDK tool: create checkout session from cart.",
179
+ schema: {
180
+ input: z.object({
181
+ api_key: z.string().optional(),
182
+ base_url: z.string().url().optional(),
183
+ cart_id: z.string(),
184
+ success_url: z.string().url(),
185
+ cancel_url: z.string().url()
186
+ })
187
+ },
188
+ execute: async (args) => {
189
+ const sdk = getSdkClient(args);
190
+ return sdk.checkout.create(args.cart_id, {
191
+ successUrl: args.success_url,
192
+ cancelUrl: args.cancel_url
193
+ });
194
+ }
195
+ },
196
+ {
197
+ name: "sdk_store",
198
+ description: "Store SDK tool: fetch store info.",
199
+ schema: {
200
+ input: z.object({
201
+ api_key: z.string().optional(),
202
+ base_url: z.string().url().optional()
203
+ })
204
+ },
205
+ execute: async (args) => {
206
+ const sdk = getSdkClient(args);
207
+ return sdk.store.info();
208
+ }
209
+ },
210
+ {
211
+ name: "sdk_media",
212
+ description: "Media SDK tool: list content library assets.",
213
+ schema: {
214
+ input: z.object({
215
+ api_key: z.string().optional(),
216
+ base_url: z.string().url().optional(),
217
+ search: z.string().optional(),
218
+ limit: z.number().optional(),
219
+ offset: z.number().optional()
220
+ })
221
+ },
222
+ execute: async (args) => {
223
+ const sdk = getSdkClient(args);
224
+ return sdk.media.list({
225
+ search: args.search,
226
+ limit: args.limit,
227
+ offset: args.offset
228
+ });
229
+ }
230
+ },
231
+ {
232
+ name: "sdk_models",
233
+ description: "Custom DB model SDK tool: list_models or create_model.",
234
+ schema: {
235
+ input: z.object({
236
+ api_key: z.string().optional(),
237
+ base_url: z.string().url().optional(),
238
+ action: z.enum(["list_models", "create_model"]),
239
+ name: z.string().optional(),
240
+ slug: z.string().optional(),
241
+ schema: z.object({
242
+ fields: z.array(z.object({
243
+ key: z.string(),
244
+ type: z.enum(["text", "number", "boolean", "image", "date", "json", "reference"]),
245
+ label: z.string().optional(),
246
+ required: z.boolean().optional()
247
+ }))
248
+ }).optional()
249
+ })
250
+ },
251
+ execute: async (args) => {
252
+ const sdk = getSdkClient(args);
253
+ if (args.action === "list_models") {
254
+ return sdk.db.listModels();
255
+ }
256
+ if (!args.name || !args.slug || !args.schema) {
257
+ throw new Error("name, slug, and schema are required for create_model");
258
+ }
259
+ return sdk.db.createModel(args.name, args.slug, args.schema);
260
+ }
261
+ },
262
+ {
263
+ name: "sdk_content",
264
+ description: "Custom DB content SDK tool: list, get, create, update, delete items for a collection slug.",
265
+ schema: {
266
+ input: z.object({
267
+ api_key: z.string().optional(),
268
+ base_url: z.string().url().optional(),
269
+ action: z.enum(["list", "get", "create", "update", "delete"]),
270
+ slug: z.string(),
271
+ id: z.string().optional(),
272
+ data: z.record(z.string(), z.any()).optional(),
273
+ options: z.object({
274
+ limit: z.number().optional(),
275
+ offset: z.number().optional(),
276
+ sort: z.string().optional(),
277
+ filter: z.record(z.string(), z.any()).optional()
278
+ }).optional()
279
+ })
280
+ },
281
+ execute: async (args) => {
282
+ const sdk = getSdkClient(args);
283
+ const collection = sdk.db.collection(args.slug);
284
+ switch (args.action) {
285
+ case "list":
286
+ return collection.list(args.options || {});
287
+ case "get":
288
+ if (!args.id)
289
+ throw new Error("id is required for get");
290
+ return collection.get(args.id);
291
+ case "create":
292
+ if (!args.data)
293
+ throw new Error("data is required for create");
294
+ return collection.create(args.data);
295
+ case "update":
296
+ if (!args.id)
297
+ throw new Error("id is required for update");
298
+ if (!args.data)
299
+ throw new Error("data is required for update");
300
+ return collection.update(args.id, args.data);
301
+ case "delete":
302
+ if (!args.id)
303
+ throw new Error("id is required for delete");
304
+ await collection.delete(args.id);
305
+ return { message: "Content item deleted" };
306
+ default:
307
+ return { error: true, message: "Invalid action" };
308
+ }
309
+ }
310
+ },
311
+ {
312
+ name: "sdk_orders",
313
+ description: "Orders SDK tool: list, get, update, transition_item.",
314
+ schema: {
315
+ input: z.object({
316
+ api_key: z.string().optional().describe("Secret API key recommended for admin operations."),
317
+ base_url: z.string().url().optional(),
318
+ action: z.enum(["list", "get", "update", "transition_item"]),
319
+ id: z.string().optional(),
320
+ options: z.object({
321
+ limit: z.number().optional(),
322
+ offset: z.number().optional(),
323
+ status: z.string().optional(),
324
+ sessionId: z.string().optional()
325
+ }).optional(),
326
+ updates: z.record(z.string(), z.any()).optional(),
327
+ order_id: z.string().optional(),
328
+ item_id: z.string().optional(),
329
+ transition: z.object({
330
+ stepId: z.string(),
331
+ metadata: z.record(z.string(), z.any()).optional(),
332
+ status: z.enum(["processing", "completed", "cancelled"]).optional()
333
+ }).optional()
334
+ })
335
+ },
336
+ execute: async (args) => {
337
+ const sdk = getSdkClient(args);
338
+ switch (args.action) {
339
+ case "list":
340
+ return sdk.orders.list(args.options || {});
341
+ case "get":
342
+ if (!args.id)
343
+ throw new Error("id is required for get");
344
+ return sdk.orders.get(args.id);
345
+ case "update":
346
+ if (!args.id)
347
+ throw new Error("id is required for update");
348
+ if (!args.updates)
349
+ throw new Error("updates is required for update");
350
+ return sdk.orders.update(args.id, args.updates);
351
+ case "transition_item":
352
+ if (!args.order_id)
353
+ throw new Error("order_id is required for transition_item");
354
+ if (!args.item_id)
355
+ throw new Error("item_id is required for transition_item");
356
+ if (!args.transition)
357
+ throw new Error("transition is required for transition_item");
358
+ if (typeof sdk.orders.transitionItem === "function") {
359
+ return sdk.orders.transitionItem(args.order_id, args.item_id, args.transition);
360
+ }
361
+ throw new Error("transition_item requires a newer @swatbloc/sdk version that includes orders.transitionItem().");
362
+ default:
363
+ return { error: true, message: "Invalid action" };
364
+ }
365
+ }
366
+ }
367
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@swatbloc/mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -20,6 +20,7 @@
20
20
  "prepublishOnly": "npm run build"
21
21
  },
22
22
  "dependencies": {
23
+ "@swatbloc/sdk": "^1.4.4",
23
24
  "@supabase/supabase-js": "^2.86.2",
24
25
  "@modelcontextprotocol/sdk": "^0.6.0",
25
26
  "zod": "^4.3.5",