@swatbloc/mcp-server 0.1.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 +20 -0
- package/dist/common/types.js +1 -0
- package/dist/index.js +7 -0
- package/dist/prompts/index.js +4 -0
- package/dist/prompts/rules.js +124 -0
- package/dist/server.js +102 -0
- package/dist/tools/index.js +383 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# MegaPlatform MCP Server
|
|
2
|
+
|
|
3
|
+
This package provides a Model Context Protocol (MCP) server for the MegaPlatform monorepo.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
- **Project Rules**: Provides project rules and guidelines to AI agents via the `project-rules` prompt.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
To use this with your AI assistant (e.g., Claude Desktop, Cursor, etc.), configure it as a local MCP server.
|
|
10
|
+
|
|
11
|
+
### Configuration
|
|
12
|
+
**Command:** `node`
|
|
13
|
+
**Args:** `[absolute-path-to-repo]/packages/mcp-server/dist/index.js`
|
|
14
|
+
|
|
15
|
+
### Development
|
|
16
|
+
- `npm install`: Install dependencies
|
|
17
|
+
- `npm run build`: Build the server
|
|
18
|
+
- `npm run dev`: Watch mode
|
|
19
|
+
|
|
20
|
+
:)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
const PROJECT_RULES_CONTENT = `
|
|
2
|
+
# SwatBloc SDK - Cursor Rules
|
|
3
|
+
|
|
4
|
+
You are building an app that uses the SwatBloc SDK for headless commerce.
|
|
5
|
+
|
|
6
|
+
## Installation
|
|
7
|
+
|
|
8
|
+
\`\`\`bash
|
|
9
|
+
npm install @swatbloc/sdk
|
|
10
|
+
\`\`\`
|
|
11
|
+
|
|
12
|
+
## Initialization
|
|
13
|
+
|
|
14
|
+
\`\`\`typescript
|
|
15
|
+
import { SwatBloc } from '@swatbloc/sdk';
|
|
16
|
+
|
|
17
|
+
// Initialize with your public API key from SwatBloc dashboard
|
|
18
|
+
const swat = new SwatBloc('pk_live_YOUR_KEY_HERE');
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
## API Reference
|
|
22
|
+
|
|
23
|
+
### Products
|
|
24
|
+
|
|
25
|
+
\`\`\`typescript
|
|
26
|
+
// List all products
|
|
27
|
+
const products = await swat.products.list();
|
|
28
|
+
|
|
29
|
+
// List with options
|
|
30
|
+
const products = await swat.products.list({
|
|
31
|
+
limit: 10,
|
|
32
|
+
offset: 0,
|
|
33
|
+
category: 'shoes',
|
|
34
|
+
search: 'running'
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Get single product by ID or slug
|
|
38
|
+
const product = await swat.products.get('prod_123');
|
|
39
|
+
const product = await swat.products.get('blue-running-shoes');
|
|
40
|
+
|
|
41
|
+
// Get by category
|
|
42
|
+
const shoes = await swat.products.byCategory('shoes');
|
|
43
|
+
|
|
44
|
+
// Search products
|
|
45
|
+
const results = await swat.products.search('running shoes');
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
### Cart
|
|
49
|
+
|
|
50
|
+
\`\`\`typescript
|
|
51
|
+
// Create a cart
|
|
52
|
+
const cart = await swat.cart.create([
|
|
53
|
+
{ productId: 'prod_123', quantity: 2 },
|
|
54
|
+
{ productId: 'prod_456', quantity: 1 }
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
// Get existing cart
|
|
58
|
+
const cart = await swat.cart.get('cart_abc123');
|
|
59
|
+
|
|
60
|
+
// Add items
|
|
61
|
+
const cart = await swat.cart.addItems('cart_abc123', [
|
|
62
|
+
{ productId: 'prod_789', quantity: 1 }
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
// Update quantity
|
|
66
|
+
const cart = await swat.cart.updateItem('cart_abc123', 'prod_123', 3);
|
|
67
|
+
|
|
68
|
+
// Remove item
|
|
69
|
+
const cart = await swat.cart.removeItem('cart_abc123', 'prod_123');
|
|
70
|
+
\`\`\`
|
|
71
|
+
|
|
72
|
+
### Checkout
|
|
73
|
+
|
|
74
|
+
\`\`\`typescript
|
|
75
|
+
// Create checkout session (redirects to Stripe)
|
|
76
|
+
const checkout = await swat.checkout.create('cart_abc123', {
|
|
77
|
+
successUrl: 'https://mysite.com/success',
|
|
78
|
+
cancelUrl: 'https://mysite.com/cart'
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Redirect user to checkout
|
|
82
|
+
window.location.href = checkout.url;
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
### Store Info
|
|
86
|
+
|
|
87
|
+
\`\`\`typescript
|
|
88
|
+
// Get store details
|
|
89
|
+
const store = await swat.store.info();
|
|
90
|
+
console.log(store.name); // "My Store"
|
|
91
|
+
console.log(store.currency); // "usd"
|
|
92
|
+
console.log(store.colors); // { primary: "#3B82F6", ... }
|
|
93
|
+
\`\`\`
|
|
94
|
+
|
|
95
|
+
## Types
|
|
96
|
+
|
|
97
|
+
All responses are fully typed. Import types if needed:
|
|
98
|
+
|
|
99
|
+
\`\`\`typescript
|
|
100
|
+
import type { Product, Cart, CheckoutSession, StoreInfo } from '@swatbloc/sdk';
|
|
101
|
+
\`\`\`
|
|
102
|
+
|
|
103
|
+
## Error Handling
|
|
104
|
+
|
|
105
|
+
\`\`\`typescript
|
|
106
|
+
try {
|
|
107
|
+
const product = await swat.products.get('invalid-id');
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(error.message); // "Product not found: invalid-id"
|
|
110
|
+
}
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
## Important Notes
|
|
114
|
+
|
|
115
|
+
- Always use the PUBLIC key (pk_live_...) in client-side code
|
|
116
|
+
- Never expose your SECRET key (sk_live_...) in frontend code
|
|
117
|
+
- Cart IDs persist across sessions - save them to localStorage
|
|
118
|
+
- Checkout URLs expire after 24 hours
|
|
119
|
+
`;
|
|
120
|
+
export const projectRulesPrompt = {
|
|
121
|
+
name: "project-rules",
|
|
122
|
+
description: "Get the project rules and guidelines for SwatBloc SDK",
|
|
123
|
+
execute: async () => PROJECT_RULES_CONTENT
|
|
124
|
+
};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { ListPromptsRequestSchema, GetPromptRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ErrorCode, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
import { prompts } from "./prompts/index.js";
|
|
5
|
+
import { tools } from "./tools/index.js";
|
|
6
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
7
|
+
export class McpServer {
|
|
8
|
+
server;
|
|
9
|
+
constructor() {
|
|
10
|
+
this.server = new Server({
|
|
11
|
+
name: "megaplatform-mcp",
|
|
12
|
+
version: "0.1.0",
|
|
13
|
+
}, {
|
|
14
|
+
capabilities: {
|
|
15
|
+
prompts: {},
|
|
16
|
+
tools: {},
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
this.setupHandlers();
|
|
20
|
+
}
|
|
21
|
+
setupHandlers() {
|
|
22
|
+
// Prompts
|
|
23
|
+
this.server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
24
|
+
return {
|
|
25
|
+
prompts: prompts.map((p) => ({
|
|
26
|
+
name: p.name,
|
|
27
|
+
description: p.description,
|
|
28
|
+
})),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
this.server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
32
|
+
const prompt = prompts.find((p) => p.name === request.params.name);
|
|
33
|
+
if (!prompt) {
|
|
34
|
+
throw new McpError(ErrorCode.MethodNotFound, "Prompt not found");
|
|
35
|
+
}
|
|
36
|
+
const result = await prompt.execute(request.params.arguments || {});
|
|
37
|
+
return {
|
|
38
|
+
messages: [
|
|
39
|
+
{
|
|
40
|
+
role: "user",
|
|
41
|
+
content: {
|
|
42
|
+
type: "text",
|
|
43
|
+
text: "Generative request", // Placeholder, commonly ignored by prompt users who just want the context
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
role: "assistant",
|
|
48
|
+
content: {
|
|
49
|
+
type: "text",
|
|
50
|
+
text: result,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
// Tools
|
|
57
|
+
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
58
|
+
return {
|
|
59
|
+
tools: tools.map((t) => ({
|
|
60
|
+
name: t.name,
|
|
61
|
+
description: t.description,
|
|
62
|
+
inputSchema: zodToJsonSchema(t.schema.input),
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
67
|
+
const tool = tools.find((t) => t.name === request.params.name);
|
|
68
|
+
if (!tool) {
|
|
69
|
+
throw new McpError(ErrorCode.MethodNotFound, "Tool not found");
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
// Validate arguments
|
|
73
|
+
const args = tool.schema.input.parse(request.params.arguments);
|
|
74
|
+
const result = await tool.execute(args);
|
|
75
|
+
return {
|
|
76
|
+
content: [
|
|
77
|
+
{
|
|
78
|
+
type: "text",
|
|
79
|
+
text: JSON.stringify(result, null, 2),
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: "text",
|
|
89
|
+
text: `Error: ${error.message}`,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
async run() {
|
|
98
|
+
const transport = new StdioServerTransport();
|
|
99
|
+
await this.server.connect(transport);
|
|
100
|
+
console.error("MCP Server running on stdio");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { supabase } from "@repo/database";
|
|
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 { store_id, name, slug, fields } = args;
|
|
43
|
+
// Check if exists (Idempotency)
|
|
44
|
+
const { data: existing } = await supabase
|
|
45
|
+
.from('content_models')
|
|
46
|
+
.select('*')
|
|
47
|
+
.eq('store_id', store_id)
|
|
48
|
+
.eq('slug', slug)
|
|
49
|
+
.single();
|
|
50
|
+
if (existing) {
|
|
51
|
+
return {
|
|
52
|
+
message: "Database already exists",
|
|
53
|
+
model: existing
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Rate Limit / Safety Check: Max 50 tables per store
|
|
57
|
+
const { count } = await supabase
|
|
58
|
+
.from('content_models')
|
|
59
|
+
.select('*', { count: 'exact', head: true })
|
|
60
|
+
.eq('store_id', store_id);
|
|
61
|
+
if (count && count >= 50) {
|
|
62
|
+
return {
|
|
63
|
+
message: "Error: Maximum number of databases (50) reached for this store. Please delete some before creating more.",
|
|
64
|
+
error: true
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const { data: newModel, error } = await supabase
|
|
68
|
+
.from('content_models')
|
|
69
|
+
.insert({
|
|
70
|
+
store_id,
|
|
71
|
+
name,
|
|
72
|
+
slug,
|
|
73
|
+
schema: { fields }
|
|
74
|
+
})
|
|
75
|
+
.select()
|
|
76
|
+
.single();
|
|
77
|
+
if (error)
|
|
78
|
+
throw new Error(error.message);
|
|
79
|
+
return {
|
|
80
|
+
message: "Database created successfully",
|
|
81
|
+
model: newModel
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
// ==================== PRODUCT TOOLS ====================
|
|
86
|
+
{
|
|
87
|
+
name: "list_products",
|
|
88
|
+
description: "List all products for a store. Returns product titles, prices, and IDs.",
|
|
89
|
+
schema: {
|
|
90
|
+
input: z.object({
|
|
91
|
+
store_id: z.string().describe("The ID of the store to list products for."),
|
|
92
|
+
limit: z.number().optional().describe("Maximum number of products to return (default: 50)"),
|
|
93
|
+
published_only: z.boolean().optional().describe("Only return published products (default: false)")
|
|
94
|
+
})
|
|
95
|
+
},
|
|
96
|
+
execute: async (args) => {
|
|
97
|
+
const { store_id, limit = 50, published_only = false } = args;
|
|
98
|
+
let query = supabase
|
|
99
|
+
.from('products')
|
|
100
|
+
.select('id, title, slug, price, published, options')
|
|
101
|
+
.eq('store_id', store_id)
|
|
102
|
+
.order('created_at', { ascending: false })
|
|
103
|
+
.limit(limit);
|
|
104
|
+
if (published_only) {
|
|
105
|
+
query = query.eq('published', true);
|
|
106
|
+
}
|
|
107
|
+
const { data: products, error } = await query;
|
|
108
|
+
if (error)
|
|
109
|
+
throw new Error(error.message);
|
|
110
|
+
return {
|
|
111
|
+
message: `Found ${products?.length || 0} products`,
|
|
112
|
+
products: products || []
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "manage_product",
|
|
118
|
+
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.",
|
|
119
|
+
schema: {
|
|
120
|
+
input: z.object({
|
|
121
|
+
store_id: z.string().describe("The ID of the store."),
|
|
122
|
+
action: z.enum(['create', 'update', 'delete']).describe("The action to perform."),
|
|
123
|
+
product_id: z.string().optional().describe("Required for update/delete actions."),
|
|
124
|
+
data: z.object({
|
|
125
|
+
title: z.string().optional().describe("Product title"),
|
|
126
|
+
description: z.string().optional().describe("Product description"),
|
|
127
|
+
price: z.number().optional().describe("Product price in cents (e.g., 1999 = $19.99)"),
|
|
128
|
+
compare_at_price: z.number().optional().describe("Compare at price in cents"),
|
|
129
|
+
options: z.array(z.object({
|
|
130
|
+
name: z.string().describe("Option name, e.g., 'Size' or 'Color'"),
|
|
131
|
+
values: z.array(z.string()).describe("Option values, e.g., ['S', 'M', 'L']")
|
|
132
|
+
})).optional().describe("Product options for variant generation"),
|
|
133
|
+
published: z.boolean().optional().describe("Whether the product is published"),
|
|
134
|
+
category: z.string().optional().describe("Product category"),
|
|
135
|
+
sku: z.string().optional().describe("Product SKU"),
|
|
136
|
+
images: z.array(z.string()).optional().describe("Array of image URLs")
|
|
137
|
+
}).optional().describe("Product data for create/update")
|
|
138
|
+
})
|
|
139
|
+
},
|
|
140
|
+
execute: async (args) => {
|
|
141
|
+
const { store_id, action, product_id, data } = args;
|
|
142
|
+
if (action === 'create') {
|
|
143
|
+
if (!data?.title) {
|
|
144
|
+
return { error: true, message: "Title is required to create a product" };
|
|
145
|
+
}
|
|
146
|
+
const slug = data.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') + '-' + Math.random().toString(36).substring(2, 7);
|
|
147
|
+
const productData = {
|
|
148
|
+
store_id,
|
|
149
|
+
title: data.title,
|
|
150
|
+
slug,
|
|
151
|
+
description: data.description || '',
|
|
152
|
+
price: data.price || 0,
|
|
153
|
+
compare_at_price: data.compare_at_price || null,
|
|
154
|
+
options: data.options || [],
|
|
155
|
+
published: data.published ?? false,
|
|
156
|
+
category: data.category || null,
|
|
157
|
+
sku: data.sku || null,
|
|
158
|
+
images: data.images || []
|
|
159
|
+
};
|
|
160
|
+
const { data: product, error } = await supabase
|
|
161
|
+
.from('products')
|
|
162
|
+
.insert(productData)
|
|
163
|
+
.select()
|
|
164
|
+
.single();
|
|
165
|
+
if (error)
|
|
166
|
+
throw new Error(error.message);
|
|
167
|
+
return {
|
|
168
|
+
message: "Product created successfully",
|
|
169
|
+
product
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (action === 'update') {
|
|
173
|
+
if (!product_id) {
|
|
174
|
+
return { error: true, message: "product_id is required for update" };
|
|
175
|
+
}
|
|
176
|
+
const updateData = {};
|
|
177
|
+
if (data?.title)
|
|
178
|
+
updateData.title = data.title;
|
|
179
|
+
if (data?.description !== undefined)
|
|
180
|
+
updateData.description = data.description;
|
|
181
|
+
if (data?.price !== undefined)
|
|
182
|
+
updateData.price = data.price;
|
|
183
|
+
if (data?.compare_at_price !== undefined)
|
|
184
|
+
updateData.compare_at_price = data.compare_at_price;
|
|
185
|
+
if (data?.options)
|
|
186
|
+
updateData.options = data.options;
|
|
187
|
+
if (data?.published !== undefined)
|
|
188
|
+
updateData.published = data.published;
|
|
189
|
+
if (data?.category !== undefined)
|
|
190
|
+
updateData.category = data.category;
|
|
191
|
+
if (data?.sku !== undefined)
|
|
192
|
+
updateData.sku = data.sku;
|
|
193
|
+
if (data?.images)
|
|
194
|
+
updateData.images = data.images;
|
|
195
|
+
const { data: product, error } = await supabase
|
|
196
|
+
.from('products')
|
|
197
|
+
.update(updateData)
|
|
198
|
+
.eq('id', product_id)
|
|
199
|
+
.eq('store_id', store_id)
|
|
200
|
+
.select()
|
|
201
|
+
.single();
|
|
202
|
+
if (error)
|
|
203
|
+
throw new Error(error.message);
|
|
204
|
+
return {
|
|
205
|
+
message: "Product updated successfully",
|
|
206
|
+
product
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
if (action === 'delete') {
|
|
210
|
+
if (!product_id) {
|
|
211
|
+
return { error: true, message: "product_id is required for delete" };
|
|
212
|
+
}
|
|
213
|
+
const { error } = await supabase
|
|
214
|
+
.from('products')
|
|
215
|
+
.delete()
|
|
216
|
+
.eq('id', product_id)
|
|
217
|
+
.eq('store_id', store_id);
|
|
218
|
+
if (error)
|
|
219
|
+
throw new Error(error.message);
|
|
220
|
+
return {
|
|
221
|
+
message: "Product deleted successfully"
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
return { error: true, message: "Invalid action" };
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "manage_variant",
|
|
229
|
+
description: "Create, update, delete, or auto-generate variants for a product. Use 'generate' action to automatically create variants from product options (Cartesian product).",
|
|
230
|
+
schema: {
|
|
231
|
+
input: z.object({
|
|
232
|
+
product_id: z.string().describe("The ID of the product."),
|
|
233
|
+
action: z.enum(['create', 'update', 'delete', 'generate', 'list']).describe("The action to perform."),
|
|
234
|
+
variant_id: z.string().optional().describe("Required for update/delete actions."),
|
|
235
|
+
data: z.object({
|
|
236
|
+
title: z.string().optional().describe("Variant title (e.g., 'S / Red')"),
|
|
237
|
+
sku: z.string().optional().describe("Variant SKU"),
|
|
238
|
+
price: z.number().optional().describe("Variant price in cents"),
|
|
239
|
+
inventory_quantity: z.number().optional().describe("Inventory count"),
|
|
240
|
+
options: z.record(z.string(), z.string()).optional().describe("Option values, e.g., { 'Size': 'S', 'Color': 'Red' }")
|
|
241
|
+
}).optional().describe("Variant data for create/update"),
|
|
242
|
+
replace_existing: z.boolean().optional().describe("For 'generate': replace existing variants (default: true)")
|
|
243
|
+
})
|
|
244
|
+
},
|
|
245
|
+
execute: async (args) => {
|
|
246
|
+
const { product_id, action, variant_id, data, replace_existing = true } = args;
|
|
247
|
+
// Verify product exists
|
|
248
|
+
const { data: product, error: productError } = await supabase
|
|
249
|
+
.from('products')
|
|
250
|
+
.select('id, price, options')
|
|
251
|
+
.eq('id', product_id)
|
|
252
|
+
.single();
|
|
253
|
+
if (productError || !product) {
|
|
254
|
+
return { error: true, message: "Product not found" };
|
|
255
|
+
}
|
|
256
|
+
if (action === 'list') {
|
|
257
|
+
const { data: variants, error } = await supabase
|
|
258
|
+
.from('product_variants')
|
|
259
|
+
.select('id, title, sku, price, inventory_quantity, options')
|
|
260
|
+
.eq('product_id', product_id)
|
|
261
|
+
.order('title');
|
|
262
|
+
if (error)
|
|
263
|
+
throw new Error(error.message);
|
|
264
|
+
return {
|
|
265
|
+
message: `Found ${variants?.length || 0} variants`,
|
|
266
|
+
variants: variants || []
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
if (action === 'generate') {
|
|
270
|
+
const options = product.options || [];
|
|
271
|
+
if (!options.length || !options.every((o) => o.values?.length > 0)) {
|
|
272
|
+
return {
|
|
273
|
+
error: true,
|
|
274
|
+
message: "Product must have at least one option with values defined. Update the product options first."
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const combinations = generateCartesianProduct(options);
|
|
278
|
+
if (combinations.length > 100) {
|
|
279
|
+
return {
|
|
280
|
+
error: true,
|
|
281
|
+
message: `Too many variants (${combinations.length}). Maximum is 100. Reduce option values.`
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (replace_existing) {
|
|
285
|
+
await supabase
|
|
286
|
+
.from('product_variants')
|
|
287
|
+
.delete()
|
|
288
|
+
.eq('product_id', product_id);
|
|
289
|
+
}
|
|
290
|
+
const variantsToInsert = combinations.map(combo => ({
|
|
291
|
+
product_id,
|
|
292
|
+
title: Object.values(combo).join(' / '),
|
|
293
|
+
sku: null,
|
|
294
|
+
price: product.price,
|
|
295
|
+
inventory_quantity: 0,
|
|
296
|
+
options: combo,
|
|
297
|
+
description: null,
|
|
298
|
+
image_url: null,
|
|
299
|
+
images: []
|
|
300
|
+
}));
|
|
301
|
+
const { data: variants, error } = await supabase
|
|
302
|
+
.from('product_variants')
|
|
303
|
+
.insert(variantsToInsert)
|
|
304
|
+
.select();
|
|
305
|
+
if (error)
|
|
306
|
+
throw new Error(error.message);
|
|
307
|
+
return {
|
|
308
|
+
message: `Generated ${variants?.length || 0} variants from options`,
|
|
309
|
+
variants: variants || []
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
if (action === 'create') {
|
|
313
|
+
const variantData = {
|
|
314
|
+
product_id,
|
|
315
|
+
title: data?.title || 'New Variant',
|
|
316
|
+
sku: data?.sku || null,
|
|
317
|
+
price: data?.price ?? product.price,
|
|
318
|
+
inventory_quantity: data?.inventory_quantity || 0,
|
|
319
|
+
options: data?.options || {},
|
|
320
|
+
description: null,
|
|
321
|
+
image_url: null,
|
|
322
|
+
images: []
|
|
323
|
+
};
|
|
324
|
+
const { data: variant, error } = await supabase
|
|
325
|
+
.from('product_variants')
|
|
326
|
+
.insert(variantData)
|
|
327
|
+
.select()
|
|
328
|
+
.single();
|
|
329
|
+
if (error)
|
|
330
|
+
throw new Error(error.message);
|
|
331
|
+
return {
|
|
332
|
+
message: "Variant created successfully",
|
|
333
|
+
variant
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (action === 'update') {
|
|
337
|
+
if (!variant_id) {
|
|
338
|
+
return { error: true, message: "variant_id is required for update" };
|
|
339
|
+
}
|
|
340
|
+
const updateData = {};
|
|
341
|
+
if (data?.title)
|
|
342
|
+
updateData.title = data.title;
|
|
343
|
+
if (data?.sku !== undefined)
|
|
344
|
+
updateData.sku = data.sku;
|
|
345
|
+
if (data?.price !== undefined)
|
|
346
|
+
updateData.price = data.price;
|
|
347
|
+
if (data?.inventory_quantity !== undefined)
|
|
348
|
+
updateData.inventory_quantity = data.inventory_quantity;
|
|
349
|
+
if (data?.options)
|
|
350
|
+
updateData.options = data.options;
|
|
351
|
+
const { data: variant, error } = await supabase
|
|
352
|
+
.from('product_variants')
|
|
353
|
+
.update(updateData)
|
|
354
|
+
.eq('id', variant_id)
|
|
355
|
+
.eq('product_id', product_id)
|
|
356
|
+
.select()
|
|
357
|
+
.single();
|
|
358
|
+
if (error)
|
|
359
|
+
throw new Error(error.message);
|
|
360
|
+
return {
|
|
361
|
+
message: "Variant updated successfully",
|
|
362
|
+
variant
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (action === 'delete') {
|
|
366
|
+
if (!variant_id) {
|
|
367
|
+
return { error: true, message: "variant_id is required for delete" };
|
|
368
|
+
}
|
|
369
|
+
const { error } = await supabase
|
|
370
|
+
.from('product_variants')
|
|
371
|
+
.delete()
|
|
372
|
+
.eq('id', variant_id)
|
|
373
|
+
.eq('product_id', product_id);
|
|
374
|
+
if (error)
|
|
375
|
+
throw new Error(error.message);
|
|
376
|
+
return {
|
|
377
|
+
message: "Variant deleted successfully"
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
return { error: true, message: "Invalid action" };
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
];
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@swatbloc/mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public"
|
|
6
|
+
},
|
|
7
|
+
"private": false,
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"swatbloc-mcp": "dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"dev": "tsc --watch",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@modelcontextprotocol/sdk": "^0.6.0",
|
|
24
|
+
"zod": "^4.3.5",
|
|
25
|
+
"zod-to-json-schema": "^3.25.1"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.10.6",
|
|
29
|
+
"typescript": "^5.3.3"
|
|
30
|
+
}
|
|
31
|
+
}
|