@brainerce/mcp-server 2.6.0 → 2.8.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.js CHANGED
@@ -438,6 +438,234 @@ function PayPalPaymentForm({ clientId, orderId, checkoutId }: { clientId: string
438
438
  }
439
439
  \`\`\``;
440
440
  }
441
+ function getCheckoutCustomFieldsSection() {
442
+ return `## Checkout Custom Fields (Optional Step Between Shipping & Payment)
443
+
444
+ Some stores configure **custom fields** at checkout \u2014 e.g., gift wrapping, floor number for delivery, installation date, custom engraving. Each field can add a **surcharge** to the order total. The server filters by visibility (always / shipping-only / pickup-only / cart-products) and calculates surcharges automatically.
445
+
446
+ ### When to Build This
447
+
448
+ Check capabilities first:
449
+ \`\`\`typescript
450
+ const caps = await fetch(\`/api/vc/\${connectionId}/capabilities\`).then(r => r.json());
451
+ if (caps.features.hasCheckoutCustomFields) {
452
+ // Build the step described below
453
+ }
454
+ \`\`\`
455
+
456
+ If \`hasCheckoutCustomFields\` is true, you MUST build this step or customers will be charged surcharges they never saw.
457
+
458
+ ### Where to Place It
459
+
460
+ After \`selectShippingMethod()\` (or pickup confirmation), BEFORE \`createPaymentIntent()\`. Pseudocode:
461
+
462
+ \`\`\`
463
+ address \u2192 shipping/pickup \u2192 custom-fields (NEW) \u2192 payment
464
+ \`\`\`
465
+
466
+ If the loaded fields array is empty for this checkout (visibility filters can hide all fields), **skip the step entirely** \u2014 go straight to payment with no UI flicker.
467
+
468
+ ### Loading Field Definitions
469
+
470
+ \`\`\`typescript
471
+ import type { CheckoutCustomFieldDefinition } from 'brainerce';
472
+
473
+ const fields = await client.getCheckoutCustomFields(checkoutId);
474
+ if (fields.length === 0) {
475
+ setStep('payment'); // Nothing to collect
476
+ return;
477
+ }
478
+ \`\`\`
479
+
480
+ Each \`CheckoutCustomFieldDefinition\` has:
481
+ - \`key\` \u2014 the form field name to use in your state
482
+ - \`name\` \u2014 display label
483
+ - \`description\` \u2014 helper text (render below the input)
484
+ - \`type\` \u2014 \`'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'SELECT' | 'DATE'\`
485
+ - \`required\` \u2014 boolean
486
+ - \`minLength\` / \`maxLength\` \u2014 for TEXT/TEXTAREA
487
+ - \`minValue\` / \`maxValue\` \u2014 for NUMBER
488
+ - \`options\` \u2014 \`Array<{value, label}>\` for SELECT
489
+ - \`pricing\` \u2014 never render this; the server uses it to compute surcharges
490
+
491
+ ### Rendering All 6 Field Types
492
+
493
+ \`\`\`typescript
494
+ function CustomFieldsStep({ fields, values, onChange, onApply, loading }: {
495
+ fields: CheckoutCustomFieldDefinition[];
496
+ values: Record<string, unknown>;
497
+ onChange: (key: string, value: unknown) => void;
498
+ onApply: () => void;
499
+ loading?: boolean;
500
+ }) {
501
+ return (
502
+ <div className="space-y-4">
503
+ {fields.map((field) => {
504
+ const value = values[field.key] ?? '';
505
+ const label = (
506
+ <label className="block text-sm font-medium">
507
+ {field.name}{field.required && <span className="text-red-500"> *</span>}
508
+ </label>
509
+ );
510
+ const help = field.description && (
511
+ <p className="text-xs text-gray-500 mt-1">{field.description}</p>
512
+ );
513
+
514
+ switch (field.type) {
515
+ case 'TEXT':
516
+ return (
517
+ <div key={field.key}>
518
+ {label}
519
+ <input
520
+ type="text"
521
+ value={value as string}
522
+ onChange={(e) => onChange(field.key, e.target.value)}
523
+ required={field.required}
524
+ minLength={field.minLength ?? undefined}
525
+ maxLength={field.maxLength ?? undefined}
526
+ className="w-full border rounded px-3 py-2"
527
+ />
528
+ {help}
529
+ </div>
530
+ );
531
+ case 'TEXTAREA':
532
+ return (
533
+ <div key={field.key}>
534
+ {label}
535
+ <textarea
536
+ value={value as string}
537
+ onChange={(e) => onChange(field.key, e.target.value)}
538
+ required={field.required}
539
+ minLength={field.minLength ?? undefined}
540
+ maxLength={field.maxLength ?? undefined}
541
+ rows={3}
542
+ className="w-full border rounded px-3 py-2"
543
+ />
544
+ {help}
545
+ </div>
546
+ );
547
+ case 'NUMBER':
548
+ return (
549
+ <div key={field.key}>
550
+ {label}
551
+ <input
552
+ type="number"
553
+ value={value as number}
554
+ onChange={(e) => onChange(field.key, e.target.valueAsNumber)}
555
+ required={field.required}
556
+ min={field.minValue ?? undefined}
557
+ max={field.maxValue ?? undefined}
558
+ className="w-full border rounded px-3 py-2"
559
+ />
560
+ {help}
561
+ </div>
562
+ );
563
+ case 'BOOLEAN':
564
+ return (
565
+ <div key={field.key} className="flex items-start gap-2">
566
+ <input
567
+ type="checkbox"
568
+ checked={value === true}
569
+ onChange={(e) => onChange(field.key, e.target.checked)}
570
+ className="mt-1"
571
+ />
572
+ <div>{label}{help}</div>
573
+ </div>
574
+ );
575
+ case 'SELECT':
576
+ return (
577
+ <div key={field.key}>
578
+ {label}
579
+ <select
580
+ value={value as string}
581
+ onChange={(e) => onChange(field.key, e.target.value)}
582
+ required={field.required}
583
+ className="w-full border rounded px-3 py-2"
584
+ >
585
+ <option value="">\u2014 Select \u2014</option>
586
+ {field.options?.map((opt) => (
587
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
588
+ ))}
589
+ </select>
590
+ {help}
591
+ </div>
592
+ );
593
+ case 'DATE':
594
+ return (
595
+ <div key={field.key}>
596
+ {label}
597
+ <input
598
+ type="date"
599
+ value={value as string}
600
+ onChange={(e) => onChange(field.key, e.target.value)}
601
+ required={field.required}
602
+ className="w-full border rounded px-3 py-2"
603
+ />
604
+ {help}
605
+ </div>
606
+ );
607
+ default:
608
+ return null;
609
+ }
610
+ })}
611
+
612
+ <button
613
+ type="button"
614
+ onClick={onApply}
615
+ disabled={loading}
616
+ className="w-full bg-black text-white py-3 rounded"
617
+ >
618
+ {loading ? 'Updating\u2026' : 'Continue to Payment'}
619
+ </button>
620
+ </div>
621
+ );
622
+ }
623
+ \`\`\`
624
+
625
+ ### Submitting Values & Recalculating Surcharges
626
+
627
+ \`\`\`typescript
628
+ async function applyCustomFields() {
629
+ setLoading(true);
630
+ try {
631
+ // Server validates required + min/max + coerces types + recalculates totals
632
+ const updated = await client.setCheckoutCustomFields(checkoutId, customFieldValues);
633
+ setCheckout(updated); // updated.surchargeAmount and updated.appliedSurcharges are now fresh
634
+ setStep('payment');
635
+ } catch (err) {
636
+ setError(err instanceof Error ? err.message : 'Failed to save selections');
637
+ } finally {
638
+ setLoading(false);
639
+ }
640
+ }
641
+ \`\`\`
642
+
643
+ ### Order Summary \u2014 Show the Surcharges
644
+
645
+ **Critical:** if you don't render \`appliedSurcharges\`, customers will see a higher total with no explanation. Add this to your order summary above the total row:
646
+
647
+ \`\`\`typescript
648
+ {checkout.appliedSurcharges?.length > 0 && (
649
+ <>
650
+ {checkout.appliedSurcharges.map((s) => (
651
+ <div key={s.key} className="flex justify-between text-sm text-gray-600">
652
+ <span>{s.name}</span>
653
+ <span>{formatPrice(s.amount, { currency })}</span>
654
+ </div>
655
+ ))}
656
+ </>
657
+ )}
658
+ \`\`\`
659
+
660
+ \`checkout.surchargeAmount\` is a string (use \`parseFloat()\`); \`checkout.totalAmount\` already includes it.
661
+
662
+ ### Notes
663
+
664
+ - The server filters fields by \`visibility\` (\`always\` / \`shipping\` / \`pickup\` / \`products\`) \u2014 your client just renders what \`getCheckoutCustomFields()\` returns.
665
+ - The server validates \`required\`, \`minLength\`/\`maxLength\`, \`minValue\`/\`maxValue\`, and SELECT options. Don't rely on client validation alone \u2014 surface server errors with a toast.
666
+ - If \`getCheckoutCustomFields()\` returns 0 fields, skip the step entirely. Don't render an empty UI.
667
+ `;
668
+ }
441
669
  function getPaymentProvidersSection() {
442
670
  return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
443
671
 
@@ -1287,6 +1515,10 @@ ${getCheckoutFlowSection(currency)}
1287
1515
 
1288
1516
  ---
1289
1517
 
1518
+ ${getCheckoutCustomFieldsSection()}
1519
+
1520
+ ---
1521
+
1290
1522
  ${getPaymentProvidersSection()}
1291
1523
 
1292
1524
  ---
@@ -1345,6 +1577,8 @@ function getSectionByTopic(topic, connectionId, currency) {
1345
1577
  return getCartSection(cur);
1346
1578
  case "checkout":
1347
1579
  return getCheckoutFlowSection(cur);
1580
+ case "checkout-custom-fields":
1581
+ return getCheckoutCustomFieldsSection();
1348
1582
  case "payment":
1349
1583
  return getPaymentProvidersSection();
1350
1584
  case "auth":
@@ -1373,7 +1607,7 @@ function getSectionByTopic(topic, connectionId, currency) {
1373
1607
  storeType: "E-commerce store"
1374
1608
  });
1375
1609
  default:
1376
- return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, critical-rules, type-reference, admin, all`;
1610
+ return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, checkout-custom-fields, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, critical-rules, type-reference, admin, all`;
1377
1611
  }
