|
|
@@ -27,13 +27,21 @@ import {
|
|
|
import {
|
|
|
McpTool,
|
|
|
LlmCustomer,
|
|
|
- LlmOrder
|
|
|
+ LlmOrder,
|
|
|
+ LlmProduct
|
|
|
} from '../_shared/mcp-types.ts';
|
|
|
import {
|
|
|
createMcpErrorResponse,
|
|
|
createMcpSuccessResponse,
|
|
|
- validateParams
|
|
|
+ validateParams,
|
|
|
+ cleanResponseData
|
|
|
} from '../_shared/mcp-helpers.ts';
|
|
|
+import {
|
|
|
+ getStoreQdrantConfig,
|
|
|
+ queryQdrantProducts,
|
|
|
+ queryQdrantOrders,
|
|
|
+ queryQdrantCustomers
|
|
|
+} from '../_shared/mcp-qdrant-helpers.ts';
|
|
|
import {
|
|
|
JsonRpcRequest,
|
|
|
createSseResponse,
|
|
|
@@ -56,10 +64,48 @@ const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
|
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
|
|
|
|
const SERVER_NAME = 'mcp-woocommerce';
|
|
|
-const SERVER_VERSION = '2.0.0';
|
|
|
+const SERVER_VERSION = '3.0.0';
|
|
|
|
|
|
// MCP Tool Definitions
|
|
|
const TOOLS: McpTool[] = [
|
|
|
+ {
|
|
|
+ name: 'woocommerce_get_products',
|
|
|
+ description: 'Get products from a WooCommerce store with filtering. Returns product catalog with details like name, SKU, price, and status. Use this to search for products or browse the catalog. Limited to 20 results maximum.',
|
|
|
+ inputSchema: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ shop_id: {
|
|
|
+ type: 'string',
|
|
|
+ description: 'The UUID of the WooCommerce store from the stores table'
|
|
|
+ },
|
|
|
+ sku: {
|
|
|
+ type: 'string',
|
|
|
+ description: 'Filter by product SKU (exact match)'
|
|
|
+ },
|
|
|
+ name: {
|
|
|
+ type: 'string',
|
|
|
+ description: 'Filter by product name (partial match, case-insensitive)'
|
|
|
+ },
|
|
|
+ status: {
|
|
|
+ type: 'string',
|
|
|
+ description: 'Filter by product status (publish, draft, pending)'
|
|
|
+ },
|
|
|
+ min_price: {
|
|
|
+ type: 'number',
|
|
|
+ description: 'Minimum price filter'
|
|
|
+ },
|
|
|
+ max_price: {
|
|
|
+ type: 'number',
|
|
|
+ description: 'Maximum price filter'
|
|
|
+ },
|
|
|
+ limit: {
|
|
|
+ type: 'number',
|
|
|
+ description: 'Number of products to return (default: 10, max: 20)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ required: ['shop_id']
|
|
|
+ }
|
|
|
+ },
|
|
|
{
|
|
|
name: 'woocommerce_get_order',
|
|
|
description: 'Get a specific order from a WooCommerce store by order ID or order number. Returns complete order details including customer info, items, status, and totals. Use this when a customer provides their order number.',
|
|
|
@@ -182,6 +228,138 @@ function formatOrderForLlm(order: WooCommerceOrder): LlmOrder {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Handle woocommerce_get_products tool
|
|
|
+ */
|
|
|
+async function handleGetProducts(args: Record<string, any>): Promise<ToolCallResult> {
|
|
|
+ const { shop_id, sku, name, status, min_price, max_price, limit = 10 } = args;
|
|
|
+
|
|
|
+ const actualLimit = Math.min(Math.max(1, limit), 20);
|
|
|
+
|
|
|
+ const { data: store, error: storeError } = await supabase
|
|
|
+ .from('stores')
|
|
|
+ .select('id, platform_name, store_name, data_access_permissions')
|
|
|
+ .eq('id', shop_id)
|
|
|
+ .eq('platform_name', 'woocommerce')
|
|
|
+ .single();
|
|
|
+
|
|
|
+ if (storeError || !store) {
|
|
|
+ return {
|
|
|
+ content: [{ type: 'text', text: JSON.stringify({ error: 'WooCommerce store not found' }) }],
|
|
|
+ isError: true
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const permissions = store.data_access_permissions as any;
|
|
|
+ if (permissions && !permissions.allow_product_access) {
|
|
|
+ return {
|
|
|
+ content: [{ type: 'text', text: JSON.stringify({ error: 'Product access not allowed for this store' }) }],
|
|
|
+ isError: true
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const qdrantConfig = await getStoreQdrantConfig(shop_id);
|
|
|
+
|
|
|
+ if (qdrantConfig && qdrantConfig.enabled && qdrantConfig.syncProducts) {
|
|
|
+ const products = await queryQdrantProducts(
|
|
|
+ shop_id,
|
|
|
+ qdrantConfig.shopname,
|
|
|
+ { sku, name, status, minPrice: min_price, maxPrice: max_price },
|
|
|
+ actualLimit
|
|
|
+ );
|
|
|
+
|
|
|
+ const cleanedProducts = cleanResponseData(products);
|
|
|
+
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({
|
|
|
+ count: products.length,
|
|
|
+ limit: actualLimit,
|
|
|
+ source: 'qdrant',
|
|
|
+ products: cleanedProducts
|
|
|
+ })
|
|
|
+ }]
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ let query = supabase
|
|
|
+ .from('woocommerce_products_cache')
|
|
|
+ .select('*')
|
|
|
+ .eq('store_id', shop_id);
|
|
|
+
|
|
|
+ if (sku) query = query.eq('sku', sku);
|
|
|
+ if (status) query = query.eq('stock_status', status);
|
|
|
+
|
|
|
+ query = query.limit(actualLimit);
|
|
|
+
|
|
|
+ const { data: cachedProducts, error: cacheError } = await query;
|
|
|
+
|
|
|
+ if (cacheError) {
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({ error: `Failed to fetch products: ${cacheError.message}` })
|
|
|
+ }],
|
|
|
+ isError: true
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ let products = cachedProducts || [];
|
|
|
+
|
|
|
+ if (name) {
|
|
|
+ const nameLower = name.toLowerCase();
|
|
|
+ products = products.filter((p: any) =>
|
|
|
+ (p.name || '').toLowerCase().includes(nameLower)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ if (min_price !== undefined) {
|
|
|
+ products = products.filter((p: any) => (parseFloat(p.price) || 0) >= min_price);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (max_price !== undefined) {
|
|
|
+ products = products.filter((p: any) => (parseFloat(p.price) || 0) <= max_price);
|
|
|
+ }
|
|
|
+
|
|
|
+ products = products.slice(0, actualLimit);
|
|
|
+
|
|
|
+ const formattedProducts: LlmProduct[] = products.map((p: any) => ({
|
|
|
+ id: p.wc_product_id || p.id,
|
|
|
+ name: p.name,
|
|
|
+ sku: p.sku || undefined,
|
|
|
+ price: p.price || undefined,
|
|
|
+ currency: p.currency || undefined,
|
|
|
+ status: p.stock_status || p.status || undefined,
|
|
|
+ description: p.description || undefined,
|
|
|
+ tags: undefined
|
|
|
+ }));
|
|
|
+
|
|
|
+ const cleanedProducts = cleanResponseData(formattedProducts);
|
|
|
+
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({
|
|
|
+ count: formattedProducts.length,
|
|
|
+ limit: actualLimit,
|
|
|
+ source: 'sql_cache',
|
|
|
+ products: cleanedProducts
|
|
|
+ })
|
|
|
+ }]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({ error: `Failed to fetch products: ${error instanceof Error ? error.message : 'Unknown error'}` })
|
|
|
+ }],
|
|
|
+ isError: true
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Fetch all orders with pagination (for filtering by customer name)
|
|
|
*/
|
|
|
@@ -258,12 +436,13 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
|
|
|
|
|
|
// Format for LLM
|
|
|
const formattedOrder = formatOrderForLlm(order);
|
|
|
+ const cleanedOrder = cleanResponseData(formattedOrder);
|
|
|
|
|
|
return {
|
|
|
content: [{
|
|
|
type: 'text',
|
|
|
text: JSON.stringify({
|
|
|
- order: formattedOrder
|
|
|
+ order: cleanedOrder
|
|
|
})
|
|
|
}]
|
|
|
};
|
|
|
@@ -445,6 +624,7 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
|
|
|
|
|
|
// Format for LLM
|
|
|
const formattedOrders = limitedOrders.map(formatOrderForLlm);
|
|
|
+ const cleanedOrders = cleanResponseData(formattedOrders);
|
|
|
|
|
|
return {
|
|
|
content: [{
|
|
|
@@ -461,7 +641,7 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
|
|
|
customer_name,
|
|
|
status
|
|
|
},
|
|
|
- orders: formattedOrders
|
|
|
+ orders: cleanedOrders
|
|
|
})
|
|
|
}]
|
|
|
};
|
|
|
@@ -540,12 +720,13 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
|
|
|
|
|
|
// Return only the first customer
|
|
|
const customer = formatCustomerForLlm(customers[0]);
|
|
|
+ const cleanedCustomer = cleanResponseData(customer);
|
|
|
|
|
|
return {
|
|
|
content: [{
|
|
|
type: 'text',
|
|
|
text: JSON.stringify({
|
|
|
- customer
|
|
|
+ customer: cleanedCustomer
|
|
|
})
|
|
|
}]
|
|
|
};
|
|
|
@@ -585,6 +766,9 @@ async function handleToolCall(params: ToolCallParams): Promise<ToolCallResult> {
|
|
|
|
|
|
// Route to appropriate handler
|
|
|
switch (name) {
|
|
|
+ case 'woocommerce_get_products':
|
|
|
+ return await handleGetProducts(args);
|
|
|
+
|
|
|
case 'woocommerce_get_order':
|
|
|
const orderValidation = validateParams(args, ['shop_id', 'order_id']);
|
|
|
if (!orderValidation.valid) {
|