@brainerce/mcp-server 3.12.0 → 3.12.2

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/stdio.js CHANGED
@@ -718,17 +718,19 @@ function getPaymentProvidersSection() {
718
718
  return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
719
719
 
720
720
  \`\`\`typescript
721
- const { hasPayments, providers } = await client.getPaymentProviders();
721
+ const { hasPayments, providers, defaultProvider } = await client.getPaymentProviders();
722
722
  if (!hasPayments) {
723
723
  // Show error: "Payment is not configured for this store"
724
724
  return;
725
725
  }
726
- const stripeProvider = providers.find(p => p.provider === 'stripe');
727
- const growProvider = providers.find(p => p.provider === 'grow');
728
- const paypalProvider = providers.find(p => p.provider === 'paypal');
726
+ // Primary vs additive (Shopify-parity): render wallets as express buttons ABOVE the card form.
727
+ const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal (WALLET)
728
+ const primary = defaultProvider && !defaultProvider.isAdditive ? defaultProvider : undefined;
729
729
  \`\`\`
730
730
 
731
- Each provider has: \`id\`, \`provider\` (flexible string \u2014 \`'stripe'\`, \`'grow'\`, \`'paypal'\`, \`'cardcom'\`, \`'morning'\`, \`'takbull'\`, \`'sandbox'\`, and future providers), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`.
731
+ Each provider has: \`id\`, \`provider\` (flexible string \u2014 \`'stripe'\`, \`'grow'\`, \`'paypal'\`, \`'cardcom'\`, \`'morning'\`, \`'takbull'\`, \`'sandbox'\`, and future providers), \`name\`, \`publicKey\`, \`stripeAccountId\` (Stripe only), \`supportedMethods\`, \`testMode\`, \`isDefault\`, plus the taxonomy fields \`methodType\` (\`'CREDIT_CARD'\` = the single primary card processor that settles the order; \`'WALLET'\`/other = additive), \`isAdditive\`, and \`presentation\` (\`'card_form'\` | \`'express_button'\` | \u2026).
732
+
733
+ **Primary vs. additive:** there is one primary card processor (the \`defaultProvider\`, \`isAdditive: false\`) plus any additive methods (\`isAdditive: true\`, e.g. PayPal). Render additive methods as accelerated-checkout **express buttons above** the card form \u2014 they sit alongside the primary, never replace it. Create the intent with the tapped provider's \`id\`. (Exception: a wallet-only store has no card processor, so its wallet becomes the \`defaultProvider\` and stands alone.)
732
734
 
733
735
  The payment intent returned from \`createPaymentIntent()\` includes a \`clientSdk\` object whose \`renderType\` tells you exactly how to render \u2014 **branch on THAT, not on the provider name**:
734
736
 
@@ -1116,11 +1118,22 @@ client-side overlay needed. Swatch colors are language-agnostic.
1116
1118
  function ProductDescription({ product }: { product: Product }) {
1117
1119
  const content = getDescriptionContent(product);
1118
1120
  if (!content) return null;
1119
- if ('html' in content) return <div dangerouslySetInnerHTML={{ __html: content.html }} />;
1121
+ // Descriptions are merchant HTML and may contain <video> + host-locked
1122
+ // YouTube/Vimeo <iframe> embeds. NEVER render unsanitized \u2014 sanitize and
1123
+ // allowlist video/source + iframe (restricted to youtube/vimeo hosts).
1124
+ if ('html' in content) {
1125
+ return <div dangerouslySetInnerHTML={{ __html: sanitizeProductHtml(content.html) }} />;
1126
+ }
1120
1127
  return <p>{content.text}</p>;
1121
1128
  }
1122
1129
  \`\`\`
1123
1130
 
1131
+ **Sanitize descriptions.** \`sanitizeProductHtml\` is the host-locked sanitizer the
1132
+ \`create-brainerce-store\` scaffold ships at \`src/lib/sanitize-html.ts\`. If you roll
1133
+ your own, allow \`video\`/\`source\` and \`iframe\` **restricted** to \`www.youtube.com\`,
1134
+ \`www.youtube-nocookie.com\`, \`player.vimeo.com\` (never arbitrary iframes), and add
1135
+ those hosts to your CSP \`frame-src\` or embeds render blank.
1136
+
1124
1137
  ### Metafields (Custom Product Fields) \u2014 display on product detail!
1125
1138
 
1126
1139
  **IMPORTANT: Render metafield values based on \`mf.type\`!** Don't just display \`mf.value\` as text for all types.
@@ -1203,7 +1216,28 @@ const { url } = await client.uploadCustomizationFile(file);
1203
1216
  // Then use the URL in metadata: metadata: { logo: url }
1204
1217
  \`\`\`
1205
1218
 
1206
- **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
1219
+ **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\` (array of \`{label, value, swatchColor?, swatchImageUrl?}\`), \`defaultValue\`, \`position\`
1220
+
1221
+ **SELECT / MULTI_SELECT \u2014 use \`option.value\` to submit, \`option.label\` to display:**
1222
+
1223
+ \`\`\`typescript
1224
+ for (const option of field.enumValues ?? []) {
1225
+ // option.value \u2192 what to submit in metadata
1226
+ // option.label \u2192 what to show the customer
1227
+ // option.swatchColor \u2192 optional hex color for color swatches
1228
+ }
1229
+ \`\`\`
1230
+
1231
+ **Display customizations in cart / checkout \u2014 no extra API call needed:**
1232
+
1233
+ CartItem and CheckoutLineItem both include a \`customizations\` object alongside \`metadata\`. Use \`customizations\` for display \u2014 it contains resolved labels, not raw keys.
1234
+
1235
+ \`\`\`typescript
1236
+ // Works for CartItem, CheckoutLineItem, and OrderItem \u2014 same shape
1237
+ const lines = Object.values(item.customizations ?? {})
1238
+ .map(c => \`\${c.label}: \${Array.isArray(c.value) ? c.value.join(', ') : c.value}\`);
1239
+ // e.g. ["Frame color: Gold", "Add-ons: Gift wrap"]
1240
+ \`\`\`
1207
1241
 
1208
1242
  ### Downloadable / Digital Products
1209
1243
 
@@ -1745,7 +1779,7 @@ The merchant can hide a group entirely for one variant by setting \`maxOverride:
1745
1779
 
1746
1780
  ### Restaurant features (advanced)
1747
1781
 
1748
- 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.`;
1782
+ 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.`;
1749
1783
  }
1750
1784
  function getProductReviewsSection() {
1751
1785
  return `## Product Reviews (\u2605 ratings + JSON-LD for SEO)
@@ -3235,6 +3269,85 @@ by device) and can also be asked of the AI assistant ("how many visits did I get
3235
3269
  this month and from where?").
3236
3270
  `;
3237
3271
  }
3272
+ function getShippingAppSection() {
3273
+ return `## App Store Shipping (Shippo)
3274
+
3275
+ Merchants install Shippo from the Brainerce App Store. After connecting their own Shippo account
3276
+ (OAuth), live carrier rates appear automatically at checkout alongside any manually configured
3277
+ zone rates. **Billing goes directly to the merchant's Shippo account \u2014 Brainerce never charges.**
3278
+
3279
+ ### Checkout \u2014 live carrier rates
3280
+
3281
+ No SDK changes needed. Once Shippo is installed and configured, the existing
3282
+ \`getShippingRates()\` call returns both manual zone rates and live Shippo quotes:
3283
+
3284
+ \`\`\`typescript
3285
+ const { rates } = await client.setShippingAddress(checkoutId, { ... });
3286
+ // rates may include:
3287
+ // { id: 'shippo:rate_8f123abc', name: 'USPS Priority Mail', price: '8.50', currency: 'USD', source: 'carrier' }
3288
+ \`\`\`
3289
+
3290
+ Rates prefixed \`shippo:\` are live Shippo quotes. Pass the full \`id\` as \`shippingRateId\`
3291
+ when calling \`selectShippingMethod()\`.
3292
+
3293
+ ### Admin \u2014 purchase a shipping label
3294
+
3295
+ After the order is placed, the merchant purchases a label by passing the Shippo rate object_id:
3296
+
3297
+ \`\`\`typescript
3298
+ const label = await admin.createShippingLabel(orderId, {
3299
+ shippoRateObjectId: 'rate_8f123456789abcdef',
3300
+ });
3301
+ console.log(label.labelUrl); // PDF ready to print
3302
+ console.log(label.trackingNumber); // auto-populated on the order
3303
+ \`\`\`
3304
+
3305
+ The \`trackingNumber\` is stored on the \`Order\` and returned in \`getMyOrders()\`
3306
+ so customers can track their shipment automatically.
3307
+
3308
+ **SDK mode:** \`createShippingLabel()\` requires \`apiKey\` (admin mode only).`;
3309
+ }
3310
+ function getLoyaltySection() {
3311
+ return `# Loyalty & Rewards
3312
+
3313
+ Logged-in customers earn points on paid orders and redeem them for a one-time discount coupon at checkout.
3314
+
3315
+ > **Storefront mode only.** These methods work only with \`new BrainerceClient({ storeId })\` + a logged-in \`customerToken\`. There is **no vibe-coded route** \u2014 in \`salesChannelId: 'vc_*'\` mode they throw. Skip loyalty if you connect vibe-coded.
3316
+
3317
+ Points are earned automatically by the platform when an order is paid \u2014 there is no earn call from the storefront. Your job is to **show** balance/rewards and to **redeem**.
3318
+
3319
+ ## Status
3320
+
3321
+ \`\`\`typescript
3322
+ // requires client.setCustomerToken(auth.token)
3323
+ const status = await client.getLoyaltyStatus();
3324
+ // { enrolled, pointsBalance, lifetimeEarned, program: { pointsName, currencyRatio, status } | null }
3325
+ \`\`\`
3326
+
3327
+ - \`program === null\` \u2192 the store has no loyalty program; hide the loyalty UI.
3328
+ - \`program.status !== 'ACTIVE'\` \u2192 program paused/draft; hide the loyalty UI.
3329
+
3330
+ ## Enroll (only if auto-enroll is off)
3331
+
3332
+ \`\`\`typescript
3333
+ await client.enrollInLoyalty(); // same status shape; no-op if already enrolled
3334
+ \`\`\`
3335
+
3336
+ ## Rewards & redemption
3337
+
3338
+ \`\`\`typescript
3339
+ const rewards = await client.getAvailableRewards();
3340
+ // [{ id, name, description, pointsCost, discountValue, minOrderAmount, maxUsesPerUser, isActive }]
3341
+
3342
+ const { couponCode, discountValue, pointsBalance } = await client.redeemLoyaltyReward(rewardId);
3343
+ await client.applyCoupon(cartId, couponCode); // applies via the normal coupon engine
3344
+ \`\`\`
3345
+
3346
+ - \`redeemLoyaltyReward\` mints a one-time fixed-amount coupon (\`usageLimit: 1\`). Apply it with the normal \`applyCoupon\` flow.
3347
+ - Throws if the customer has insufficient points. If minting fails, points are refunded automatically.
3348
+ - Disable a reward's redeem button when \`pointsBalance < reward.pointsCost\`.
3349
+ `;
3350
+ }
3238
3351
  function getSectionByTopic(topic, connectionId, currency) {
3239
3352
  const cid = connectionId || "vc_YOUR_CONNECTION_ID";
3240
3353
  const cur = currency || "USD";
@@ -3261,6 +3374,8 @@ function getSectionByTopic(topic, connectionId, currency) {
3261
3374
  return getInventorySection();
3262
3375
  case "discounts":
3263
3376
  return getDiscountsSection();
3377
+ case "loyalty":
3378
+ return getLoyaltySection();
3264
3379
  case "recommendations":
3265
3380
  return getRecommendationsSection();
3266
3381
  case "reviews":
@@ -3294,6 +3409,9 @@ function getSectionByTopic(topic, connectionId, currency) {
3294
3409
  case "analytics":
3295
3410
  case "traffic-analytics":
3296
3411
  return getStorefrontAnalyticsSection(cid);
3412
+ case "shipping-app":
3413
+ case "shippo":
3414
+ return getShippingAppSection();
3297
3415
  case "all":
3298
3416
  return [
3299
3417
  "# Brainerce SDK \u2014 full topic dump",
@@ -3424,6 +3542,7 @@ var GET_SDK_DOCS_SCHEMA = {
3424
3542
  "order-confirmation",
3425
3543
  "inventory",
3426
3544
  "discounts",
3545
+ "loyalty",
3427
3546
  "recommendations",
3428
3547
  "reviews",
3429
3548
  "product-customization-fields",
@@ -3576,11 +3695,18 @@ interface ProductCustomizationField {
3576
3695
  maxLength?: number | null;
3577
3696
  minValue?: number | null; // NUMBER bounds
3578
3697
  maxValue?: number | null;
3579
- enumValues?: string[]; // REQUIRED for SELECT / MULTI_SELECT
3698
+ enumValues?: CustomizationFieldOption[]; // REQUIRED for SELECT / MULTI_SELECT
3580
3699
  defaultValue?: string | null;
3581
3700
  position: number; // Render order
3582
3701
  }
3583
3702
 
3703
+ interface CustomizationFieldOption {
3704
+ label: string; // Display label (e.g., "Rose Gold")
3705
+ value: string; // Value stored in CartItem.metadata
3706
+ swatchColor?: string | null; // Optional hex color (e.g., "#B76E79")
3707
+ swatchImageUrl?: string | null; // Optional swatch image URL
3708
+ }
3709
+
3584
3710
  interface ProductQueryParams {
3585
3711
  page?: number;
3586
3712
  limit?: number;
@@ -3674,6 +3800,7 @@ interface CartItem {
3674
3800
  image?: ProductImage | string | null;
3675
3801
  } | null;
3676
3802
  metadata?: Record<string, unknown> | null; // Buyer-submitted customization values, keyed by ProductCustomizationField.key
3803
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 use this to display "Color: Silver" without extra getProduct() call
3677
3804
  createdAt: string;
3678
3805
  updatedAt: string;
3679
3806
  }
@@ -3773,6 +3900,7 @@ interface CheckoutLineItem {
3773
3900
  product: { id: string; name: string; sku: string; images?: ProductImage[]; };
3774
3901
  variant?: { id: string; name?: string | null; sku?: string | null; image?: ProductImage | string | null; } | null;
3775
3902
  metadata?: Record<string, unknown> | null; // Copied from CartItem.metadata \u2014 buyer customization values
3903
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 same shape as OrderItem.customizations
3776
3904
  }
3777
3905
 
3778
3906
  interface CheckoutAddress {
@@ -4009,7 +4137,10 @@ interface PaymentProviderConfig {
4009
4137
  stripeAccountId?: string; // Stripe Connect only
4010
4138
  supportedMethods: string[];
4011
4139
  testMode: boolean;
4012
- isDefault: boolean;
4140
+ isDefault: boolean; // only a primary (CREDIT_CARD) is ever the default
4141
+ methodType: 'CREDIT_CARD' | 'WALLET' | 'OFFSITE' | 'LOCAL' | 'REDEEMABLE';
4142
+ presentation: 'card_form' | 'express_button' | 'redirect' | 'local_option' | 'redeemable_field';
4143
+ isAdditive: boolean; // wallet/alt method shown ALONGSIDE the primary (e.g. PayPal)
4013
4144
  }
4014
4145
  type PaymentProvider = PaymentProviderConfig;
4015
4146
 
@@ -4749,6 +4880,43 @@ interface TaxEstimateResponse {
4749
4880
  // Non-binding by design: the authoritative tax runs at checkout against the
4750
4881
  // full shipping address. The country comes from your edge runtime
4751
4882
  // (CF-IPCountry / request.geo.country / etc.), NOT the request IP.`;
4883
+ var LOYALTY_TYPES = `// ---- Loyalty & Rewards (storefront mode only) ----
4884
+ interface LoyaltyStatus {
4885
+ enrolled: boolean;
4886
+ pointsBalance: number; // redeemable points (pending earns excluded)
4887
+ lifetimeEarned: number;
4888
+ program: {
4889
+ pointsName: string; // e.g. "points", "coins"
4890
+ currencyRatio: number; // points needed to redeem 1 unit of store currency
4891
+ status: 'DRAFT' | 'ACTIVE' | 'PAUSED';
4892
+ } | null; // null when the store has no loyalty program
4893
+ }
4894
+
4895
+ interface LoyaltyReward {
4896
+ id: string;
4897
+ name: string;
4898
+ description: string | null;
4899
+ pointsCost: number;
4900
+ discountValue: number; // fixed amount off, store currency
4901
+ minOrderAmount: number | null;
4902
+ maxUsesPerUser: number;
4903
+ isActive: boolean;
4904
+ createdAt: string;
4905
+ updatedAt: string;
4906
+ }
4907
+
4908
+ interface RedeemRewardResult {
4909
+ couponCode: string; // one-time coupon to apply at checkout
4910
+ discountValue: number;
4911
+ pointsSpent: number;
4912
+ pointsBalance: number; // remaining balance after redemption
4913
+ }
4914
+
4915
+ // client.getLoyaltyStatus(): Promise<LoyaltyStatus>
4916
+ // client.enrollInLoyalty(): Promise<LoyaltyStatus>
4917
+ // client.getAvailableRewards(): Promise<LoyaltyReward[]>
4918
+ // client.redeemLoyaltyReward(rewardId: string): Promise<RedeemRewardResult>
4919
+ `;
4752
4920
  var TYPES_BY_DOMAIN = {
4753
4921
  products: PRODUCTS_TYPES,
4754
4922
  cart: CART_TYPES,
@@ -4761,7 +4929,8 @@ var TYPES_BY_DOMAIN = {
4761
4929
  reviews: REVIEWS_TYPES,
4762
4930
  "modifier-groups": MODIFIER_GROUPS_TYPES,
4763
4931
  content: CONTENT_TYPES,
4764
- regions: REGIONS_TYPES
4932
+ regions: REGIONS_TYPES,
4933
+ loyalty: LOYALTY_TYPES
4765
4934
  };
4766
4935
  function getTypesByDomain(domain) {
4767
4936
  if (domain === "all") {
@@ -4795,6 +4964,7 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
4795
4964
  "modifier-groups",
4796
4965
  "content",
4797
4966
  "regions",
4967
+ "loyalty",
4798
4968
  "all"
4799
4969
  ]).describe(
4800
4970
  'The domain of types to retrieve. Use "helpers" for helper function signatures and common types like StoreInfo, PaginatedResponse. Use "content" for FAQ / Footer / Header / Announcement / RichText / Page. Use "regions" for Region / TaxClass admin types.'
@@ -5057,7 +5227,15 @@ const checkout = await client.getCheckout(checkoutId);
5057
5227
  import { client } from './brainerce';
5058
5228
 
5059
5229
  const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
5060
- // each provider: { id, provider, name, publicKey, supportedMethods, testMode, isDefault, clientSdk? }
5230
+ // each provider: { id, provider, name, publicKey, supportedMethods, testMode,
5231
+ // isDefault, methodType, presentation, isAdditive, clientSdk? }
5232
+ //
5233
+ // methodType splits providers into ONE primary card processor (CREDIT_CARD, the
5234
+ // defaultProvider that settles the order) and additive methods (isAdditive:true,
5235
+ // e.g. PayPal as a WALLET). Render additive methods as express buttons ABOVE the
5236
+ // card form \u2014 they sit alongside the primary, never replace it. Call
5237
+ // createPaymentIntent({ providerId }) with the tapped provider's id. (Wallet-only
5238
+ // store: with no card processor, the wallet becomes defaultProvider and stands alone.)
5061
5239
  //
5062
5240
  // After createPaymentIntent, the response carries a clientSdk.renderType that
5063
5241
  // tells your UI how to render the payment step. The 5 possible values:
@@ -6233,6 +6411,9 @@ function formatCapabilities(caps) {
6233
6411
  lines.push(
6234
6412
  caps.features.hasCheckoutCustomFields ? "\u2705 Checkout custom fields configured (surcharges may apply)" : "\u274C No checkout custom fields"
6235
6413
  );
6414
+ lines.push(
6415
+ caps.features.hasLoyaltyProgram ? "\u2705 Loyalty program active (points & rewards \u2014 storefront-mode only)" : "\u274C No loyalty program"
6416
+ );
6236
6417
  lines.push("");
6237
6418
  lines.push("## Connection Settings");
6238
6419
  lines.push(
@@ -6278,6 +6459,11 @@ function formatCapabilities(caps) {
6278
6459
  if (caps.features.hasCoupons) {
6279
6460
  suggestions.push('Add a coupon input to the cart. Use get-code-example("coupon-input").');
6280
6461
  }
6462
+ if (caps.features.hasLoyaltyProgram) {
6463
+ suggestions.push(
6464
+ "A loyalty program is active. In a storefront-mode client (new BrainerceClient({ storeId })), show the logged-in customer their points balance and redeemable rewards, and let them redeem for a coupon at checkout. Use getLoyaltyStatus(), getAvailableRewards(), and redeemLoyaltyReward(id). Note: loyalty is NOT available in vibe-coded mode."
6465
+ );
6466
+ }
6281
6467
  if (caps.features.hasCheckoutCustomFields) {
6282
6468
  suggestions.push(
6283
6469
  `Checkout custom fields are configured. Build a step between shipping/pickup and payment using getCheckoutCustomFields() + setCheckoutCustomFields(). Render checkout.appliedSurcharges in the order summary so customers see what they're paying for. Use get-code-example("checkout-custom-fields").`
package/dist/index.d.mts CHANGED
@@ -75,6 +75,8 @@ interface StoreCapabilities {
75
75
  hasCheckoutCustomFields: boolean;
76
76
  /** True when at least one PUBLISHED Content row exists for the store. */
77
77
  hasContent?: boolean;
78
+ /** True when the store has an ACTIVE loyalty program. Storefront-mode only. */
79
+ hasLoyaltyProgram?: boolean;
78
80
  };
79
81
  }
80
82
  declare function fetchStoreCapabilities(connectionId: string, baseUrl?: string): Promise<StoreCapabilities>;
package/dist/index.d.ts CHANGED
@@ -75,6 +75,8 @@ interface StoreCapabilities {
75
75
  hasCheckoutCustomFields: boolean;
76
76
  /** True when at least one PUBLISHED Content row exists for the store. */
77
77
  hasContent?: boolean;
78
+ /** True when the store has an ACTIVE loyalty program. Storefront-mode only. */
79
+ hasLoyaltyProgram?: boolean;
78
80
  };
79
81
  }
80
82
  declare function fetchStoreCapabilities(connectionId: string, baseUrl?: string): Promise<StoreCapabilities>;