Browse Source

feat: implement MCP HTTP servers for webshop API access #76

Claude 5 months ago
parent
commit
8f9929ccd5

+ 77 - 0
supabase/functions/_shared/mcp-helpers.ts

@@ -0,0 +1,77 @@
+/**
+ * MCP Helper Functions
+ *
+ * Common utilities for MCP HTTP servers including response formatting
+ * and error handling.
+ */
+
+import { McpToolCallResponse, McpErrorResponse } from './mcp-types.ts';
+
+/**
+ * Create a successful MCP tool response
+ */
+export function createMcpResponse(data: any): McpToolCallResponse {
+  return {
+    content: [
+      {
+        type: 'text',
+        text: typeof data === 'string' ? data : JSON.stringify(data, null, 2)
+      }
+    ]
+  };
+}
+
+/**
+ * Create an MCP error response
+ */
+export function createMcpError(code: string, message: string): McpErrorResponse {
+  return {
+    error: {
+      code,
+      message
+    },
+    isError: true
+  };
+}
+
+/**
+ * Create HTTP Response for MCP error
+ */
+export function createMcpErrorResponse(code: string, message: string, status: number = 400): Response {
+  return new Response(
+    JSON.stringify(createMcpError(code, message)),
+    {
+      status,
+      headers: { 'Content-Type': 'application/json' }
+    }
+  );
+}
+
+/**
+ * Create HTTP Response for MCP success
+ */
+export function createMcpSuccessResponse(data: any): Response {
+  return new Response(
+    JSON.stringify(createMcpResponse(data)),
+    {
+      status: 200,
+      headers: { 'Content-Type': 'application/json' }
+    }
+  );
+}
+
+/**
+ * Validate required parameters in MCP tool call
+ */
+export function validateParams(
+  args: Record<string, any>,
+  required: string[]
+): { valid: boolean; missing?: string[] } {
+  const missing = required.filter(param => !args[param]);
+
+  if (missing.length > 0) {
+    return { valid: false, missing };
+  }
+
+  return { valid: true };
+}

+ 77 - 0
supabase/functions/_shared/mcp-types.ts

@@ -0,0 +1,77 @@
+/**
+ * MCP (Model Context Protocol) Types for HTTP MCP Servers
+ *
+ * Defines types for MCP tool descriptions and responses following
+ * the MCP specification for HTTP-based tool servers.
+ */
+
+export interface McpTool {
+  name: string;
+  description: string;
+  inputSchema: {
+    type: string;
+    properties: Record<string, any>;
+    required: string[];
+  };
+}
+
+export interface McpToolsListResponse {
+  tools: McpTool[];
+}
+
+export interface McpToolCallRequest {
+  name: string;
+  arguments: Record<string, any>;
+}
+
+export interface McpToolCallResponse {
+  content: Array<{
+    type: 'text' | 'image' | 'resource';
+    text?: string;
+    data?: string;
+    mimeType?: string;
+  }>;
+  isError?: boolean;
+}
+
+export interface McpErrorResponse {
+  error: {
+    code: string;
+    message: string;
+  };
+  isError: true;
+}
+
+/**
+ * Compact LLM-friendly customer representation
+ */
+export interface LlmCustomer {
+  id: string;
+  name: string;
+  email: string;
+  phone?: string;
+  ordersCount?: number;
+  totalSpent?: string;
+}
+
+/**
+ * Compact LLM-friendly order representation
+ */
+export interface LlmOrder {
+  id: string;
+  orderNumber: string;
+  customer: {
+    name: string;
+    email: string;
+    phone?: string;
+  };
+  status: string;
+  total: string;
+  currency?: string;
+  items: Array<{
+    name: string;
+    quantity: number;
+    price: string;
+  }>;
+  createdAt: string;
+}

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

