@brainerce/mcp-server 3.5.1 → 3.6.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/bin/http.js CHANGED
@@ -1555,6 +1555,53 @@ The merchant can hide a group entirely for one variant by setting \`maxOverride:
1555
1555
 
1556
1556
  Allergens (informational chips), scheduled availability windows, nested combos (depth \u2264 3 via \`nestedByModifierId\`), and downsell modifiers (negative \`priceDelta\`) are all built on the same data model. See INTEGRATION-OPTIONAL.md "Restaurant / build-your-own products" for those flows.`;
1557
1557
  }
1558
+ function getProductReviewsSection() {
1559
+ return `## Product Reviews (\u2605 ratings + JSON-LD for SEO)
1560
+
1561
+ Reviews publish immediately on submit. Merchants hide individual reviews via the admin surface \u2014 no PENDING queue.
1562
+
1563
+ \`\`\`typescript
1564
+ // PDP \u2014 list visible reviews
1565
+ const { data, meta } = await client.listProductReviews(productId, { page: 1, limit: 20 });
1566
+ data.forEach(r => render(r.authorName, r.rating, r.body, r.verifiedPurchase));
1567
+
1568
+ // PDP \u2014 submit form
1569
+ await client.submitProductReview(productId, {
1570
+ authorName, authorEmail, rating: 5, body,
1571
+ });
1572
+ \`\`\`
1573
+
1574
+ Each \`Product\` carries denormalized rollups: \`product.avgRating\`, \`product.reviewCount\`. Use these on PLP cards.
1575
+
1576
+ ### JSON-LD (mandatory for SEO stars in Google)
1577
+
1578
+ Emit \`aggregateRating\` ONLY when \`product.reviewCount > 0\`:
1579
+
1580
+ \`\`\`typescript
1581
+ const productJsonLd = {
1582
+ '@context': 'https://schema.org',
1583
+ '@type': 'Product',
1584
+ name: product.name,
1585
+ // \u2026existing fields
1586
+ ...(product.reviewCount > 0 && {
1587
+ aggregateRating: {
1588
+ '@type': 'AggregateRating',
1589
+ ratingValue: product.avgRating,
1590
+ reviewCount: product.reviewCount,
1591
+ bestRating: 5,
1592
+ worstRating: 1,
1593
+ },
1594
+ }),
1595
+ };
1596
+ \`\`\`
1597
+
1598
+ ### Rules
1599
+ - Rating must be an integer 1-5 (DB constraint).
1600
+ - Duplicate from same email \u2192 409 Conflict.
1601
+ - Submit endpoint is throttled to 3 / 60s / IP \u2014 surface a friendly "please wait" on 429.
1602
+ - \`verifiedPurchase\` is derived server-side from DELIVERED orders \u2014 DO NOT try to set it.
1603
+ - Stores with \`reviewsEnabled = false\` return 403 on storefront endpoints.`;
1604
+ }
1558
1605
  function getInventorySection() {
1559
1606
  return `## Inventory, Stock Display & Reservation Countdown
1560
1607
 
@@ -2501,6 +2548,8 @@ function getSectionByTopic(topic, connectionId, currency) {
2501
2548
  return getDiscountsSection();
2502
2549
  case "recommendations":
2503
2550
  return getRecommendationsSection();
2551
+ case "reviews":
2552
+ return getProductReviewsSection();
2504
2553
  case "product-customization-fields":
2505
2554
  return getProductCustomizationFieldsSection();
2506
2555
  case "modifier-groups":
@@ -2573,6 +2622,10 @@ function getSectionByTopic(topic, connectionId, currency) {
2573
2622
  "",
2574
2623
  "---",
2575
2624
  "",
2625
+ getProductReviewsSection(),
2626
+ "",
2627
+ "---",
2628
+ "",
2576
2629
  getDiscountsSection(),
2577
2630
  "",
2578
2631
  "---",
@@ -2624,6 +2677,7 @@ var GET_SDK_DOCS_SCHEMA = {
2624
2677
  "inventory",
2625
2678
  "discounts",
2626
2679
  "recommendations",
2680
+ "reviews",
2627
2681
  "product-customization-fields",
2628
2682
  "tax",
2629
2683
  "critical-rules",
@@ -3355,6 +3409,37 @@ interface CartBundleOffer {
3355
3409
  totalOriginalPrice: string;
3356
3410
  totalDiscountedPrice: string;
3357
3411
  }`;
3412
+ var REVIEWS_TYPES = `// ---- Product Reviews ----
3413
+
3414
+ interface ProductReview {
3415
+ id: string;
3416
+ productId: string;
3417
+ authorName: string; // max 100 chars
3418
+ rating: number; // integer 1-5
3419
+ body: string | null; // plain text, max 5000 chars
3420
+ verifiedPurchase: boolean; // derived server-side from DELIVERED orders
3421
+ hiddenAt?: string | null; // ISO datetime; admin responses only \u2014 null = visible
3422
+ createdAt: string; // ISO datetime
3423
+ }
3424
+
3425
+ interface ProductReviewAdmin extends ProductReview {
3426
+ customerId: string | null;
3427
+ authorEmail: string | null;
3428
+ orderId: string | null;
3429
+ updatedAt: string;
3430
+ }
3431
+
3432
+ interface SubmitProductReviewInput {
3433
+ authorName: string;
3434
+ authorEmail?: string; // recommended \u2014 used for guest dedupe + verified-purchase derivation
3435
+ rating: number; // 1-5
3436
+ body?: string; // optional
3437
+ }
3438
+
3439
+ // Each Product carries denormalized review stats:
3440
+ // product.avgRating: number // 0 when reviewCount is 0
3441
+ // product.reviewCount: number // count of visible (hiddenAt IS NULL) reviews
3442
+ `;
3358
3443
  var INQUIRIES_TYPES = `// ---- Contact Inquiries & Forms ----
3359
3444
 
3360
3445
  // Two shapes \u2014 legacy and flexible. The two may be mixed; \`fields\` wins on key collision.
@@ -3598,6 +3683,7 @@ var TYPES_BY_DOMAIN = {
3598
3683
  payments: PAYMENTS_TYPES,
3599
3684
  helpers: HELPERS_TYPES,
3600
3685
  inquiries: INQUIRIES_TYPES,
3686
+ reviews: REVIEWS_TYPES,
3601
3687
  "modifier-groups": MODIFIER_GROUPS_TYPES
3602
3688
  };
3603
3689
  function getTypesByDomain(domain) {
@@ -3628,6 +3714,7 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
3628
3714
  "payments",
3629
3715
  "helpers",
3630
3716
  "inquiries",
3717
+ "reviews",
3631
3718
  "all"
3632
3719
  ]).describe(
3633
3720
  'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse.'
@@ -4531,6 +4618,93 @@ function renderCartItem(item: CartItem): string {
4531
4618
  // multiply unitPrice \xD7 quantity directly.
4532
4619
  function lineTotal(item: CartItem): number {
4533
4620
  return item.unitPrice * item.quantity;
4621
+ }`,
4622
+ "product-reviews": `// PDP: list visible reviews + submit form + JSON-LD aggregateRating
4623
+ import { client } from '@/lib/brainerce';
4624
+ import type { Product, ProductReview, SubmitProductReviewInput } from 'brainerce';
4625
+
4626
+ export async function ProductReviewsSection({ product }: { product: Product }) {
4627
+ const { data } = await client.listProductReviews(product.id, { page: 1, limit: 20 });
4628
+
4629
+ return (
4630
+ <section>
4631
+ <h2>Reviews ({product.reviewCount ?? 0})</h2>
4632
+ {data.length === 0 && <p>No reviews yet \u2014 be the first.</p>}
4633
+ {data.map((r) => (
4634
+ <article key={r.id}>
4635
+ <strong>{r.authorName}</strong>
4636
+ {r.verifiedPurchase && <span> \xB7 Verified</span>}
4637
+ <div>{'\u2605'.repeat(r.rating)}{'\u2606'.repeat(5 - r.rating)}</div>
4638
+ {r.body && <p>{r.body}</p>}
4639
+ </article>
4640
+ ))}
4641
+ <ReviewForm productId={product.id} />
4642
+ </section>
4643
+ );
4644
+ }
4645
+
4646
+ // JSON-LD \u2014 emit aggregateRating only when there is data
4647
+ function productJsonLd(product: Product, url: string) {
4648
+ return {
4649
+ '@context': 'https://schema.org',
4650
+ '@type': 'Product',
4651
+ name: product.name,
4652
+ image: product.images?.[0]?.url,
4653
+ url,
4654
+ sku: product.sku ?? product.id,
4655
+ ...(product.reviewCount && product.reviewCount > 0 && {
4656
+ aggregateRating: {
4657
+ '@type': 'AggregateRating',
4658
+ ratingValue: product.avgRating,
4659
+ reviewCount: product.reviewCount,
4660
+ bestRating: 5,
4661
+ worstRating: 1,
4662
+ },
4663
+ }),
4664
+ };
4665
+ }
4666
+
4667
+ // Client-side form
4668
+ 'use client';
4669
+ import { useState } from 'react';
4670
+
4671
+ function ReviewForm({ productId }: { productId: string }) {
4672
+ const [rating, setRating] = useState(5);
4673
+ const [body, setBody] = useState('');
4674
+ const [name, setName] = useState('');
4675
+ const [email, setEmail] = useState('');
4676
+ const [error, setError] = useState<string | null>(null);
4677
+
4678
+ async function submit(e: React.FormEvent) {
4679
+ e.preventDefault();
4680
+ setError(null);
4681
+ try {
4682
+ await client.submitProductReview(productId, {
4683
+ authorName: name,
4684
+ authorEmail: email,
4685
+ rating,
4686
+ body: body || undefined,
4687
+ });
4688
+ window.location.reload();
4689
+ } catch (err: any) {
4690
+ if (err?.status === 409) setError("You've already reviewed this product.");
4691
+ else if (err?.status === 429) setError('Too many submissions. Please wait a moment.');
4692
+ else setError('Could not submit review. Please try again.');
4693
+ }
4694
+ }
4695
+
4696
+ return (
4697
+ <form onSubmit={submit}>
4698
+ <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" required maxLength={100} />
4699
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
4700
+ <select value={rating} onChange={(e) => setRating(Number(e.target.value))}>
4701
+ {[5,4,3,2,1].map(n => <option key={n} value={n}>{'\u2605'.repeat(n)}</option>)}
4702
+ </select>
4703
+ <textarea value={body} onChange={(e) => setBody(e.target.value)} maxLength={5000} placeholder="Tell us what you think (optional)" />
4704
+ <button type="submit">Submit review</button>
4705
+ {error && <p role="alert">{error}</p>}
4706
+ </form>
4707
+ );
4534
4708
  }`
4535
4709
  };
