@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/bin/http.js CHANGED
@@ -411,6 +411,234 @@ function PayPalPaymentForm({ clientId, orderId, checkoutId }: { clientId: string
411
411
  }
412
412
  \`\`\``;
413
413
  }
414
+ function getCheckoutCustomFieldsSection() {
415
+ return `## Checkout Custom Fields (Optional Step Between Shipping & Payment)
416
+
417
+ 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.
418
+
419
+ ### When to Build This
420
+
421
+ Check capabilities first:
422
+ \`\`\`typescript
423
+ const caps = await fetch(\`/api/vc/\${connectionId}/capabilities\`).then(r => r.json());
424
+ if (caps.features.hasCheckoutCustomFields) {
425
+ // Build the step described below
426
+ }
427
+ \`\`\`
428
+
429
+ If \`hasCheckoutCustomFields\` is true, you MUST build this step or customers will be charged surcharges they never saw.
430
+
431
+ ### Where to Place It
432
+
433
+ After \`selectShippingMethod()\` (or pickup confirmation), BEFORE \`createPaymentIntent()\`. Pseudocode:
434
+
435
+ \`\`\`
436
+ address \u2192 shipping/pickup \u2192 custom-fields (NEW) \u2192 payment
437
+ \`\`\`
438
+
439
+ 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.
440
+
441
+ ### Loading Field Definitions
442
+
443
+ \`\`\`typescript
444
+ import type { CheckoutCustomFieldDefinition } from 'brainerce';
445
+
446
+ const fields = await client.getCheckoutCustomFields(checkoutId);
447
+ if (fields.length === 0) {
448
+ setStep('payment'); // Nothing to collect
449
+ return;
450
+ }
451
+ \`\`\`
452
+
453
+ Each \`CheckoutCustomFieldDefinition\` has:
454
+ - \`key\` \u2014 the form field name to use in your state
455
+ - \`name\` \u2014 display label
456
+ - \`description\` \u2014 helper text (render below the input)
457
+ - \`type\` \u2014 \`'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'SELECT' | 'DATE'\`
458
+ - \`required\` \u2014 boolean
459
+ - \`minLength\` / \`maxLength\` \u2014 for TEXT/TEXTAREA
460
+ - \`minValue\` / \`maxValue\` \u2014 for NUMBER
461
+ - \`options\` \u2014 \`Array<{value, label}>\` for SELECT
462
+ - \`pricing\` \u2014 never render this; the server uses it to compute surcharges
463
+
464
+ ### Rendering All 6 Field Types
465
+
466
+ \`\`\`typescript
467
+ function CustomFieldsStep({ fields, values, onChange, onApply, loading }: {
468
+ fields: CheckoutCustomFieldDefinition[];
469
+ values: Record<string, unknown>;
470
+ onChange: (key: string, value: unknown) => void;
471
+ onApply: () => void;
472
+ loading?: boolean;
473
+ }) {
474
+ return (
475
+ <div className="space-y-4">
476
+ {fields.map((field) => {
477
+ const value = values[field.key] ?? '';
478
+ const label = (
479
+ <label className="block text-sm font-medium">
480
+ {field.name}{field.required && <span className="text-red-500"> *</span>}
481
+ </label>
482
+ );
483
+ const help = field.description && (
484
+ <p className="text-xs text-gray-500 mt-1">{field.description}</p>
485
+ );
486
+
487
+ switch (field.type) {
488
+ case 'TEXT':
489
+ return (
490
+ <div key={field.key}>
491
+ {label}
492
+ <input
493
+ type="text"
494
+ value={value as string}
495
+ onChange={(e) => onChange(field.key, e.target.value)}
496
+ required={field.required}
497
+ minLength={field.minLength ?? undefined}
498
+ maxLength={field.maxLength ?? undefined}
499
+ className="w-full border rounded px-3 py-2"
500
+ />
501
+ {help}
502
+ </div>
503
+ );
504
+ case 'TEXTAREA':
505
+ return (
506
+ <div key={field.key}>
507
+ {label}
508
+ <textarea
509
+ value={value as string}
510
+ onChange={(e) => onChange(field.key, e.target.value)}
511
+ required={field.required}
512
+ minLength={field.minLength ?? undefined}
513
+ maxLength={field.maxLength ?? undefined}
514
+ rows={3}
515
+ className="w-full border rounded px-3 py-2"
516
+ />
517
+ {help}
518
+ </div>
519
+ );
520
+ case 'NUMBER':
521
+ return (
522
+ <div key={field.key}>
523
+ {label}
524
+ <input
525
+ type="number"
526
+ value={value as number}
527
+ onChange={(e) => onChange(field.key, e.target.valueAsNumber)}
528
+ required={field.required}
529
+ min={field.minValue ?? undefined}
530
+ max={field.maxValue ?? undefined}
531
+ className="w-full border rounded px-3 py-2"
532
+ />
533
+ {help}
534
+ </div>
535
+ );
536
+ case 'BOOLEAN':
537
+ return (
538
+ <div key={field.key} className="flex items-start gap-2">
539
+ <input
540
+ type="checkbox"
541
+ checked={value === true}
542
+ onChange={(e) => onChange(field.key, e.target.checked)}
543
+ className="mt-1"
544
+ />
545
+ <div>{label}{help}</div>
546
+ </div>
547
+ );
548
+ case 'SELECT':
549
+ return (
550
+ <div key={field.key}>
551
+ {label}
552
+ <select
553
+ value={value as string}
554
+ onChange={(e) => onChange(field.key, e.target.value)}
555
+ required={field.required}
556
+ className="w-full border rounded px-3 py-2"
557
+ >
558
+ <option value="">\u2014 Select \u2014</option>
559
+ {field.options?.map((opt) => (
560
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
561
+ ))}
562
+ </select>
563
+ {help}
564
+ </div>
565
+ );
566
+ case 'DATE':
567
+ return (
568
+ <div key={field.key}>
569
+ {label}
570
+ <input
571
+ type="date"
572
+ value={value as string}
573
+ onChange={(e) => onChange(field.key, e.target.value)}
574
+ required={field.required}
575
+ className="w-full border rounded px-3 py-2"
576
+ />
577
+ {help}
578
+ </div>
579
+ );
580
+ default:
581
+ return null;
582
+ }
583
+ })}
584
+
585
+ <button
586
+ type="button"
587
+ onClick={onApply}
588
+ disabled={loading}
589
+ className="w-full bg-black text-white py-3 rounded"
590
+ >
591
+ {loading ? 'Updating\u2026' : 'Continue to Payment'}
592
+ </button>
593
+ </div>
594
+ );
595
+ }
596
+ \`\`\`
597
+
598
+ ### Submitting Values & Recalculating Surcharges
599
+
600
+ \`\`\`typescript
601
+ async function applyCustomFields() {
602
+ setLoading(true);
603
+ try {
604
+ // Server validates required + min/max + coerces types + recalculates totals
605
+ const updated = await client.setCheckoutCustomFields(checkoutId, customFieldValues);
606
+ setCheckout(updated); // updated.surchargeAmount and updated.appliedSurcharges are now fresh
607
+ setStep('payment');
608
+ } catch (err) {
609
+ setError(err instanceof Error ? err.message : 'Failed to save selections');
610
+ } finally {
611
+ setLoading(false);
612
+ }
613
+ }
614
+ \`\`\`
615
+
616
+ ### Order Summary \u2014 Show the Surcharges
617
+
618
+ **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:
619
+
620
+ \`\`\`typescript
621
+ {checkout.appliedSurcharges?.length > 0 && (
622
+ <>
623
+ {checkout.appliedSurcharges.map((s) => (
624
+ <div key={s.key} className="flex justify-between text-sm text-gray-600">
625
+ <span>{s.name}</span>
626
+ <span>{formatPrice(s.amount, { currency })}</span>
627
+ </div>
628
+ ))}
629
+ </>
630
+ )}
631
+ \`\`\`
632
+
633
+ \`checkout.surchargeAmount\` is a string (use \`parseFloat()\`); \`checkout.totalAmount\` already includes it.
634
+
635
+ ### Notes
636
+
637
+ - The server filters fields by \`visibility\` (\`always\` / \`shipping\` / \`pickup\` / \`products\`) \u2014 your client just renders what \`getCheckoutCustomFields()\` returns.
638
+ - 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.
639
+ - If \`getCheckoutCustomFields()\` returns 0 fields, skip the step entirely. Don't render an empty UI.
640
+ `;
641
+ }
414
642
  function getPaymentProvidersSection() {
415
643
  return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
416
644
 
@@ -1260,6 +1488,10 @@ ${getCheckoutFlowSection(currency)}
1260
1488
 
1261
1489
  ---
1262
1490
 
1491
+ ${getCheckoutCustomFieldsSection()}
1492
+
1493
+ ---
1494
+
1263
1495
  ${getPaymentProvidersSection()}
1264
1496
 
1265
1497
  ---
@@ -1318,6 +1550,8 @@ function getSectionByTopic(topic, connectionId, currency) {
1318
1550
  return getCartSection(cur);
1319
1551
  case "checkout":
1320
1552
  return getCheckoutFlowSection(cur);
1553
+ case "checkout-custom-fields":
1554
+ return getCheckoutCustomFieldsSection();
1321
1555
  case "payment":
1322
1556
  return getPaymentProvidersSection();
1323
1557
  case "auth":
@@ -1346,7 +1580,7 @@ function getSectionByTopic(topic, connectionId, currency) {
1346
1580
  storeType: "E-commerce store"
1347
1581
  });
1348
1582
  default:
1349
- return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, critical-rules, type-reference, admin, all`;
1583
+ 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`;
1350
1584
  }
