@agentman/chat-widget
Version:
Agentman Chat Widget for easy integration with web applications
474 lines (388 loc) • 13.1 kB
Markdown
# 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