4536
4710
  async function handleGetCodeExample(args) {
@@ -5333,6 +5507,13 @@ var FEATURES = [
5333
5507
  sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
5334
5508
  mandatory: "mandatory"
5335
5509
  },
5510
+ {
5511
+ id: "product-reviews",
5512
+ title: "Display + collect product reviews with stars + JSON-LD",
5513
+ description: 'On the PDP show a Reviews section: list of customer reviews with star rating, author name, verified-purchase badge, body, and date. Include a submit form (name + email + 1-5 stars + optional body). On PLP cards show \u2605 avgRating and (reviewCount). Emit Product JSON-LD with aggregateRating ONLY when product.reviewCount > 0 \u2014 this is what unlocks the star rich snippet in Google. Reviews publish immediately; the merchant moderates from the admin surface. Rate-limited to 3 submits / 60s / IP. Show a friendly "you already reviewed this" message on 409.',
5514
+ sdk: "client.listProductReviews(productId), client.submitProductReview(productId, { authorName, authorEmail, rating, body }). Product.avgRating + Product.reviewCount are denormalized rollups returned on every product fetch.",
5515
+ mandatory: "mandatory"
5516
+ },
5336
5517
  {
5337
5518
  id: "product-customization-fields",
5338
5519
  title: "Render buyer-input customization fields on the product page",
package/dist/bin/stdio.js CHANGED
@@ -1553,6 +1553,53 @@ The merchant can hide a group entirely for one variant by setting \`maxOverride:
1553
1553
 
1554
1554
  Allergens (informational chips), scheduled availability windows, nested combos (depth \u2264 3 via \`nestedByModifierId\`), and downsell modifiers (negative \`priceDelta\`) are all built on the same data model. See INTEGRATION-OPTIONAL.md "Restaurant / build-your-own products" for those flows.`;
1555
1555
  }
1556
+ function getProductReviewsSection() {
1557
+ return `## Product Reviews (\u2605 ratings + JSON-LD for SEO)
1558
+
1559
+ Reviews publish immediately on submit. Merchants hide individual reviews via the admin surface \u2014 no PENDING queue.
1560
+
1561
+ \`\`\`typescript
1562
+ // PDP \u2014 list visible reviews
1563
+ const { data, meta } = await client.listProductReviews(productId, { page: 1, limit: 20 });
1564
+ data.forEach(r => render(r.authorName, r.rating, r.body, r.verifiedPurchase));
1565
+
1566
+ // PDP \u2014 submit form
1567
+ await client.submitProductReview(productId, {
1568
+ authorName, authorEmail, rating: 5, body,
1569
+ });
1570
+ \`\`\`
1571
+
1572
+ Each \`Product\` carries denormalized rollups: \`product.avgRating\`, \`product.reviewCount\`. Use these on PLP cards.
1573
+
1574
+ ### JSON-LD (mandatory for SEO stars in Google)
1575
+
1576
+ Emit \`aggregateRating\` ONLY when \`product.reviewCount > 0\`:
1577
+
1578
+ \`\`\`typescript
1579
+ const productJsonLd = {
1580
+ '@context': 'https://schema.org',
1581
+ '@type': 'Product',
1582
+ name: product.name,
1583
+ // \u2026existing fields
1584
+ ...(product.reviewCount > 0 && {
1585
+ aggregateRating: {
1586
+ '@type': 'AggregateRating',
1587
+ ratingValue: product.avgRating,
1588
+ reviewCount: product.reviewCount,
1589
+ bestRating: 5,
1590
+ worstRating: 1,
1591
+ },
1592
+ }),
1593
+ };
1594
+ \`\`\`
1595
+
1596
+ ### Rules
1597
+ - Rating must be an integer 1-5 (DB constraint).
1598
+ - Duplicate from same email \u2192 409 Conflict.
1599
+ - Submit endpoint is throttled to 3 / 60s / IP \u2014 surface a friendly "please wait" on 429.
1600
+ - \`verifiedPurchase\` is derived server-side from DELIVERED orders \u2014 DO NOT try to set it.
1601
+ - Stores with \`reviewsEnabled = false\` return 403 on storefront endpoints.`;
1602
+ }
1556
1603
  function getInventorySection() {
1557
1604
  return `## Inventory, Stock Display & Reservation Countdown
1558
1605
 
@@ -2499,6 +2546,8 @@ function getSectionByTopic(topic, connectionId, currency) {
2499
2546
  return getDiscountsSection();
2500
2547
  case "recommendations":
2501
2548
  return getRecommendationsSection();
2549
+ case "reviews":
2550
+ return getProductReviewsSection();
2502
2551
  case "product-customization-fields":
2503
2552
  return getProductCustomizationFieldsSection();
2504
2553
  case "modifier-groups":
@@ -2571,6 +2620,10 @@ function getSectionByTopic(topic, connectionId, currency) {
2571
2620
  "",
2572
2621
  "---",
2573
2622
  "",
2623
+ getProductReviewsSection(),
2624
+ "",
2625
+ "---",
2626
+ "",
2574
2627
  getDiscountsSection(),
2575
2628
  "",
2576
2629
  "---",
@@ -2622,6 +2675,7 @@ var GET_SDK_DOCS_SCHEMA = {
2622
2675
  "inventory",
2623
2676
  "discounts",
2624
2677
  "recommendations",
2678
+ "reviews",
2625
2679
  "product-customization-fields",
2626
2680
  "tax",
2627
2681
  "critical-rules",
@@ -3353,6 +3407,37 @@ interface CartBundleOffer {
3353
3407
  totalOriginalPrice: string;
3354
3408
  totalDiscountedPrice: string;
3355
3409
  }`;
3410
+ var REVIEWS_TYPES = `// ---- Product Reviews ----
3411
+
3412
+ interface ProductReview {
3413
+ id: string;
3414
+ productId: string;
3415
+ authorName: string; // max 100 chars
3416
+ rating: number; // integer 1-5
3417
+ body: string | null; // plain text, max 5000 chars
3418
+ verifiedPurchase: boolean; // derived server-side from DELIVERED orders
3419
+ hiddenAt?: string | null; // ISO datetime; admin responses only \u2014 null = visible
3420
+ createdAt: string; // ISO datetime
3421
+ }
3422
+
3423
+ interface ProductReviewAdmin extends ProductReview {
3424
+ customerId: string | null;
3425
+ authorEmail: string | null;
3426
+ orderId: string | null;
3427
+ updatedAt: string;
3428
+ }
3429
+
3430
+ interface SubmitProductReviewInput {
3431
+ authorName: string;
3432
+ authorEmail?: string; // recommended \u2014 used for guest dedupe + verified-purchase derivation
3433
+ rating: number; // 1-5
3434
+ body?: string; // optional
3435
+ }
3436
+
3437
+ // Each Product carries denormalized review stats:
3438
+ // product.avgRating: number // 0 when reviewCount is 0
3439
+ // product.reviewCount: number // count of visible (hiddenAt IS NULL) reviews
3440
+ `;
3356
3441
  var INQUIRIES_TYPES = `// ---- Contact Inquiries & Forms ----
3357
3442
 
3358
3443
  // Two shapes \u2014 legacy and flexible. The two may be mixed; \`fields\` wins on key collision.
@@ -3596,6 +3681,7 @@ var TYPES_BY_DOMAIN = {
3596
3681
  payments: PAYMENTS_TYPES,
3597
3682
  helpers: HELPERS_TYPES,
3598
3683
  inquiries: INQUIRIES_TYPES,
3684
+ reviews: REVIEWS_TYPES,
3599
3685
  "modifier-groups": MODIFIER_GROUPS_TYPES
3600
3686
  };
3601
3687
  function getTypesByDomain(domain) {
@@ -3626,6 +3712,7 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
3626
3712
  "payments",
3627
3713
  "helpers",
3628
3714
  "inquiries",
3715
+ "reviews",
3629
3716
  "all"
3630
3717
  ]).describe(
3631
3718
  'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse.'
@@ -4529,6 +4616,93 @@ function renderCartItem(item: CartItem): string {
4529
4616
  // multiply unitPrice \xD7 quantity directly.
4530
4617
  function lineTotal(item: CartItem): number {
4531
4618
  return item.unitPrice * item.quantity;
4619
+ }`,
4620
+ "product-reviews": `// PDP: list visible reviews + submit form + JSON-LD aggregateRating
4621
+ import { client } from '@/lib/brainerce';
4622
+ import type { Product, ProductReview, SubmitProductReviewInput } from 'brainerce';
4623
+
4624
+ export async function ProductReviewsSection({ product }: { product: Product }) {
4625
+ const { data } = await client.listProductReviews(product.id, { page: 1, limit: 20 });
4626
+
4627
+ return (
4628
+ <section>
4629
+ <h2>Reviews ({product.reviewCount ?? 0})</h2>
4630
+ {data.length === 0 && <p>No reviews yet \u2014 be the first.</p>}
4631
+ {data.map((r) => (
4632
+ <article key={r.id}>
4633
+ <strong>{r.authorName}</strong>
4634
+ {r.verifiedPurchase && <span> \xB7 Verified</span>}
4635
+ <div>{'\u2605'.repeat(r.rating)}{'\u2606'.repeat(5 - r.rating)}</div>
4636
+ {r.body && <p>{r.body}</p>}
4637
+ </article>
4638
+ ))}
4639
+ <ReviewForm productId={product.id} />
4640
+ </section>
4641
+ );
4642
+ }
4643
+
4644
+ // JSON-LD \u2014 emit aggregateRating only when there is data
4645
+ function productJsonLd(product: Product, url: string) {
4646
+ return {
4647
+ '@context': 'https://schema.org',
4648
+ '@type': 'Product',
4649
+ name: product.name,
4650
+ image: product.images?.[0]?.url,
4651
+ url,
4652
+ sku: product.sku ?? product.id,
4653
+ ...(product.reviewCount && product.reviewCount > 0 && {
4654
+ aggregateRating: {
4655
+ '@type': 'AggregateRating',
4656
+ ratingValue: product.avgRating,
4657
+ reviewCount: product.reviewCount,
4658
+ bestRating: 5,
4659
+ worstRating: 1,
4660
+ },
4661
+ }),
4662
+ };
4663
+ }
4664
+
4665
+ // Client-side form
4666
+ 'use client';
4667
+ import { useState } from 'react';
4668
+
4669
+ function ReviewForm({ productId }: { productId: string }) {
4670
+ const [rating, setRating] = useState(5);
4671
+ const [body, setBody] = useState('');
4672
+ const [name, setName] = useState('');
4673
+ const [email, setEmail] = useState('');
4674
+ const [error, setError] = useState<string | null>(null);
4675
+
4676
+ async function submit(e: React.FormEvent) {
4677
+ e.preventDefault();
4678
+ setError(null);
4679
+ try {
4680
+ await client.submitProductReview(productId, {
4681
+ authorName: name,
4682
+ authorEmail: email,
4683
+ rating,
4684
+ body: body || undefined,
4685
+ });
4686
+ window.location.reload();
4687
+ } catch (err: any) {
4688
+ if (err?.status === 409) setError("You've already reviewed this product.");
4689
+ else if (err?.status === 429) setError('Too many submissions. Please wait a moment.');
4690
+ else setError('Could not submit review. Please try again.');
4691
+ }
4692
+ }
4693
+
4694
+ return (
4695
+ <form onSubmit={submit}>
4696
+ <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" required maxLength={100} />
4697
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
4698
+ <select value={rating} onChange={(e) => setRating(Number(e.target.value))}>
4699
+ {[5,4,3,2,1].map(n => <option key={n} value={n}>{'\u2605'.repeat(n)}</option>)}
4700
+ </select>
4701
+ <textarea value={body} onChange={(e) => setBody(e.target.value)} maxLength={5000} placeholder="Tell us what you think (optional)" />
4702
+ <button type="submit">Submit review</button>
4703
+ {error && <p role="alert">{error}</p>}
4704
+ </form>
4705
+ );
4532
4706
  }`
4533
4707
  };
4534
4708
  async function handleGetCodeExample(args) {
@@ -5331,6 +5505,13 @@ var FEATURES = [
5331
5505
  sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
5332
5506
  mandatory: "mandatory"
5333
5507
  },
5508
+ {
5509
+ id: "product-reviews",
5510
+ title: "Display + collect product reviews with stars + JSON-LD",
5511
+ description: 'On the PDP show a Reviews section: list of customer reviews with star rating, author name, verified-purchase badge, body, and date. Include a submit form (name + email + 1-5 stars + optional body). On PLP cards show \u2605 avgRating and (reviewCount). Emit Product JSON-LD with aggregateRating ONLY when product.reviewCount > 0 \u2014 this is what unlocks the star rich snippet in Google. Reviews publish immediately; the merchant moderates from the admin surface. Rate-limited to 3 submits / 60s / IP. Show a friendly "you already reviewed this" message on 409.',
5512
+ sdk: "client.listProductReviews(productId), client.submitProductReview(productId, { authorName, authorEmail, rating, body }). Product.avgRating + Product.reviewCount are denormalized rollups returned on every product fetch.",
5513
+ mandatory: "mandatory"
5514
+ },
5334
5515
  {
5335
5516
  id: "product-customization-fields",
5336
5517
  title: "Render buyer-input customization fields on the product page",
package/dist/index.js CHANGED
@@ -1579,6 +1579,53 @@ The merchant can hide a group entirely for one variant by setting \`maxOverride:
1579
1579
 
1580
1580
  Allergens (informational chips), scheduled availability windows, nested combos (depth \u2264 3 via \`nestedByModifierId\`), and downsell modifiers (negative \`priceDelta\`) are all built on the same data model. See INTEGRATION-OPTIONAL.md "Restaurant / build-your-own products" for those flows.`;
1581
1581
  }
1582
+ function getProductReviewsSection() {
1583
+ return `## Product Reviews (\u2605 ratings + JSON-LD for SEO)
1584
+
1585
+ Reviews publish immediately on submit. Merchants hide individual reviews via the admin surface \u2014 no PENDING queue.
1586
+
1587
+ \`\`\`typescript
1588
+ // PDP \u2014 list visible reviews
1589
+ const { data, meta } = await client.listProductReviews(productId, { page: 1, limit: 20 });
1590
+ data.forEach(r => render(r.authorName, r.rating, r.body, r.verifiedPurchase));
1591
+
1592
+ // PDP \u2014 submit form
1593
+ await client.submitProductReview(productId, {
1594
+ authorName, authorEmail, rating: 5, body,
1595
+ });
1596
+ \`\`\`
1597
+
1598
+ Each \`Product\` carries denormalized rollups: \`product.avgRating\`, \`product.reviewCount\`. Use these on PLP cards.
1599
+
1600
+ ### JSON-LD (mandatory for SEO stars in Google)
1601
+
1602
+ Emit \`aggregateRating\` ONLY when \`product.reviewCount > 0\`:
1603
+
1604
+ \`\`\`typescript
1605
+ const productJsonLd = {
1606
+ '@context': 'https://schema.org',
1607
+ '@type': 'Product',
1608
+ name: product.name,
1609
+ // \u2026existing fields
1610
+ ...(product.reviewCount > 0 && {
1611
+ aggregateRating: {
1612
+ '@type': 'AggregateRating',
1613
+ ratingValue: product.avgRating,
1614
+ reviewCount: product.reviewCount,
1615
+ bestRating: 5,
1616
+ worstRating: 1,
1617
+ },
1618
+ }),
1619
+ };
1620
+ \`\`\`
1621
+
1622
+ ### Rules
1623
+ - Rating must be an integer 1-5 (DB constraint).
1624
+ - Duplicate from same email \u2192 409 Conflict.
1625
+ - Submit endpoint is throttled to 3 / 60s / IP \u2014 surface a friendly "please wait" on 429.
1626
+ - \`verifiedPurchase\` is derived server-side from DELIVERED orders \u2014 DO NOT try to set it.
1627
+ - Stores with \`reviewsEnabled = false\` return 403 on storefront endpoints.`;
1628
+ }
1582
1629
  function getInventorySection() {
1583
1630
  return `## Inventory, Stock Display & Reservation Countdown
1584
1631
 
@@ -2525,6 +2572,8 @@ function getSectionByTopic(topic, connectionId, currency) {
2525
2572
  return getDiscountsSection();
2526
2573
  case "recommendations":
2527
2574
  return getRecommendationsSection();
2575
+ case "reviews":
2576
+ return getProductReviewsSection();
2528
2577
  case "product-customization-fields":
2529
2578
  return getProductCustomizationFieldsSection();
2530
2579
  case "modifier-groups":
@@ -2597,6 +2646,10 @@ function getSectionByTopic(topic, connectionId, currency) {
2597
2646
  "",
2598
2647
  "---",
2599
2648
  "",
2649
+ getProductReviewsSection(),
2650
+ "",
2651
+ "---",
2652
+ "",
2600
2653
  getDiscountsSection(),
2601
2654
  "",
2602
2655
  "---",
@@ -2648,6 +2701,7 @@ var GET_SDK_DOCS_SCHEMA = {
2648
2701
  "inventory",
2649
2702
  "discounts",
2650
2703
  "recommendations",
2704
+ "reviews",
2651
2705
  "product-customization-fields",
2652
2706
  "tax",
2653
2707
  "critical-rules",
@@ -3379,6 +3433,37 @@ interface CartBundleOffer {
3379
3433
  totalOriginalPrice: string;
3380
3434
  totalDiscountedPrice: string;
3381
3435
  }`;
3436
+ var REVIEWS_TYPES = `// ---- Product Reviews ----
3437
+
3438
+ interface ProductReview {
3439
+ id: string;
3440
+ productId: string;
3441
+ authorName: string; // max 100 chars
3442
+ rating: number; // integer 1-5
3443
+ body: string | null; // plain text, max 5000 chars
3444
+ verifiedPurchase: boolean; // derived server-side from DELIVERED orders
3445
+ hiddenAt?: string | null; // ISO datetime; admin responses only \u2014 null = visible
3446
+ createdAt: string; // ISO datetime
3447
+ }
3448
+
3449
+ interface ProductReviewAdmin extends ProductReview {
3450
+ customerId: string | null;
3451
+ authorEmail: string | null;
3452
+ orderId: string | null;
3453
+ updatedAt: string;
3454
+ }
3455
+
3456
+ interface SubmitProductReviewInput {
3457
+ authorName: string;
3458
+ authorEmail?: string; // recommended \u2014 used for guest dedupe + verified-purchase derivation
3459
+ rating: number; // 1-5
3460
+ body?: string; // optional
3461
+ }
3462
+
3463
+ // Each Product carries denormalized review stats:
3464
+ // product.avgRating: number // 0 when reviewCount is 0
3465
+ // product.reviewCount: number // count of visible (hiddenAt IS NULL) reviews
3466
+ `;
3382
3467
  var INQUIRIES_TYPES = `// ---- Contact Inquiries & Forms ----
3383
3468
 
3384
3469
  // Two shapes \u2014 legacy and flexible. The two may be mixed; \`fields\` wins on key collision.
@@ -3622,6 +3707,7 @@ var TYPES_BY_DOMAIN = {
3622
3707
  payments: PAYMENTS_TYPES,
3623
3708
  helpers: HELPERS_TYPES,
3624
3709
  inquiries: INQUIRIES_TYPES,
3710
+ reviews: REVIEWS_TYPES,
3625
3711
  "modifier-groups": MODIFIER_GROUPS_TYPES
3626
3712
  };
3627
3713
  function getTypesByDomain(domain) {
@@ -3652,6 +3738,7 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
3652
3738
  "payments",
3653
3739
  "helpers",
3654
3740
  "inquiries",
3741
+ "reviews",
3655
3742
  "all"
3656
3743
  ]).describe(
3657
3744
  'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse.'
@@ -4555,6 +4642,93 @@ function renderCartItem(item: CartItem): string {
4555
4642
  // multiply unitPrice \xD7 quantity directly.
4556
4643
  function lineTotal(item: CartItem): number {
4557
4644
  return item.unitPrice * item.quantity;
4645
+ }`,
4646
+ "product-reviews": `// PDP: list visible reviews + submit form + JSON-LD aggregateRating
4647
+ import { client } from '@/lib/brainerce';
4648
+ import type { Product, ProductReview, SubmitProductReviewInput } from 'brainerce';
4649
+
4650
+ export async function ProductReviewsSection({ product }: { product: Product }) {
4651
+ const { data } = await client.listProductReviews(product.id, { page: 1, limit: 20 });
4652
+
4653
+ return (
4654
+ <section>
4655
+ <h2>Reviews ({product.reviewCount ?? 0})</h2>
4656
+ {data.length === 0 && <p>No reviews yet \u2014 be the first.</p>}
4657
+ {data.map((r) => (
4658
+ <article key={r.id}>
4659
+ <strong>{r.authorName}</strong>
4660
+ {r.verifiedPurchase && <span> \xB7 Verified</span>}
4661
+ <div>{'\u2605'.repeat(r.rating)}{'\u2606'.repeat(5 - r.rating)}</div>
4662
+ {r.body && <p>{r.body}</p>}
4663
+ </article>
4664
+ ))}
4665
+ <ReviewForm productId={product.id} />
4666
+ </section>
4667
+ );
4668
+ }
4669
+
4670
+ // JSON-LD \u2014 emit aggregateRating only when there is data
4671
+ function productJsonLd(product: Product, url: string) {
4672
+ return {
4673
+ '@context': 'https://schema.org',
4674
+ '@type': 'Product',
4675
+ name: product.name,
4676
+ image: product.images?.[0]?.url,
4677
+ url,
4678
+ sku: product.sku ?? product.id,
4679
+ ...(product.reviewCount && product.reviewCount > 0 && {
4680
+ aggregateRating: {
4681
+ '@type': 'AggregateRating',
4682
+ ratingValue: product.avgRating,
4683
+ reviewCount: product.reviewCount,
4684
+ bestRating: 5,
4685
+ worstRating: 1,
4686
+ },
4687
+ }),
4688
+ };
4689
+ }
4690
+
4691
+ // Client-side form
4692
+ 'use client';
4693
+ import { useState } from 'react';
4694
+
4695
+ function ReviewForm({ productId }: { productId: string }) {
4696
+ const [rating, setRating] = useState(5);
4697
+ const [body, setBody] = useState('');
4698
+ const [name, setName] = useState('');
4699
+ const [email, setEmail] = useState('');
4700
+ const [error, setError] = useState<string | null>(null);
4701
+
4702
+ async function submit(e: React.FormEvent) {
4703
+ e.preventDefault();
4704
+ setError(null);
4705
+ try {
4706
+ await client.submitProductReview(productId, {
4707
+ authorName: name,
4708
+ authorEmail: email,
4709
+ rating,
4710
+ body: body || undefined,
4711
+ });
4712
+ window.location.reload();
4713
+ } catch (err: any) {
4714
+ if (err?.status === 409) setError("You've already reviewed this product.");
4715
+ else if (err?.status === 429) setError('Too many submissions. Please wait a moment.');
4716
+ else setError('Could not submit review. Please try again.');
4717
+ }
4718
+ }
4719
+
4720
+ return (
4721
+ <form onSubmit={submit}>
4722
+ <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" required maxLength={100} />
4723
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
4724
+ <select value={rating} onChange={(e) => setRating(Number(e.target.value))}>
4725
+ {[5,4,3,2,1].map(n => <option key={n} value={n}>{'\u2605'.repeat(n)}</option>)}
4726
+ </select>
4727
+ <textarea value={body} onChange={(e) => setBody(e.target.value)} maxLength={5000} placeholder="Tell us what you think (optional)" />
4728
+ <button type="submit">Submit review</button>
4729
+ {error && <p role="alert">{error}</p>}
4730
+ </form>
4731
+ );
4558
4732
  }`
4559
4733
  };
4560
4734
  async function handleGetCodeExample(args) {
@@ -5357,6 +5531,13 @@ var FEATURES = [
5357
5531
  sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
5358
5532
  mandatory: "mandatory"
5359
5533
  },
5534
+ {
5535
+ id: "product-reviews",
5536
+ title: "Display + collect product reviews with stars + JSON-LD",
5537
+ description: 'On the PDP show a Reviews section: list of customer reviews with star rating, author name, verified-purchase badge, body, and date. Include a submit form (name + email + 1-5 stars + optional body). On PLP cards show \u2605 avgRating and (reviewCount). Emit Product JSON-LD with aggregateRating ONLY when product.reviewCount > 0 \u2014 this is what unlocks the star rich snippet in Google. Reviews publish immediately; the merchant moderates from the admin surface. Rate-limited to 3 submits / 60s / IP. Show a friendly "you already reviewed this" message on 409.',
5538
+ sdk: "client.listProductReviews(productId), client.submitProductReview(productId, { authorName, authorEmail, rating, body }). Product.avgRating + Product.reviewCount are denormalized rollups returned on every product fetch.",
5539
+ mandatory: "mandatory"
5540
+ },
5360
5541
  {
5361
5542
  id: "product-customization-fields",
5362
5543
  title: "Render buyer-input customization fields on the product page",
package/dist/index.mjs CHANGED
@@ -1547,6 +1547,53 @@ The merchant can hide a group entirely for one variant by setting \`maxOverride:
1547
1547
 
1548
1548
  Allergens (informational chips), scheduled availability windows, nested combos (depth \u2264 3 via \`nestedByModifierId\`), and downsell modifiers (negative \`priceDelta\`) are all built on the same data model. See INTEGRATION-OPTIONAL.md "Restaurant / build-your-own products" for those flows.`;
1549
1549
  }
1550
+ function getProductReviewsSection() {
1551
+ return `## Product Reviews (\u2605 ratings + JSON-LD for SEO)
1552
+
1553
+ Reviews publish immediately on submit. Merchants hide individual reviews via the admin surface \u2014 no PENDING queue.
1554
+
1555
+ \`\`\`typescript
1556
+ // PDP \u2014 list visible reviews
1557
+ const { data, meta } = await client.listProductReviews(productId, { page: 1, limit: 20 });
1558
+ data.forEach(r => render(r.authorName, r.rating, r.body, r.verifiedPurchase));
1559
+
1560
+ // PDP \u2014 submit form
1561
+ await client.submitProductReview(productId, {
1562
+ authorName, authorEmail, rating: 5, body,
1563
+ });
1564
+ \`\`\`
1565
+
1566
+ Each \`Product\` carries denormalized rollups: \`product.avgRating\`, \`product.reviewCount\`. Use these on PLP cards.
1567
+
1568
+ ### JSON-LD (mandatory for SEO stars in Google)
1569
+
1570
+ Emit \`aggregateRating\` ONLY when \`product.reviewCount > 0\`:
1571
+
1572
+ \`\`\`typescript
1573
+ const productJsonLd = {
1574
+ '@context': 'https://schema.org',
1575
+ '@type': 'Product',
1576
+ name: product.name,
1577
+ // \u2026existing fields
1578
+ ...(product.reviewCount > 0 && {
1579
+ aggregateRating: {
1580
+ '@type': 'AggregateRating',
1581
+ ratingValue: product.avgRating,
1582
+ reviewCount: product.reviewCount,
1583
+ bestRating: 5,
1584
+ worstRating: 1,
1585
+ },
1586
+ }),
1587
+ };
1588
+ \`\`\`
1589
+
1590
+ ### Rules
1591
+ - Rating must be an integer 1-5 (DB constraint).
1592
+ - Duplicate from same email \u2192 409 Conflict.
1593
+ - Submit endpoint is throttled to 3 / 60s / IP \u2014 surface a friendly "please wait" on 429.
1594
+ - \`verifiedPurchase\` is derived server-side from DELIVERED orders \u2014 DO NOT try to set it.
1595
+ - Stores with \`reviewsEnabled = false\` return 403 on storefront endpoints.`;
1596
+ }
1550
1597
  function getInventorySection() {
1551
1598
  return `## Inventory, Stock Display & Reservation Countdown
1552
1599
 
@@ -2493,6 +2540,8 @@ function getSectionByTopic(topic, connectionId, currency) {
2493
2540
  return getDiscountsSection();
2494
2541
  case "recommendations":
2495
2542
  return getRecommendationsSection();
2543
+ case "reviews":
2544
+ return getProductReviewsSection();
2496
2545
  case "product-customization-fields":
2497
2546
  return getProductCustomizationFieldsSection();
2498
2547
  case "modifier-groups":
@@ -2565,6 +2614,10 @@ function getSectionByTopic(topic, connectionId, currency) {
2565
2614
  "",
2566
2615
  "---",
2567
2616
  "",
2617
+ getProductReviewsSection(),
2618
+ "",
2619
+ "---",
2620
+ "",
2568
2621
  getDiscountsSection(),
2569
2622
  "",
2570
2623
  "---",
@@ -2616,6 +2669,7 @@ var GET_SDK_DOCS_SCHEMA = {
2616
2669
  "inventory",
2617
2670
  "discounts",
2618
2671
  "recommendations",
2672
+ "reviews",
2619
2673
  "product-customization-fields",
2620
2674
  "tax",
2621
2675
  "critical-rules",
@@ -3347,6 +3401,37 @@ interface CartBundleOffer {
3347
3401
  totalOriginalPrice: string;
3348
3402
  totalDiscountedPrice: string;
3349
3403
  }`;
3404
+ var REVIEWS_TYPES = `// ---- Product Reviews ----
3405
+
3406
+ interface ProductReview {
3407
+ id: string;
3408
+ productId: string;
3409
+ authorName: string; // max 100 chars
3410
+ rating: number; // integer 1-5
3411
+ body: string | null; // plain text, max 5000 chars
3412
+ verifiedPurchase: boolean; // derived server-side from DELIVERED orders
3413
+ hiddenAt?: string | null; // ISO datetime; admin responses only \u2014 null = visible
3414
+ createdAt: string; // ISO datetime
3415
+ }
3416
+
3417
+ interface ProductReviewAdmin extends ProductReview {
3418
+ customerId: string | null;
3419
+ authorEmail: string | null;
3420
+ orderId: string | null;
3421
+ updatedAt: string;
3422
+ }
3423
+
3424
+ interface SubmitProductReviewInput {
3425
+ authorName: string;
3426
+ authorEmail?: string; // recommended \u2014 used for guest dedupe + verified-purchase derivation
3427
+ rating: number; // 1-5
3428
+ body?: string; // optional
3429
+ }
3430
+
3431
+ // Each Product carries denormalized review stats:
3432
+ // product.avgRating: number // 0 when reviewCount is 0
3433
+ // product.reviewCount: number // count of visible (hiddenAt IS NULL) reviews
3434
+ `;
3350
3435
  var INQUIRIES_TYPES = `// ---- Contact Inquiries & Forms ----
3351
3436
 
3352
3437
  // Two shapes \u2014 legacy and flexible. The two may be mixed; \`fields\` wins on key collision.
@@ -3590,6 +3675,7 @@ var TYPES_BY_DOMAIN = {
3590
3675
  payments: PAYMENTS_TYPES,
3591
3676
  helpers: HELPERS_TYPES,
3592
3677
  inquiries: INQUIRIES_TYPES,
3678
+ reviews: REVIEWS_TYPES,
3593
3679
  "modifier-groups": MODIFIER_GROUPS_TYPES
3594
3680
  };
3595
3681
  function getTypesByDomain(domain) {
@@ -3620,6 +3706,7 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
3620
3706
  "payments",
3621
3707
  "helpers",
3622
3708
  "inquiries",
3709
+ "reviews",
3623
3710
  "all"
3624
3711
  ]).describe(
3625
3712
  'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse.'
@@ -4523,6 +4610,93 @@ function renderCartItem(item: CartItem): string {
4523
4610
  // multiply unitPrice \xD7 quantity directly.
4524
4611
  function lineTotal(item: CartItem): number {
4525
4612
  return item.unitPrice * item.quantity;
4613
+ }`,
4614
+ "product-reviews": `// PDP: list visible reviews + submit form + JSON-LD aggregateRating
4615
+ import { client } from '@/lib/brainerce';
4616
+ import type { Product, ProductReview, SubmitProductReviewInput } from 'brainerce';
4617
+
4618
+ export async function ProductReviewsSection({ product }: { product: Product }) {
4619
+ const { data } = await client.listProductReviews(product.id, { page: 1, limit: 20 });
4620
+
4621
+ return (
4622
+ <section>
4623
+ <h2>Reviews ({product.reviewCount ?? 0})</h2>
4624
+ {data.length === 0 && <p>No reviews yet \u2014 be the first.</p>}
4625
+ {data.map((r) => (
4626
+ <article key={r.id}>
4627
+ <strong>{r.authorName}</strong>
4628
+ {r.verifiedPurchase && <span> \xB7 Verified</span>}
4629
+ <div>{'\u2605'.repeat(r.rating)}{'\u2606'.repeat(5 - r.rating)}</div>
4630
+ {r.body && <p>{r.body}</p>}
4631
+ </article>
4632
+ ))}
4633
+ <ReviewForm productId={product.id} />
4634
+ </section>
4635
+ );
4636
+ }
4637
+
4638
+ // JSON-LD \u2014 emit aggregateRating only when there is data
4639
+ function productJsonLd(product: Product, url: string) {
4640
+ return {
4641
+ '@context': 'https://schema.org',
4642
+ '@type': 'Product',
4643
+ name: product.name,
4644
+ image: product.images?.[0]?.url,
4645
+ url,
4646
+ sku: product.sku ?? product.id,
4647
+ ...(product.reviewCount && product.reviewCount > 0 && {
4648
+ aggregateRating: {
4649
+ '@type': 'AggregateRating',
4650
+ ratingValue: product.avgRating,
4651
+ reviewCount: product.reviewCount,
4652
+ bestRating: 5,
4653
+ worstRating: 1,
4654
+ },
4655
+ }),
4656
+ };
4657
+ }
4658
+
4659
+ // Client-side form
4660
+ 'use client';
4661
+ import { useState } from 'react';
4662
+
4663
+ function ReviewForm({ productId }: { productId: string }) {
4664
+ const [rating, setRating] = useState(5);
4665
+ const [body, setBody] = useState('');
4666
+ const [name, setName] = useState('');
4667
+ const [email, setEmail] = useState('');
4668
+ const [error, setError] = useState<string | null>(null);
4669
+
4670
+ async function submit(e: React.FormEvent) {
4671
+ e.preventDefault();
4672
+ setError(null);
4673
+ try {
4674
+ await client.submitProductReview(productId, {
4675
+ authorName: name,
4676
+ authorEmail: email,
4677
+ rating,
4678
+ body: body || undefined,
4679
+ });
4680
+ window.location.reload();
4681
+ } catch (err: any) {
4682
+ if (err?.status === 409) setError("You've already reviewed this product.");
4683
+ else if (err?.status === 429) setError('Too many submissions. Please wait a moment.');
4684
+ else setError('Could not submit review. Please try again.');
4685
+ }
4686
+ }
4687
+
4688
+ return (
4689
+ <form onSubmit={submit}>
4690
+ <input value={name} onChange={(e) => setName(e.target.value)} placeholder="Your name" required maxLength={100} />
4691
+ <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
4692
+ <select value={rating} onChange={(e) => setRating(Number(e.target.value))}>
4693
+ {[5,4,3,2,1].map(n => <option key={n} value={n}>{'\u2605'.repeat(n)}</option>)}
4694
+ </select>
4695
+ <textarea value={body} onChange={(e) => setBody(e.target.value)} maxLength={5000} placeholder="Tell us what you think (optional)" />
4696
+ <button type="submit">Submit review</button>
4697
+ {error && <p role="alert">{error}</p>}
4698
+ </form>
4699
+ );
4526
4700
  }`
4527
4701
  };
4528
4702
  async function handleGetCodeExample(args) {
@@ -5325,6 +5499,13 @@ var FEATURES = [
5325
5499
  sdk: "client.getProductBySlug(slug). Helpers: getProductPriceInfo, getVariantPrice, getStockStatus, getVariantOptions, getProductSwatches, getDescriptionContent, getProductMetafieldValue.",
5326
5500
  mandatory: "mandatory"
5327
5501
  },
5502
+ {
5503
+ id: "product-reviews",
5504
+ title: "Display + collect product reviews with stars + JSON-LD",
5505
+ description: 'On the PDP show a Reviews section: list of customer reviews with star rating, author name, verified-purchase badge, body, and date. Include a submit form (name + email + 1-5 stars + optional body). On PLP cards show \u2605 avgRating and (reviewCount). Emit Product JSON-LD with aggregateRating ONLY when product.reviewCount > 0 \u2014 this is what unlocks the star rich snippet in Google. Reviews publish immediately; the merchant moderates from the admin surface. Rate-limited to 3 submits / 60s / IP. Show a friendly "you already reviewed this" message on 409.',
5506
+ sdk: "client.listProductReviews(productId), client.submitProductReview(productId, { authorName, authorEmail, rating, body }). Product.avgRating + Product.reviewCount are denormalized rollups returned on every product fetch.",
5507
+ mandatory: "mandatory"
5508
+ },
5328
5509
  {
5329
5510
  id: "product-customization-fields",
5330
5511
  title: "Render buyer-input customization fields on the product page",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainerce/mcp-server",
3
- "version": "3.5.1",
3
+ "version": "3.6.0",
4
4
  "description": "Framework-agnostic domain knowledge API for Brainerce. Provides SDK docs, types, business flows, critical rules, and store capabilities to AI tools like Lovable, Cursor, Bolt, v0, and Claude Code. Does NOT provide framework-specific boilerplate — clients build in whatever framework fits.",
5
5
  "bin": {
6
6
  "brainerce-mcp": "dist/bin/stdio.js"