Просмотр исходного кода

feat: add get_order tools and improve WooCommerce email search #76

- Add get_order tool to all 3 MCP servers (WooCommerce, Shopify, ShopRenter)
  - WooCommerce: get order by numeric order ID
  - Shopify: get order by ID or order name (e.g. #1001)
  - ShopRenter: get order by order ID
- Fix WooCommerce customer email search issue:
  - Increase customer lookup limit from 1 to 10
  - Add fallback: fetch orders and filter by billing email locally
  - Better error handling and logging
- Add fetchOrder functions to Shopify and ShopRenter API clients
- All changes maintain backward compatibility
Claude 5 месяцев назад
Родитель
Сommit
0cd3f461dc

+ 13 - 0
supabase/functions/_shared/shopify-client.ts

@@ -365,6 +365,19 @@ export async function fetchAllOrders(
   return allOrders
   return allOrders
 }
 }
 
 
+// Fetch a single order by ID
+export async function fetchOrder(storeId: string, orderId: number): Promise<ShopifyOrder> {
+  const response = await shopifyApiRequest(storeId, `/orders/${orderId}.json`)
+  return response.order
+}
+
+// Fetch a single order by name (e.g., "#1001")
+export async function fetchOrderByName(storeId: string, orderName: string): Promise<ShopifyOrder | null> {
+  // Shopify's order name field is searchable
+  const orders = await fetchOrders(storeId, 1, undefined, { name: orderName })
+  return orders.length > 0 ? orders[0] : null
+}
+
 export interface ShopifyCustomerFilters {
 export interface ShopifyCustomerFilters {
   email?: string;    // Filter by exact email
   email?: string;    // Filter by exact email
   query?: string;    // GraphQL-style query for searching
   query?: string;    // GraphQL-style query for searching

+ 6 - 0
supabase/functions/_shared/shoprenter-client.ts

@@ -305,6 +305,12 @@ export async function fetchOrders(
   return shopRenterApiRequest(storeId, endpoint, 'GET')
   return shopRenterApiRequest(storeId, endpoint, 'GET')
 }
 }
 
 
+// Fetch a single order by ID
+export async function fetchOrder(storeId: string, orderId: string): Promise<ShopRenterOrder> {
+  const response = await shopRenterApiRequest(storeId, `/orders/${orderId}`, 'GET')
+  return response
+}
+
 // Register webhook
 // Register webhook
 export async function registerWebhook(storeId: string, event: string, callbackUrl: string): Promise<any> {
 export async function registerWebhook(storeId: string, event: string, callbackUrl: string): Promise<any> {
   return shopRenterApiRequest(
   return shopRenterApiRequest(

+ 121 - 0
supabase/functions/mcp-shopify/index.ts

@@ -17,6 +17,8 @@ import {
 } from '../_shared/internal-api-key-auth.ts';
 } from '../_shared/internal-api-key-auth.ts';
 import {
 import {
   fetchAllOrders,
   fetchAllOrders,
+  fetchOrder,
+  fetchOrderByName,
   fetchCustomers,
   fetchCustomers,
   ShopifyOrder,
   ShopifyOrder,
   ShopifyCustomer,
   ShopifyCustomer,
@@ -57,6 +59,28 @@ const SERVER_VERSION = '2.0.0';
 
 
 // MCP Tool Definitions
 // MCP Tool Definitions
 const TOOLS: McpTool[] = [
 const TOOLS: McpTool[] = [
+  {
+    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.',
+    inputSchema: {
+      type: 'object',
+      properties: {
+        shop_id: {
+          type: 'string',
+          description: 'The UUID of the Shopify store from the stores table'
+        },
+        order_id: {
+          type: 'number',
+          description: 'The Shopify order ID (numeric)'
+        },
+        order_name: {
+          type: 'string',
+          description: 'The Shopify order name (e.g., "#1001", "1001")'
+        }
+      },
+      required: ['shop_id']
+    }
+  },
   {
   {
     name: 'shopify_list_orders',
     name: 'shopify_list_orders',
     description: 'List orders from a Shopify store with filtering. At least one filter is REQUIRED (created_at_min, created_at_max, updated_at_min, updated_at_max, customer_email, or customer_name). Returns order details including customer info, items, status, and totals. Limited to 20 results maximum.',
     description: 'List orders from a Shopify store with filtering. At least one filter is REQUIRED (created_at_min, created_at_max, updated_at_min, updated_at_max, customer_email, or customer_name). Returns order details including customer info, items, status, and totals. Limited to 20 results maximum.',
@@ -162,6 +186,100 @@ function formatOrderForLlm(order: ShopifyOrder): LlmOrder {
   };
   };
 }
 }
 
 
+/**
+ * Handle shopify_get_order tool
+ */
+async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult> {
+  const { shop_id, order_id, order_name } = args;
+
+  // Validate at least one identifier is provided
+  if (!order_id && !order_name) {
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({
+          error: 'Either order_id or order_name parameter is required'
+        })
+      }],
+      isError: true
+    };
+  }
+
+  // Validate shop exists and is Shopify
+  const { data: store, error: storeError } = await supabase
+    .from('stores')
+    .select('id, platform_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
+    };
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && !permissions.allow_order_access) {
+    return {
+      content: [{ type: 'text', text: JSON.stringify({ error: 'Order access not allowed for this store' }) }],
+      isError: true
+    };
+  }
+
+  try {
+    let order: ShopifyOrder | null = null;
+
+    // Try fetching by ID first
+    if (order_id) {
+      order = await fetchOrder(shop_id, order_id);
+    }
+    // Otherwise try by name
+    else if (order_name) {
+      // Normalize order name (add # if not present)
+      const normalizedName = order_name.startsWith('#') ? order_name : `#${order_name}`;
+      order = await fetchOrderByName(shop_id, normalizedName);
+    }
+
+    if (!order) {
+      return {
+        content: [{
+          type: 'text',
+          text: JSON.stringify({
+            error: `Order not found: ${order_id || order_name}`
+          })
+        }],
+        isError: true
+      };
+    }
+
+    // Format for LLM
+    const formattedOrder = formatOrderForLlm(order);
+
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({
+          order: formattedOrder
+        })
+      }]
+    };
+  } catch (error) {
+    console.error('[MCP Shopify] Error fetching order:', error);
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({
+          error: `Failed to fetch order: ${error instanceof Error ? error.message : 'Unknown error'}`
+        })
+      }],
+      isError: true
+    };
+  }
+}
+
 /**
 /**
  * Handle shopify_list_orders tool
  * Handle shopify_list_orders tool
  */
  */