1378
1612
  }
1379
1613
 
@@ -1386,6 +1620,7 @@ var GET_SDK_DOCS_SCHEMA = {
1386
1620
  "products",
1387
1621
  "cart",
1388
1622
  "checkout",
1623
+ "checkout-custom-fields",
1389
1624
  "payment",
1390
1625
  "auth",
1391
1626
  "order-confirmation",
@@ -3407,6 +3642,7 @@ import type {
3407
3642
  ShippingDestinations,
3408
3643
  PickupLocation,
3409
3644
  CheckoutBumpsResponse,
3645
+ CheckoutCustomFieldDefinition,
3410
3646
  } from 'brainerce';
3411
3647
  import { formatPrice } from 'brainerce';
3412
3648
  import { getClient } from '@/lib/brainerce';
@@ -3416,6 +3652,7 @@ import { ShippingStep } from '@/components/checkout/shipping-step';
3416
3652
  import { PaymentStep } from '@/components/checkout/payment-step';
3417
3653
  import { DeliveryMethodStep } from '@/components/checkout/delivery-method-step';
3418
3654
  import { PickupStep } from '@/components/checkout/pickup-step';
3655
+ import { CustomFieldsStep } from '@/components/checkout/custom-fields-step';
3419
3656
  import { TaxDisplay } from '@/components/checkout/tax-display';
3420
3657
  import { OrderBumpCard } from '@/components/checkout/order-bump-card';
3421
3658
  import { CouponInput } from '@/components/cart/coupon-input';
@@ -3424,7 +3661,7 @@ import { LoadingSpinner } from '@/components/shared/loading-spinner';
3424
3661
  import { useTranslations } from '@/lib/translations';
3425
3662
  import { cn } from '@/lib/utils';
3426
3663
 
3427
- type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'payment';
3664
+ type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
3428
3665
 
3429
3666
  function CheckoutContent() {
3430
3667
  const searchParams = useSearchParams();
@@ -3457,6 +3694,9 @@ function CheckoutContent() {
3457
3694
  const [orderBumps, setOrderBumps] = useState<CheckoutBumpsResponse | null>(null);
3458
3695
  const [addedBumpIds, setAddedBumpIds] = useState<Set<string>>(new Set());
3459
3696
  const [bumpLoading, setBumpLoading] = useState<string | null>(null);
3697
+ const [customFields, setCustomFields] = useState<CheckoutCustomFieldDefinition[]>([]);
3698
+ const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({});
3699
+ const [customFieldsLoading, setCustomFieldsLoading] = useState(false);
3460
3700
 
3461
3701
  // Check for returning from canceled payment
3462
3702
  const canceled = searchParams.get('canceled') === 'true';
@@ -3508,6 +3748,23 @@ function CheckoutContent() {
3508
3748
  const existing = await client.getCheckout(existingCheckoutId);
3509
3749
  setCheckout(existing);
3510
3750
 
3751
+ // Preload custom field definitions and any existing values so the
3752
+ // step indicator and "change options" affordance work on resume.
3753
+ client
3754
+ .getCheckoutCustomFields(existing.id)
3755
+ .then((fields) => {
3756
+ setCustomFields(fields);
3757
+ const existingValues = (
3758
+ existing as unknown as {
3759
+ customFieldValues?: Record<string, unknown> | null;
3760
+ }
3761
+ ).customFieldValues;
3762
+ if (existingValues) setCustomFieldValues(existingValues);
3763
+ })
3764
+ .catch(() => {
3765
+ setCustomFields([]);
3766
+ });
3767
+
3511
3768
  // Determine step based on checkout state
3512
3769
  const allDigital = existing.lineItems.every(
3513
3770
  (i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
@@ -3678,6 +3935,21 @@ function CheckoutContent() {
3678
3935
  }
3679
3936
  }
3680
3937
 
3938
+ // After shipping/pickup is set, decide whether to show the custom-fields step
3939
+ // or jump straight to payment. Returns the next step.
3940
+ async function loadCustomFieldsOrSkip(checkoutId: string): Promise<CheckoutStep> {
3941
+ try {
3942
+ const fields = await getClient().getCheckoutCustomFields(checkoutId);
3943
+ setCustomFields(fields);
3944
+ return fields.length > 0 ? 'custom-fields' : 'payment';
3945
+ } catch {
3946
+ // If the endpoint isn't available or fails, fall through to payment
3947
+ // rather than blocking the customer.
3948
+ setCustomFields([]);
3949
+ return 'payment';
3950
+ }
3951
+ }
3952
+
3681
3953
  // Handle shipping method selection
3682
3954
  async function handleShippingSelect(rateId: string) {
3683
3955
  if (!checkout) return;
@@ -3690,7 +3962,7 @@ function CheckoutContent() {
3690
3962
 
3691
3963
  const updated = await client.selectShippingMethod(checkout.id, rateId);
3692
3964
  setCheckout(updated);
3693
- setStep('payment');
3965
+ setStep(await loadCustomFieldsOrSkip(updated.id));
3694
3966
  } catch (err) {
3695
3967
  const message = err instanceof Error ? err.message : t('failedToSelectShipping');
3696
3968
  setError(message);
@@ -3699,6 +3971,23 @@ function CheckoutContent() {
3699
3971
  }
3700
3972
  }
3701
3973
 
3974
+ // Submit custom fields
3975
+ async function handleCustomFieldsApply() {
3976
+ if (!checkout) return;
3977
+ try {
3978
+ setCustomFieldsLoading(true);
3979
+ setError(null);
3980
+ const updated = await getClient().setCheckoutCustomFields(checkout.id, customFieldValues);
3981
+ setCheckout(updated);
3982
+ setStep('payment');
3983
+ } catch (err) {
3984
+ const message = err instanceof Error ? err.message : t('customFieldsFailed');
3985
+ setError(message);
3986
+ } finally {
3987
+ setCustomFieldsLoading(false);
3988
+ }
3989
+ }
3990
+
3702
3991
  // Handle delivery method selection
3703
3992
  async function handleDeliveryTypeSelect(method: 'shipping' | 'pickup') {
3704
3993
  if (!checkout) return;
@@ -3744,7 +4033,7 @@ function CheckoutContent() {
3744
4033
  phone: customerInfo.phone,
3745
4034
  });
3746
4035
  setCheckout(updated);
3747
- setStep('payment');
4036
+ setStep(await loadCustomFieldsOrSkip(updated.id));
3748
4037
  } catch (err) {
3749
4038
  const message = err instanceof Error ? err.message : t('failedToSelectPickup');
3750
4039
  setError(message);
@@ -3806,9 +4095,15 @@ function CheckoutContent() {
3806
4095
  );
3807
4096
  }
3808
4097
 
4098
+ const customFieldsStep =
4099
+ customFields.length > 0
4100
+ ? [{ key: 'custom-fields' as CheckoutStep, label: t('stepCustomFields') }]
4101
+ : [];
4102
+
3809
4103
  const steps: { key: CheckoutStep; label: string }[] = isAllDigital
3810
4104
  ? [
3811
4105
  { key: 'address', label: t('stepContactInfo') },
4106
+ ...customFieldsStep,
3812
4107
  { key: 'payment', label: t('stepPayment') },
3813
4108
  ]
3814
4109
  : pickupLocations.length > 0
@@ -3816,17 +4111,20 @@ function CheckoutContent() {
3816
4111
  ? [
3817
4112
  { key: 'method', label: t('stepMethod') },
3818
4113
  { key: 'pickup', label: t('stepPickup') },
4114
+ ...customFieldsStep,
3819
4115
  { key: 'payment', label: t('stepPayment') },
3820
4116
  ]
3821
4117
  : [
3822
4118
  { key: 'method', label: t('stepMethod') },
3823
4119
  { key: 'address', label: t('stepAddress') },
3824
4120
  { key: 'shipping', label: t('stepShipping') },
4121
+ ...customFieldsStep,
3825
4122
  { key: 'payment', label: t('stepPayment') },
3826
4123
  ]
3827
4124
  : [
3828
4125
  { key: 'address', label: t('stepAddress') },
3829
4126
  { key: 'shipping', label: t('stepShipping') },
4127
+ ...customFieldsStep,
3830
4128
  { key: 'payment', label: t('stepPayment') },
3831
4129
  ];
3832
4130
 
@@ -4025,19 +4323,54 @@ function CheckoutContent() {
4025
4323
  </div>
4026
4324
  )}
4027
4325
 
4326
+ {/* Custom Fields (optional, between shipping/pickup and payment) */}
4327
+ {step === 'custom-fields' && checkout && (
4328
+ <div>
4329
+ <div className="mb-4 flex items-center justify-between">
4330
+ <h2 className="text-foreground text-lg font-semibold">{t('customFieldsTitle')}</h2>
4331
+ <button
4332
+ type="button"
4333
+ onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
4334
+ className="text-primary text-sm hover:underline"
4335
+ >
4336
+ {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
4337
+ </button>
4338
+ </div>
4339
+ <CustomFieldsStep
4340
+ fields={customFields}
4341
+ values={customFieldValues}
4342
+ onChange={(key, value) =>
4343
+ setCustomFieldValues((prev) => ({ ...prev, [key]: value }))
4344
+ }
4345
+ onApply={handleCustomFieldsApply}
4346
+ loading={customFieldsLoading}
4347
+ />
4348
+ </div>
4349
+ )}
4350
+
4028
4351
  {/* Payment */}
4029
4352
  {step === 'payment' && checkout && (
4030
4353
  <div>
4031
4354
  <div className="mb-4 flex items-center justify-between">
4032
4355
  <h2 className="text-foreground text-lg font-semibold">{t('payment')}</h2>
4033
- {!isAllDigital && (
4356
+ {customFields.length > 0 ? (
4034
4357
  <button
4035
4358
  type="button"
4036
- onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
4359
+ onClick={() => setStep('custom-fields')}
4037
4360
  className="text-primary text-sm hover:underline"
4038
4361
  >
4039
- {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
4362
+ {t('changeOptions')}
4040
4363
  </button>
4364
+ ) : (
4365
+ !isAllDigital && (
4366
+ <button
4367
+ type="button"
4368
+ onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
4369
+ className="text-primary text-sm hover:underline"
4370
+ >
4371
+ {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
4372
+ </button>
4373
+ )
4041
4374
  )}
4042
4375
  </div>
4043
4376
 
@@ -4224,6 +4557,20 @@ function CheckoutContent() {
4224
4557
  taxBreakdown={checkout.taxBreakdown}
4225
4558
  />
4226
4559
 
4560
+ {/* Custom field surcharges (one line per applied surcharge) */}
4561
+ {checkout.appliedSurcharges && checkout.appliedSurcharges.length > 0 && (
4562
+ <>
4563
+ {checkout.appliedSurcharges.map((s) => (
4564
+ <div key={s.key} className="flex items-center justify-between">
4565
+ <span className="text-muted-foreground">{s.name}</span>
4566
+ <span className="text-foreground">
4567
+ {formatPrice(Number(s.amount), { currency }) as string}
4568
+ </span>
4569
+ </div>
4570
+ ))}
4571
+ </>
4572
+ )}
4573
+
4227
4574
  <div className="border-border mt-2 border-t pt-2">
4228
4575
  <div className="flex items-center justify-between">
4229
4576
  <span className="text-foreground font-semibold">{tc('total')}</span>
@@ -11242,7 +11589,17 @@ async function handleGetRequiredPages(args) {
11242
11589
  var import_zod5 = require("zod");
11243
11590
 
11244
11591
  // src/utils/fetch-store-info.ts
11245
- async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com") {
11592
+ var KNOWN_API_URLS = {
11593
+ production: "https://api.brainerce.com",
11594
+ staging: "https://api-staging.brainerce.com"
11595
+ };
11596
+ var ConnectionNotFoundError = class extends Error {
11597
+ constructor(connectionId, baseUrl) {
11598
+ super(`Connection "${connectionId}" not found at ${baseUrl}`);
11599
+ this.code = "NOT_FOUND";
11600
+ }
11601
+ };
11602
+ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production) {
11246
11603
  const url = `${baseUrl}/api/vc/${connectionId}/info`;
11247
11604
  const controller = new AbortController();
11248
11605
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -11251,55 +11608,90 @@ async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com
11251
11608
  res = await fetch(url, { signal: controller.signal });
11252
11609
  } catch (err) {
11253
11610
  if (err.name === "AbortError") {
11254
- throw new Error("Request timed out");
11611
+ throw new Error(`Request to ${baseUrl} timed out`);
11255
11612
  }
11256
- throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11613
+ throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
11257
11614
  } finally {
11258
11615
  clearTimeout(timeout);
11259
11616
  }
11260
11617
  if (res.status === 404) {
11261
- throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11618
+ throw new ConnectionNotFoundError(connectionId, baseUrl);
11262
11619
  }
11263
11620
  if (!res.ok) {
11264
- throw new Error(`API returned status ${res.status}`);
11621
+ throw new Error(`${baseUrl} returned status ${res.status}`);
11265
11622
  }
11266
11623
  let json;
11267
11624
  try {
11268
11625
  json = await res.json();
11269
11626
  } catch {
11270
- throw new Error("Invalid response from API");
11627
+ throw new Error(`Invalid response from ${baseUrl}`);
11271
11628
  }
11629
+ const storeName = json.storeName || json.name || "My Store";
11630
+ const displayName = json.channelName || storeName;
11272
11631
  return {
11273
- name: json.name || json.storeName || "My Store",
11632
+ name: displayName,
11633
+ storeName,
11274
11634
  currency: json.currency || "USD",
11275
11635
  language: json.language || "en",
11276
11636
  ...json.i18n ? { i18n: json.i18n } : {}
11277
11637
  };
11278
11638
  }
11639
+ async function resolveStoreInfo(connectionId, candidateUrls) {
11640
+ const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
11641
+ if (urls.length === 0) {
11642
+ throw new Error("No API URLs to try");
11643
+ }
11644
+ let lastError;
11645
+ let allNotFound = true;
11646
+ for (let i = 0; i < urls.length; i++) {
11647
+ const baseUrl = urls[i];
11648
+ try {
11649
+ const info = await fetchStoreInfo(connectionId, baseUrl);
11650
+ return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
11651
+ } catch (err) {
11652
+ lastError = err;
11653
+ const isNotFound = err.code === "NOT_FOUND";
11654
+ if (!isNotFound) {
11655
+ allNotFound = false;
11656
+ }
11657
+ }
11658
+ }
11659
+ if (allNotFound) {
11660
+ throw new Error(
11661
+ `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
11662
+ );
11663
+ }
11664
+ throw lastError || new Error("Failed to resolve store info");
11665
+ }
11666
+ function getCandidateApiUrls() {
11667
+ const explicit = (process.env.BRAINERCE_API_URL || "").replace(/\/$/, "");
11668
+ if (explicit) return [explicit];
11669
+ return [KNOWN_API_URLS.production, KNOWN_API_URLS.staging];
11670
+ }
11279
11671
 
11280
11672
  // src/tools/get-store-info.ts
11281
11673
  var GET_STORE_INFO_NAME = "get-store-info";
11282
- var GET_STORE_INFO_DESCRIPTION = "Fetch live store information from the Brainerce API using a connection ID. Returns the store name, currency, and language. Use this to personalize the store being built.";
11674
+ var GET_STORE_INFO_DESCRIPTION = "Fetch live store information from the Brainerce API using a connection ID. Returns the channel display name (what the user sees in their dashboard), parent store name, currency, and language. Use this to personalize the store being built \u2014 prefer the channel name for user-facing text since a single Brainerce store can have multiple sales channels.";
11283
11675
  var GET_STORE_INFO_SCHEMA = {
11284
11676
  connectionId: import_zod5.z.string().describe("Vibe-coded connection ID (starts with vc_)")
11285
11677
  };
11286
11678
  async function handleGetStoreInfo(args) {
11287
11679
  try {
11288
- const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11289
- /\/$/,
11290
- ""
11291
- );
11292
- const info = await fetchStoreInfo(args.connectionId, baseUrl);
11680
+ const resolved = await resolveStoreInfo(args.connectionId, getCandidateApiUrls());
11293
11681
  return {
11294
11682
  content: [
11295
11683
  {
11296
11684
  type: "text",
11297
11685
  text: JSON.stringify(
11298
11686
  {
11299
- name: info.name,
11300
- currency: info.currency,
11301
- language: info.language,
11302
- connectionId: args.connectionId
11687
+ // `name` is the channel display name (from the dashboard sales-channel card);
11688
+ // falls back to storeName when a channel doesn't have its own name.
11689
+ name: resolved.info.name,
11690
+ storeName: resolved.info.storeName,
11691
+ currency: resolved.info.currency,
11692
+ language: resolved.info.language,
11693
+ connectionId: args.connectionId,
11694
+ apiBaseUrl: resolved.apiBaseUrl
11303
11695
  },
11304
11696
  null,
11305
11697
  2
@@ -11320,6 +11712,12 @@ async function handleGetStoreInfo(args) {
11320
11712
  var import_zod6 = require("zod");
11321
11713
 
11322
11714
  // src/utils/fetch-store-capabilities.ts
11715
+ var ConnectionNotFoundError2 = class extends Error {
11716
+ constructor(connectionId, baseUrl) {
11717
+ super(`Connection "${connectionId}" not found at ${baseUrl}`);
11718
+ this.code = "NOT_FOUND";
11719
+ }
11720
+ };
11323
11721
  async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brainerce.com") {
11324
11722
  const url = `${baseUrl}/api/vc/${connectionId}/capabilities`;
11325
11723
  const controller = new AbortController();
@@ -11329,24 +11727,51 @@ async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brain
11329
11727
  res = await fetch(url, { signal: controller.signal });
11330
11728
  } catch (err) {
11331
11729
  if (err.name === "AbortError") {
11332
- throw new Error("Request timed out");
11730
+ throw new Error(`Request to ${baseUrl} timed out`);
11333
11731
  }
11334
- throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11732
+ throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
11335
11733
  } finally {
11336
11734
  clearTimeout(timeout);
11337
11735
  }
11338
11736
  if (res.status === 404) {
11339
- throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11737
+ throw new ConnectionNotFoundError2(connectionId, baseUrl);
11340
11738
  }
11341
11739
  if (!res.ok) {
11342
- throw new Error(`API returned status ${res.status}`);
11740
+ throw new Error(`${baseUrl} returned status ${res.status}`);
11343
11741
  }
11344
11742
  try {
11345
11743
  return await res.json();
11346
11744
  } catch {
11347
- throw new Error("Invalid response from API");
11745
+ throw new Error(`Invalid response from ${baseUrl}`);
11348
11746
  }
11349
11747
  }
11748
+ async function resolveStoreCapabilities(connectionId, candidateUrls) {
11749
+ const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
11750
+ if (urls.length === 0) {
11751
+ throw new Error("No API URLs to try");
11752
+ }
11753
+ let lastError;
11754
+ let allNotFound = true;
11755
+ for (let i = 0; i < urls.length; i++) {
11756
+ const baseUrl = urls[i];
11757
+ try {
11758
+ const capabilities = await fetchStoreCapabilities(connectionId, baseUrl);
11759
+ return { capabilities, apiBaseUrl: baseUrl, fellBack: i > 0 };
11760
+ } catch (err) {
11761
+ lastError = err;
11762
+ const isNotFound = err.code === "NOT_FOUND";
11763
+ if (!isNotFound) {
11764
+ allNotFound = false;
11765
+ }
11766
+ }
11767
+ }
11768
+ if (allNotFound) {
11769
+ throw new Error(
11770
+ `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
11771
+ );
11772
+ }
11773
+ throw lastError || new Error("Failed to resolve store capabilities");
11774
+ }
11350
11775
 
11351
11776
  // src/tools/get-store-capabilities.ts
11352
11777
  var GET_STORE_CAPABILITIES_NAME = "get-store-capabilities";
@@ -11356,7 +11781,8 @@ var GET_STORE_CAPABILITIES_SCHEMA = {
11356
11781
  };
11357
11782
  function formatCapabilities(caps) {
11358
11783
  const lines = [];
11359
- lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
11784
+ const channelSuffix = caps.store.channelName && caps.store.channelName !== caps.store.name ? ` \u2014 Channel: "${caps.store.channelName}"` : "";
11785
+ lines.push(`## Store: ${caps.store.name} (${caps.store.currency})${channelSuffix}`);
11360
11786
  lines.push(`Language: ${caps.store.language}`);
11361
11787
  if (caps.store.i18n?.enabled) {
11362
11788
  lines.push(
@@ -11389,6 +11815,9 @@ function formatCapabilities(caps) {
11389
11815
  caps.features.hasDownloadableProducts ? "\u2705 Downloadable products available" : "\u274C No downloadable products"
11390
11816
  );
11391
11817
  lines.push(caps.features.hasCoupons ? "\u2705 Coupons available" : "\u274C No coupons");
11818
+ lines.push(
11819
+ caps.features.hasCheckoutCustomFields ? "\u2705 Checkout custom fields configured (surcharges may apply)" : "\u274C No checkout custom fields"
11820
+ );
11392
11821
  lines.push("");
11393
11822
  lines.push("## Connection Settings");
11394
11823
  lines.push(
@@ -11434,6 +11863,11 @@ function formatCapabilities(caps) {
11434
11863
  if (caps.features.hasCoupons) {
11435
11864
  suggestions.push('Add a coupon input to the cart. Use get-code-example("coupon-input").');
11436
11865
  }
11866
+ if (caps.features.hasCheckoutCustomFields) {
11867
+ suggestions.push(
11868
+ `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").`
11869
+ );
11870
+ }
11437
11871
  if (caps.connection.requireEmailVerification) {
11438
11872
  suggestions.push(
11439
11873
  'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
@@ -11464,13 +11898,9 @@ function formatCapabilities(caps) {
11464
11898
  }
11465
11899
  async function handleGetStoreCapabilities(args) {
11466
11900
  try {
11467
- const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11468
- /\/$/,
11469
- ""
11470
- );
11471
- const caps = await fetchStoreCapabilities(args.connectionId, baseUrl);
11901
+ const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
11472
11902
  return {
11473
- content: [{ type: "text", text: formatCapabilities(caps) }]
11903
+ content: [{ type: "text", text: formatCapabilities(resolved.capabilities) }]
11474
11904
  };
11475
11905
  } catch (error) {
11476
11906
  const message = error instanceof Error ? error.message : "Failed to fetch store capabilities";
@@ -11733,13 +12163,10 @@ var BUILD_STORE_SCHEMA = {
11733
12163
  storeStyle: import_zod7.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
11734
12164
  };
11735
12165
  async function handleBuildStore(args) {
11736
- const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11737
- /\/$/,
11738
- ""
11739
- );
11740
12166
  let capabilities = null;
11741
12167
  try {
11742
- capabilities = await fetchStoreCapabilities(args.connectionId, baseUrl);
12168
+ const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
12169
+ capabilities = resolved.capabilities;
11743
12170
  } catch {
11744
12171
  }
11745
12172
  const bundle = buildStoreBundle({