Browse Source

feat: add 3-state access policy to all MCP tool handlers #95

- Updated WooCommerce MCP handlers (products, orders, customers)
- Updated Shopify MCP handlers (products, orders, customers)
- Updated ShopRenter MCP handlers (products, orders, customers)
- All handlers now support sync/api_only/not_allowed modes
- API-only mode fetches directly from platform APIs
- Sync mode uses Qdrant or SQL cache
- Not-allowed mode returns access denied error
- Added access_mode field to all responses
- Added notice field for api_only mode transparency
Claude 5 months ago
parent
commit
8121dd71da

+ 134 - 93
supabase/functions/mcp-shopify/index.ts

@@ -41,6 +41,17 @@ import {
   queryQdrantOrders,
   queryQdrantCustomers
 } from '../_shared/mcp-qdrant-helpers.ts';
+import {
+  getAccessPolicyConfig,
+  validateAccess,
+  isSyncAllowed,
+  isApiOnlyMode,
+  getDataTypePolicy,
+  createApiOnlyNotice
+} from '../_shared/access-policy-helpers.ts';
+import {
+  fetchProducts as fetchShopifyProducts
+} from '../_shared/shopify-client.ts';
 import {
   JsonRpcRequest,
   createSseResponse,
@@ -240,40 +251,68 @@ async function handleGetProducts(args: Record<string, any>): Promise<ToolCallRes
 
   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
-    };
+  // Check access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'products');
+  if (accessError) {
+    return accessError;
   }
 
-  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
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'products');
 
   try {
-    const qdrantConfig = await getStoreQdrantConfig(shop_id);
+    // Handle api_only mode: Direct API call
+    if (isApiOnlyMode(policy)) {
+      console.log('[MCP Shopify] Using direct API access (api_only mode)');
+
+      // Fetch from Shopify API directly
+      const apiProducts = await fetchShopifyProducts(shop_id, actualLimit);
+
+      let products = apiProducts;
+
+      // Apply client-side filters
+      if (sku) {
+        products = products.filter((p: any) =>
+          p.variants?.some((v: any) => v.sku === sku)
+        );
+      }
+
+      if (name) {
+        const nameLower = name.toLowerCase();
+        products = products.filter((p: any) =>
+          (p.title || '').toLowerCase().includes(nameLower)
+        );
+      }
+
+      if (status) {
+        products = products.filter((p: any) => p.status === status);
+      }
+
+      if (min_price !== undefined || max_price !== undefined) {
+        products = products.filter((p: any) => {
+          const price = parseFloat(p.variants?.[0]?.price || '0');
+          if (min_price !== undefined && price < min_price) return false;
+          if (max_price !== undefined && price > max_price) return false;
+          return true;
+        });
+      }
+
+      // Limit results
+      products = products.slice(0, actualLimit);
 
-    if (qdrantConfig && qdrantConfig.enabled && qdrantConfig.syncProducts) {
-      const products = await queryQdrantProducts(
-        shop_id,
-        qdrantConfig.shopname,
-        { sku, name, status, minPrice: min_price, maxPrice: max_price },
-        actualLimit
-      );
+      // Format for LLM
+      const formattedProducts: LlmProduct[] = products.map((p: any) => ({
+        id: p.id.toString(),
+        name: p.title,
+        sku: p.variants?.[0]?.sku || undefined,
+        price: p.variants?.[0]?.price || undefined,
+        currency: undefined,
+        status: p.status || undefined,
+        description: p.body_html || undefined,
+        tags: p.tags || undefined
+      }));
 
-      const cleanedProducts = cleanResponseData(products);
+      const cleanedProducts = cleanResponseData(formattedProducts);
 
       return {
         content: [{
@@ -281,12 +320,42 @@ async function handleGetProducts(args: Record<string, any>): Promise<ToolCallRes
           text: JSON.stringify({
             count: products.length,
             limit: actualLimit,
-            source: 'qdrant',
+            source: 'api_direct',
+            access_mode: 'api_only',
+            notice: createApiOnlyNotice('products'),
             products: cleanedProducts
           })
         }]
       };
-    } else {
+    }
+
+    // Handle sync mode: Use Qdrant or SQL cache
+    if (isSyncAllowed(policy)) {
+      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',
+              access_mode: 'sync',
+              products: cleanedProducts
+            })
+          }]
+        };
+      } else {
       let query = supabase
         .from('shopify_products_cache')
         .select('*')
@@ -348,11 +417,22 @@ async function handleGetProducts(args: Record<string, any>): Promise<ToolCallRes
             count: formattedProducts.length,
             limit: actualLimit,
             source: 'sql_cache',
+            access_mode: 'sync',
             products: cleanedProducts
           })
         }]
       };
