@brainerce/mcp-server 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -685,20 +685,25 @@ Display: banners in header, badges on product cards (strikethrough + discounted
685
685
  function getRecommendationsSection() {
686
686
  return `## Product Recommendations (Cross-Sells & Upsells)
687
687
 
688
+ Recommendations come **embedded in the product response** \u2014 no extra API call needed for product pages.
689
+
688
690
  \`\`\`typescript
689
691
  import type { ProductRecommendation, ProductRecommendationsResponse, CartRecommendationsResponse } from 'brainerce';
690
692
 
691
- // Product page \u2014 cross-sells, upsells, related
692
- const recs: ProductRecommendationsResponse = await client.getProductRecommendations(productId);
693
- // recs.crossSells, recs.upsells, recs.related
693
+ // Product page \u2014 recommendations are embedded in the product response
694
+ const product = await client.getProductBySlug('some-slug');
695
+ const recs = (product as any).recommendations as ProductRecommendationsResponse | undefined;
696
+ // recs?.upsells \u2014 premium alternatives (show on product page)
697
+ // recs?.related \u2014 similar products (show at bottom of product page)
698
+ // recs?.crossSells \u2014 complementary products (typically used on cart page)
694
699
 
695
- // Cart page \u2014 cross-sell suggestions for cart items
700
+ // Cart page \u2014 cross-sell suggestions for cart items (separate call)
696
701
  const cartRecs: CartRecommendationsResponse = await client.getCartRecommendations(cartId, 4);
697
- // cartRecs.recommendations \u2014 deduplicated cross-sells
702
+ // cartRecs.recommendations \u2014 deduplicated cross-sells (excludes items already in cart)
698
703
  \`\`\`
699
704
 
700
705
  ProductRecommendation: \`id\`, \`name\`, \`slug\`, \`basePrice\`, \`salePrice\`, \`images\`, \`type\`, \`inventory\`, \`relationType\`.
701
- Display "You might also like" on product pages, "Recommended for you" in cart.`;
706
+ Display upsells + related on product pages, cross-sells on cart page.`;
702
707
  }
703
708
  function getInventorySection() {
704
709
  return `## Inventory, Stock Display & Reservation Countdown
@@ -2372,7 +2377,7 @@ export default function RootLayout({
2372
2377
  `,
2373
2378
  "src/app/products/page.tsx": `'use client';
2374
2379
 
2375
- import { Suspense, useEffect, useState, useCallback } from 'react';
2380
+ import { Suspense, useEffect, useState, useCallback, useRef } from 'react';
2376
2381
  import { useSearchParams, useRouter } from 'next/navigation';
2377
2382
  import type { Product } from 'brainerce';
2378
2383
  import type { ProductQueryParams } from 'brainerce';
@@ -2398,9 +2403,198 @@ const sortOptions: SortOption[] = [
2398
2403
  { labelKey: 'sortPriceHigh', sortBy: 'price', sortOrder: 'desc' },
2399
2404
  ];
2400
2405
 
2401
- interface CategoryFilter {
2406
+ interface CategoryNode {
2402
2407
  id: string;
2403
2408
  name: string;
2409
+ parentId?: string | null;
2410
+ children: CategoryNode[];
2411
+ }
2412
+
2413
+ /** Collect all descendant IDs (including self) */
2414
+ function getAllDescendantIds(node: CategoryNode): string[] {
2415
+ const ids = [node.id];
2416
+ for (const child of node.children) {
2417
+ ids.push(...getAllDescendantIds(child));
2418
+ }
2419
+ return ids;
2420
+ }
2421
+
2422
+ /** Check if a category or any of its descendants matches the selected ID */
2423
+ function isActiveInTree(node: CategoryNode, selectedId: string): boolean {
2424
+ if (node.id === selectedId) return true;
2425
+ return node.children.some((child) => isActiveInTree(child, selectedId));
2426
+ }
2427
+
2428
+ /** Chevron down SVG */
2429
+ function ChevronDown({ className }: { className?: string }) {
2430
+ return (
2431
+ <svg
2432
+ className={className}
2433
+ width="12"
2434
+ height="12"
2435
+ viewBox="0 0 12 12"
2436
+ fill="none"
2437
+ stroke="currentColor"
2438
+ strokeWidth="2"
2439
+ strokeLinecap="round"
2440
+ strokeLinejoin="round"
2441
+ >
2442
+ <path d="M3 4.5L6 7.5L9 4.5" />
2443
+ </svg>
2444
+ );
2445
+ }
2446
+
2447
+ /** Recursive dropdown items for nested categories */
2448
+ function CategoryDropdownItems({
2449
+ children,
2450
+ depth,
2451
+ selectedId,
2452
+ onSelect,
2453
+ }: {
2454
+ children: CategoryNode[];
2455
+ depth: number;
2456
+ selectedId: string;
2457
+ onSelect: (id: string) => void;
2458
+ }) {
2459
+ return (
2460
+ <>
2461
+ {children.map((child) => (
2462
+ <div key={child.id}>
2463
+ <button
2464
+ onClick={() => onSelect(child.id)}
2465
+ className={cn(
2466
+ 'w-full text-start px-4 py-2 text-sm transition-colors hover:bg-muted',
2467
+ selectedId === child.id && 'bg-primary/10 text-primary font-medium'
2468
+ )}
2469
+ style={{ paddingInlineStart: \`\${(depth + 1) * 16}px\` }}
2470
+ >
2471
+ {child.name}
2472
+ </button>
2473
+ {child.children.length > 0 && (
2474
+ <CategoryDropdownItems
2475
+ children={child.children}
2476
+ depth={depth + 1}
2477
+ selectedId={selectedId}
2478
+ onSelect={onSelect}
2479
+ />
2480
+ )}
2481
+ </div>
2482
+ ))}
2483
+ </>
2484
+ );
2485
+ }
2486
+
2487
+ /** Category chip with dropdown for subcategories */
2488
+ function CategoryChip({
2489
+ category,
2490
+ selectedId,
2491
+ onSelect,
2492
+ tc,
2493
+ }: {
2494
+ category: CategoryNode;
2495
+ selectedId: string;
2496
+ onSelect: (id: string) => void;
2497
+ tc: (key: string) => string;
2498
+ }) {
2499
+ const [open, setOpen] = useState(false);
2500
+ const ref = useRef<HTMLDivElement>(null);
2501
+ const hasChildren = category.children.length > 0;
2502
+ const isActive = isActiveInTree(category, selectedId);
2503
+
2504
+ // Find the display name for the selected subcategory
2505
+ function findName(nodes: CategoryNode[], id: string): string | null {
2506
+ for (const n of nodes) {
2507
+ if (n.id === id) return n.name;
2508
+ const found = findName(n.children, id);
2509
+ if (found) return found;
2510
+ }
2511
+ return null;
2512
+ }
2513
+
2514
+ const selectedChildName =
2515
+ isActive && selectedId !== category.id ? findName(category.children, selectedId) : null;
2516
+
2517
+ // Close dropdown on outside click
2518
+ useEffect(() => {
2519
+ if (!open) return;
2520
+ function handleClick(e: MouseEvent) {
2521
+ if (ref.current && !ref.current.contains(e.target as Node)) {
2522
+ setOpen(false);
2523
+ }
2524
+ }
2525
+ document.addEventListener('mousedown', handleClick);
2526
+ return () => document.removeEventListener('mousedown', handleClick);
2527
+ }, [open]);
2528
+
2529
+ if (!hasChildren) {
2530
+ return (
2531
+ <button
2532
+ onClick={() => onSelect(category.id)}
2533
+ className={cn(
2534
+ 'rounded-full border px-3 py-1.5 text-sm transition-colors',
2535
+ selectedId === category.id
2536
+ ? 'bg-primary text-primary-foreground border-primary'
2537
+ : 'border-border text-muted-foreground hover:border-primary hover:text-foreground'
2538
+ )}
2539
+ >
2540
+ {category.name}
2541
+ </button>
2542
+ );
2543
+ }
2544
+
2545
+ return (
2546
+ <div ref={ref} className="relative">
2547
+ <button
2548
+ onClick={() => setOpen((prev) => !prev)}
2549
+ className={cn(
2550
+ 'inline-flex items-center gap-1 rounded-full border px-3 py-1.5 text-sm transition-colors',
2551
+ isActive
2552
+ ? 'bg-primary text-primary-foreground border-primary'
2553
+ : 'border-border text-muted-foreground hover:border-primary hover:text-foreground'
2554
+ )}
2555
+ >
2556
+ {category.name}
2557
+ {selectedChildName && (
2558
+ <span className="opacity-80">
2559
+ {'\xB7'} {selectedChildName}
2560
+ </span>
2561
+ )}
2562
+ <ChevronDown
2563
+ className={cn('transition-transform ms-0.5', open && 'rotate-180')}
2564
+ />
2565
+ </button>
2566
+
2567
+ {open && (
2568
+ <div className="bg-background border-border absolute start-0 top-full z-50 mt-1 min-w-[180px] overflow-hidden rounded-lg border shadow-lg">
2569
+ {/* "All in [category]" option */}
2570
+ <button
2571
+ onClick={() => {
2572
+ onSelect(category.id);
2573
+ setOpen(false);
2574
+ }}
2575
+ className={cn(
2576
+ 'w-full text-start px-4 py-2 text-sm font-medium transition-colors hover:bg-muted',
2577
+ selectedId === category.id && 'bg-primary/10 text-primary'
2578
+ )}
2579
+ >
2580
+ {tc('all')} {category.name}
2581
+ </button>
2582
+ <div className="bg-border mx-2 h-px" />
2583
+ {/* Recursive children */}
2584
+ <div
2585
+ onClick={() => setOpen(false)}
2586
+ >
2587
+ <CategoryDropdownItems
2588
+ children={category.children}
2589
+ depth={0}
2590
+ selectedId={selectedId}
2591
+ onSelect={onSelect}
2592
+ />
2593
+ </div>
2594
+ </div>
2595
+ )}
2596
+ </div>
2597
+ );
2404
2598
  }
2405
2599
 
2406
2600
  function ProductsContent() {
@@ -2419,18 +2613,18 @@ function ProductsContent() {
2419
2613
  const [page, setPage] = useState(1);
2420
2614
  const [totalPages, setTotalPages] = useState(1);
2421
2615
  const [total, setTotal] = useState(0);
2422
- const [categories, setCategories] = useState<CategoryFilter[]>([]);
2616
+ const [categories, setCategories] = useState<CategoryNode[]>([]);
2423
2617
 
2424
2618
  const sortIndex = parseInt(sortParam, 10) || 0;
2425
2619
  const currentSort = sortOptions[sortIndex] || sortOptions[0];
2426
2620
 
2427
- // Load categories
2621
+ // Load categories (keep tree structure)
2428
2622
  useEffect(() => {
2429
2623
  async function loadCategories() {
2430
2624
  try {
2431
2625
  const client = getClient();
2432
2626
  const result = await client.getCategories();
2433
- setCategories(result.categories.map((c) => ({ id: c.id, name: c.name })));
2627
+ setCategories(result.categories as CategoryNode[]);
2434
2628
  } catch {
2435
2629
  // Categories endpoint may not be available in all modes
2436
2630
  }
@@ -2499,6 +2693,10 @@ function ProductsContent() {
2499
2693
  router.push(\`/products?\${params.toString()}\`);
2500
2694
  }
2501
2695
 
2696
+ function handleCategorySelect(id: string) {
2697
+ updateParam('category', id);
2698
+ }
2699
+
2502
2700
  return (
2503
2701
  <div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
2504
2702
  {/* Page Header */}
@@ -2530,18 +2728,13 @@ function ProductsContent() {
2530
2728
  {tc('all')}
2531
2729
  </button>
2532
2730
  {categories.map((cat) => (
2533
- <button
2731
+ <CategoryChip
2534
2732
  key={cat.id}
2535
- onClick={() => updateParam('category', cat.id)}
2536
- className={cn(
2537
- 'rounded-full border px-3 py-1.5 text-sm transition-colors',
2538
- categoryId === cat.id
2539
- ? 'bg-primary text-primary-foreground border-primary'
2540
- : 'border-border text-muted-foreground hover:border-primary hover:text-foreground'
2541
- )}
2542
- >
2543
- {cat.name}
2544
- </button>
2733
+ category={cat}
2734
+ selectedId={categoryId}
2735
+ onSelect={handleCategorySelect}
2736
+ tc={tc as (key: string) => string}
2737
+ />
2545
2738
  ))}
2546
2739
  </div>
2547
2740
  )}
@@ -2623,7 +2816,18 @@ import { useEffect, useState, useMemo } from 'react';
2623
2816
  import { useParams } from 'next/navigation';
2624
2817
  import Image from 'next/image';
2625
2818
  import Link from 'next/link';
2626
- import type { Product, ProductVariant, ProductImage, ProductMetafield } from 'brainerce';
2819
+ import type {
2820
+ Product,
2821
+ ProductVariant,
2822
+ ProductImage,
2823
+ ProductMetafield,
2824
+ ProductRecommendationsResponse,
2825
+ DownloadFile,
2826
+ } from 'brainerce';
2827
+
2828
+ type ProductWithRecommendations = Product & {
2829
+ recommendations?: ProductRecommendationsResponse;
2830
+ };
2627
2831
  import { getProductPriceInfo, getDescriptionContent } from 'brainerce';
2628
2832
  import { getClient } from '@/lib/brainerce';
2629
2833
  import { useCart } from '@/providers/store-provider';
@@ -2631,6 +2835,7 @@ import { PriceDisplay } from '@/components/shared/price-display';
2631
2835
  import { LoadingSpinner } from '@/components/shared/loading-spinner';
2632
2836
  import { VariantSelector } from '@/components/products/variant-selector';
2633
2837
  import { StockBadge } from '@/components/products/stock-badge';
2838
+ import { RecommendationSection } from '@/components/products/recommendation-section';
2634
2839
  import { useTranslations } from '@/lib/translations';
2635
2840
  import { cn } from '@/lib/utils';
2636
2841
 
@@ -2724,7 +2929,7 @@ export default function ProductDetailPage() {
2724
2929
  const { refreshCart } = useCart();
2725
2930
  const t = useTranslations('productDetail');
2726
2931
  const tc = useTranslations('common');
2727
- const [product, setProduct] = useState<Product | null>(null);
2932
+ const [product, setProduct] = useState<ProductWithRecommendations | null>(null);
2728
2933
  const [loading, setLoading] = useState(true);
2729
2934
  const [error, setError] = useState<string | null>(null);
2730
2935
  const [selectedVariant, setSelectedVariant] = useState<ProductVariant | null>(null);
@@ -2733,6 +2938,8 @@ export default function ProductDetailPage() {
2733
2938
  const [addingToCart, setAddingToCart] = useState(false);
2734
2939
  const [addedMessage, setAddedMessage] = useState(false);
2735
2940
 
2941
+ const recommendations = product?.recommendations ?? null;
2942
+
2736
2943
  // Load product
2737
2944
  useEffect(() => {
2738
2945
  async function load() {
@@ -2741,7 +2948,7 @@ export default function ProductDetailPage() {
2741
2948
  setError(null);
2742
2949
  const client = getClient();
2743
2950
  const p = await client.getProductBySlug(slug);
2744
- setProduct(p);
2951
+ setProduct(p as ProductWithRecommendations);
2745
2952
 
2746
2953
  // Auto-select first variant
2747
2954
  if (p.variants && p.variants.length > 0) {
@@ -2947,8 +3154,43 @@ export default function ProductDetailPage() {
2947
3154
  size="lg"
2948
3155
  />
2949
3156
 
2950
- {/* Stock */}
2951
- <StockBadge inventory={inventory} lowStockThreshold={5} />
3157
+ {/* Stock / Digital badge */}
3158
+ {product.isDownloadable ? (
3159
+ <span className="inline-flex items-center gap-1.5 rounded-full bg-green-100 px-3 py-1 text-xs font-medium text-green-800 dark:bg-green-950/30 dark:text-green-400">
3160
+ <svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
3161
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
3162
+ </svg>
3163
+ {t('instantDownload')}
3164
+ </span>
3165
+ ) : (
3166
+ <StockBadge inventory={inventory} lowStockThreshold={5} />
3167
+ )}
3168
+
3169
+ {/* Downloadable files info */}
3170
+ {product.isDownloadable && product.downloads && product.downloads.length > 0 && (
3171
+ <div className="bg-muted/50 rounded-lg border p-4">
3172
+ <p className="text-foreground mb-2 text-sm font-medium">
3173
+ {t('filesIncluded', { count: product.downloads.length })}
3174
+ </p>
3175
+ <ul className="space-y-1.5">
3176
+ {product.downloads.map((file: DownloadFile) => (
3177
+ <li key={file.id} className="text-muted-foreground flex items-center gap-2 text-sm">
3178
+ <svg className="h-4 w-4 flex-shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
3179
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
3180
+ </svg>
3181
+ <span className="truncate">{file.name}</span>
3182
+ {file.size && (
3183
+ <span className="flex-shrink-0 text-xs">
3184
+ ({file.size < 1024 * 1024
3185
+ ? \`\${(file.size / 1024).toFixed(0)} KB\`
3186
+ : \`\${(file.size / (1024 * 1024)).toFixed(1)} MB\`})
3187
+ </span>
3188
+ )}
3189
+ </li>
3190
+ ))}
3191
+ </ul>
3192
+ </div>
3193
+ )}
2952
3194
 
2953
3195
  {/* Variant Selector */}
2954
3196
  {product.type === 'VARIABLE' && product.variants && product.variants.length > 0 && (
@@ -3012,6 +3254,13 @@ export default function ProductDetailPage() {
3012
3254
  </button>
3013
3255
  </div>
3014
3256
 
3257
+ {/* Download after purchase note */}
3258
+ {product.isDownloadable && (
3259
+ <p className="text-muted-foreground text-sm">
3260
+ {t('downloadAfterPurchase')}
3261
+ </p>
3262
+ )}
3263
+
3015
3264
  {/* Description */}
3016
3265
  {description && (
3017
3266
  <div className="border-border border-t pt-4">
@@ -3049,19 +3298,41 @@ export default function ProductDetailPage() {
3049
3298
  )}
3050
3299
  </div>
3051
3300
  </div>
3301
+
3302
+ {/* Upsells \u2014 premium alternatives (product page) */}
3303
+ {recommendations?.upsells && recommendations.upsells.length > 0 && (
3304
+ <RecommendationSection
3305
+ title={t('upgradeYourChoice')}
3306
+ items={recommendations.upsells}
3307
+ className="mt-12"
3308
+ />
3309
+ )}
3310
+
3311
+ {/* Related products \u2014 similar items (bottom of product page) */}
3312
+ {recommendations?.related && recommendations.related.length > 0 && (
3313
+ <RecommendationSection
3314
+ title={t('similarProducts')}
3315
+ items={recommendations.related}
3316
+ className="mt-12"
3317
+ />
3318
+ )}
3052
3319
  </div>
3053
3320
  );
3054
3321
  }
3055
3322
  `,
3056
3323
  "src/app/cart/page.tsx": `'use client';
3057
3324
 
3325
+ import { useEffect, useState } from 'react';
3058
3326
  import Link from 'next/link';
3327
+ import type { CartRecommendationsResponse } from 'brainerce';
3328
+ import { getClient } from '@/lib/brainerce';
3059
3329
  import { useCart } from '@/providers/store-provider';
3060
3330
  import { CartItem } from '@/components/cart/cart-item';
3061
3331
  import { CartSummary } from '@/components/cart/cart-summary';
3062
3332
  import { CouponInput } from '@/components/cart/coupon-input';
3063
3333
  import { CartNudges } from '@/components/cart/cart-nudges';
3064
3334
  import { ReservationCountdown } from '@/components/cart/reservation-countdown';
3335
+ import { CartRecommendationSection } from '@/components/products/recommendation-section';
3065
3336
  import { LoadingSpinner } from '@/components/shared/loading-spinner';
3066
3337
  import { useTranslations } from '@/lib/translations';
3067
3338
 
@@ -3069,6 +3340,17 @@ export default function CartPage() {
3069
3340
  const { cart, cartLoading, refreshCart, itemCount } = useCart();
3070
3341
  const t = useTranslations('cart');
3071
3342
  const tc = useTranslations('common');
3343
+ const [cartRecs, setCartRecs] = useState<CartRecommendationsResponse | null>(null);
3344
+
3345
+ // Load cross-sell recommendations when cart changes
3346
+ useEffect(() => {
3347
+ if (!cart?.id || cart.items.length === 0) {
3348
+ setCartRecs(null);
3349
+ return;
3350
+ }
3351
+ const client = getClient();
3352
+ client.getCartRecommendations(cart.id, 4).then(setCartRecs).catch(() => {});
3353
+ }, [cart?.id, cart?.items.length]);
3072
3354
 
3073
3355
  if (cartLoading) {
3074
3356
  return (
@@ -3160,6 +3442,15 @@ export default function CartPage() {
3160
3442
  </div>
3161
3443
  </div>
3162
3444
  </div>
3445
+
3446
+ {/* Cross-sell recommendations */}
3447
+ {cartRecs?.recommendations && cartRecs.recommendations.length > 0 && (
3448
+ <CartRecommendationSection
3449
+ title={t('youMightAlsoNeed')}
3450
+ items={cartRecs.recommendations}
3451
+ className="mt-10"
3452
+ />
3453
+ )}
3163
3454
  </div>
3164
3455
  );
3165
3456
  }
@@ -4704,6 +4995,12 @@ async function proxyRequest(
4704
4995
  'Content-Type': 'application/json',
4705
4996
  };
4706
4997
 
4998
+ // Forward Origin/Referer so backend BrowserOriginGuard accepts proxied requests
4999
+ const origin = request.headers.get('origin');
5000
+ const referer = request.headers.get('referer');
5001
+ if (origin) headers['Origin'] = origin;
5002
+ if (referer) headers['Referer'] = referer;
5003
+
4707
5004
  // Forward SDK version header if present
4708
5005
  const sdkVersion = request.headers.get('x-sdk-version');
4709
5006
  if (sdkVersion) {
@@ -4989,6 +5286,7 @@ interface ProductCardProps {
4989
5286
 
4990
5287
  export function ProductCard({ product, className }: ProductCardProps) {
4991
5288
  const t = useTranslations('common');
5289
+ const tp = useTranslations('productDetail');
4992
5290
  const { price, originalPrice, isOnSale } = getProductPriceInfo(product);
4993
5291
  const mainImage = product.images?.[0];
4994
5292
  const imageUrl = mainImage?.url || null;
@@ -5033,6 +5331,11 @@ export function ProductCard({ product, className }: ProductCardProps) {
5033
5331
  </span>
5034
5332
  )}
5035
5333
  <DiscountBadge discount={product.discount} />
5334
+ {product.isDownloadable && (
5335
+ <span className="bg-primary text-primary-foreground rounded px-2 py-1 text-xs font-bold">
5336
+ {tp('digitalProduct')}
5337
+ </span>
5338
+ )}
5036
5339
  </div>
5037
5340
  </div>
5038
5341
 
@@ -7368,10 +7671,11 @@ export function ProfileSection({ profile, onProfileUpdate, className }: ProfileS
7368
7671
  `,
7369
7672
  "src/components/account/order-history.tsx": `'use client';
7370
7673
 
7371
- import { useState } from 'react';
7674
+ import { useState, useEffect } from 'react';
7372
7675
  import Image from 'next/image';
7373
- import type { Order, OrderStatus } from 'brainerce';
7676
+ import type { Order, OrderStatus, OrderDownloadLink } from 'brainerce';
7374
7677
  import { formatPrice } from 'brainerce';
7678
+ import { getClient } from '@/lib/brainerce';
7375
7679
  import { useTranslations } from '@/lib/translations';
7376
7680
  import { cn } from '@/lib/utils';
7377
7681
 
@@ -7555,6 +7859,11 @@ function OrderCard({ order }: { order: Order }) {
7555
7859
  </div>
7556
7860
  ))}
7557
7861
 
7862
+ {/* Downloads section */}
7863
+ {order.hasDownloads && (
7864
+ <OrderDownloads orderId={order.id} />
7865
+ )}
7866
+
7558
7867
  <OrderFinancialSummary order={order} currency={currency} />
7559
7868
  </div>
7560
7869
  )}
@@ -7562,6 +7871,71 @@ function OrderCard({ order }: { order: Order }) {
7562
7871
  );
7563
7872
  }
7564
7873
 
7874
+ function OrderDownloads({ orderId }: { orderId: string }) {
7875
+ const t = useTranslations('account');
7876
+ const [downloads, setDownloads] = useState<OrderDownloadLink[] | null>(null);
7877
+ const [loading, setLoading] = useState(true);
7878
+
7879
+ useEffect(() => {
7880
+ let cancelled = false;
7881
+ async function fetch() {
7882
+ try {
7883
+ const client = getClient();
7884
+ const links = await client.getOrderDownloads(orderId);
7885
+ if (!cancelled) setDownloads(links);
7886
+ } catch {
7887
+ if (!cancelled) setDownloads([]);
7888
+ } finally {
7889
+ if (!cancelled) setLoading(false);
7890
+ }
7891
+ }
7892
+ fetch();
7893
+ return () => { cancelled = true; };
7894
+ }, [orderId]);
7895
+
7896
+ if (loading) {
7897
+ return (
7898
+ <div className="border-border border-t pt-2">
7899
+ <p className="text-muted-foreground animate-pulse text-xs">{t('downloads')}...</p>
7900
+ </div>
7901
+ );
7902
+ }
7903
+
7904
+ if (!downloads || downloads.length === 0) return null;
7905
+
7906
+ return (
7907
+ <div className="border-border space-y-2 border-t pt-2">
7908
+ <p className="text-foreground text-sm font-medium">{t('downloads')}</p>
7909
+ {downloads.map((link, idx) => (
7910
+ <div key={idx} className="flex items-center gap-3">
7911
+ <div className="min-w-0 flex-1">
7912
+ <p className="text-foreground truncate text-sm">{link.fileName}</p>
7913
+ <p className="text-muted-foreground text-xs">
7914
+ {link.productName}
7915
+ {' \xB7 '}
7916
+ {link.downloadLimit != null
7917
+ ? t('downloadsRemaining', { used: link.downloadsUsed, limit: link.downloadLimit })
7918
+ : t('unlimitedDownloads')}
7919
+ {' \xB7 '}
7920
+ {link.expiresAt
7921
+ ? t('expiresAt', { date: new Date(link.expiresAt).toLocaleDateString() })
7922
+ : t('noExpiry')}
7923
+ </p>
7924
+ </div>
7925
+ <a
7926
+ href={link.downloadUrl}
7927
+ target="_blank"
7928
+ rel="noopener noreferrer"
7929
+ className="bg-primary text-primary-foreground flex-shrink-0 rounded px-3 py-1 text-xs font-medium hover:opacity-90"
7930
+ >
7931
+ {t('downloadFile')}
7932
+ </a>
7933
+ </div>
7934
+ ))}
7935
+ </div>
7936
+ );
7937
+ }
7938
+
7565
7939
  function OrderFinancialSummary({ order, currency }: { order: Order; currency: string }) {
7566
7940
  const tc = useTranslations('common');
7567
7941
  const totalAmount = order.totalAmount || order.total || '0';
@@ -8443,7 +8817,7 @@ var DETAILED = `# Required Pages \u2014 Detailed Guide
8443
8817
  - Stock badge
8444
8818
  - Discount badge (\`getProductDiscountBadge()\`)
8445
8819
  - Add to cart button (disabled when \`!canPurchase\`)
8446
- - Product recommendations (\`getProductRecommendations()\`)
8820
+ - Product recommendations (from \`product.recommendations\` \u2014 embedded in product response)
8447
8821
  - Use \`client.getProductBySlug(slug)\`
8448
8822
 
8449
8823
  ## 4. Cart (\`/cart\`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainerce/mcp-server",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "MCP server for AI-powered Brainerce store building. Provides SDK docs, types, and code examples to AI tools like Lovable, Cursor, and Claude Code.",
5
5
  "bin": {
6
6
  "brainerce-mcp": "dist/bin/stdio.js"