@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/http.js CHANGED
@@ -721,17 +721,19 @@ function getPaymentProvidersSection() {
721
721
  return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
722
722
 
723
723
  \`\`\`typescript
724
- const { hasPayments, providers } = await client.getPaymentProviders();
724
+ const { hasPayments, providers, defaultProvider } = await client.getPaymentProviders();
725
725
  if (!hasPayments) {
726
726
  // Show error: "Payment is not configured for this store"
727
727
  return;
728
728
  }
729
- const stripeProvider = providers.find(p => p.provider === 'stripe');
730
- const growProvider = providers.find(p => p.provider === 'grow');
731
- const paypalProvider = providers.find(p => p.provider === 'paypal');
729
+ // Primary vs additive (Shopify-parity): render wallets as express buttons ABOVE the card form.
730
+ const expressMethods = providers.filter(p => p.isAdditive); // e.g. PayPal (WALLET)
731
+ const primary = defaultProvider && !defaultProvider.isAdditive ? defaultProvider : undefined;
732
732
  \`\`\`
733
733
 
734
- 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\`.
734
+ 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).
735
+
736
+ **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.)
735
737
 
736
738
  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**:
737
739
 
@@ -1119,11 +1121,22 @@ client-side overlay needed. Swatch colors are language-agnostic.
1119
1121
  function ProductDescription({ product }: { product: Product }) {
1120
1122
  const content = getDescriptionContent(product);
1121
1123
  if (!content) return null;
1122
- if ('html' in content) return <div dangerouslySetInnerHTML={{ __html: content.html }} />;
1124
+ // Descriptions are merchant HTML and may contain <video> + host-locked
1125
+ // YouTube/Vimeo <iframe> embeds. NEVER render unsanitized \u2014 sanitize and
1126
+ // allowlist video/source + iframe (restricted to youtube/vimeo hosts).
1127
+ if ('html' in content) {
1128
+ return <div dangerouslySetInnerHTML={{ __html: sanitizeProductHtml(content.html) }} />;
1129
+ }
1123
1130
  return <p>{content.text}</p>;
1124
1131
  }
1125
1132
  \`\`\`
1126
1133
 
1134
+ **Sanitize descriptions.** \`sanitizeProductHtml\` is the host-locked sanitizer the
1135
+ \`create-brainerce-store\` scaffold ships at \`src/lib/sanitize-html.ts\`. If you roll
1136
+ your own, allow \`video\`/\`source\` and \`iframe\` **restricted** to \`www.youtube.com\`,
1137
+ \`www.youtube-nocookie.com\`, \`player.vimeo.com\` (never arbitrary iframes), and add
1138
+ those hosts to your CSP \`frame-src\` or embeds render blank.
1139
+
1127
1140
  ### Metafields (Custom Product Fields) \u2014 display on product detail!
1128
1141
 
1129
1142
  **IMPORTANT: Render metafield values based on \`mf.type\`!** Don't just display \`mf.value\` as text for all types.
@@ -1206,7 +1219,28 @@ const { url } = await client.uploadCustomizationFile(file);
1206
1219
  // Then use the URL in metadata: metadata: { logo: url }
1207
1220
  \`\`\`
1208
1221
 
1209
- **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\`, \`defaultValue\`, \`position\`
1222
+ **Customization field properties:** \`key\`, \`name\` (display label), \`type\`, \`required\`, \`minLength\`, \`maxLength\`, \`minValue\`, \`maxValue\`, \`enumValues\` (array of \`{label, value, swatchColor?, swatchImageUrl?}\`), \`defaultValue\`, \`position\`
1223
+
1224
+ **SELECT / MULTI_SELECT \u2014 use \`option.value\` to submit, \`option.label\` to display:**
1225
+
1226
+ \`\`\`typescript
1227
+ for (const option of field.enumValues ?? []) {
1228
+ // option.value \u2192 what to submit in metadata
1229
+ // option.label \u2192 what to show the customer
1230
+ // option.swatchColor \u2192 optional hex color for color swatches
1231
+ }
1232
+ \`\`\`
1233
+
1234
+ **Display customizations in cart / checkout \u2014 no extra API call needed:**
1235
+
1236
+ CartItem and CheckoutLineItem both include a \`customizations\` object alongside \`metadata\`. Use \`customizations\` for display \u2014 it contains resolved labels, not raw keys.
1237
+
1238
+ \`\`\`typescript
1239
+ // Works for CartItem, CheckoutLineItem, and OrderItem \u2014 same shape
1240
+ const lines = Object.values(item.customizations ?? {})
1241
+ .map(c => \`\${c.label}: \${Array.isArray(c.value) ? c.value.join(', ') : c.value}\`);
1242
+ // e.g. ["Frame color: Gold", "Add-ons: Gift wrap"]
1243
+ \`\`\`
1210
1244
 
1211
1245
  ### Downloadable / Digital Products
1212
1246
 
@@ -1748,7 +1782,7 @@ The merchant can hide a group entirely for one variant by setting \`maxOverride:
1748
1782
 
1749
1783
  ### Restaurant features (advanced)
1750
1784
 
1751
- 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.`;
1785
+ 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.`;
1752
1786
  }
1753
1787
  function getProductReviewsSection() {
1754
1788
  return `## Product Reviews (\u2605 ratings + JSON-LD for SEO)
@@ -3238,6 +3272,85 @@ by device) and can also be asked of the AI assistant ("how many visits did I get
3238
3272
  this month and from where?").
3239
3273
  `;
3240
3274
  }
3275
+ function getShippingAppSection() {
3276
+ return `## App Store Shipping (Shippo)
3277
+
3278
+ Merchants install Shippo from the Brainerce App Store. After connecting their own Shippo account
3279
+ (OAuth), live carrier rates appear automatically at checkout alongside any manually configured
3280
+ zone rates. **Billing goes directly to the merchant's Shippo account \u2014 Brainerce never charges.**
3281
+
3282
+ ### Checkout \u2014 live carrier rates
3283
+
3284
+ No SDK changes needed. Once Shippo is installed and configured, the existing
3285
+ \`getShippingRates()\` call returns both manual zone rates and live Shippo quotes:
3286
+
3287
+ \`\`\`typescript
3288
+ const { rates } = await client.setShippingAddress(checkoutId, { ... });
3289
+ // rates may include:
3290
+ // { id: 'shippo:rate_8f123abc', name: 'USPS Priority Mail', price: '8.50', currency: 'USD', source: 'carrier' }
3291
+ \`\`\`
3292
+
3293
+ Rates prefixed \`shippo:\` are live Shippo quotes. Pass the full \`id\` as \`shippingRateId\`
3294
+ when calling \`selectShippingMethod()\`.
3295
+
3296
+ ### Admin \u2014 purchase a shipping label
3297
+
3298
+ After the order is placed, the merchant purchases a label by passing the Shippo rate object_id:
3299
+
3300
+ \`\`\`typescript
3301
+ const label = await admin.createShippingLabel(orderId, {
3302
+ shippoRateObjectId: 'rate_8f123456789abcdef',
3303
+ });
3304
+ console.log(label.labelUrl); // PDF ready to print
3305
+ console.log(label.trackingNumber); // auto-populated on the order
3306
+ \`\`\`
3307
+
3308
+ The \`trackingNumber\` is stored on the \`Order\` and returned in \`getMyOrders()\`
3309
+ so customers can track their shipment automatically.
3310
+
3311
+ **SDK mode:** \`createShippingLabel()\` requires \`apiKey\` (admin mode only).`;
3312
+ }
3313
+ function getLoyaltySection() {
3314
+ return `# Loyalty & Rewards
3315
+
3316
+ Logged-in customers earn points on paid orders and redeem them for a one-time discount coupon at checkout.
3317
+
3318
+ > **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.
3319
+
3320
+ 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**.
3321
+
3322
+ ## Status
3323
+
3324
+ \`\`\`typescript
3325
+ // requires client.setCustomerToken(auth.token)
3326
+ const status = await client.getLoyaltyStatus();
3327
+ // { enrolled, pointsBalance, lifetimeEarned, program: { pointsName, currencyRatio, status } | null }
3328
+ \`\`\`
3329
+
3330
+ - \`program === null\` \u2192 the store has no loyalty program; hide the loyalty UI.
3331
+ - \`program.status !== 'ACTIVE'\` \u2192 program paused/draft; hide the loyalty UI.
3332
+
3333
+ ## Enroll (only if auto-enroll is off)
3334
+
3335
+ \`\`\`typescript
3336
+ await client.enrollInLoyalty(); // same status shape; no-op if already enrolled
3337
+ \`\`\`
3338
+
3339
+ ## Rewards & redemption
3340
+
3341
+ \`\`\`typescript
3342
+ const rewards = await client.getAvailableRewards();
3343
+ // [{ id, name, description, pointsCost, discountValue, minOrderAmount, maxUsesPerUser, isActive }]
3344
+
3345
+ const { couponCode, discountValue, pointsBalance } = await client.redeemLoyaltyReward(rewardId);
3346
+ await client.applyCoupon(cartId, couponCode); // applies via the normal coupon engine
3347
+ \`\`\`
3348
+
3349
+ - \`redeemLoyaltyReward\` mints a one-time fixed-amount coupon (\`usageLimit: 1\`). Apply it with the normal \`applyCoupon\` flow.
3350
+ - Throws if the customer has insufficient points. If minting fails, points are refunded automatically.
3351
+ - Disable a reward's redeem button when \`pointsBalance < reward.pointsCost\`.
3352
+ `;
3353
+ }
3241
3354
  function getSectionByTopic(topic, connectionId, currency) {
3242
3355
  const cid = connectionId || "vc_YOUR_CONNECTION_ID";
3243
3356
  const cur = currency || "USD";
@@ -3264,6 +3377,8 @@ function getSectionByTopic(topic, connectionId, currency) {
3264
3377
  return getInventorySection();
3265
3378
  case "discounts":
3266
3379
  return getDiscountsSection();
3380
+ case "loyalty":
3381
+ return getLoyaltySection();
3267
3382
  case "recommendations":
3268
3383
  return getRecommendationsSection();
3269
3384
  case "reviews":
@@ -3297,6 +3412,9 @@ function getSectionByTopic(topic, connectionId, currency) {
3297
3412
  case "analytics":
3298
3413
  case "traffic-analytics":
3299
3414
  return getStorefrontAnalyticsSection(cid);
3415
+ case "shipping-app":
3416
+ case "shippo":
3417
+ return getShippingAppSection();
3300
3418
  case "all":
3301
3419
  return [
3302
3420
  "# Brainerce SDK \u2014 full topic dump",
@@ -3427,6 +3545,7 @@ var GET_SDK_DOCS_SCHEMA = {
3427
3545
  "order-confirmation",
3428
3546
  "inventory",
3429
3547
  "discounts",
3548
+ "loyalty",
3430
3549
  "recommendations",
3431
3550
  "reviews",
3432
3551
  "product-customization-fields",
@@ -3579,11 +3698,18 @@ interface ProductCustomizationField {
3579
3698
  maxLength?: number | null;
3580
3699
  minValue?: number | null; // NUMBER bounds
3581
3700
  maxValue?: number | null;
3582
- enumValues?: string[]; // REQUIRED for SELECT / MULTI_SELECT
3701
+ enumValues?: CustomizationFieldOption[]; // REQUIRED for SELECT / MULTI_SELECT
3583
3702
  defaultValue?: string | null;
3584
3703
  position: number; // Render order
3585
3704
  }
3586
3705
 
3706
+ interface CustomizationFieldOption {
3707
+ label: string; // Display label (e.g., "Rose Gold")
3708
+ value: string; // Value stored in CartItem.metadata
3709
+ swatchColor?: string | null; // Optional hex color (e.g., "#B76E79")
3710
+ swatchImageUrl?: string | null; // Optional swatch image URL
3711
+ }
3712
+
3587
3713
  interface ProductQueryParams {
3588
3714
  page?: number;
3589
3715
  limit?: number;
@@ -3677,6 +3803,7 @@ interface CartItem {
3677
3803
  image?: ProductImage | string | null;
3678
3804
  } | null;
3679
3805
  metadata?: Record<string, unknown> | null; // Buyer-submitted customization values, keyed by ProductCustomizationField.key
3806
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 use this to display "Color: Silver" without extra getProduct() call
3680
3807
  createdAt: string;
3681
3808
  updatedAt: string;
3682
3809
  }
@@ -3776,6 +3903,7 @@ interface CheckoutLineItem {
3776
3903
  product: { id: string; name: string; sku: string; images?: ProductImage[]; };
3777
3904
  variant?: { id: string; name?: string | null; sku?: string | null; image?: ProductImage | string | null; } | null;
3778
3905
  metadata?: Record<string, unknown> | null; // Copied from CartItem.metadata \u2014 buyer customization values
3906
+ customizations?: Record<string, { label: string; value: string | string[]; type: string }>; // Resolved labels \u2014 same shape as OrderItem.customizations
3779
3907
  }
3780
3908
 
3781
3909
  interface CheckoutAddress {
@@ -4012,7 +4140,10 @@ interface PaymentProviderConfig {
4012
4140
  stripeAccountId?: string; // Stripe Connect only
4013
4141
  supportedMethods: string[];
4014
4142
  testMode: boolean;
4015
- isDefault: boolean;
4143
+ isDefault: boolean; // only a primary (CREDIT_CARD) is ever the default
4144
+ methodType: 'CREDIT_CARD' | 'WALLET' | 'OFFSITE' | 'LOCAL' | 'REDEEMABLE';
4145
+ presentation: 'card_form' | 'express_button' | 'redirect' | 'local_option' | 'redeemable_field';
4146
+ isAdditive: boolean; // wallet/alt method shown ALONGSIDE the primary (e.g. PayPal)
4016
4147
  }
4017
4148
  type PaymentProvider = PaymentProviderConfig;
4018
4149
 
@@ -4752,6 +4883,43 @@ interface TaxEstimateResponse {
4752
4883
  // Non-binding by design: the authoritative tax runs at checkout against the
4753
4884
  // full shipping address. The country comes from your edge runtime
4754
4885
  // (CF-IPCountry / request.geo.country / etc.), NOT the request IP.`;
4886
+ var LOYALTY_TYPES = `// ---- Loyalty & Rewards (storefront mode only) ----
4887
+ interface LoyaltyStatus {
4888
+ enrolled: boolean;
4889
+ pointsBalance: number; // redeemable points (pending earns excluded)
4890
+ lifetimeEarned: number;
4891
+ program: {
4892
+ pointsName: string; // e.g. "points", "coins"
4893
+ currencyRatio: number; // points needed to redeem 1 unit of store currency
4894
+ status: 'DRAFT' | 'ACTIVE' | 'PAUSED';
4895
+ } | null; // null when the store has no loyalty program
4896
+ }
4897
+
4898
+ interface LoyaltyReward {
4899
+ id: string;
4900
+ name: string;
4901
+ description: string | null;
4902
+ pointsCost: number;
4903
+ discountValue: number; // fixed amount off, store currency
4904
+ minOrderAmount: number | null;
4905
+ maxUsesPerUser: number;
4906
+ isActive: boolean;
4907
+ createdAt: string;
4908
+ updatedAt: string;
4909
+ }
4910
+
4911
+ interface RedeemRewardResult {
4912
+ couponCode: string; // one-time coupon to apply at checkout
4913
+ discountValue: number;
4914
+ pointsSpent: number;
4915
+ pointsBalance: number; // remaining balance after redemption
4916
+ }
4917
+
4918
+ // client.getLoyaltyStatus(): Promise<LoyaltyStatus>
4919
+ // client.enrollInLoyalty(): Promise<LoyaltyStatus>
4920
+ // client.getAvailableRewards(): Promise<LoyaltyReward[]>
4921
+ // client.redeemLoyaltyReward(rewardId: string): Promise<RedeemRewardResult>
4922
+ `;
4755
4923
  var TYPES_BY_DOMAIN = {
4756
4924
  products: PRODUCTS_TYPES,
4757
4925
  cart: CART_TYPES,
@@ -4764,7 +4932,8 @@ var TYPES_BY_DOMAIN = {
4764
4932
  reviews: REVIEWS_TYPES,
4765
4933
  "modifier-groups": MODIFIER_GROUPS_TYPES,
4766
4934
  content: CONTENT_TYPES,
4767
- regions: REGIONS_TYPES
4935
+ regions: REGIONS_TYPES,
4936
+ loyalty: LOYALTY_TYPES
4768
4937
  };
4769
4938
  function getTypesByDomain(domain) {
4770
4939
  if (domain === "all") {
@@ -4798,6 +4967,7 @@ var GET_TYPE_DEFINITIONS_SCHEMA = {
4798
4967
  "modifier-groups",
4799
4968
  "content",
4800
4969
  "regions",
4970
+ "loyalty",
4801
4971
  "all"
4802
4972
  ]).describe(
4803
4973
  '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.'
@@ -5060,7 +5230,15 @@ const checkout = await client.getCheckout(checkoutId);
5060
5230
  import { client } from './brainerce';
5061
5231
 
5062
5232
  const { providers, hasPayments, defaultProvider } = await client.getPaymentProviders();
5063
- // each provider: { id, provider, name, publicKey, supportedMethods, testMode, isDefault, clientSdk? }
5233
+ // each provider: { id, provider, name, publicKey, supportedMethods, testMode,
5234
+ // isDefault, methodType, presentation, isAdditive, clientSdk? }
5235
+ //
5236
+ // methodType splits providers into ONE primary card processor (CREDIT_CARD, the
5237
+ // defaultProvider that settles the order) and additive methods (isAdditive:true,
5238
+ // e.g. PayPal as a WALLET). Render additive methods as express buttons ABOVE the
5239
+ // card form \u2014 they sit alongside the primary, never replace it. Call
5240
+ // createPaymentIntent({ providerId }) with the tapped provider's id. (Wallet-only
5241
+ // store: with no card processor, the wallet becomes defaultProvider and stands alone.)
5064
5242
  //
5065
5243
  // After createPaymentIntent, the response carries a clientSdk.renderType that
5066
5244
  // tells your UI how to render the payment step. The 5 possible values:
@@ -6236,6 +6414,9 @@ function formatCapabilities(caps) {
6236
6414
  lines.push(
6237
6415
  caps.features.hasCheckoutCustomFields ? "\u2705 Checkout custom fields configured (surcharges may apply)" : "\u274C No checkout custom fields"
6238
6416
  );
6417
+ lines.push(
6418
+ caps.features.hasLoyaltyProgram ? "\u2705 Loyalty program active (points & rewards \u2014 storefront-mode only)" : "\u274C No loyalty program"
6419
+ );
6239
6420
  lines.push("");
6240
6421
  lines.push("## Connection Settings");
6241
6422
  lines.push(
@@ -6281,6 +6462,11 @@ function formatCapabilities(caps) {
6281
6462
  if (caps.features.hasCoupons) {
6282
6463
  suggestions.push('Add a coupon input to the cart. Use get-code-example("coupon-input").');
6283
6464
  }
6465
+ if (caps.features.hasLoyaltyProgram) {
6466
+ suggestions.push(
6467
+ "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."
6468
+ );
6469
+ }
6284
6470
  if (caps.features.hasCheckoutCustomFields) {
6285
6471
  suggestions.push(
6286
6472
  `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").`