1351
1585
  }
1352
1586
 
@@ -1359,6 +1593,7 @@ var GET_SDK_DOCS_SCHEMA = {
1359
1593
  "products",
1360
1594
  "cart",
1361
1595
  "checkout",
1596
+ "checkout-custom-fields",
1362
1597
  "payment",
1363
1598
  "auth",
1364
1599
  "order-confirmation",
@@ -3380,6 +3615,7 @@ import type {
3380
3615
  ShippingDestinations,
3381
3616
  PickupLocation,
3382
3617
  CheckoutBumpsResponse,
3618
+ CheckoutCustomFieldDefinition,
3383
3619
  } from 'brainerce';
3384
3620
  import { formatPrice } from 'brainerce';
3385
3621
  import { getClient } from '@/lib/brainerce';
@@ -3389,6 +3625,7 @@ import { ShippingStep } from '@/components/checkout/shipping-step';
3389
3625
  import { PaymentStep } from '@/components/checkout/payment-step';
3390
3626
  import { DeliveryMethodStep } from '@/components/checkout/delivery-method-step';
3391
3627
  import { PickupStep } from '@/components/checkout/pickup-step';
3628
+ import { CustomFieldsStep } from '@/components/checkout/custom-fields-step';
3392
3629
  import { TaxDisplay } from '@/components/checkout/tax-display';
3393
3630
  import { OrderBumpCard } from '@/components/checkout/order-bump-card';
3394
3631
  import { CouponInput } from '@/components/cart/coupon-input';
@@ -3397,7 +3634,7 @@ import { LoadingSpinner } from '@/components/shared/loading-spinner';
3397
3634
  import { useTranslations } from '@/lib/translations';
3398
3635
  import { cn } from '@/lib/utils';
3399
3636
 
3400
- type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'payment';
3637
+ type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
3401
3638
 
3402
3639
  function CheckoutContent() {
3403
3640
  const searchParams = useSearchParams();
@@ -3430,6 +3667,9 @@ function CheckoutContent() {
3430
3667
  const [orderBumps, setOrderBumps] = useState<CheckoutBumpsResponse | null>(null);
3431
3668
  const [addedBumpIds, setAddedBumpIds] = useState<Set<string>>(new Set());
3432
3669
  const [bumpLoading, setBumpLoading] = useState<string | null>(null);
3670
+ const [customFields, setCustomFields] = useState<CheckoutCustomFieldDefinition[]>([]);
3671
+ const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({});
3672
+ const [customFieldsLoading, setCustomFieldsLoading] = useState(false);
3433
3673
 
3434
3674
  // Check for returning from canceled payment
3435
3675
  const canceled = searchParams.get('canceled') === 'true';
@@ -3481,6 +3721,23 @@ function CheckoutContent() {
3481
3721
  const existing = await client.getCheckout(existingCheckoutId);
3482
3722
  setCheckout(existing);
3483
3723
 
3724
+ // Preload custom field definitions and any existing values so the
3725
+ // step indicator and "change options" affordance work on resume.
3726
+ client
3727
+ .getCheckoutCustomFields(existing.id)
3728
+ .then((fields) => {
3729
+ setCustomFields(fields);
3730
+ const existingValues = (
3731
+ existing as unknown as {
3732
+ customFieldValues?: Record<string, unknown> | null;
3733
+ }
3734
+ ).customFieldValues;
3735
+ if (existingValues) setCustomFieldValues(existingValues);
3736
+ })
3737
+ .catch(() => {
3738
+ setCustomFields([]);
3739
+ });
3740
+
3484
3741
  // Determine step based on checkout state
3485
3742
  const allDigital = existing.lineItems.every(
3486
3743
  (i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
@@ -3651,6 +3908,21 @@ function CheckoutContent() {
3651
3908
  }
3652
3909
  }
3653
3910
 
3911
+ // After shipping/pickup is set, decide whether to show the custom-fields step
3912
+ // or jump straight to payment. Returns the next step.
3913
+ async function loadCustomFieldsOrSkip(checkoutId: string): Promise<CheckoutStep> {
3914
+ try {
3915
+ const fields = await getClient().getCheckoutCustomFields(checkoutId);
3916
+ setCustomFields(fields);
3917
+ return fields.length > 0 ? 'custom-fields' : 'payment';
3918
+ } catch {
3919
+ // If the endpoint isn't available or fails, fall through to payment
3920
+ // rather than blocking the customer.
3921
+ setCustomFields([]);
3922
+ return 'payment';
3923
+ }
3924
+ }
3925
+
3654
3926
  // Handle shipping method selection
3655
3927
  async function handleShippingSelect(rateId: string) {
3656
3928
  if (!checkout) return;
@@ -3663,7 +3935,7 @@ function CheckoutContent() {
3663
3935
 
3664
3936
  const updated = await client.selectShippingMethod(checkout.id, rateId);
3665
3937
  setCheckout(updated);
3666
- setStep('payment');
3938
+ setStep(await loadCustomFieldsOrSkip(updated.id));
3667
3939
  } catch (err) {
3668
3940
  const message = err instanceof Error ? err.message : t('failedToSelectShipping');
3669
3941
  setError(message);
@@ -3672,6 +3944,23 @@ function CheckoutContent() {
3672
3944
  }
3673
3945
  }
3674
3946
 
3947
+ // Submit custom fields
3948
+ async function handleCustomFieldsApply() {
3949
+ if (!checkout) return;
3950
+ try {
3951
+ setCustomFieldsLoading(true);
3952
+ setError(null);
3953
+ const updated = await getClient().setCheckoutCustomFields(checkout.id, customFieldValues);
3954
+ setCheckout(updated);
3955
+ setStep('payment');
3956
+ } catch (err) {
3957
+ const message = err instanceof Error ? err.message : t('customFieldsFailed');
3958
+ setError(message);
3959
+ } finally {
3960
+ setCustomFieldsLoading(false);
3961
+ }
3962
+ }
3963
+
3675
3964
  // Handle delivery method selection
3676
3965
  async function handleDeliveryTypeSelect(method: 'shipping' | 'pickup') {
3677
3966
  if (!checkout) return;
@@ -3717,7 +4006,7 @@ function CheckoutContent() {
3717
4006
  phone: customerInfo.phone,
3718
4007
  });
3719
4008
  setCheckout(updated);
3720
- setStep('payment');
4009
+ setStep(await loadCustomFieldsOrSkip(updated.id));
3721
4010
  } catch (err) {
3722
4011
  const message = err instanceof Error ? err.message : t('failedToSelectPickup');
3723
4012
  setError(message);
@@ -3779,9 +4068,15 @@ function CheckoutContent() {
3779
4068
  );
3780
4069
  }
3781
4070
 
4071
+ const customFieldsStep =
4072
+ customFields.length > 0
4073
+ ? [{ key: 'custom-fields' as CheckoutStep, label: t('stepCustomFields') }]
4074
+ : [];
4075
+
3782
4076
  const steps: { key: CheckoutStep; label: string }[] = isAllDigital
3783
4077
  ? [
3784
4078
  { key: 'address', label: t('stepContactInfo') },
4079
+ ...customFieldsStep,
3785
4080
  { key: 'payment', label: t('stepPayment') },
3786
4081
  ]
3787
4082
  : pickupLocations.length > 0
@@ -3789,17 +4084,20 @@ function CheckoutContent() {
3789
4084
  ? [
3790
4085
  { key: 'method', label: t('stepMethod') },
3791
4086
  { key: 'pickup', label: t('stepPickup') },
4087
+ ...customFieldsStep,
3792
4088
  { key: 'payment', label: t('stepPayment') },
3793
4089
  ]
3794
4090
  : [
3795
4091
  { key: 'method', label: t('stepMethod') },
3796
4092
  { key: 'address', label: t('stepAddress') },
3797
4093
  { key: 'shipping', label: t('stepShipping') },
4094
+ ...customFieldsStep,
3798
4095
  { key: 'payment', label: t('stepPayment') },
3799
4096
  ]
3800
4097
  : [
3801
4098
  { key: 'address', label: t('stepAddress') },
3802
4099
  { key: 'shipping', label: t('stepShipping') },
4100
+ ...customFieldsStep,
3803
4101
  { key: 'payment', label: t('stepPayment') },
3804
4102
  ];
3805
4103
 
@@ -3998,19 +4296,54 @@ function CheckoutContent() {
3998
4296
  </div>
3999
4297
  )}
4000
4298
 
4299
+ {/* Custom Fields (optional, between shipping/pickup and payment) */}
4300
+ {step === 'custom-fields' && checkout && (
4301
+ <div>
4302
+ <div className="mb-4 flex items-center justify-between">
4303
+ <h2 className="text-foreground text-lg font-semibold">{t('customFieldsTitle')}</h2>
4304
+ <button
4305
+ type="button"
4306
+ onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
4307
+ className="text-primary text-sm hover:underline"
4308
+ >
4309
+ {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
4310
+ </button>
4311
+ </div>
4312
+ <CustomFieldsStep
4313
+ fields={customFields}
4314
+ values={customFieldValues}
4315
+ onChange={(key, value) =>
4316
+ setCustomFieldValues((prev) => ({ ...prev, [key]: value }))
4317
+ }
4318
+ onApply={handleCustomFieldsApply}
4319
+ loading={customFieldsLoading}
4320
+ />
4321
+ </div>
4322
+ )}
4323
+
4001
4324
  {/* Payment */}
4002
4325
  {step === 'payment' && checkout && (
4003
4326
  <div>
4004
4327
  <div className="mb-4 flex items-center justify-between">
4005
4328
  <h2 className="text-foreground text-lg font-semibold">{t('payment')}</h2>
4006
- {!isAllDigital && (
4329
+ {customFields.length > 0 ? (
4007
4330
  <button
4008
4331
  type="button"
4009
- onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
4332
+ onClick={() => setStep('custom-fields')}
4010
4333
  className="text-primary text-sm hover:underline"
4011
4334
  >
4012
- {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
4335
+ {t('changeOptions')}
4013
4336
  </button>
4337
+ ) : (
4338
+ !isAllDigital && (
4339
+ <button
4340
+ type="button"
4341
+ onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
4342
+ className="text-primary text-sm hover:underline"
4343
+ >
4344
+ {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
4345
+ </button>
4346
+ )
4014
4347
  )}
4015
4348
  </div>
4016
4349
 
@@ -4197,6 +4530,20 @@ function CheckoutContent() {
4197
4530
  taxBreakdown={checkout.taxBreakdown}
4198
4531
  />
4199
4532
 
4533
+ {/* Custom field surcharges (one line per applied surcharge) */}
4534
+ {checkout.appliedSurcharges && checkout.appliedSurcharges.length > 0 && (
4535
+ <>
4536
+ {checkout.appliedSurcharges.map((s) => (
4537
+ <div key={s.key} className="flex items-center justify-between">
4538
+ <span className="text-muted-foreground">{s.name}</span>
4539
+ <span className="text-foreground">
4540
+ {formatPrice(Number(s.amount), { currency }) as string}
4541
+ </span>
4542
+ </div>
4543
+ ))}
4544
+ </>
4545
+ )}
4546
+
4200
4547
  <div className="border-border mt-2 border-t pt-2">
4201
4548
  <div className="flex items-center justify-between">
4202
4549
  <span className="text-foreground font-semibold">{tc('total')}</span>
@@ -11215,7 +11562,17 @@ async function handleGetRequiredPages(args) {
11215
11562
  var import_zod5 = require("zod");
11216
11563
 
11217
11564
  // src/utils/fetch-store-info.ts
11218
- async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com") {
11565
+ var KNOWN_API_URLS = {
11566
+ production: "https://api.brainerce.com",
11567
+ staging: "https://api-staging.brainerce.com"
11568
+ };
11569
+ var ConnectionNotFoundError = class extends Error {
11570
+ constructor(connectionId, baseUrl) {
11571
+ super(`Connection "${connectionId}" not found at ${baseUrl}`);
11572
+ this.code = "NOT_FOUND";
11573
+ }
11574
+ };
11575
+ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production) {
11219
11576
  const url = `${baseUrl}/api/vc/${connectionId}/info`;
11220
11577
  const controller = new AbortController();
11221
11578
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -11224,55 +11581,90 @@ async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com
11224
11581
  res = await fetch(url, { signal: controller.signal });
11225
11582
  } catch (err) {
11226
11583
  if (err.name === "AbortError") {
11227
- throw new Error("Request timed out");
11584
+ throw new Error(`Request to ${baseUrl} timed out`);
11228
11585
  }
11229
- throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11586
+ throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
11230
11587
  } finally {
11231
11588
  clearTimeout(timeout);
11232
11589
  }
11233
11590
  if (res.status === 404) {
11234
- throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11591
+ throw new ConnectionNotFoundError(connectionId, baseUrl);
11235
11592
  }
11236
11593
  if (!res.ok) {
11237
- throw new Error(`API returned status ${res.status}`);
11594
+ throw new Error(`${baseUrl} returned status ${res.status}`);
11238
11595
  }
11239
11596
  let json;
11240
11597
  try {
11241
11598
  json = await res.json();
11242
11599
  } catch {
11243
- throw new Error("Invalid response from API");
11600
+ throw new Error(`Invalid response from ${baseUrl}`);
11244
11601
  }
11602
+ const storeName = json.storeName || json.name || "My Store";
11603
+ const displayName = json.channelName || storeName;
11245
11604
  return {
11246
- name: json.name || json.storeName || "My Store",
11605
+ name: displayName,
11606
+ storeName,
11247
11607
  currency: json.currency || "USD",
11248
11608
  language: json.language || "en",
11249
11609
  ...json.i18n ? { i18n: json.i18n } : {}
11250
11610
  };
11251
11611
  }
11612
+ async function resolveStoreInfo(connectionId, candidateUrls) {
11613
+ const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
11614
+ if (urls.length === 0) {
11615
+ throw new Error("No API URLs to try");
11616
+ }
11617
+ let lastError;
11618
+ let allNotFound = true;
11619
+ for (let i = 0; i < urls.length; i++) {
11620
+ const baseUrl = urls[i];
11621
+ try {
11622
+ const info = await fetchStoreInfo(connectionId, baseUrl);
11623
+ return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
11624
+ } catch (err) {
11625
+ lastError = err;
11626
+ const isNotFound = err.code === "NOT_FOUND";
11627
+ if (!isNotFound) {
11628
+ allNotFound = false;
11629
+ }
11630
+ }
11631
+ }
11632
+ if (allNotFound) {
11633
+ throw new Error(
11634
+ `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
11635
+ );
11636
+ }
11637
+ throw lastError || new Error("Failed to resolve store info");
11638
+ }
11639
+ function getCandidateApiUrls() {
11640
+ const explicit = (process.env.BRAINERCE_API_URL || "").replace(/\/$/, "");
11641
+ if (explicit) return [explicit];
11642
+ return [KNOWN_API_URLS.production, KNOWN_API_URLS.staging];
11643
+ }
11252
11644
 
11253
11645
  // src/tools/get-store-info.ts
11254
11646
  var GET_STORE_INFO_NAME = "get-store-info";
11255
- 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.";
11647
+ 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.";
11256
11648
  var GET_STORE_INFO_SCHEMA = {
11257
11649
  connectionId: import_zod5.z.string().describe("Vibe-coded connection ID (starts with vc_)")
11258
11650
  };
11259
11651
  async function handleGetStoreInfo(args) {
11260
11652
  try {
11261
- const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11262
- /\/$/,
11263
- ""
11264
- );
11265
- const info = await fetchStoreInfo(args.connectionId, baseUrl);
11653
+ const resolved = await resolveStoreInfo(args.connectionId, getCandidateApiUrls());
11266
11654
  return {
11267
11655
  content: [
11268
11656
  {
11269
11657
  type: "text",
11270
11658
  text: JSON.stringify(
11271
11659
  {
11272
- name: info.name,
11273
- currency: info.currency,
11274
- language: info.language,
11275
- connectionId: args.connectionId
11660
+ // `name` is the channel display name (from the dashboard sales-channel card);
11661
+ // falls back to storeName when a channel doesn't have its own name.
11662
+ name: resolved.info.name,
11663
+ storeName: resolved.info.storeName,
11664
+ currency: resolved.info.currency,
11665
+ language: resolved.info.language,
11666
+ connectionId: args.connectionId,
11667
+ apiBaseUrl: resolved.apiBaseUrl
11276
11668
  },
11277
11669
  null,
11278
11670
  2
@@ -11293,6 +11685,12 @@ async function handleGetStoreInfo(args) {
11293
11685
  var import_zod6 = require("zod");
11294
11686
 
11295
11687
  // src/utils/fetch-store-capabilities.ts
11688
+ var ConnectionNotFoundError2 = class extends Error {
11689
+ constructor(connectionId, baseUrl) {
11690
+ super(`Connection "${connectionId}" not found at ${baseUrl}`);
11691
+ this.code = "NOT_FOUND";
11692
+ }
11693
+ };
11296
11694
  async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brainerce.com") {
11297
11695
  const url = `${baseUrl}/api/vc/${connectionId}/capabilities`;
11298
11696
  const controller = new AbortController();
@@ -11302,24 +11700,51 @@ async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brain
11302
11700
  res = await fetch(url, { signal: controller.signal });
11303
11701
  } catch (err) {
11304
11702
  if (err.name === "AbortError") {
11305
- throw new Error("Request timed out");
11703
+ throw new Error(`Request to ${baseUrl} timed out`);
11306
11704
  }
11307
- throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11705
+ throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
11308
11706
  } finally {
11309
11707
  clearTimeout(timeout);
11310
11708
  }
11311
11709
  if (res.status === 404) {
11312
- throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11710
+ throw new ConnectionNotFoundError2(connectionId, baseUrl);
11313
11711
  }
11314
11712
  if (!res.ok) {
11315
- throw new Error(`API returned status ${res.status}`);
11713
+ throw new Error(`${baseUrl} returned status ${res.status}`);
11316
11714
  }
11317
11715
  try {
11318
11716
  return await res.json();
11319
11717
  } catch {
11320
- throw new Error("Invalid response from API");
11718
+ throw new Error(`Invalid response from ${baseUrl}`);
11321
11719
  }
11322
11720
  }
11721
+ async function resolveStoreCapabilities(connectionId, candidateUrls) {
11722
+ const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
11723
+ if (urls.length === 0) {
11724
+ throw new Error("No API URLs to try");
11725
+ }
11726
+ let lastError;
11727
+ let allNotFound = true;
11728
+ for (let i = 0; i < urls.length; i++) {
11729
+ const baseUrl = urls[i];
11730
+ try {
11731
+ const capabilities = await fetchStoreCapabilities(connectionId, baseUrl);
11732
+ return { capabilities, apiBaseUrl: baseUrl, fellBack: i > 0 };
11733
+ } catch (err) {
11734
+ lastError = err;
11735
+ const isNotFound = err.code === "NOT_FOUND";
11736
+ if (!isNotFound) {
11737
+ allNotFound = false;
11738
+ }
11739
+ }
11740
+ }
11741
+ if (allNotFound) {
11742
+ throw new Error(
11743
+ `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
11744
+ );
11745
+ }
11746
+ throw lastError || new Error("Failed to resolve store capabilities");
11747
+ }
11323
11748
 
11324
11749
  // src/tools/get-store-capabilities.ts
11325
11750
  var GET_STORE_CAPABILITIES_NAME = "get-store-capabilities";
@@ -11329,7 +11754,8 @@ var GET_STORE_CAPABILITIES_SCHEMA = {
11329
11754
  };
11330
11755
  function formatCapabilities(caps) {
11331
11756
  const lines = [];
11332
- lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
11757
+ const channelSuffix = caps.store.channelName && caps.store.channelName !== caps.store.name ? ` \u2014 Channel: "${caps.store.channelName}"` : "";
11758
+ lines.push(`## Store: ${caps.store.name} (${caps.store.currency})${channelSuffix}`);
11333
11759
  lines.push(`Language: ${caps.store.language}`);
11334
11760
  if (caps.store.i18n?.enabled) {
11335
11761
  lines.push(
@@ -11362,6 +11788,9 @@ function formatCapabilities(caps) {
11362
11788
  caps.features.hasDownloadableProducts ? "\u2705 Downloadable products available" : "\u274C No downloadable products"
11363
11789
  );
11364
11790
  lines.push(caps.features.hasCoupons ? "\u2705 Coupons available" : "\u274C No coupons");
11791
+ lines.push(
11792
+ caps.features.hasCheckoutCustomFields ? "\u2705 Checkout custom fields configured (surcharges may apply)" : "\u274C No checkout custom fields"
11793
+ );
11365
11794
  lines.push("");
11366
11795
  lines.push("## Connection Settings");
11367
11796
  lines.push(
@@ -11407,6 +11836,11 @@ function formatCapabilities(caps) {
11407
11836
  if (caps.features.hasCoupons) {
11408
11837
  suggestions.push('Add a coupon input to the cart. Use get-code-example("coupon-input").');
11409
11838
  }
11839
+ if (caps.features.hasCheckoutCustomFields) {
11840
+ suggestions.push(
11841
+ `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").`
11842
+ );
11843
+ }
11410
11844
  if (caps.connection.requireEmailVerification) {
11411
11845
  suggestions.push(
11412
11846
  'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
@@ -11437,13 +11871,9 @@ function formatCapabilities(caps) {
11437
11871
  }
11438
11872
  async function handleGetStoreCapabilities(args) {
11439
11873
  try {
11440
- const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11441
- /\/$/,
11442
- ""
11443
- );
11444
- const caps = await fetchStoreCapabilities(args.connectionId, baseUrl);
11874
+ const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
11445
11875
  return {
11446
- content: [{ type: "text", text: formatCapabilities(caps) }]
11876
+ content: [{ type: "text", text: formatCapabilities(resolved.capabilities) }]
11447
11877
  };
11448
11878
  } catch (error) {
11449
11879
  const message = error instanceof Error ? error.message : "Failed to fetch store capabilities";
@@ -11706,13 +12136,10 @@ var BUILD_STORE_SCHEMA = {
11706
12136
  storeStyle: import_zod7.z.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
11707
12137
  };
11708
12138
  async function handleBuildStore(args) {
11709
- const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11710
- /\/$/,
11711
- ""
11712
- );
11713
12139
  let capabilities = null;
11714
12140
  try {
11715
- capabilities = await fetchStoreCapabilities(args.connectionId, baseUrl);
12141
+ const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
12142
+ capabilities = resolved.capabilities;
11716
12143
  } catch {
11717
12144
  }
11718
12145
  const bundle = buildStoreBundle({