@@ -379,6 +497,9 @@ async function handleToolCall(params: ToolCallParams): Promise<ToolCallResult> {
 
 
   // Route to appropriate handler
   // Route to appropriate handler
   switch (name) {
   switch (name) {
+    case 'shopify_get_order':
+      return await handleGetOrder(args);
+
     case 'shopify_list_orders':
     case 'shopify_list_orders':
       return await handleListOrders(args);
       return await handleListOrders(args);
 
 

+ 106 - 0
supabase/functions/mcp-shoprenter/index.ts

@@ -17,6 +17,7 @@ import {
 } from '../_shared/internal-api-key-auth.ts';
 } from '../_shared/internal-api-key-auth.ts';
 import {
 import {
   fetchOrders,
   fetchOrders,
+  fetchOrder,
   fetchCustomers,
   fetchCustomers,
   ShopRenterOrder,
   ShopRenterOrder,
   ShopRenterCustomer,
   ShopRenterCustomer,
@@ -59,6 +60,24 @@ const SERVER_VERSION = '2.0.0';
 
 
 // MCP Tool Definitions
 // MCP Tool Definitions
 const TOOLS: McpTool[] = [
 const TOOLS: McpTool[] = [
+  {
+    name: 'shoprenter_get_order',
+    description: 'Get a specific order from a ShopRenter store by order ID. Returns complete order details including customer info, items, status, and totals. Use this when a customer provides their order number.',
+    inputSchema: {
+      type: 'object',
+      properties: {
+        shop_id: {
+          type: 'string',
+          description: 'The UUID of the ShopRenter store from the stores table'
+        },
+        order_id: {
+          type: 'string',
+          description: 'The ShopRenter order ID'
+        }
+      },
+      required: ['shop_id', 'order_id']
+    }
+  },
   {
   {
     name: 'shoprenter_list_orders',
     name: 'shoprenter_list_orders',
     description: 'List orders from a ShopRenter store with filtering. At least one filter is REQUIRED (created_from, created_to, updated_from, updated_to, customer_email, or customer_name). Returns order details including customer info, items, status, and totals. Limited to 20 results maximum.',
     description: 'List orders from a ShopRenter store with filtering. At least one filter is REQUIRED (created_from, created_to, updated_from, updated_to, customer_email, or customer_name). Returns order details including customer info, items, status, and totals. Limited to 20 results maximum.',
@@ -196,6 +215,78 @@ async function fetchAllOrdersPages(
   return allOrders;
   return allOrders;
 }
 }
 
 
+/**
+ * Handle shoprenter_get_order tool
+ */
+async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult> {
+  const { shop_id, order_id } = args;
+
+  // Validate order_id is provided
+  if (!order_id) {
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({
+          error: 'order_id parameter is required'
+        })
+      }],
+      isError: true
+    };
+  }
+
+  // Validate shop exists and is ShopRenter
+  const { data: store, error: storeError } = await supabase
+    .from('stores')
+    .select('id, platform_name, data_access_permissions')
+    .eq('id', shop_id)
+    .eq('platform_name', 'shoprenter')
+    .single();
+
+  if (storeError || !store) {
+    return {
+      content: [{ type: 'text', text: JSON.stringify({ error: 'ShopRenter store not found' }) }],
+      isError: true
+    };
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && !permissions.allow_order_access) {
+    return {
+      content: [{ type: 'text', text: JSON.stringify({ error: 'Order access not allowed for this store' }) }],
+      isError: true
+    };
+  }
+
+  try {
+    // Fetch the specific order
+    const order = await fetchOrder(shop_id, order_id);
+
+    // Format for LLM
+    const formattedOrder = formatOrderForLlm(order);
+
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({
+          order: formattedOrder
+        })
+      }]
+    };
+  } catch (error) {
+    console.error('[MCP ShopRenter] Error fetching order:', error);
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({
+          error: `Failed to fetch order: ${error instanceof Error ? error.message : 'Unknown error'}`
+        })
+      }],
+      isError: true
+    };
+  }
+}
+
 /**
 /**
  * Handle shoprenter_list_orders tool
  * Handle shoprenter_list_orders tool
  */
  */
