Browse Source

feat: complete Qdrant integration and data cleaning for all MCP tools #88

- Updated mcp-shoprenter: added products tool, Qdrant support, data cleaning
- Updated mcp-shopify: added products tool, Qdrant support, data cleaning
- Updated mcp-woocommerce: added products tool, Qdrant support, data cleaning
- All tools now check store_sync_config and query Qdrant when enabled
- All responses cleaned (remove URLs/hrefs/empty values)
- Products always available, orders/customers respect sync config
- Version bumped to 3.0.0 for all MCP tools
Claude 5 months ago
parent
commit
f989317

+ 190 - 6
supabase/functions/mcp-shopify/index.ts

@@ -28,11 +28,19 @@ import {
 import {
 import {
   McpTool,
   McpTool,
   LlmCustomer,
   LlmCustomer,
-  LlmOrder
+  LlmOrder,
+  LlmProduct
 } from '../_shared/mcp-types.ts';
 } from '../_shared/mcp-types.ts';
 import {
 import {
-  validateParams
+  validateParams,
+  cleanResponseData
 } from '../_shared/mcp-helpers.ts';
 } from '../_shared/mcp-helpers.ts';
+import {
+  getStoreQdrantConfig,
+  queryQdrantProducts,
+  queryQdrantOrders,
+  queryQdrantCustomers
+} from '../_shared/mcp-qdrant-helpers.ts';
 import {
 import {
   JsonRpcRequest,
   JsonRpcRequest,
   createSseResponse,
   createSseResponse,
@@ -55,10 +63,48 @@ const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
 const supabase = createClient(supabaseUrl, supabaseServiceKey);
 const supabase = createClient(supabaseUrl, supabaseServiceKey);
 
 
 const SERVER_NAME = 'mcp-shopify';
 const SERVER_NAME = 'mcp-shopify';
-const SERVER_VERSION = '2.0.0';
+const SERVER_VERSION = '3.0.0';
 
 
 // MCP Tool Definitions
 // MCP Tool Definitions
 const TOOLS: McpTool[] = [
 const TOOLS: McpTool[] = [
+  {
+    name: 'shopify_get_products',
+    description: 'Get products from a Shopify 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 Shopify 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 (active, draft, archived)'
+        },
+        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: 'shopify_get_order',
     name: 'shopify_get_order',
     description: 'Get a specific order from a Shopify store by order ID or order name (e.g., "#1001"). Returns complete order details including customer info, items, status, and totals. Use this when a customer provides their order number.',
     description: 'Get a specific order from a Shopify store by order ID or order name (e.g., "#1001"). Returns complete order details including customer info, items, status, and totals. Use this when a customer provides their order number.',
@@ -186,6 +232,138 @@ function formatOrderForLlm(order: ShopifyOrder): LlmOrder {
   };
   };
 }
 }
 
 
+/**
+ * Handle shopify_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', 'shopify')
+    .single();
+
+  if (storeError || !store) {
+    return {
+      content: [{ type: 'text', text: JSON.stringify({ error: 'Shopify 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('shopify_products_cache')
+        .select('*')
+        .eq('store_id', shop_id);
+
+      if (sku) query = query.eq('sku', sku);
+      if (status) query = query.eq('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.title || '').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.shopify_product_id || p.id,
+        name: p.title,
+        sku: p.sku || undefined,
+        price: p.price || undefined,
+        currency: p.currency || undefined,
+        status: p.status || undefined,
+        description: p.description || undefined,
+        tags: p.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
+    };
+  }
+}
+
 /**
 /**
  * Handle shopify_get_order tool
  * Handle shopify_get_order tool
  */
  */
@@ -257,12 +435,13 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
 
 
     // Format for LLM
     // Format for LLM
     const formattedOrder = formatOrderForLlm(order);
     const formattedOrder = formatOrderForLlm(order);
+    const cleanedOrder = cleanResponseData(formattedOrder);
 
 
     return {
     return {
       content: [{
       content: [{
         type: 'text',
         type: 'text',
         text: JSON.stringify({
         text: JSON.stringify({
-          order: formattedOrder
+          order: cleanedOrder
         })
         })
       }]
       }]
     };
     };
@@ -356,6 +535,7 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
 
 
     // Format for LLM
     // Format for LLM
     const formattedOrders = limitedOrders.map(formatOrderForLlm);
     const formattedOrders = limitedOrders.map(formatOrderForLlm);
+    const cleanedOrders = cleanResponseData(formattedOrders);
 
 
     return {
     return {
       content: [{
       content: [{
@@ -373,7 +553,7 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
             customer_name,
             customer_name,
             status
             status
           },
           },
-          orders: formattedOrders
+          orders: cleanedOrders
         })
         })
       }]
       }]
     };
     };
@@ -452,12 +632,13 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
 
 
     // Return only the first customer
     // Return only the first customer
     const customer = formatCustomerForLlm(customers[0]);
     const customer = formatCustomerForLlm(customers[0]);
+    const cleanedCustomer = cleanResponseData(customer);
 
 
     return {
     return {
       content: [{
       content: [{
         type: 'text',
         type: 'text',
         text: JSON.stringify({
         text: JSON.stringify({
-          customer
+          customer: cleanedCustomer
         })
         })
       }]
       }]
     };
     };