+      }
     }
+
+    // Fallback (should not reach here)
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({ error: 'Invalid access policy configuration' })
+      }],
+      isError: true
+    };
   } catch (error) {
     return {
       content: [{
@@ -383,29 +463,14 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
     };
   }
 
-  // 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 access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'orders');
+  if (accessError) {
+    return accessError;
   }
 
-  // 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
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'orders');
 
   try {
     let order: ShopifyOrder | null = null;
@@ -441,6 +506,8 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
       content: [{
         type: 'text',
         text: JSON.stringify({
+          access_mode: policy,
+          notice: isApiOnlyMode(policy) ? createApiOnlyNotice('orders') : undefined,
           order: cleanedOrder
         })
       }]
@@ -492,29 +559,14 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
   // Enforce limit constraints
   const actualLimit = Math.min(Math.max(1, limit), 20);
 
-  // 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 access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'orders');
+  if (accessError) {
+    return accessError;
   }
 
-  // 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
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'orders');
 
   try {
     // Build filters
@@ -544,6 +596,8 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
           count: formattedOrders.length,
           limit: actualLimit,
           total_available: allOrders.length,
+          access_mode: policy,
+          notice: isApiOnlyMode(policy) ? createApiOnlyNotice('orders') : undefined,
           filters_applied: {
             created_at_min,
             created_at_max,
@@ -590,29 +644,14 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
     };
   }
 
-  // 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 access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'customers');
+  if (accessError) {
+    return accessError;
   }
 
-  // Check permissions
-  const permissions = store.data_access_permissions as any;
-  if (permissions && !permissions.allow_customer_access) {
-    return {
-      content: [{ type: 'text', text: JSON.stringify({ error: 'Customer access not allowed for this store' }) }],
-      isError: true
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'customers');
 
   try {
     // Fetch customer by email from Shopify API
@@ -638,6 +677,8 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
       content: [{
         type: 'text',
         text: JSON.stringify({
+          access_mode: policy,
+          notice: isApiOnlyMode(policy) ? createApiOnlyNotice('customers') : undefined,
           customer: cleanedCustomer
         })
       }]

+ 153 - 126
supabase/functions/mcp-shoprenter/index.ts

@@ -19,6 +19,7 @@ import {
   fetchOrders,
   fetchOrder,
   fetchCustomers,
+  fetchProducts as fetchShopRenterProducts,
   ShopRenterOrder,
   ShopRenterCustomer,
   ShopRenterOrderFilters,
@@ -42,6 +43,14 @@ import {
   queryQdrantOrders,
   queryQdrantCustomers
 } from '../_shared/mcp-qdrant-helpers.ts';
+import {
+  getAccessPolicyConfig,
+  validateAccess,
+  isSyncAllowed,
+  isApiOnlyMode,
+  getDataTypePolicy,
+  createApiOnlyNotice
+} from '../_shared/access-policy-helpers.ts';
 import {
   JsonRpcRequest,
   createSseResponse,
@@ -293,50 +302,64 @@ async function handleGetProducts(args: Record<string, any>): Promise<ToolCallRes
   // Enforce limit constraints
   const actualLimit = Math.min(Math.max(1, limit), 20);
 
-  // Validate shop exists and is ShopRenter
-  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', 'shoprenter')
-    .single();
-
-  if (storeError || !store) {
-    console.error('[MCP ShopRenter] Store not found:', { shop_id, error: storeError });
-    return {
-      content: [{ type: 'text', text: JSON.stringify({ error: 'ShopRenter store not found' }) }],
-      isError: true
-    };
+  // Check access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'products');
+  if (accessError) {
+    return accessError;
   }
 
-  console.log('[MCP ShopRenter] Store found:', { id: store.id, store_name: store.store_name });
-
-  // Check permissions
-  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
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'products');
 
   try {
-    // Check if Qdrant is enabled for this store
-    const qdrantConfig = await getStoreQdrantConfig(shop_id);
+    // Handle api_only mode: Direct API call
+    if (isApiOnlyMode(policy)) {
+      console.log('[MCP ShopRenter] Using direct API access (api_only mode)');
 
-    if (qdrantConfig && qdrantConfig.enabled && qdrantConfig.syncProducts) {
-      console.log('[MCP ShopRenter] Using Qdrant for products');
+      // Fetch from ShopRenter API directly
+      const apiProducts = await fetchShopRenterProducts(shop_id, 0, actualLimit);
+      let products = Array.isArray(apiProducts) ? apiProducts : (apiProducts.items || apiProducts.data || []);
 
-      // Query from Qdrant
-      const products = await queryQdrantProducts(
-        shop_id,
-        qdrantConfig.shopname,
-        { sku, name, status, minPrice: min_price, maxPrice: max_price },
-        actualLimit
-      );
+      // Apply client-side filters
+      if (sku) {
+        products = products.filter((p: any) => p.sku === sku);
+      }
 
-      // Clean response data
-      const cleanedProducts = cleanResponseData(products);
+      if (name) {
+        const nameLower = name.toLowerCase();
+        products = products.filter((p: any) =>
+          (p.name || p.title || '').toLowerCase().includes(nameLower)
+        );
+      }
+
+      if (status) {
+        products = products.filter((p: any) => p.status === status);
+      }
+
+      if (min_price !== undefined) {
+        products = products.filter((p: any) => (p.price || 0) >= min_price);
+      }
+
+      if (max_price !== undefined) {
+        products = products.filter((p: any) => (p.price || 0) <= max_price);
+      }
+
+      // Limit results
+      products = products.slice(0, actualLimit);
+
+      // Format for LLM
+      const formattedProducts: LlmProduct[] = products.map((p: any) => ({
+        id: p.id || p.product_id,
+        name: p.name || p.title,
+        sku: p.sku || undefined,
+        price: p.price ? p.price.toString() : undefined,
+        currency: p.currency || undefined,
+        status: p.status || undefined,
+        description: p.description || undefined,
+        tags: p.tags || undefined
+      }));
+
+      const cleanedProducts = cleanResponseData(formattedProducts);
 
       return {
         content: [{
@@ -344,13 +367,48 @@ async function handleGetProducts(args: Record<string, any>): Promise<ToolCallRes
           text: JSON.stringify({
             count: products.length,
             limit: actualLimit,
-            source: 'qdrant',
+            source: 'api_direct',
+            access_mode: 'api_only',
+            notice: createApiOnlyNotice('products'),
             products: cleanedProducts
           })
         }]
       };
-    } else {
-      console.log('[MCP ShopRenter] Qdrant not enabled, using SQL cache');
+    }
+
+    // Handle sync mode: Use Qdrant or SQL cache
+    if (isSyncAllowed(policy)) {
+      // Check if Qdrant is enabled for this store
+      const qdrantConfig = await getStoreQdrantConfig(shop_id);
+
+      if (qdrantConfig && qdrantConfig.enabled && qdrantConfig.syncProducts) {
+        console.log('[MCP ShopRenter] Using Qdrant for products');
+
+        // Query from Qdrant
+        const products = await queryQdrantProducts(
+          shop_id,
+          qdrantConfig.shopname,
+          { sku, name, status, minPrice: min_price, maxPrice: max_price },
+          actualLimit
+        );
+
+        // Clean response data
+        const cleanedProducts = cleanResponseData(products);
+
+        return {
+          content: [{
+            type: 'text',
+            text: JSON.stringify({
+              count: products.length,
+              limit: actualLimit,
+              source: 'qdrant',
+              access_mode: 'sync',
+              products: cleanedProducts
+            })
+          }]
+        };
+      } else {
+        console.log('[MCP ShopRenter] Qdrant not enabled, using SQL cache');
 
       // Fallback: Query from SQL cache (shoprenter_products_cache)
       let query = supabase
@@ -422,21 +480,32 @@ async function handleGetProducts(args: Record<string, any>): Promise<ToolCallRes
         tags: p.tags || undefined
       }));
 
-      // Clean response data
-      const cleanedProducts = cleanResponseData(formattedProducts);
+        // Clean response data
+        const cleanedProducts = cleanResponseData(formattedProducts);
 
-      return {
-        content: [{
-          type: 'text',
-          text: JSON.stringify({
-            count: formattedProducts.length,
-            limit: actualLimit,
-            source: 'sql_cache',
-            products: cleanedProducts
-          })
-        }]
-      };
+        return {
+          content: [{
+            type: 'text',
+            text: JSON.stringify({
+              count: formattedProducts.length,
+              limit: actualLimit,
+              source: 'sql_cache',
+              access_mode: 'sync',
+              products: cleanedProducts
+            })
+          }]
+        };
+      }
     }
+
+    // Fallback (should not reach here)
+    return {
+      content: [{
+        type: 'text',
+        text: JSON.stringify({ error: 'Invalid access policy configuration' })
+      }],
+      isError: true
+    };
   } catch (error) {
     console.error('[MCP ShopRenter] Error fetching products:', error);
     return {
@@ -500,32 +569,14 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
     };
   }
 
-  // Validate shop exists and is ShopRenter
-  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', 'shoprenter')
-    .single();
-
-  if (storeError || !store) {
-    console.error('[MCP ShopRenter] Store not found:', { shop_id, error: storeError });
-    return {
-      content: [{ type: 'text', text: JSON.stringify({ error: 'ShopRenter store not found' }) }],
-      isError: true
-    };
+  // Check access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'orders');
+  if (accessError) {
+    return accessError;
   }
 
-  console.log('[MCP ShopRenter] Store found:', { id: store.id, store_name: store.store_name });
-
-  // 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
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'orders');
 
   try {
     // Check if Qdrant is enabled for this store
@@ -566,7 +617,9 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
         content: [{
           type: 'text',
           text: JSON.stringify({
-            source: 'qdrant',
+            source: isApiOnlyMode(policy) ? 'api_direct' : 'qdrant',
+            access_mode: policy,
+            notice: isApiOnlyMode(policy) ? createApiOnlyNotice('orders') : undefined,
             order: cleanedOrder
           })
         }]
@@ -608,7 +661,9 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
         content: [{
           type: 'text',
           text: JSON.stringify({
-            source: 'api',
+            source: isApiOnlyMode(policy) ? 'api_direct' : 'api',
+            access_mode: policy,
+            notice: isApiOnlyMode(policy) ? createApiOnlyNotice('orders') : undefined,
             order: cleanedOrder
           })
         }]
@@ -662,32 +717,14 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
   // Enforce limit constraints
   const actualLimit = Math.min(Math.max(1, limit), 20);
 
-  // Validate shop exists and is ShopRenter
-  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', 'shoprenter')
-    .single();
-
-  if (storeError || !store) {
-    console.error('[MCP ShopRenter] Store not found:', { shop_id, error: storeError });
-    return {
-      content: [{ type: 'text', text: JSON.stringify({ error: 'ShopRenter store not found' }) }],
-      isError: true
-    };
+  // Check access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'orders');
+  if (accessError) {
+    return accessError;
   }
 
-  console.log('[MCP ShopRenter] Store found:', { id: store.id, store_name: store.store_name });
-
-  // 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
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'orders');
 
   try {
     // Check if Qdrant is enabled for this store
@@ -752,6 +789,7 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
             count: filteredOrders.length,
             limit: actualLimit,
             source: 'qdrant',
+            access_mode: 'sync',
             filters_applied: {
               createdAtMin,
               createdAtMax,
@@ -797,7 +835,9 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
           text: JSON.stringify({
             count: formattedOrders.length,
             limit: actualLimit,
-            source: 'api',
+            source: isApiOnlyMode(policy) ? 'api_direct' : 'api',
+            access_mode: policy,
+            notice: isApiOnlyMode(policy) ? createApiOnlyNotice('orders') : undefined,
             filters_applied: {
               createdAtMin,
               createdAtMax,
@@ -844,29 +884,14 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
     };
   }
 
-  // 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 access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'customers');
+  if (accessError) {
+    return accessError;
   }
 
-  // Check permissions
-  const permissions = store.data_access_permissions as any;
-  if (permissions && !permissions.allow_customer_access) {
-    return {
-      content: [{ type: 'text', text: JSON.stringify({ error: 'Customer access not allowed for this store' }) }],
-      isError: true
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'customers');
 
   try {
     // Fetch customer by email from ShopRenter API
@@ -895,7 +920,9 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
       content: [{
         type: 'text',
         text: JSON.stringify({
-          source: 'api',
+          source: isApiOnlyMode(policy) ? 'api_direct' : 'api',
+          access_mode: policy,
+          notice: isApiOnlyMode(policy) ? createApiOnlyNotice('customers') : undefined,
           customer: cleanedCustomer
         })
       }]

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

@@ -477,32 +477,17 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
     };
   }
 
-  // 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 access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'orders');
+  if (accessError) {
+    return accessError;
   }
 
-  // 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
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'orders');
 
   try {
-    // Fetch the specific order
+    // Always fetch from API (no cache for single order lookups)
     const order = await fetchOrder(shop_id, order_id);
 
     // Format for LLM
@@ -513,6 +498,8 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
       content: [{
         type: 'text',
         text: JSON.stringify({
+          access_mode: policy,
+          notice: isApiOnlyMode(policy) ? createApiOnlyNotice('orders') : undefined,
           order: cleanedOrder
         })
       }]
@@ -564,29 +551,14 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
   // Enforce limit constraints
   const actualLimit = Math.min(Math.max(1, limit), 20);
 
-  // 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 access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'orders');
+  if (accessError) {
+    return accessError;
   }
 
-  // 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
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'orders');
 
   try {
     // Build filters
@@ -703,6 +675,9 @@ async function handleListOrders(args: Record<string, any>): Promise<ToolCallResu
         text: JSON.stringify({
           count: formattedOrders.length,
           limit: actualLimit,
+          source: isApiOnlyMode(policy) ? 'api_direct' : 'api',
+          access_mode: policy,
+          notice: isApiOnlyMode(policy) ? createApiOnlyNotice('orders') : undefined,
           filters_applied: {
             created_after,
             created_before,
@@ -749,29 +724,14 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
     };
   }
 
-  // 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 access policy
+  const accessConfig = await getAccessPolicyConfig(shop_id);
+  const accessError = validateAccess(accessConfig, 'customers');
+  if (accessError) {
+    return accessError;
   }
 
-  // Check permissions
-  const permissions = store.data_access_permissions as any;
-  if (permissions && !permissions.allow_customer_access) {
-    return {
-      content: [{ type: 'text', text: JSON.stringify({ error: 'Customer access not allowed for this store' }) }],
-      isError: true
-    };
-  }
+  const policy = getDataTypePolicy(accessConfig!, 'customers');
 
   try {
     // Fetch customer by email from WooCommerce API
@@ -797,6 +757,8 @@ async function handleGetCustomer(args: Record<string, any>): Promise<ToolCallRes
       content: [{
         type: 'text',
         text: JSON.stringify({
+          access_mode: policy,
+          notice: isApiOnlyMode(policy) ? createApiOnlyNotice('customers') : undefined,
           customer: cleanedCustomer
         })
       }]