UNPKG

@agentman/chat-widget

Version:

Agentman Chat Widget for easy integration with web applications

474 lines (388 loc) 13.1 kB
# Shopify MCP Integration ## Overview This document describes how to handle Shopify-specific MCP responses, optimize large product payloads, and create rich product display components while maintaining minimal LLM token usage. ## Handling Large Product Payloads ### The Problem Shopify MCP servers can return hundreds of products with rich data: - Full product descriptions (500+ words each) - Multiple images per product (5-10 URLs) - Variant information (size, color, SKU) - Inventory data - SEO metadata - Reviews and ratings Sending all this to an LLM would: - Cost $0.50-$2.00 per request in tokens - Cause slow response times - Hit token limits quickly ### The Solution: Three-Tier Data Separation ```typescript interface ShopifyMCPResponse extends MCPResponse { uiType: 'shopify-products' | 'shopify-cart' | 'shopify-order'; // Tier 1: Minimal data for LLM (5-10 products max) structuredContent: { query: string; totalProducts: number; priceRange: { min: number; max: number; currency: string; }; categories: string[]; topProducts: Array<{ id: string; title: string; price: number; category: string; }>; summary: string; // "Found 47 blue shirts ranging from $19-$89" }; // Tier 2: Optional narrative (rarely needed) content?: Array<{ type: 'text'; text: string; }>; // Tier 3: Full data for UI (never sent to LLM) _meta: { products: DetailedProduct[]; filters: FilterOptions; sorting: SortOptions; pagination: PaginationInfo; searchMetadata: SearchMetadata; }; } ``` ## Payload Optimization Strategies ### 1. Smart Data Reduction ```typescript class ShopifyPayloadOptimizer { optimizeForLLM(products: ShopifyProduct[]): any { // Only send top 5 products to LLM const topProducts = this.selectTopProducts(products, 5); return { structuredContent: { totalProducts: products.length, priceRange: this.calculatePriceRange(products), categories: this.extractCategories(products), brands: this.extractBrands(products), // Minimal product data topProducts: topProducts.map(p => ({ id: p.id, title: this.truncateTitle(p.title, 50), price: p.price, category: p.category, inStock: p.inventory > 0 })), // Statistical summary instead of full data stats: { averagePrice: this.calculateAverage(products.map(p => p.price)), averageRating: this.calculateAverage(products.map(p => p.rating || 0)), inStockCount: products.filter(p => p.inventory > 0).length, onSaleCount: products.filter(p => p.compareAtPrice > p.price).length } } }; } private selectTopProducts(products: ShopifyProduct[], count: number): ShopifyProduct[] { // Select products based on relevance, popularity, or other criteria return products .sort((a, b) => { // Prioritize: in-stock, highly rated, best sellers const scoreA = this.calculateProductScore(a); const scoreB = this.calculateProductScore(b); return scoreB - scoreA; }) .slice(0, count); } private calculateProductScore(product: ShopifyProduct): number { let score = 0; // In stock bonus if (product.inventory > 0) score += 10; // Rating bonus score += (product.rating || 0) * 2; // Review count bonus (popularity) score += Math.min(product.reviewCount || 0, 100) / 10; // On sale bonus if (product.compareAtPrice > product.price) score += 5; // Best seller bonus if (product.tags?.includes('bestseller')) score += 15; return score; } } ``` ### 2. Progressive Loading Strategy ```typescript class ProgressiveProductLoader { private loadedProducts: Map<string, ShopifyProduct> = new Map(); async loadProducts(response: ShopifyMCPResponse): Promise<void> { // 1. Immediately render skeleton UI this.renderSkeleton(response.structuredContent.totalProducts); // 2. Load and render first batch (visible products) const firstBatch = response._meta.products.slice(0, 12); await this.renderBatch(firstBatch); // 3. Lazy load remaining products as user scrolls const remaining = response._meta.products.slice(12); this.queueLazyLoad(remaining); // 4. Preload images for better performance this.preloadImages(firstBatch); } private renderSkeleton(count: number): void { const container = document.getElementById('products-container'); container!.innerHTML = Array(Math.min(count, 12)) .fill(0) .map(() => ` <div class="product-skeleton"> <div class="skeleton-image"></div> <div class="skeleton-title"></div> <div class="skeleton-price"></div> </div> `) .join(''); } private async renderBatch(products: ShopifyProduct[]): Promise<void> { for (const product of products) { const card = new ShopifyProductCard({ data: product }); const element = card.render(); // Replace skeleton with actual product const skeleton = document.querySelector('.product-skeleton'); if (skeleton) { skeleton.replaceWith(element); } // Store for quick access this.loadedProducts.set(product.id, product); // Small delay for smooth rendering await this.delay(50); } } private queueLazyLoad(products: ShopifyProduct[]): void { const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { this.loadNextBatch(); } }); }); // Observe scroll trigger element const trigger = document.getElementById('load-more-trigger'); if (trigger) observer.observe(trigger); } private preloadImages(products: ShopifyProduct[]): void { products.forEach(product => { if (product.image) { const img = new Image(); img.src = product.image; } }); } } ``` ## Shopify-Specific Response Types ### Cart Response ```typescript interface ShopifyCartResponse extends MCPResponse { uiType: 'shopify-cart'; structuredContent: { itemCount: number; subtotal: number; currency: string; hasDiscounts: boolean; }; _meta: { items: CartItem[]; totals: CartTotals; discounts: Discount[]; checkoutUrl: string; abandonedCartRecovery?: { email?: string; recoveryUrl: string; }; }; } ``` ### Collection Response ```typescript interface ShopifyCollectionResponse extends MCPResponse { uiType: 'shopify-collection'; structuredContent: { collectionName: string; productCount: number; description: string; }; _meta: { collection: { id: string; handle: string; title: string; description: string; image: string; rules?: CollectionRule[]; }; products: ShopifyProduct[]; seo: SEOData; }; } ``` ## Implementation Guide ### 1. MCP Response Handler for Shopify ```typescript class ShopifyMCPHandler { async handleShopifyResponse(response: MCPResponse): Promise<void> { switch (response.uiType) { case 'shopify-products': await this.handleProductsResponse(response as ShopifyProductsResponse); break; case 'shopify-cart': await this.handleCartResponse(response as ShopifyCartResponse); break; case 'shopify-collection': await this.handleCollectionResponse(response as ShopifyCollectionResponse); break; default: console.warn('Unknown Shopify response type:', response.uiType); } } private async handleProductsResponse(response: ShopifyProductsResponse): Promise<void> { // 1. Check if should go to LLM if (this.shouldSendToLLM(response)) { // Send only structuredContent await this.sendToLLM(response.structuredContent); } // 2. Always render the UI from _meta const grid = new ShopifyProductGrid({ data: response._meta, callbacks: { onAction: this.handleProductAction.bind(this) } }); const element = grid.render(); this.chatWidget.addComponent(element); } private shouldSendToLLM(response: ShopifyProductsResponse): boolean { // Don't send to LLM if: // - User explicitly asked for product display // - Less than 10 products // - Simple browsing query if (response.structuredContent.totalProducts <= 10) return false; if (this.isSimpleBrowsingQuery()) return false; if (this.userWantsDirectDisplay()) return false; return true; } } ``` ### 2. Shopify Action Handlers ```typescript class ShopifyActionHandler { async handleAddToCart(data: AddToCartData): Promise<void> { // 1. Optimistically update UI this.updateCartUI(data); // 2. Call Shopify API try { const result = await this.shopifyAPI.addToCart({ variantId: data.variantId, quantity: data.quantity }); // 3. Update with actual cart data this.syncCartUI(result.cart); // 4. Show success message this.showNotification('Added to cart!', 'success'); } catch (error) { // Revert optimistic update this.revertCartUI(); this.showNotification('Failed to add to cart', 'error'); } } async handleQuickBuy(data: QuickBuyData): Promise<void> { // Show quick buy modal with payment form const modal = new QuickBuyModal({ product: data.product, variant: data.variant, onSubmit: async (paymentData) => { await this.processQuickBuy(paymentData); } }); modal.show(); } async handleProductFilter(filters: FilterData): Promise<void> { // 1. Apply filters locally if possible const filtered = this.applyLocalFilters(this.cachedProducts, filters); if (filtered) { // Update UI immediately this.updateProductGrid(filtered); } else { // Need server filtering const response = await this.mcpClient.callTool('filter_products', filters); this.handleShopifyResponse(response); } } } ``` ### 3. Performance Monitoring ```typescript class ShopifyPerformanceMonitor { private metrics: PerformanceMetrics = { tokensSaved: 0, apiCallsReduced: 0, loadTimeImproved: 0 }; trackResponse(response: ShopifyMCPResponse): void { // Calculate tokens saved const fullTokens = this.estimateTokens(response._meta); const reducedTokens = this.estimateTokens(response.structuredContent); this.metrics.tokensSaved += (fullTokens - reducedTokens); // Track API call reduction through caching if (this.servedFromCache(response)) { this.metrics.apiCallsReduced++; } // Log metrics console.log('Shopify Performance Metrics:', { tokensSaved: this.metrics.tokensSaved, costSaved: `$${(this.metrics.tokensSaved * 0.00002).toFixed(2)}`, apiCallsReduced: this.metrics.apiCallsReduced }); } private estimateTokens(data: any): number { // Rough estimation: 1 token ≈ 4 characters const jsonString = JSON.stringify(data); return Math.ceil(jsonString.length / 4); } } ``` ## Best Practices 1. **Always separate data into three tiers** (structuredContent, content, _meta) 2. **Never send full product descriptions to LLM** - use summaries 3. **Implement progressive loading** for large product sets 4. **Cache product data aggressively** to reduce API calls 5. **Use skeleton UI** for perceived performance 6. **Preload images** for visible products 7. **Implement local filtering** when possible 8. **Track performance metrics** to measure improvements ## Testing Shopify Components ```typescript describe('ShopifyProductCard', () => { test('should render product with variants', () => { const product = mockShopifyProduct(); const card = new ShopifyProductCard({ data: product }); const element = card.render(); expect(element.querySelector('.product-card__title')).toHaveTextContent(product.title); expect(element.querySelectorAll('.variant-option')).toHaveLength(product.variants.length); }); test('should handle add to cart action', async () => { const product = mockShopifyProduct(); const onAction = jest.fn(); const card = new ShopifyProductCard({ data: product, callbacks: { onAction } }); const element = card.render(); const addToCartBtn = element.querySelector('.btn-add-to-cart'); addToCartBtn.click(); expect(onAction).toHaveBeenCalledWith('add-to-cart', expect.objectContaining({ productId: product.id })); }); }); ``` ## Next Steps 1. See `ARCHITECTURE.md` for overall system design 2. Check `COMPONENT_REGISTRY.md` for component system 3. Review `ROUTING_LOGIC.md` for intelligent routing 4. Read `PERFORMANCE.md` for optimization strategies