@@ -424,6 +515,21 @@ async function handleToolCall(params: ToolCallParams): Promise<ToolCallResult> {
 
 
   // Route to appropriate handler
   // Route to appropriate handler
   switch (name) {
   switch (name) {
+    case 'shoprenter_get_order':
+      const orderValidation = validateParams(args, ['shop_id', 'order_id']);
+      if (!orderValidation.valid) {
+        return {
+          content: [{
+            type: 'text',
+            text: JSON.stringify({
+              error: `Missing required parameters: ${orderValidation.missing?.join(', ')}`
+            })
+          }],
+          isError: true
+        };
+      }
+      return await handleGetOrder(args);
+
     case 'shoprenter_list_orders':
     case 'shoprenter_list_orders':
       return await handleListOrders(args);
       return await handleListOrders(args);
 
 

+ 176 - 26
supabase/functions/mcp-woocommerce/index.ts

@@ -17,6 +17,7 @@ import {
 } from '../_shared/internal-api-key-auth.ts';
 } from '../_shared/internal-api-key-auth.ts';
 import {
 import {
   fetchOrders,
   fetchOrders,
+  fetchOrder,
   fetchCustomers,
   fetchCustomers,
   WooCommerceOrder,
   WooCommerceOrder,
   WooCommerceCustomer,
   WooCommerceCustomer,
@@ -59,6 +60,24 @@ const SERVER_VERSION = '2.0.0';
 
 
 // MCP Tool Definitions
 // MCP Tool Definitions
 const TOOLS: McpTool[] = [
 const TOOLS: McpTool[] = [
+  {
+    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.',
+    inputSchema: {
+      type: 'object',
+      properties: {
+        shop_id: {
+          type: 'string',
+          description: 'The UUID of the WooCommerce store from the stores table'
+        },
+        order_id: {
+          type: 'number',
+          description: 'The WooCommerce order ID (numeric)'
+        }
+      },
+      required: ['shop_id', 'order_id']
+    }
+  },
   {
   {
     name: 'woocommerce_list_orders',
     name: 'woocommerce_list_orders',
     description: 'List orders from a WooCommerce store with filtering. At least one filter is REQUIRED (created_after, created_before, updated_after, updated_before, customer_email, or customer_name). Returns order details including customer info, items, status, and totals. Limited to 20 results maximum.',
     description: 'List orders from a WooCommerce store with filtering. At least one filter is REQUIRED (created_after, created_before, updated_after, updated_before, customer_email, or customer_name). Returns order details including customer info, items, status, and totals. Limited to 20 results maximum.',
@@ -190,6 +209,78 @@ async function fetchAllOrdersPages(
   return allOrders;
   return allOrders;
 }
 }
 
 
+/**
+ * Handle woocommerce_get_order tool
+ */
+async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult> {
+  const { shop_id, order_id } = args;
+
+  // Validate order_id is provided
+  if (!order_id) {
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({
+          error: 'order_id parameter is required'
+        })
+      }],
+      isError: true
+    };
+  }
+
+  // Validate shop exists and is WooCommerce
+  const { data: store, error: storeError } = await supabase
+    .from('stores')
+    .select('id, platform_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
+    };
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && !permissions.allow_order_access) {
+    return {
+      content: [{ type: 'text', text: JSON.stringify({ error: 'Order access not allowed for this store' }) }],
+      isError: true
+    };
+  }
+
+  try {
+    // Fetch the specific order
+    const order = await fetchOrder(shop_id, order_id);
+
+    // Format for LLM
+    const formattedOrder = formatOrderForLlm(order);
+
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({
+          order: formattedOrder
+        })
+      }]
+    };
+  } catch (error) {
+    console.error('[MCP WooCommerce] Error fetching order:', error);
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({
+          error: `Failed to fetch order: ${error instanceof Error ? error.message : 'Unknown error'}`
+        })
+      }],
+      isError: true
+    };
+  }
+}
+
 /**
 /**
  * Handle woocommerce_list_orders tool
  * Handle woocommerce_list_orders tool
  */
  */