@@ -0,0 +1,370 @@
+/**
+ * MCP HTTP Server for Shopify
+ *
+ * Provides MCP tools for accessing Shopify orders and customers data
+ * without storing personal information locally (GDPR compliance).
+ *
+ * Authentication: Internal API keys from internal_api_keys table
+ * Required parameter: shop_id (store ID from stores table)
+ */
+
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
+import {
+  requireInternalApiKey,
+  createInternalApiKeyErrorResponse
+} from '../_shared/internal-api-key-auth.ts';
+import {
+  fetchAllOrders,
+  fetchAllCustomers,
+  ShopifyOrder,
+  ShopifyCustomer
+} from '../_shared/shopify-client.ts';
+import {
+  McpToolsListResponse,
+  McpToolCallRequest,
+  LlmCustomer,
+  LlmOrder
+} from '../_shared/mcp-types.ts';
+import {
+  createMcpErrorResponse,
+  createMcpSuccessResponse,
+  validateParams
+} from '../_shared/mcp-helpers.ts';
+
+// Initialize Supabase client
+const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
+const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
+const supabase = createClient(supabaseUrl, supabaseServiceKey);
+
+// MCP Tool Definitions
+const TOOLS: McpToolsListResponse = {
+  tools: [
+    {
+      name: 'shopify_list_orders',
+      description: 'List orders from a Shopify store. Returns order details including customer info, items, status, and totals.',
+      inputSchema: {
+        type: 'object',
+        properties: {
+          shop_id: {
+            type: 'string',
+            description: 'The UUID of the Shopify store from the stores table'
+          },
+          status: {
+            type: 'string',
+            description: 'Filter by order status: any (default), open, closed, cancelled',
+            enum: ['any', 'open', 'closed', 'cancelled']
+          },
+          limit: {
+            type: 'number',
+            description: 'Maximum number of orders to return (default: 50, max: 250)'
+          }
+        },
+        required: ['shop_id']
+      }
+    },
+    {
+      name: 'shopify_list_customers',
+      description: 'List customers from a Shopify store. Returns customer details including contact info, order count, and total spent.',
+      inputSchema: {
+        type: 'object',
+        properties: {
+          shop_id: {
+            type: 'string',
+            description: 'The UUID of the Shopify store from the stores table'
+          },
+          limit: {
+            type: 'number',
+            description: 'Maximum number of customers to return (default: 50, max: 250)'
+          }
+        },
+        required: ['shop_id']
+      }
+    },
+    {
+      name: 'shopify_get_customer_orders',
+      description: 'Get all orders for a specific customer by customer ID. Returns detailed order history.',
+      inputSchema: {
+        type: 'object',
+        properties: {
+          shop_id: {
+            type: 'string',
+            description: 'The UUID of the Shopify store from the stores table'
+          },
+          customer_id: {
+            type: 'string',
+            description: 'The Shopify customer ID'
+          }
+        },
+        required: ['shop_id', 'customer_id']
+      }
+    }
+  ]
+};
+
+/**
+ * Convert Shopify customer to LLM-friendly format
+ */
+function formatCustomerForLlm(customer: ShopifyCustomer): LlmCustomer {
+  return {
+    id: customer.id.toString(),
+    name: `${customer.first_name} ${customer.last_name}`.trim(),
+    email: customer.email,
+    phone: customer.phone || customer.default_address?.phone || undefined,
+    ordersCount: customer.orders_count,
+    totalSpent: `${customer.total_spent} ${customer.currency}`
+  };
+}
+
+/**
+ * Convert Shopify order to LLM-friendly format
+ */
+function formatOrderForLlm(order: ShopifyOrder): LlmOrder {
+  return {
+    id: order.id.toString(),
+    orderNumber: order.name,
+    customer: {
+      name: `${order.customer.first_name} ${order.customer.last_name}`.trim(),
+      email: order.customer.email,
+      phone: order.customer.phone || order.billing_address?.phone || order.shipping_address?.phone || undefined
+    },
+    status: `${order.financial_status}/${order.fulfillment_status || 'unfulfilled'}`,
+    total: order.current_total_price,
+    currency: order.currency,
+    items: order.line_items.map(item => ({
+      name: item.title,
+      quantity: item.quantity,
+      price: item.price
+    })),
+    createdAt: order.created_at
+  };
+}
+
+/**
+ * Handle shopify_list_orders tool
+ */
+async function handleListOrders(args: Record<string, any>): Promise<Response> {
+  const { shop_id, status = 'any', limit = 50 } = args;
+
+  // 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 createMcpErrorResponse('STORE_NOT_FOUND', 'Shopify store not found', 404);
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && !permissions.allow_order_access) {
+    return createMcpErrorResponse('PERMISSION_DENIED', 'Order access not allowed for this store', 403);
+  }
+
+  try {
+    // Fetch orders from Shopify API
+    const allOrders = await fetchAllOrders(shop_id, status);
+
+    // Apply limit
+    const orders = allOrders.slice(0, Math.min(limit, 250));
+
+    // Format for LLM
+    const formattedOrders = orders.map(formatOrderForLlm);
+
+    return createMcpSuccessResponse({
+      count: formattedOrders.length,
+      total: allOrders.length,
+      orders: formattedOrders
+    });
+  } catch (error) {
+    console.error('[MCP Shopify] Error fetching orders:', error);
+    return createMcpErrorResponse(
+      'FETCH_ERROR',
+      `Failed to fetch orders: ${error instanceof Error ? error.message : 'Unknown error'}`,
+      500
+    );
+  }
+}
+
+/**
+ * Handle shopify_list_customers tool
+ */
+async function handleListCustomers(args: Record<string, any>): Promise<Response> {
+  const { shop_id, limit = 50 } = args;
+
+  // 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 createMcpErrorResponse('STORE_NOT_FOUND', 'Shopify store not found', 404);
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && !permissions.allow_customer_access) {
+    return createMcpErrorResponse('PERMISSION_DENIED', 'Customer access not allowed for this store', 403);
+  }
+
+  try {
+    // Fetch customers from Shopify API
+    const allCustomers = await fetchAllCustomers(shop_id);
+
+    // Apply limit
+    const customers = allCustomers.slice(0, Math.min(limit, 250));
+
+    // Format for LLM
+    const formattedCustomers = customers.map(formatCustomerForLlm);
+
+    return createMcpSuccessResponse({
+      count: formattedCustomers.length,
+      total: allCustomers.length,
+      customers: formattedCustomers
+    });
+  } catch (error) {
+    console.error('[MCP Shopify] Error fetching customers:', error);
+    return createMcpErrorResponse(
+      'FETCH_ERROR',
+      `Failed to fetch customers: ${error instanceof Error ? error.message : 'Unknown error'}`,
+      500
+    );
+  }
+}
+
+/**
+ * Handle shopify_get_customer_orders tool
+ */
+async function handleGetCustomerOrders(args: Record<string, any>): Promise<Response> {
+  const { shop_id, customer_id } = args;
+
+  // 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 createMcpErrorResponse('STORE_NOT_FOUND', 'Shopify store not found', 404);
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && (!permissions.allow_order_access || !permissions.allow_customer_access)) {
+    return createMcpErrorResponse('PERMISSION_DENIED', 'Order and customer access required', 403);
+  }
+
+  try {
+    // Fetch all orders and filter by customer ID
+    const allOrders = await fetchAllOrders(shop_id);
+    const customerOrders = allOrders.filter(order => order.customer.id.toString() === customer_id);
+
+    // Format for LLM
+    const formattedOrders = customerOrders.map(formatOrderForLlm);
+
+    return createMcpSuccessResponse({
+      customerId: customer_id,
+      count: formattedOrders.length,
+      orders: formattedOrders
+    });
+  } catch (error) {
+    console.error('[MCP Shopify] Error fetching customer orders:', error);
+    return createMcpErrorResponse(
+      'FETCH_ERROR',
+      `Failed to fetch customer orders: ${error instanceof Error ? error.message : 'Unknown error'}`,
+      500
+    );
+  }
+}
+
+/**
+ * Main request handler
+ */
+serve(async (req: Request) => {
+  // CORS headers
+  if (req.method === 'OPTIONS') {
+    return new Response(null, {
+      headers: {
+        'Access-Control-Allow-Origin': '*',
+        'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
+        'Access-Control-Allow-Headers': 'Authorization, Content-Type'
+      }
+    });
+  }
+
+  const url = new URL(req.url);
+
+  // Handle /tools endpoint - list available tools
+  if (url.pathname.endsWith('/tools') && req.method === 'GET') {
+    return new Response(JSON.stringify(TOOLS), {
+      headers: {
+        'Content-Type': 'application/json',
+        'Access-Control-Allow-Origin': '*'
+      }
+    });
+  }
+
+  // Handle /call endpoint - execute tool
+  if (url.pathname.endsWith('/call') && req.method === 'POST') {
+    // Authenticate with internal API key
+    const authResult = await requireInternalApiKey(req, supabase, 'read_orders');
+
+    if (!authResult.valid) {
+      return createInternalApiKeyErrorResponse(authResult);
+    }
+
+    try {
+      const body: McpToolCallRequest = await req.json();
+      const { name, arguments: args } = body;
+
+      // Validate shop_id is provided
+      const validation = validateParams(args, ['shop_id']);
+      if (!validation.valid) {
+        return createMcpErrorResponse(
+          'MISSING_PARAMS',
+          `Missing required parameters: ${validation.missing?.join(', ')}`
+        );
+      }
+
+      // Route to appropriate handler
+      switch (name) {
+        case 'shopify_list_orders':
+          return await handleListOrders(args);
+
+        case 'shopify_list_customers':
+          return await handleListCustomers(args);
+
+        case 'shopify_get_customer_orders':
+          const customerValidation = validateParams(args, ['shop_id', 'customer_id']);
+          if (!customerValidation.valid) {
+            return createMcpErrorResponse(
+              'MISSING_PARAMS',
+              `Missing required parameters: ${customerValidation.missing?.join(', ')}`
+            );
+          }
+          return await handleGetCustomerOrders(args);
+
+        default:
+          return createMcpErrorResponse('UNKNOWN_TOOL', `Unknown tool: ${name}`, 404);
+      }
+    } catch (error) {
+      console.error('[MCP Shopify] Error handling request:', error);
+      return createMcpErrorResponse(
+        'INTERNAL_ERROR',
+        error instanceof Error ? error.message : 'Internal server error',
+        500
+      );
+    }
+  }
+
+  // 404 for unknown endpoints
+  return createMcpErrorResponse('NOT_FOUND', 'Endpoint not found. Use /tools or /call', 404);
+});

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

