|
|
@@ -27,13 +27,21 @@ import {
|
|
|
import {
|
|
|
McpTool,
|
|
|
LlmCustomer,
|
|
|
- LlmOrder
|
|
|
+ LlmOrder,
|
|
|
+ LlmProduct
|
|
|
} from '../_shared/mcp-types.ts';
|
|
|
import {
|
|
|
createMcpErrorResponse,
|
|
|
createMcpSuccessResponse,
|
|
|
- validateParams
|
|
|
+ validateParams,
|
|
|
+ cleanResponseData
|
|
|
} from '../_shared/mcp-helpers.ts';
|
|
|
+import {
|
|
|
+ getStoreQdrantConfig,
|
|
|
+ queryQdrantProducts,
|
|
|
+ queryQdrantOrders,
|
|
|
+ queryQdrantCustomers
|
|
|
+} from '../_shared/mcp-qdrant-helpers.ts';
|
|
|
import {
|
|
|
JsonRpcRequest,
|
|
|
createSseResponse,
|
|
|
@@ -56,10 +64,48 @@ const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
|
|
|
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
|
|
|
|
const SERVER_NAME = 'mcp-shoprenter';
|
|
|
-const SERVER_VERSION = '2.0.0';
|
|
|
+const SERVER_VERSION = '3.0.0';
|
|
|
|
|
|
// MCP Tool Definitions
|
|
|
const TOOLS: McpTool[] = [
|
|
|
+ {
|
|
|
+ name: 'shoprenter_get_products',
|
|
|
+ description: 'Get products from a ShopRenter store with filtering. Returns product catalog with details like name, SKU, price, and status. Use this to search for products or browse the catalog. Limited to 20 results maximum.',
|
|
|
+ inputSchema: {
|
|
|
+ type: 'object',
|
|
|
+ properties: {
|
|
|
+ shop_id: {
|
|
|
+ type: 'string',
|
|
|
+ description: 'The UUID of the ShopRenter store from the stores table'
|
|
|
+ },
|
|
|
+ sku: {
|
|
|
+ type: 'string',
|
|
|
+ description: 'Filter by product SKU (exact match)'
|
|
|
+ },
|
|
|
+ name: {
|
|
|
+ type: 'string',
|
|
|
+ description: 'Filter by product name (partial match, case-insensitive)'
|
|
|
+ },
|
|
|
+ status: {
|
|
|
+ type: 'string',
|
|
|
+ description: 'Filter by product status (e.g., active, draft)'
|
|
|
+ },
|
|
|
+ min_price: {
|
|
|
+ type: 'number',
|
|
|
+ description: 'Minimum price filter'
|
|
|
+ },
|
|
|
+ max_price: {
|
|
|
+ type: 'number',
|
|
|
+ description: 'Maximum price filter'
|
|
|
+ },
|
|
|
+ limit: {
|
|
|
+ type: 'number',
|
|
|
+ description: 'Number of products to return (default: 10, max: 20)'
|
|
|
+ }
|
|
|
+ },
|
|
|
+ required: ['shop_id']
|
|
|
+ }
|
|
|
+ },
|
|
|
{
|
|
|
name: 'shoprenter_get_order',
|
|
|
description: 'Get a specific order from a ShopRenter store by customer-visible order number (innerId). Returns complete order details including customer info, items, status, and totals. Use this when a customer provides their order number.',
|
|
|
@@ -183,6 +229,175 @@ function formatOrderForLlm(order: any): LlmOrder {
|
|
|
};
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Handle shoprenter_get_products tool
|
|
|
+ */
|
|
|
+async function handleGetProducts(args: Record<string, any>): Promise<ToolCallResult> {
|
|
|
+ const { shop_id, sku, name, status, min_price, max_price, limit = 10 } = args;
|
|
|
+
|
|
|
+ console.log('[MCP ShopRenter] handleGetProducts called with:', { shop_id, sku, name, status, min_price, max_price, limit });
|
|
|
+
|
|
|
+ // 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
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ 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
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 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',
|
|
|
+ products: cleanedProducts
|
|
|
+ })
|
|
|
+ }]
|
|
|
+ };
|
|
|
+ } else {
|
|
|
+ console.log('[MCP ShopRenter] Qdrant not enabled, using SQL cache');
|
|
|
+
|
|
|
+ // Fallback: Query from SQL cache (shoprenter_products_cache)
|
|
|
+ let query = supabase
|
|
|
+ .from('shoprenter_products_cache')
|
|
|
+ .select('product_data')
|
|
|
+ .eq('store_id', shop_id);
|
|
|
+
|
|
|
+ // Apply filters (limited by JSONB querying)
|
|
|
+ if (sku) {
|
|
|
+ query = query.eq('product_data->>sku', sku);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (status) {
|
|
|
+ query = query.eq('product_data->>status', status);
|
|
|
+ }
|
|
|
+
|
|
|
+ query = query.limit(actualLimit);
|
|
|
+
|
|
|
+ const { data: cachedProducts, error: cacheError } = await query;
|
|
|
+
|
|
|
+ if (cacheError) {
|
|
|
+ console.error('[MCP ShopRenter] Error querying SQL cache:', cacheError);
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({
|
|
|
+ error: `Failed to fetch products from cache: ${cacheError.message}`
|
|
|
+ })
|
|
|
+ }],
|
|
|
+ isError: true
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // Extract product data
|
|
|
+ let products = (cachedProducts || []).map((p: any) => p.product_data);
|
|
|
+
|
|
|
+ // Apply client-side filters
|
|
|
+ if (name) {
|
|
|
+ const nameLower = name.toLowerCase();
|
|
|
+ products = products.filter((p: any) =>
|
|
|
+ (p.name || p.title || '').toLowerCase().includes(nameLower)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ 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 after filtering
|
|
|
+ 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 || 'Unknown',
|
|
|
+ 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
|
|
|
+ }));
|
|
|
+
|
|
|
+ // Clean response data
|
|
|
+ const cleanedProducts = cleanResponseData(formattedProducts);
|
|
|
+
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({
|
|
|
+ count: formattedProducts.length,
|
|
|
+ limit: actualLimit,
|
|
|
+ source: 'sql_cache',
|
|
|
+ products: cleanedProducts
|
|
|
+ })
|
|
|
+ }]
|
|
|
+ };
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('[MCP ShopRenter] Error fetching products:', error);
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({
|
|
|
+ error: `Failed to fetch products: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
|
+ })
|
|
|
+ }],
|
|
|
+ isError: true
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Fetch all orders with pagination (for filtering by customer name)
|
|
|
*/
|
|
|
@@ -260,69 +475,99 @@ async function handleGetOrder(args: Record<string, any>): Promise<ToolCallResult
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- // Search for order by innerId (customer-visible order number)
|
|
|
- // The order_id parameter is actually the innerId from the customer's perspective
|
|
|
- console.log('[MCP ShopRenter] Fetching order with innerId:', order_id);
|
|
|
- const response = await fetchOrders(shop_id, 0, 1, { innerId: order_id });
|
|
|
-
|
|
|
- console.log('[MCP ShopRenter] Raw response type:', typeof response, Array.isArray(response) ? 'array' : '');
|
|
|
- console.log('[MCP ShopRenter] Response structure:', {
|
|
|
- isArray: Array.isArray(response),
|
|
|
- hasData: !!response?.data,
|
|
|
- hasOrders: !!response?.orders,
|
|
|
- responseKeys: response && typeof response === 'object' ? Object.keys(response) : []
|
|
|
- });
|
|
|
-
|
|
|
- const orders = Array.isArray(response) ? response : (response.items || response.data || response.orders || []);
|
|
|
+ // Check if Qdrant is enabled for this store
|
|
|
+ const qdrantConfig = await getStoreQdrantConfig(shop_id);
|
|
|
+
|
|
|
+ if (qdrantConfig && qdrantConfig.enabled && qdrantConfig.syncOrders) {
|
|
|
+ console.log('[MCP ShopRenter] Using Qdrant for order lookup');
|
|
|
+
|
|
|
+ // Query from Qdrant - scroll through orders to find matching order_id
|
|
|
+ const orders = await queryQdrantOrders(
|
|
|
+ shop_id,
|
|
|
+ qdrantConfig.shopname,
|
|
|
+ {},
|
|
|
+ 100 // Get more to search through
|
|
|
+ );
|
|
|
+
|
|
|
+ // Find order by innerId (order number)
|
|
|
+ const order = orders.find((o: any) =>
|
|
|
+ o.orderNumber === order_id || o.id === order_id
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!order) {
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({
|
|
|
+ error: `No order found with order number: ${order_id}`
|
|
|
+ })
|
|
|
+ }],
|
|
|
+ isError: true
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- console.log('[MCP ShopRenter] API response:', { ordersCount: orders.length });
|
|
|
+ // Clean response data
|
|
|
+ const cleanedOrder = cleanResponseData(order);
|
|
|
|
|
|
- if (orders.length === 0) {
|
|
|
- console.warn('[MCP ShopRenter] No order found with innerId:', order_id);
|
|
|
- console.log('[MCP ShopRenter] Full response for debugging:', JSON.stringify(response).substring(0, 500));
|
|
|
return {
|
|
|
content: [{
|
|
|
type: 'text',
|
|
|
text: JSON.stringify({
|
|
|
- error: `No order found with order number: ${order_id}`,
|
|
|
- debug: {
|
|
|
- shop_id,
|
|
|
- store_name: store.store_name,
|
|
|
- searched_innerId: order_id,
|
|
|
- responseType: typeof response,
|
|
|
- responseKeys: response && typeof response === 'object' ? Object.keys(response) : []
|
|
|
- }
|
|
|
+ source: 'qdrant',
|
|
|
+ order: cleanedOrder
|
|
|
})
|
|
|
- }],
|
|
|
- isError: true
|
|
|
+ }]
|
|
|
};
|
|
|
- }
|
|
|
+ } else {
|
|
|
+ console.log('[MCP ShopRenter] Qdrant not enabled, using ShopRenter API');
|
|
|
|
|
|
- console.log('[MCP ShopRenter] Order found:', { innerId: orders[0].innerId || orders[0].id });
|
|
|
+ // Search for order by innerId (customer-visible order number)
|
|
|
+ // The order_id parameter is actually the innerId from the customer's perspective
|
|
|
+ console.log('[MCP ShopRenter] Fetching order with innerId:', order_id);
|
|
|
+ const response = await fetchOrders(shop_id, 0, 1, { innerId: order_id });
|
|
|
|
|
|
- // Format for LLM
|
|
|
- const formattedOrder = formatOrderForLlm(orders[0]);
|
|
|
+ const orders = Array.isArray(response) ? response : (response.items || response.data || response.orders || []);
|
|
|
|
|
|
- return {
|
|
|
- content: [{
|
|
|
- type: 'text',
|
|
|
- text: JSON.stringify({
|
|
|
- order: formattedOrder
|
|
|
- })
|
|
|
- }]
|
|
|
- };
|
|
|
+ console.log('[MCP ShopRenter] API response:', { ordersCount: orders.length });
|
|
|
+
|
|
|
+ if (orders.length === 0) {
|
|
|
+ console.warn('[MCP ShopRenter] No order found with innerId:', order_id);
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({
|
|
|
+ error: `No order found with order number: ${order_id}`
|
|
|
+ })
|
|
|
+ }],
|
|
|
+ isError: true
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('[MCP ShopRenter] Order found:', { innerId: orders[0].innerId || orders[0].id });
|
|
|
+
|
|
|
+ // Format for LLM
|
|
|
+ const formattedOrder = formatOrderForLlm(orders[0]);
|
|
|
+
|
|
|
+ // Clean response data
|
|
|
+ const cleanedOrder = cleanResponseData(formattedOrder);
|
|
|
+
|
|
|
+ return {
|
|
|
+ content: [{
|
|
|
+ type: 'text',
|
|
|
+ text: JSON.stringify({
|
|
|
+ source: 'api',
|
|
|
+ order: cleanedOrder
|
|
|
+ })
|
|
|
+ }]
|
|
|
+ };
|
|
|
+ }
|
|
|
} 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'}`,
|
|
|
- debug: {
|
|
|
- shop_id,
|
|
|
- order_id,
|
|
|
- errorDetails: error instanceof Error ? error.stack : String(error)
|
|
|
- }
|
|
|
+ error: `Failed to fetch order: ${error instanceof Error ? error.message : 'Unknown error'}`
|
|
|
})
|
|
|
}],
|
|
|
isError: true
|
|
|
@@ -545,6 +790,9 @@ async function handleToolCall(params: ToolCallParams): Promise<ToolCallResult> {
|
|
|
|
|
|
// Route to appropriate handler
|
|
|
switch (name) {
|
|
|
+ case 'shoprenter_get_products':
|
|
|
+ return await handleGetProducts(args);
|
|
|
+
|
|
|
case 'shoprenter_get_order':
|
|
|
const orderValidation = validateParams(args, ['shop_id', 'order_id']);
|
|
|
if (!orderValidation.valid) {
|