@@ -258,34 +349,78 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
 
 
     // Handle customer_email: WooCommerce requires customer ID (integer), not email
     // Handle customer_email: WooCommerce requires customer ID (integer), not email
     if (customer_email) {
     if (customer_email) {
-      // First, search for customer by email to get their ID
-      const customers = await fetchCustomers(shop_id, 1, 1, { email: customer_email });
+      try {
+        // First, search for customer by email to get their ID
+        // Fetch more customers (up to 10) to handle edge cases
+        const customers = await fetchCustomers(shop_id, 1, 10, { email: customer_email });
 
 
-      if (customers.length === 0) {
-        return {
-          content: [{
-            type: 'text',
-            text: JSON.stringify({
-              count: 0,
-              limit: actualLimit,
-              filters_applied: {
-                created_after,
-                created_before,
-                updated_after,
-                updated_before,
-                customer_email,
-                customer_name,
-                status
-              },
-              orders: [],
-              message: `No customer found with email: ${customer_email}`
-            })
-          }]
-        };
-      }
+        console.log(`[MCP WooCommerce] Customer search for email "${customer_email}": found ${customers.length} customers`);
+
+        if (customers.length === 0) {
+          // Fallback: Try fetching recent orders and filter by billing email locally
+          console.log(`[MCP WooCommerce] No customer found, trying fallback: fetch orders and filter by billing email`);
+
+          const fallbackOrders = await fetchAllOrdersPages(shop_id, 5, filters);
+          const emailLower = customer_email.toLowerCase();
+          const matchedOrders = fallbackOrders.filter(order =>
+            order.billing.email && order.billing.email.toLowerCase() === emailLower
+          );
+
+          if (matchedOrders.length === 0) {
+            return {
+              content: [{
+                type: 'text',
+                text: JSON.stringify({
+                  count: 0,
+                  limit: actualLimit,
+                  filters_applied: {
+                    created_after,
+                    created_before,
+                    updated_after,
+                    updated_before,
+                    customer_email,
+                    customer_name,
+                    status
+                  },
+                  orders: [],
+                  message: `No orders found for email: ${customer_email}`
+                })
+              }]
+            };
+          }
+
+          // Use the matched orders
+          orders = matchedOrders.slice(0, actualLimit);
+          const formattedOrders = orders.map(formatOrderForLlm);
+
+          return {
+            content: [{
+              type: 'text',
+              text: JSON.stringify({
+                count: formattedOrders.length,
+                limit: actualLimit,
+                filters_applied: {
+                  created_after,
+                  created_before,
+                  updated_after,
+                  updated_before,
+                  customer_email,
+                  customer_name,
+                  status
+                },
+                orders: formattedOrders,
+                note: 'Found via billing email search (customer not found in customer list)'
+              })
+            }]
+          };
+        }
 
 
-      // Use the customer ID for filtering
-      filters.customer = customers[0].id.toString();
+        // Use the customer ID for filtering
+        filters.customer = customers[0].id.toString();
+      } catch (error) {
+        console.error('[MCP WooCommerce] Error searching customer by email:', error);
+        // Continue without customer filter (will fallback to other filters if available)
+      }
     }
     }
 
 
     // Fetch orders from WooCommerce API
     // Fetch orders from WooCommerce API
@@ -450,6 +585,21 @@ async function handleToolCall(params: ToolCallParams): Promise<ToolCallResult> {
 
 
   // Route to appropriate handler
   // Route to appropriate handler
   switch (name) {
   switch (name) {
+    case 'woocommerce_get_order':
+      const orderValidation = validateParams(args, ['shop_id', 'order_id']);
+      if (!orderValidation.valid) {
+        return {
+          content: [{
+            type: 'text',
+            text: JSON.stringify({
+              error: `Missing required parameters: ${orderValidation.missing?.join(', ')}`
+            })
+          }],
+          isError: true
+        };
+      }
+      return await handleGetOrder(args);
+
     case 'woocommerce_list_orders':
     case 'woocommerce_list_orders':
       return await handleListOrders(args);
       return await handleListOrders(args);