@@ -0,0 +1,423 @@
+/**
+ * MCP HTTP Server for ShopRenter
+ *
+ * Provides MCP tools for accessing ShopRenter orders and customers data
+ * without storing personal information locally (GDPR compliance).
+ *
+ * Authentication: Internal API keys from internal_api_keys table
+ * Required parameter: shop_id (store ID from stores table)
+ */
+
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
+import {
+  requireInternalApiKey,
+  createInternalApiKeyErrorResponse
+} from '../_shared/internal-api-key-auth.ts';
+import {
+  fetchOrders,
+  fetchCustomers,
+  ShopRenterOrder,
+  ShopRenterCustomer
+} from '../_shared/shoprenter-client.ts';
+import {
+  McpToolsListResponse,
+  McpToolCallRequest,
+  LlmCustomer,
+  LlmOrder
+} from '../_shared/mcp-types.ts';
+import {
+  createMcpErrorResponse,
+  createMcpSuccessResponse,
+  validateParams
+} from '../_shared/mcp-helpers.ts';
+
+// Initialize Supabase client
+const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
+const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
+const supabase = createClient(supabaseUrl, supabaseServiceKey);
+
+// MCP Tool Definitions
+const TOOLS: McpToolsListResponse = {
+  tools: [
+    {
+      name: 'shoprenter_list_orders',
+      description: 'List orders from a ShopRenter store. Returns order details including customer info, items, status, and totals.',
+      inputSchema: {
+        type: 'object',
+        properties: {
+          shop_id: {
+            type: 'string',
+            description: 'The UUID of the ShopRenter store from the stores table'
+          },
+          page: {
+            type: 'number',
+            description: 'Page number for pagination (default: 1)'
+          },
+          limit: {
+            type: 'number',
+            description: 'Number of orders per page (default: 25, max: 100)'
+          }
+        },
+        required: ['shop_id']
+      }
+    },
+    {
+      name: 'shoprenter_list_customers',
+      description: 'List customers from a ShopRenter store. Returns customer details including contact info and order history.',
+      inputSchema: {
+        type: 'object',
+        properties: {
+          shop_id: {
+            type: 'string',
+            description: 'The UUID of the ShopRenter store from the stores table'
+          },
+          page: {
+            type: 'number',
+            description: 'Page number for pagination (default: 1)'
+          },
+          limit: {
+            type: 'number',
+            description: 'Number of customers per page (default: 25, max: 100)'
+          }
+        },
+        required: ['shop_id']
+      }
+    },
+    {
+      name: 'shoprenter_get_customer_orders',
+      description: 'Get all orders for a specific customer by customer email. Returns detailed order history.',
+      inputSchema: {
+        type: 'object',
+        properties: {
+          shop_id: {
+            type: 'string',
+            description: 'The UUID of the ShopRenter store from the stores table'
+          },
+          customer_email: {
+            type: 'string',
+            description: 'The customer email address'
+          }
+        },
+        required: ['shop_id', 'customer_email']
+      }
+    }
+  ]
+};
+
+/**
+ * Extract phone number from various ShopRenter structures
+ */
+function extractPhone(data: any): string | undefined {
+  if (data?.phone) return data.phone;
+  if (data?.billing_address?.phone) return data.billing_address.phone;
+  if (data?.shipping_address?.phone) return data.shipping_address.phone;
+  return undefined;
+}
+
+/**
+ * Convert ShopRenter customer to LLM-friendly format
+ */
+function formatCustomerForLlm(customer: ShopRenterCustomer): LlmCustomer {
+  return {
+    id: customer.id,
+    name: `${customer.firstname} ${customer.lastname}`.trim(),
+    email: customer.email,
+    phone: customer.phone || undefined
+  };
+}
+
+/**
+ * Convert ShopRenter order to LLM-friendly format
+ */
+function formatOrderForLlm(order: ShopRenterOrder): LlmOrder {
+  // Extract customer name and email
+  const customerName = order.customer_name ||
+    (order.customer ? `${order.customer.firstname || ''} ${order.customer.lastname || ''}`.trim() : 'Unknown');
+  const customerEmail = order.customer_email || order.customer?.email || '';
+  const customerPhone = order.customer_phone ||
+    order.customer?.phone ||
+    extractPhone(order.billing_address) ||
+    extractPhone(order.shipping_address);
+
+  return {
+    id: order.id,
+    orderNumber: order.order_number || order.number || order.id,
+    customer: {
+      name: customerName,
+      email: customerEmail,
+      phone: customerPhone
+    },
+    status: order.status,
+    total: order.total,
+    currency: order.currency || 'HUF',
+    items: (order.items || order.line_items || []).map((item: any) => ({
+      name: item.name || item.product_name || 'Unknown',
+      quantity: item.quantity || 1,
+      price: item.price || item.total || '0'
+    })),
+    createdAt: order.created_at || order.date_created || new Date().toISOString()
+  };
+}
+
+/**
+ * Fetch all orders with pagination
+ */
+async function fetchAllOrdersPages(
+  shop_id: string,
+  maxPages: number = 10
+): Promise<any[]> {
+  const allOrders: any[] = [];
+  let page = 1;
+  let hasMore = true;
+
+  while (hasMore && page <= maxPages) {
+    try {
+      const response = await fetchOrders(shop_id, page, 100);
+      const orders = Array.isArray(response) ? response : (response.data || response.orders || []);
+
+      if (orders.length === 0) {
+        hasMore = false;
+      } else {
+        allOrders.push(...orders);
+        hasMore = orders.length === 100; // If we got max results, there might be more
+        page++;
+      }
+    } catch (error) {
+      console.error(`[MCP ShopRenter] Error fetching page ${page}:`, error);
+      hasMore = false;
+    }
+  }
+
+  return allOrders;
+}
+
+/**
+ * Handle shoprenter_list_orders tool
+ */
+async function handleListOrders(args: Record<string, any>): Promise<Response> {
+  const { shop_id, page = 1, limit = 25 } = args;
+
+  // 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 createMcpErrorResponse('STORE_NOT_FOUND', 'ShopRenter store not found', 404);
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && !permissions.allow_order_access) {
+    return createMcpErrorResponse('PERMISSION_DENIED', 'Order access not allowed for this store', 403);
+  }
+
+  try {
+    // Fetch orders from ShopRenter API
+    const response = await fetchOrders(shop_id, page, Math.min(limit, 100));
+    const orders = Array.isArray(response) ? response : (response.data || response.orders || []);
+
+    // Format for LLM
+    const formattedOrders = orders.map(formatOrderForLlm);
+
+    return createMcpSuccessResponse({
+      count: formattedOrders.length,
+      page,
+      limit,
+      orders: formattedOrders
+    });
+  } catch (error) {
+    console.error('[MCP ShopRenter] Error fetching orders:', error);
+    return createMcpErrorResponse(
+      'FETCH_ERROR',
+      `Failed to fetch orders: ${error instanceof Error ? error.message : 'Unknown error'}`,
+      500
+    );
+  }
+}
+
+/**
+ * Handle shoprenter_list_customers tool
+ */
+async function handleListCustomers(args: Record<string, any>): Promise<Response> {
+  const { shop_id, page = 1, limit = 25 } = args;
+
+  // 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 createMcpErrorResponse('STORE_NOT_FOUND', 'ShopRenter store not found', 404);
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && !permissions.allow_customer_access) {
+    return createMcpErrorResponse('PERMISSION_DENIED', 'Customer access not allowed for this store', 403);
+  }
+
+  try {
+    // Fetch customers from ShopRenter API
+    const response = await fetchCustomers(shop_id, page, Math.min(limit, 100));
+    const customers = Array.isArray(response) ? response : (response.data || response.customers || []);
+
+    // Format for LLM
+    const formattedCustomers = customers.map(formatCustomerForLlm);
+
+    return createMcpSuccessResponse({
+      count: formattedCustomers.length,
+      page,
+      limit,
+      customers: formattedCustomers
+    });
+  } catch (error) {
+    console.error('[MCP ShopRenter] Error fetching customers:', error);
+    return createMcpErrorResponse(
+      'FETCH_ERROR',
+      `Failed to fetch customers: ${error instanceof Error ? error.message : 'Unknown error'}`,
+      500
+    );
+  }
+}
+
+/**
+ * Handle shoprenter_get_customer_orders tool
+ */
+async function handleGetCustomerOrders(args: Record<string, any>): Promise<Response> {
+  const { shop_id, customer_email } = args;
+
+  // 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 createMcpErrorResponse('STORE_NOT_FOUND', 'ShopRenter store not found', 404);
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && (!permissions.allow_order_access || !permissions.allow_customer_access)) {
+    return createMcpErrorResponse('PERMISSION_DENIED', 'Order and customer access required', 403);
+  }
+
+  try {
+    // Fetch orders and filter by customer email
+    const allOrders = await fetchAllOrdersPages(shop_id, 10);
+    const customerOrders = allOrders.filter(order => {
+      const orderEmail = order.customer_email || order.customer?.email || '';
+      return orderEmail.toLowerCase() === customer_email.toLowerCase();
+    });
+
+    // Format for LLM
+    const formattedOrders = customerOrders.map(formatOrderForLlm);
+
+    return createMcpSuccessResponse({
+      customerEmail: customer_email,
+      count: formattedOrders.length,
+      orders: formattedOrders
+    });
+  } catch (error) {
+    console.error('[MCP ShopRenter] Error fetching customer orders:', error);
+    return createMcpErrorResponse(
+      'FETCH_ERROR',
+      `Failed to fetch customer orders: ${error instanceof Error ? error.message : 'Unknown error'}`,
+      500
+    );
+  }
+}
+
+/**
+ * Main request handler
+ */
+serve(async (req: Request) => {
+  // CORS headers
+  if (req.method === 'OPTIONS') {
+    return new Response(null, {
+      headers: {
+        'Access-Control-Allow-Origin': '*',
+        'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
+        'Access-Control-Allow-Headers': 'Authorization, Content-Type'
+      }
+    });
+  }
+
+  const url = new URL(req.url);
+
+  // Handle /tools endpoint - list available tools
+  if (url.pathname.endsWith('/tools') && req.method === 'GET') {
+    return new Response(JSON.stringify(TOOLS), {
+      headers: {
+        'Content-Type': 'application/json',
+        'Access-Control-Allow-Origin': '*'
+      }
+    });
+  }
+
+  // Handle /call endpoint - execute tool
+  if (url.pathname.endsWith('/call') && req.method === 'POST') {
+    // Authenticate with internal API key
+    const authResult = await requireInternalApiKey(req, supabase, 'read_orders');
+
+    if (!authResult.valid) {
+      return createInternalApiKeyErrorResponse(authResult);
+    }
+
+    try {
+      const body: McpToolCallRequest = await req.json();
+      const { name, arguments: args } = body;
+
+      // Validate shop_id is provided
+      const validation = validateParams(args, ['shop_id']);
+      if (!validation.valid) {
+        return createMcpErrorResponse(
+          'MISSING_PARAMS',
+          `Missing required parameters: ${validation.missing?.join(', ')}`
+        );
+      }
+
+      // Route to appropriate handler
+      switch (name) {
+        case 'shoprenter_list_orders':
+          return await handleListOrders(args);
+
+        case 'shoprenter_list_customers':
+          return await handleListCustomers(args);
+
+        case 'shoprenter_get_customer_orders':
+          const customerValidation = validateParams(args, ['shop_id', 'customer_email']);
+          if (!customerValidation.valid) {
+            return createMcpErrorResponse(
+              'MISSING_PARAMS',
+              `Missing required parameters: ${customerValidation.missing?.join(', ')}`
+            );
+          }
+          return await handleGetCustomerOrders(args);
+
+        default:
+          return createMcpErrorResponse('UNKNOWN_TOOL', `Unknown tool: ${name}`, 404);
+      }
+    } catch (error) {
+      console.error('[MCP ShopRenter] Error handling request:', error);
+      return createMcpErrorResponse(
+        'INTERNAL_ERROR',
+        error instanceof Error ? error.message : 'Internal server error',
+        500
+      );
+    }
+  }
+
+  // 404 for unknown endpoints
+  return createMcpErrorResponse('NOT_FOUND', 'Endpoint not found. Use /tools or /call', 404);
+});

