|
|
@@ -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
|
|
|
})
|
|
|
}]
|