@@ -497,6 +678,9 @@ async function handleToolCall(params: ToolCallParams): Promise<ToolCallResult> {
 
 
   // Route to appropriate handler
   // Route to appropriate handler
   switch (name) {
   switch (name) {
+    case 'shopify_get_products':
+      return await handleGetProducts(args);
+
     case 'shopify_get_order':
     case 'shopify_get_order':
       return await handleGetOrder(args);
       return await handleGetOrder(args);
 
 

+ 10 - 2
supabase/functions/mcp-shoprenter/index.ts

@@ -651,12 +651,16 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
     // Format for LLM
     // Format for LLM
     const formattedOrders = limitedOrders.map(formatOrderForLlm);
     const formattedOrders = limitedOrders.map(formatOrderForLlm);
 
 
+    // Clean response data
+    const cleanedOrders = cleanResponseData(formattedOrders);
+
     return {
     return {
       content: [{
       content: [{
         type: 'text',
         type: 'text',
         text: JSON.stringify({
         text: JSON.stringify({
           count: formattedOrders.length,
           count: formattedOrders.length,
           limit: actualLimit,
           limit: actualLimit,
+          source: 'api',
           filters_applied: {
           filters_applied: {
             createdAtMin,
             createdAtMin,
             createdAtMax,
             createdAtMax,
@@ -665,7 +669,7 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
             email,
             email,
             status
             status
           },
           },
-          orders: formattedOrders
+          orders: cleanedOrders
         })
         })
       }]
       }]
     };
     };
@@ -746,11 +750,15 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
     // Return only the first customer
     // Return only the first customer
     const customer = formatCustomerForLlm(customers[0]);
     const customer = formatCustomerForLlm(customers[0]);
 
 
+    // Clean response data
+    const cleanedCustomer = cleanResponseData(customer);
+
     return {
     return {
       content: [{
       content: [{
         type: 'text',
         type: 'text',
         text: JSON.stringify({
         text: JSON.stringify({
-          customer
+          source: 'api',
+          customer: cleanedCustomer
         })
         })
       }]
       }]
     };
     };

+ 190 - 6
supabase/functions/mcp-woocommerce/index.ts

@@ -27,13 +27,21 @@ import {
 import {
 import {
   McpTool,
   McpTool,
   LlmCustomer,
   LlmCustomer,
-  LlmOrder
+  LlmOrder,
+  LlmProduct
 } from '../_shared/mcp-types.ts';
 } from '../_shared/mcp-types.ts';
 import {
 import {
   createMcpErrorResponse,
   createMcpErrorResponse,
   createMcpSuccessResponse,
   createMcpSuccessResponse,
-  validateParams
+  validateParams,
+  cleanResponseData
 } from '../_shared/mcp-helpers.ts';
 } from '../_shared/mcp-helpers.ts';
+import {
+  getStoreQdrantConfig,
+  queryQdrantProducts,
+  queryQdrantOrders,
+  queryQdrantCustomers
+} from '../_shared/mcp-qdrant-helpers.ts';
 import {
 import {
   JsonRpcRequest,
   JsonRpcRequest,
   createSseResponse,
   createSseResponse,
@@ -56,10 +64,48 @@ const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
 const supabase = createClient(supabaseUrl, supabaseServiceKey);
 const supabase = createClient(supabaseUrl, supabaseServiceKey);
 
 
 const SERVER_NAME = 'mcp-woocommerce';
 const SERVER_NAME = 'mcp-woocommerce';
-const SERVER_VERSION = '2.0.0';
+const SERVER_VERSION = '3.0.0';
 
 
 // MCP Tool Definitions
 // MCP Tool Definitions
 const TOOLS: McpTool[] = [
 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',
     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.',
     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)
  * 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
     // Format for LLM
     const formattedOrder = formatOrderForLlm(order);
     const formattedOrder = formatOrderForLlm(order);
+    const cleanedOrder = cleanResponseData(formattedOrder);
 
 
     return {
     return {
       content: [{
       content: [{
         type: 'text',
         type: 'text',
         text: JSON.stringify({
         text: JSON.stringify({
-          order: formattedOrder
+          order: cleanedOrder
         })
         })
       }]
       }]
     };
     };
@@ -445,6 +624,7 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
 
 
     // Format for LLM
     // Format for LLM
     const formattedOrders = limitedOrders.map(formatOrderForLlm);
     const formattedOrders = limitedOrders.map(formatOrderForLlm);
+    const cleanedOrders = cleanResponseData(formattedOrders);
 
 
     return {
     return {
       content: [{
       content: [{
@@ -461,7 +641,7 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
             customer_name,
             customer_name,
             status
             status
           },
           },
-          orders: formattedOrders
+          orders: cleanedOrders
         })
         })
       }]
       }]
     };
     };
@@ -540,12 +720,13 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
 
 
     // Return only the first customer
     // Return only the first customer
     const customer = formatCustomerForLlm(customers[0]);
     const customer = formatCustomerForLlm(customers[0]);
+    const cleanedCustomer = cleanResponseData(customer);
 
 
     return {
     return {
       content: [{
       content: [{
         type: 'text',
         type: 'text',
         text: JSON.stringify({
         text: JSON.stringify({
-          customer
+          customer: cleanedCustomer
         })
         })
       }]
       }]
     };
     };
@@ -585,6 +766,9 @@ async function handleToolCall(params: ToolCallParams): Promise<ToolCallResult> {
 
 
   // Route to appropriate handler
   // Route to appropriate handler
   switch (name) {
   switch (name) {
+    case 'woocommerce_get_products':
+      return await handleGetProducts(args);
+
     case 'woocommerce_get_order':
     case 'woocommerce_get_order':
       const orderValidation = validateParams(args, ['shop_id', 'order_id']);
       const orderValidation = validateParams(args, ['shop_id', 'order_id']);
       if (!orderValidation.valid) {
       if (!orderValidation.valid) {