+ 402 - 0
supabase/functions/mcp-woocommerce/index.ts

@@ -0,0 +1,402 @@
+/**
+ * MCP HTTP Server for WooCommerce
+ *
+ * Provides MCP tools for accessing WooCommerce orders and customers data
+ * without storing personal information locally (GDPR compliance).
+ *
+ * Authentication: Internal API keys from internal_api_keys table
+ * Required parameter: shop_id (store ID from stores table)
+ */
+
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
+import {
+  requireInternalApiKey,
+  createInternalApiKeyErrorResponse
+} from '../_shared/internal-api-key-auth.ts';
+import {
+  fetchOrders,
+  fetchCustomers,
+  WooCommerceOrder,
+  WooCommerceCustomer
+} from '../_shared/woocommerce-client.ts';
+import {
+  McpToolsListResponse,
+  McpToolCallRequest,
+  LlmCustomer,
+  LlmOrder
+} from '../_shared/mcp-types.ts';
+import {
+  createMcpErrorResponse,
+  createMcpSuccessResponse,
+  validateParams
+} from '../_shared/mcp-helpers.ts';
+
+// Initialize Supabase client
+const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
+const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
+const supabase = createClient(supabaseUrl, supabaseServiceKey);
+
+// MCP Tool Definitions
+const TOOLS: McpToolsListResponse = {
+  tools: [
+    {
+      name: 'woocommerce_list_orders',
+      description: 'List orders from a WooCommerce store. Returns order details including customer info, items, status, and totals.',
+      inputSchema: {
+        type: 'object',
+        properties: {
+          shop_id: {
+            type: 'string',
+            description: 'The UUID of the WooCommerce store from the stores table'
+          },
+          status: {
+            type: 'string',
+            description: 'Filter by order status: any, pending, processing, on-hold, completed, cancelled, refunded, failed'
+          },
+          page: {
+            type: 'number',
+            description: 'Page number for pagination (default: 1)'
+          },
+          per_page: {
+            type: 'number',
+            description: 'Number of orders per page (default: 25, max: 100)'
+          }
+        },
+        required: ['shop_id']
+      }
+    },
+    {
+      name: 'woocommerce_list_customers',
+      description: 'List customers from a WooCommerce store. Returns customer details including contact info, order count, and total spent.',
+      inputSchema: {
+        type: 'object',
+        properties: {
+          shop_id: {
+            type: 'string',
+            description: 'The UUID of the WooCommerce store from the stores table'
+          },
+          page: {
+            type: 'number',
+            description: 'Page number for pagination (default: 1)'
+          },
+          per_page: {
+            type: 'number',
+            description: 'Number of customers per page (default: 25, max: 100)'
+          }
+        },
+        required: ['shop_id']
+      }
+    },
+    {
+      name: 'woocommerce_get_customer_orders',
+      description: 'Get all orders for a specific customer by customer email. Returns detailed order history.',
+      inputSchema: {
+        type: 'object',
+        properties: {
+          shop_id: {
+            type: 'string',
+            description: 'The UUID of the WooCommerce store from the stores table'
+          },
+          customer_email: {
+            type: 'string',
+            description: 'The customer email address'
+          }
+        },
+        required: ['shop_id', 'customer_email']
+      }
+    }
+  ]
+};
+
+/**
+ * Convert WooCommerce customer to LLM-friendly format
+ */
+function formatCustomerForLlm(customer: WooCommerceCustomer): LlmCustomer {
+  return {
+    id: customer.id.toString(),
+    name: `${customer.first_name} ${customer.last_name}`.trim(),
+    email: customer.email,
+    phone: customer.billing?.phone || undefined,
+    ordersCount: customer.orders_count,
+    totalSpent: customer.total_spent ? `${customer.total_spent}` : undefined
+  };
+}
+
+/**
+ * Convert WooCommerce order to LLM-friendly format
+ */
+function formatOrderForLlm(order: WooCommerceOrder): LlmOrder {
+  return {
+    id: order.id.toString(),
+    orderNumber: order.number,
+    customer: {
+      name: `${order.billing.first_name} ${order.billing.last_name}`.trim(),
+      email: order.billing.email,
+      phone: order.billing.phone || undefined
+    },
+    status: order.status,
+    total: order.total,
+    currency: order.currency,
+    items: order.line_items.map(item => ({
+      name: item.name,
+      quantity: item.quantity,
+      price: item.total
+    })),
+    createdAt: order.date_created
+  };
+}
+
+/**
+ * Fetch all orders with pagination
+ */
+async function fetchAllOrdersPages(
+  shop_id: string,
+  maxPages: number = 10,
+  status?: string
+): Promise<WooCommerceOrder[]> {
+  const allOrders: WooCommerceOrder[] = [];
+  let page = 1;
+  let hasMore = true;
+
+  while (hasMore && page <= maxPages) {
+    const orders = await fetchOrders(shop_id, page, 100, status);
+
+    if (orders.length === 0) {
+      hasMore = false;
+    } else {
+      allOrders.push(...orders);
+      hasMore = orders.length === 100; // If we got max results, there might be more
+      page++;
+    }
+  }
+
+  return allOrders;
+}
+
+/**
+ * Handle woocommerce_list_orders tool
+ */
+async function handleListOrders(args: Record<string, any>): Promise<Response> {
+  const { shop_id, status, page = 1, per_page = 25 } = args;
+
+  // 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 createMcpErrorResponse('STORE_NOT_FOUND', 'WooCommerce store not found', 404);
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && !permissions.allow_order_access) {
+    return createMcpErrorResponse('PERMISSION_DENIED', 'Order access not allowed for this store', 403);
+  }
+
+  try {
+    // Fetch orders from WooCommerce API
+    const orders = await fetchOrders(shop_id, page, Math.min(per_page, 100), status);
+
+    // Format for LLM
+    const formattedOrders = orders.map(formatOrderForLlm);
+
+    return createMcpSuccessResponse({
+      count: formattedOrders.length,
+      page,
+      per_page,
+      orders: formattedOrders
+    });
+  } catch (error) {
+    console.error('[MCP WooCommerce] Error fetching orders:', error);
+    return createMcpErrorResponse(
+      'FETCH_ERROR',
+      `Failed to fetch orders: ${error instanceof Error ? error.message : 'Unknown error'}`,
+      500
+    );
+  }
+}
+
+/**
+ * Handle woocommerce_list_customers tool
+ */
+async function handleListCustomers(args: Record<string, any>): Promise<Response> {
+  const { shop_id, page = 1, per_page = 25 } = args;
+
+  // 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 createMcpErrorResponse('STORE_NOT_FOUND', 'WooCommerce store not found', 404);
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && !permissions.allow_customer_access) {
+    return createMcpErrorResponse('PERMISSION_DENIED', 'Customer access not allowed for this store', 403);
+  }
+
+  try {
+    // Fetch customers from WooCommerce API
+    const customers = await fetchCustomers(shop_id, page, Math.min(per_page, 100));
+
+    // Format for LLM
+    const formattedCustomers = customers.map(formatCustomerForLlm);
+
+    return createMcpSuccessResponse({
+      count: formattedCustomers.length,
+      page,
+      per_page,
+      customers: formattedCustomers
+    });
+  } catch (error) {
+    console.error('[MCP WooCommerce] Error fetching customers:', error);
+    return createMcpErrorResponse(
+      'FETCH_ERROR',
+      `Failed to fetch customers: ${error instanceof Error ? error.message : 'Unknown error'}`,
+      500
+    );
+  }
+}
+
+/**
+ * Handle woocommerce_get_customer_orders tool
+ */
+async function handleGetCustomerOrders(args: Record<string, any>): Promise<Response> {
+  const { shop_id, customer_email } = args;
+
+  // 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 createMcpErrorResponse('STORE_NOT_FOUND', 'WooCommerce store not found', 404);
+  }
+
+  // Check permissions
+  const permissions = store.data_access_permissions as any;
+  if (permissions && (!permissions.allow_order_access || !permissions.allow_customer_access)) {
+    return createMcpErrorResponse('PERMISSION_DENIED', 'Order and customer access required', 403);
+  }
+
+  try {
+    // Fetch orders and filter by customer email
+    const allOrders = await fetchAllOrdersPages(shop_id, 10);
+    const customerOrders = allOrders.filter(
+      order => order.billing.email.toLowerCase() === customer_email.toLowerCase()
+    );
+
+    // Format for LLM
+    const formattedOrders = customerOrders.map(formatOrderForLlm);
+
+    return createMcpSuccessResponse({
+      customerEmail: customer_email,
+      count: formattedOrders.length,
+      orders: formattedOrders
+    });
+  } catch (error) {
+    console.error('[MCP WooCommerce] Error fetching customer orders:', error);
+    return createMcpErrorResponse(
+      'FETCH_ERROR',
+      `Failed to fetch customer orders: ${error instanceof Error ? error.message : 'Unknown error'}`,
+      500
+    );
+  }
+}
+
+/**
+ * Main request handler
+ */
+serve(async (req: Request) => {
+  // CORS headers
+  if (req.method === 'OPTIONS') {
+    return new Response(null, {
+      headers: {
+        'Access-Control-Allow-Origin': '*',
+        'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
+        'Access-Control-Allow-Headers': 'Authorization, Content-Type'
+      }
+    });
+  }
+
+  const url = new URL(req.url);
+
+  // Handle /tools endpoint - list available tools
+  if (url.pathname.endsWith('/tools') && req.method === 'GET') {
+    return new Response(JSON.stringify(TOOLS), {
+      headers: {
+        'Content-Type': 'application/json',
+        'Access-Control-Allow-Origin': '*'
+      }
+    });
+  }
+
+  // Handle /call endpoint - execute tool
+  if (url.pathname.endsWith('/call') && req.method === 'POST') {
+    // Authenticate with internal API key
+    const authResult = await requireInternalApiKey(req, supabase, 'read_orders');
+
+    if (!authResult.valid) {
+      return createInternalApiKeyErrorResponse(authResult);
+    }
+
+    try {
+      const body: McpToolCallRequest = await req.json();
+      const { name, arguments: args } = body;
+
+      // Validate shop_id is provided
+      const validation = validateParams(args, ['shop_id']);
+      if (!validation.valid) {
+        return createMcpErrorResponse(
+          'MISSING_PARAMS',
+          `Missing required parameters: ${validation.missing?.join(', ')}`
+        );
+      }
+
+      // Route to appropriate handler
+      switch (name) {
+        case 'woocommerce_list_orders':
+          return await handleListOrders(args);
+
+        case 'woocommerce_list_customers':
+          return await handleListCustomers(args);
+
+        case 'woocommerce_get_customer_orders':
+          const customerValidation = validateParams(args, ['shop_id', 'customer_email']);
+          if (!customerValidation.valid) {
+            return createMcpErrorResponse(
+              'MISSING_PARAMS',
+              `Missing required parameters: ${customerValidation.missing?.join(', ')}`
+            );
+          }
+          return await handleGetCustomerOrders(args);
+
+        default:
+          return createMcpErrorResponse('UNKNOWN_TOOL', `Unknown tool: ${name}`, 404);
+      }
+    } catch (error) {
+      console.error('[MCP WooCommerce] Error handling request:', error);
+      return createMcpErrorResponse(
+        'INTERNAL_ERROR',
+        error instanceof Error ? error.message : 'Internal server error',
+        500
+      );
+    }
+  }
+
+  // 404 for unknown endpoints
+  return createMcpErrorResponse('NOT_FOUND', 'Endpoint not found. Use /tools or /call', 404);
+});