@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.mjs CHANGED
@@ -403,6 +403,234 @@ function PayPalPaymentForm({ clientId, orderId, checkoutId }: { clientId: string
403
403
  }
404
404
  \`\`\``;
405
405
  }
406
+ function getCheckoutCustomFieldsSection() {
407
+ return `## Checkout Custom Fields (Optional Step Between Shipping & Payment)
408
+
409
+ 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.
410
+
411
+ ### When to Build This
412
+
413
+ Check capabilities first:
414
+ \`\`\`typescript
415
+ const caps = await fetch(\`/api/vc/\${connectionId}/capabilities\`).then(r => r.json());
416
+ if (caps.features.hasCheckoutCustomFields) {
417
+ // Build the step described below
418
+ }
419
+ \`\`\`
420
+
421
+ If \`hasCheckoutCustomFields\` is true, you MUST build this step or customers will be charged surcharges they never saw.
422
+
423
+ ### Where to Place It
424
+
425
+ After \`selectShippingMethod()\` (or pickup confirmation), BEFORE \`createPaymentIntent()\`. Pseudocode:
426
+
427
+ \`\`\`
428
+ address \u2192 shipping/pickup \u2192 custom-fields (NEW) \u2192 payment
429
+ \`\`\`
430
+
431
+ 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.
432
+
433
+ ### Loading Field Definitions
434
+
435
+ \`\`\`typescript
436
+ import type { CheckoutCustomFieldDefinition } from 'brainerce';
437
+
438
+ const fields = await client.getCheckoutCustomFields(checkoutId);
439
+ if (fields.length === 0) {
440
+ setStep('payment'); // Nothing to collect
441
+ return;
442
+ }
443
+ \`\`\`
444
+
445
+ Each \`CheckoutCustomFieldDefinition\` has:
446
+ - \`key\` \u2014 the form field name to use in your state
447
+ - \`name\` \u2014 display label
448
+ - \`description\` \u2014 helper text (render below the input)
449
+ - \`type\` \u2014 \`'TEXT' | 'TEXTAREA' | 'NUMBER' | 'BOOLEAN' | 'SELECT' | 'DATE'\`
450
+ - \`required\` \u2014 boolean
451
+ - \`minLength\` / \`maxLength\` \u2014 for TEXT/TEXTAREA
452
+ - \`minValue\` / \`maxValue\` \u2014 for NUMBER
453
+ - \`options\` \u2014 \`Array<{value, label}>\` for SELECT
454
+ - \`pricing\` \u2014 never render this; the server uses it to compute surcharges
455
+
456
+ ### Rendering All 6 Field Types
457
+
458
+ \`\`\`typescript
459
+ function CustomFieldsStep({ fields, values, onChange, onApply, loading }: {
460
+ fields: CheckoutCustomFieldDefinition[];
461
+ values: Record<string, unknown>;
462
+ onChange: (key: string, value: unknown) => void;
463
+ onApply: () => void;
464
+ loading?: boolean;
465
+ }) {
466
+ return (
467
+ <div className="space-y-4">
468
+ {fields.map((field) => {
469
+ const value = values[field.key] ?? '';
470
+ const label = (
471
+ <label className="block text-sm font-medium">
472
+ {field.name}{field.required && <span className="text-red-500"> *</span>}
473
+ </label>
474
+ );
475
+ const help = field.description && (
476
+ <p className="text-xs text-gray-500 mt-1">{field.description}</p>
477
+ );
478
+
479
+ switch (field.type) {
480
+ case 'TEXT':
481
+ return (
482
+ <div key={field.key}>
483
+ {label}
484
+ <input
485
+ type="text"
486
+ value={value as string}
487
+ onChange={(e) => onChange(field.key, e.target.value)}
488
+ required={field.required}
489
+ minLength={field.minLength ?? undefined}
490
+ maxLength={field.maxLength ?? undefined}
491
+ className="w-full border rounded px-3 py-2"
492
+ />
493
+ {help}
494
+ </div>
495
+ );
496
+ case 'TEXTAREA':
497
+ return (
498
+ <div key={field.key}>
499
+ {label}
500
+ <textarea
501
+ value={value as string}
502
+ onChange={(e) => onChange(field.key, e.target.value)}
503
+ required={field.required}
504
+ minLength={field.minLength ?? undefined}
505
+ maxLength={field.maxLength ?? undefined}
506
+ rows={3}
507
+ className="w-full border rounded px-3 py-2"
508
+ />
509
+ {help}
510
+ </div>
511
+ );
512
+ case 'NUMBER':
513
+ return (
514
+ <div key={field.key}>
515
+ {label}
516
+ <input
517
+ type="number"
518
+ value={value as number}
519
+ onChange={(e) => onChange(field.key, e.target.valueAsNumber)}
520
+ required={field.required}
521
+ min={field.minValue ?? undefined}
522
+ max={field.maxValue ?? undefined}
523
+ className="w-full border rounded px-3 py-2"
524
+ />
525
+ {help}
526
+ </div>
527
+ );
528
+ case 'BOOLEAN':
529
+ return (
530
+ <div key={field.key} className="flex items-start gap-2">
531
+ <input
532
+ type="checkbox"
533
+ checked={value === true}
534
+ onChange={(e) => onChange(field.key, e.target.checked)}
535
+ className="mt-1"
536
+ />
537
+ <div>{label}{help}</div>
538
+ </div>
539
+ );
540
+ case 'SELECT':
541
+ return (
542
+ <div key={field.key}>
543
+ {label}
544
+ <select
545
+ value={value as string}
546
+ onChange={(e) => onChange(field.key, e.target.value)}
547
+ required={field.required}
548
+ className="w-full border rounded px-3 py-2"
549
+ >
550
+ <option value="">\u2014 Select \u2014</option>
551
+ {field.options?.map((opt) => (
552
+ <option key={opt.value} value={opt.value}>{opt.label}</option>
553
+ ))}
554
+ </select>
555
+ {help}
556
+ </div>
557
+ );
558
+ case 'DATE':
559
+ return (
560
+ <div key={field.key}>
561
+ {label}
562
+ <input
563
+ type="date"
564
+ value={value as string}
565
+ onChange={(e) => onChange(field.key, e.target.value)}
566
+ required={field.required}
567
+ className="w-full border rounded px-3 py-2"
568
+ />
569
+ {help}
570
+ </div>
571
+ );
572
+ default:
573
+ return null;
574
+ }
575
+ })}
576
+
577
+ <button
578
+ type="button"
579
+ onClick={onApply}
580
+ disabled={loading}
581
+ className="w-full bg-black text-white py-3 rounded"
582
+ >
583
+ {loading ? 'Updating\u2026' : 'Continue to Payment'}
584
+ </button>
585
+ </div>
586
+ );
587
+ }
588
+ \`\`\`
589
+
590
+ ### Submitting Values & Recalculating Surcharges
591
+
592
+ \`\`\`typescript
593
+ async function applyCustomFields() {
594
+ setLoading(true);
595
+ try {
596
+ // Server validates required + min/max + coerces types + recalculates totals
597
+ const updated = await client.setCheckoutCustomFields(checkoutId, customFieldValues);
598
+ setCheckout(updated); // updated.surchargeAmount and updated.appliedSurcharges are now fresh
599
+ setStep('payment');
600
+ } catch (err) {
601
+ setError(err instanceof Error ? err.message : 'Failed to save selections');
602
+ } finally {
603
+ setLoading(false);
604
+ }
605
+ }
606
+ \`\`\`
607
+
608
+ ### Order Summary \u2014 Show the Surcharges
609
+
610
+ **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:
611
+
612
+ \`\`\`typescript
613
+ {checkout.appliedSurcharges?.length > 0 && (
614
+ <>
615
+ {checkout.appliedSurcharges.map((s) => (
616
+ <div key={s.key} className="flex justify-between text-sm text-gray-600">
617
+ <span>{s.name}</span>
618
+ <span>{formatPrice(s.amount, { currency })}</span>
619
+ </div>
620
+ ))}
621
+ </>
622
+ )}
623
+ \`\`\`
624
+
625
+ \`checkout.surchargeAmount\` is a string (use \`parseFloat()\`); \`checkout.totalAmount\` already includes it.
626
+
627
+ ### Notes
628
+
629
+ - The server filters fields by \`visibility\` (\`always\` / \`shipping\` / \`pickup\` / \`products\`) \u2014 your client just renders what \`getCheckoutCustomFields()\` returns.
630
+ - 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.
631
+ - If \`getCheckoutCustomFields()\` returns 0 fields, skip the step entirely. Don't render an empty UI.
632
+ `;
633
+ }
406
634
  function getPaymentProvidersSection() {
407
635
  return `## Payment Provider Detection (DO THIS FIRST on checkout page!)
408
636
 
@@ -1252,6 +1480,10 @@ ${getCheckoutFlowSection(currency)}
1252
1480
 
1253
1481
  ---
1254
1482
 
1483
+ ${getCheckoutCustomFieldsSection()}
1484
+
1485
+ ---
1486
+
1255
1487
  ${getPaymentProvidersSection()}
1256
1488
 
1257
1489
  ---
@@ -1310,6 +1542,8 @@ function getSectionByTopic(topic, connectionId, currency) {
1310
1542
  return getCartSection(cur);
1311
1543
  case "checkout":
1312
1544
  return getCheckoutFlowSection(cur);
1545
+ case "checkout-custom-fields":
1546
+ return getCheckoutCustomFieldsSection();
1313
1547
  case "payment":
1314
1548
  return getPaymentProvidersSection();
1315
1549
  case "auth":
@@ -1338,7 +1572,7 @@ function getSectionByTopic(topic, connectionId, currency) {
1338
1572
  storeType: "E-commerce store"
1339
1573
  });
1340
1574
  default:
1341
- return `Unknown topic: "${topic}". Available topics: setup, products, cart, checkout, payment, auth, order-confirmation, inventory, discounts, recommendations, tax, critical-rules, type-reference, admin, all`;
1575
+ 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`;
1342
1576
  }
1343
1577
  }
1344
1578
 
@@ -1351,6 +1585,7 @@ var GET_SDK_DOCS_SCHEMA = {
1351
1585
  "products",
1352
1586
  "cart",
1353
1587
  "checkout",
1588
+ "checkout-custom-fields",
1354
1589
  "payment",
1355
1590
  "auth",
1356
1591
  "order-confirmation",
@@ -3372,6 +3607,7 @@ import type {
3372
3607
  ShippingDestinations,
3373
3608
  PickupLocation,
3374
3609
  CheckoutBumpsResponse,
3610
+ CheckoutCustomFieldDefinition,
3375
3611
  } from 'brainerce';
3376
3612
  import { formatPrice } from 'brainerce';
3377
3613
  import { getClient } from '@/lib/brainerce';
@@ -3381,6 +3617,7 @@ import { ShippingStep } from '@/components/checkout/shipping-step';
3381
3617
  import { PaymentStep } from '@/components/checkout/payment-step';
3382
3618
  import { DeliveryMethodStep } from '@/components/checkout/delivery-method-step';
3383
3619
  import { PickupStep } from '@/components/checkout/pickup-step';
3620
+ import { CustomFieldsStep } from '@/components/checkout/custom-fields-step';
3384
3621
  import { TaxDisplay } from '@/components/checkout/tax-display';
3385
3622
  import { OrderBumpCard } from '@/components/checkout/order-bump-card';
3386
3623
  import { CouponInput } from '@/components/cart/coupon-input';
@@ -3389,7 +3626,7 @@ import { LoadingSpinner } from '@/components/shared/loading-spinner';
3389
3626
  import { useTranslations } from '@/lib/translations';
3390
3627
  import { cn } from '@/lib/utils';
3391
3628
 
3392
- type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'payment';
3629
+ type CheckoutStep = 'method' | 'address' | 'shipping' | 'pickup' | 'custom-fields' | 'payment';
3393
3630
 
3394
3631
  function CheckoutContent() {
3395
3632
  const searchParams = useSearchParams();
@@ -3422,6 +3659,9 @@ function CheckoutContent() {
3422
3659
  const [orderBumps, setOrderBumps] = useState<CheckoutBumpsResponse | null>(null);
3423
3660
  const [addedBumpIds, setAddedBumpIds] = useState<Set<string>>(new Set());
3424
3661
  const [bumpLoading, setBumpLoading] = useState<string | null>(null);
3662
+ const [customFields, setCustomFields] = useState<CheckoutCustomFieldDefinition[]>([]);
3663
+ const [customFieldValues, setCustomFieldValues] = useState<Record<string, unknown>>({});
3664
+ const [customFieldsLoading, setCustomFieldsLoading] = useState(false);
3425
3665
 
3426
3666
  // Check for returning from canceled payment
3427
3667
  const canceled = searchParams.get('canceled') === 'true';
@@ -3473,6 +3713,23 @@ function CheckoutContent() {
3473
3713
  const existing = await client.getCheckout(existingCheckoutId);
3474
3714
  setCheckout(existing);
3475
3715
 
3716
+ // Preload custom field definitions and any existing values so the
3717
+ // step indicator and "change options" affordance work on resume.
3718
+ client
3719
+ .getCheckoutCustomFields(existing.id)
3720
+ .then((fields) => {
3721
+ setCustomFields(fields);
3722
+ const existingValues = (
3723
+ existing as unknown as {
3724
+ customFieldValues?: Record<string, unknown> | null;
3725
+ }
3726
+ ).customFieldValues;
3727
+ if (existingValues) setCustomFieldValues(existingValues);
3728
+ })
3729
+ .catch(() => {
3730
+ setCustomFields([]);
3731
+ });
3732
+
3476
3733
  // Determine step based on checkout state
3477
3734
  const allDigital = existing.lineItems.every(
3478
3735
  (i) => (i.product as unknown as { isDownloadable?: boolean }).isDownloadable
@@ -3643,6 +3900,21 @@ function CheckoutContent() {
3643
3900
  }
3644
3901
  }
3645
3902
 
3903
+ // After shipping/pickup is set, decide whether to show the custom-fields step
3904
+ // or jump straight to payment. Returns the next step.
3905
+ async function loadCustomFieldsOrSkip(checkoutId: string): Promise<CheckoutStep> {
3906
+ try {
3907
+ const fields = await getClient().getCheckoutCustomFields(checkoutId);
3908
+ setCustomFields(fields);
3909
+ return fields.length > 0 ? 'custom-fields' : 'payment';
3910
+ } catch {
3911
+ // If the endpoint isn't available or fails, fall through to payment
3912
+ // rather than blocking the customer.
3913
+ setCustomFields([]);
3914
+ return 'payment';
3915
+ }
3916
+ }
3917
+
3646
3918
  // Handle shipping method selection
3647
3919
  async function handleShippingSelect(rateId: string) {
3648
3920
  if (!checkout) return;
@@ -3655,7 +3927,7 @@ function CheckoutContent() {
3655
3927
 
3656
3928
  const updated = await client.selectShippingMethod(checkout.id, rateId);
3657
3929
  setCheckout(updated);
3658
- setStep('payment');
3930
+ setStep(await loadCustomFieldsOrSkip(updated.id));
3659
3931
  } catch (err) {
3660
3932
  const message = err instanceof Error ? err.message : t('failedToSelectShipping');
3661
3933
  setError(message);
@@ -3664,6 +3936,23 @@ function CheckoutContent() {
3664
3936
  }
3665
3937
  }
3666
3938
 
3939
+ // Submit custom fields
3940
+ async function handleCustomFieldsApply() {
3941
+ if (!checkout) return;
3942
+ try {
3943
+ setCustomFieldsLoading(true);
3944
+ setError(null);
3945
+ const updated = await getClient().setCheckoutCustomFields(checkout.id, customFieldValues);
3946
+ setCheckout(updated);
3947
+ setStep('payment');
3948
+ } catch (err) {
3949
+ const message = err instanceof Error ? err.message : t('customFieldsFailed');
3950
+ setError(message);
3951
+ } finally {
3952
+ setCustomFieldsLoading(false);
3953
+ }
3954
+ }
3955
+
3667
3956
  // Handle delivery method selection
3668
3957
  async function handleDeliveryTypeSelect(method: 'shipping' | 'pickup') {
3669
3958
  if (!checkout) return;
@@ -3709,7 +3998,7 @@ function CheckoutContent() {
3709
3998
  phone: customerInfo.phone,
3710
3999
  });
3711
4000
  setCheckout(updated);
3712
- setStep('payment');
4001
+ setStep(await loadCustomFieldsOrSkip(updated.id));
3713
4002
  } catch (err) {
3714
4003
  const message = err instanceof Error ? err.message : t('failedToSelectPickup');
3715
4004
  setError(message);
@@ -3771,9 +4060,15 @@ function CheckoutContent() {
3771
4060
  );
3772
4061
  }
3773
4062
 
4063
+ const customFieldsStep =
4064
+ customFields.length > 0
4065
+ ? [{ key: 'custom-fields' as CheckoutStep, label: t('stepCustomFields') }]
4066
+ : [];
4067
+
3774
4068
  const steps: { key: CheckoutStep; label: string }[] = isAllDigital
3775
4069
  ? [
3776
4070
  { key: 'address', label: t('stepContactInfo') },
4071
+ ...customFieldsStep,
3777
4072
  { key: 'payment', label: t('stepPayment') },
3778
4073
  ]
3779
4074
  : pickupLocations.length > 0
@@ -3781,17 +4076,20 @@ function CheckoutContent() {
3781
4076
  ? [
3782
4077
  { key: 'method', label: t('stepMethod') },
3783
4078
  { key: 'pickup', label: t('stepPickup') },
4079
+ ...customFieldsStep,
3784
4080
  { key: 'payment', label: t('stepPayment') },
3785
4081
  ]
3786
4082
  : [
3787
4083
  { key: 'method', label: t('stepMethod') },
3788
4084
  { key: 'address', label: t('stepAddress') },
3789
4085
  { key: 'shipping', label: t('stepShipping') },
4086
+ ...customFieldsStep,
3790
4087
  { key: 'payment', label: t('stepPayment') },
3791
4088
  ]
3792
4089
  : [
3793
4090
  { key: 'address', label: t('stepAddress') },
3794
4091
  { key: 'shipping', label: t('stepShipping') },
4092
+ ...customFieldsStep,
3795
4093
  { key: 'payment', label: t('stepPayment') },
3796
4094
  ];
3797
4095
 
@@ -3990,19 +4288,54 @@ function CheckoutContent() {
3990
4288
  </div>
3991
4289
  )}
3992
4290
 
4291
+ {/* Custom Fields (optional, between shipping/pickup and payment) */}
4292
+ {step === 'custom-fields' && checkout && (
4293
+ <div>
4294
+ <div className="mb-4 flex items-center justify-between">
4295
+ <h2 className="text-foreground text-lg font-semibold">{t('customFieldsTitle')}</h2>
4296
+ <button
4297
+ type="button"
4298
+ onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
4299
+ className="text-primary text-sm hover:underline"
4300
+ >
4301
+ {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
4302
+ </button>
4303
+ </div>
4304
+ <CustomFieldsStep
4305
+ fields={customFields}
4306
+ values={customFieldValues}
4307
+ onChange={(key, value) =>
4308
+ setCustomFieldValues((prev) => ({ ...prev, [key]: value }))
4309
+ }
4310
+ onApply={handleCustomFieldsApply}
4311
+ loading={customFieldsLoading}
4312
+ />
4313
+ </div>
4314
+ )}
4315
+
3993
4316
  {/* Payment */}
3994
4317
  {step === 'payment' && checkout && (
3995
4318
  <div>
3996
4319
  <div className="mb-4 flex items-center justify-between">
3997
4320
  <h2 className="text-foreground text-lg font-semibold">{t('payment')}</h2>
3998
- {!isAllDigital && (
4321
+ {customFields.length > 0 ? (
3999
4322
  <button
4000
4323
  type="button"
4001
- onClick={() => setStep(deliveryType === 'pickup' ? 'pickup' : 'shipping')}
4324
+ onClick={() => setStep('custom-fields')}
4002
4325
  className="text-primary text-sm hover:underline"
4003
4326
  >
4004
- {deliveryType === 'pickup' ? t('changePickup') : t('changeShipping')}
4327
+ {t('changeOptions')}
4005
4328
  </button>
4329
+ ) : (
4330
+ !isAllDigital && (
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
+ )
4006
4339
  )}
4007
4340
  </div>
4008
4341
 
@@ -4189,6 +4522,20 @@ function CheckoutContent() {
4189
4522
  taxBreakdown={checkout.taxBreakdown}
4190
4523
  />
4191
4524
 
4525
+ {/* Custom field surcharges (one line per applied surcharge) */}
4526
+ {checkout.appliedSurcharges && checkout.appliedSurcharges.length > 0 && (
4527
+ <>
4528
+ {checkout.appliedSurcharges.map((s) => (
4529
+ <div key={s.key} className="flex items-center justify-between">
4530
+ <span className="text-muted-foreground">{s.name}</span>
4531
+ <span className="text-foreground">
4532
+ {formatPrice(Number(s.amount), { currency }) as string}
4533
+ </span>
4534
+ </div>
4535
+ ))}
4536
+ </>
4537
+ )}
4538
+
4192
4539
  <div className="border-border mt-2 border-t pt-2">
4193
4540
  <div className="flex items-center justify-between">
4194
4541
  <span className="text-foreground font-semibold">{tc('total')}</span>
@@ -11207,7 +11554,17 @@ async function handleGetRequiredPages(args) {
11207
11554
  import { z as z5 } from "zod";
11208
11555
 
11209
11556
  // src/utils/fetch-store-info.ts
11210
- async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com") {
11557
+ var KNOWN_API_URLS = {
11558
+ production: "https://api.brainerce.com",
11559
+ staging: "https://api-staging.brainerce.com"
11560
+ };
11561
+ var ConnectionNotFoundError = class extends Error {
11562
+ constructor(connectionId, baseUrl) {
11563
+ super(`Connection "${connectionId}" not found at ${baseUrl}`);
11564
+ this.code = "NOT_FOUND";
11565
+ }
11566
+ };
11567
+ async function fetchStoreInfo(connectionId, baseUrl = KNOWN_API_URLS.production) {
11211
11568
  const url = `${baseUrl}/api/vc/${connectionId}/info`;
11212
11569
  const controller = new AbortController();
11213
11570
  const timeout = setTimeout(() => controller.abort(), 1e4);
@@ -11216,55 +11573,90 @@ async function fetchStoreInfo(connectionId, baseUrl = "https://api.brainerce.com
11216
11573
  res = await fetch(url, { signal: controller.signal });
11217
11574
  } catch (err) {
11218
11575
  if (err.name === "AbortError") {
11219
- throw new Error("Request timed out");
11576
+ throw new Error(`Request to ${baseUrl} timed out`);
11220
11577
  }
11221
- throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11578
+ throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
11222
11579
  } finally {
11223
11580
  clearTimeout(timeout);
11224
11581
  }
11225
11582
  if (res.status === 404) {
11226
- throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11583
+ throw new ConnectionNotFoundError(connectionId, baseUrl);
11227
11584
  }
11228
11585
  if (!res.ok) {
11229
- throw new Error(`API returned status ${res.status}`);
11586
+ throw new Error(`${baseUrl} returned status ${res.status}`);
11230
11587
  }
11231
11588
  let json;
11232
11589
  try {
11233
11590
  json = await res.json();
11234
11591
  } catch {
11235
- throw new Error("Invalid response from API");
11592
+ throw new Error(`Invalid response from ${baseUrl}`);
11236
11593
  }
11594
+ const storeName = json.storeName || json.name || "My Store";
11595
+ const displayName = json.channelName || storeName;
11237
11596
  return {
11238
- name: json.name || json.storeName || "My Store",
11597
+ name: displayName,
11598
+ storeName,
11239
11599
  currency: json.currency || "USD",
11240
11600
  language: json.language || "en",
11241
11601
  ...json.i18n ? { i18n: json.i18n } : {}
11242
11602
  };
11243
11603
  }
11604
+ async function resolveStoreInfo(connectionId, candidateUrls) {
11605
+ const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
11606
+ if (urls.length === 0) {
11607
+ throw new Error("No API URLs to try");
11608
+ }
11609
+ let lastError;
11610
+ let allNotFound = true;
11611
+ for (let i = 0; i < urls.length; i++) {
11612
+ const baseUrl = urls[i];
11613
+ try {
11614
+ const info = await fetchStoreInfo(connectionId, baseUrl);
11615
+ return { info, apiBaseUrl: baseUrl, fellBack: i > 0 };
11616
+ } catch (err) {
11617
+ lastError = err;
11618
+ const isNotFound = err.code === "NOT_FOUND";
11619
+ if (!isNotFound) {
11620
+ allNotFound = false;
11621
+ }
11622
+ }
11623
+ }
11624
+ if (allNotFound) {
11625
+ throw new Error(
11626
+ `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
11627
+ );
11628
+ }
11629
+ throw lastError || new Error("Failed to resolve store info");
11630
+ }
11631
+ function getCandidateApiUrls() {
11632
+ const explicit = (process.env.BRAINERCE_API_URL || "").replace(/\/$/, "");
11633
+ if (explicit) return [explicit];
11634
+ return [KNOWN_API_URLS.production, KNOWN_API_URLS.staging];
11635
+ }
11244
11636
 
11245
11637
  // src/tools/get-store-info.ts
11246
11638
  var GET_STORE_INFO_NAME = "get-store-info";
11247
- 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.";
11639
+ 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.";
11248
11640
  var GET_STORE_INFO_SCHEMA = {
11249
11641
  connectionId: z5.string().describe("Vibe-coded connection ID (starts with vc_)")
11250
11642
  };
11251
11643
  async function handleGetStoreInfo(args) {
11252
11644
  try {
11253
- const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11254
- /\/$/,
11255
- ""
11256
- );
11257
- const info = await fetchStoreInfo(args.connectionId, baseUrl);
11645
+ const resolved = await resolveStoreInfo(args.connectionId, getCandidateApiUrls());
11258
11646
  return {
11259
11647
  content: [
11260
11648
  {
11261
11649
  type: "text",
11262
11650
  text: JSON.stringify(
11263
11651
  {
11264
- name: info.name,
11265
- currency: info.currency,
11266
- language: info.language,
11267
- connectionId: args.connectionId
11652
+ // `name` is the channel display name (from the dashboard sales-channel card);
11653
+ // falls back to storeName when a channel doesn't have its own name.
11654
+ name: resolved.info.name,
11655
+ storeName: resolved.info.storeName,
11656
+ currency: resolved.info.currency,
11657
+ language: resolved.info.language,
11658
+ connectionId: args.connectionId,
11659
+ apiBaseUrl: resolved.apiBaseUrl
11268
11660
  },
11269
11661
  null,
11270
11662
  2
@@ -11285,6 +11677,12 @@ async function handleGetStoreInfo(args) {
11285
11677
  import { z as z6 } from "zod";
11286
11678
 
11287
11679
  // src/utils/fetch-store-capabilities.ts
11680
+ var ConnectionNotFoundError2 = class extends Error {
11681
+ constructor(connectionId, baseUrl) {
11682
+ super(`Connection "${connectionId}" not found at ${baseUrl}`);
11683
+ this.code = "NOT_FOUND";
11684
+ }
11685
+ };
11288
11686
  async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brainerce.com") {
11289
11687
  const url = `${baseUrl}/api/vc/${connectionId}/capabilities`;
11290
11688
  const controller = new AbortController();
@@ -11294,24 +11692,51 @@ async function fetchStoreCapabilities(connectionId, baseUrl = "https://api.brain
11294
11692
  res = await fetch(url, { signal: controller.signal });
11295
11693
  } catch (err) {
11296
11694
  if (err.name === "AbortError") {
11297
- throw new Error("Request timed out");
11695
+ throw new Error(`Request to ${baseUrl} timed out`);
11298
11696
  }
11299
- throw new Error(`Failed to connect to Brainerce API: ${err.message}`);
11697
+ throw new Error(`Failed to connect to ${baseUrl}: ${err.message}`);
11300
11698
  } finally {
11301
11699
  clearTimeout(timeout);
11302
11700
  }
11303
11701
  if (res.status === 404) {
11304
- throw new Error(`Connection ID "${connectionId}" not found. Check your dashboard.`);
11702
+ throw new ConnectionNotFoundError2(connectionId, baseUrl);
11305
11703
  }
11306
11704
  if (!res.ok) {
11307
- throw new Error(`API returned status ${res.status}`);
11705
+ throw new Error(`${baseUrl} returned status ${res.status}`);
11308
11706
  }
11309
11707
  try {
11310
11708
  return await res.json();
11311
11709
  } catch {
11312
- throw new Error("Invalid response from API");
11710
+ throw new Error(`Invalid response from ${baseUrl}`);
11313
11711
  }
11314
11712
  }
11713
+ async function resolveStoreCapabilities(connectionId, candidateUrls) {
11714
+ const urls = candidateUrls.filter((u, i) => u && candidateUrls.indexOf(u) === i);
11715
+ if (urls.length === 0) {
11716
+ throw new Error("No API URLs to try");
11717
+ }
11718
+ let lastError;
11719
+ let allNotFound = true;
11720
+ for (let i = 0; i < urls.length; i++) {
11721
+ const baseUrl = urls[i];
11722
+ try {
11723
+ const capabilities = await fetchStoreCapabilities(connectionId, baseUrl);
11724
+ return { capabilities, apiBaseUrl: baseUrl, fellBack: i > 0 };
11725
+ } catch (err) {
11726
+ lastError = err;
11727
+ const isNotFound = err.code === "NOT_FOUND";
11728
+ if (!isNotFound) {
11729
+ allNotFound = false;
11730
+ }
11731
+ }
11732
+ }
11733
+ if (allNotFound) {
11734
+ throw new Error(
11735
+ `Connection "${connectionId}" not found in any known environment (${urls.join(", ")}).`
11736
+ );
11737
+ }
11738
+ throw lastError || new Error("Failed to resolve store capabilities");
11739
+ }
11315
11740
 
11316
11741
  // src/tools/get-store-capabilities.ts
11317
11742
  var GET_STORE_CAPABILITIES_NAME = "get-store-capabilities";
@@ -11321,7 +11746,8 @@ var GET_STORE_CAPABILITIES_SCHEMA = {
11321
11746
  };
11322
11747
  function formatCapabilities(caps) {
11323
11748
  const lines = [];
11324
- lines.push(`## Store: ${caps.store.name} (${caps.store.currency})`);
11749
+ const channelSuffix = caps.store.channelName && caps.store.channelName !== caps.store.name ? ` \u2014 Channel: "${caps.store.channelName}"` : "";
11750
+ lines.push(`## Store: ${caps.store.name} (${caps.store.currency})${channelSuffix}`);
11325
11751
  lines.push(`Language: ${caps.store.language}`);
11326
11752
  if (caps.store.i18n?.enabled) {
11327
11753
  lines.push(
@@ -11354,6 +11780,9 @@ function formatCapabilities(caps) {
11354
11780
  caps.features.hasDownloadableProducts ? "\u2705 Downloadable products available" : "\u274C No downloadable products"
11355
11781
  );
11356
11782
  lines.push(caps.features.hasCoupons ? "\u2705 Coupons available" : "\u274C No coupons");
11783
+ lines.push(
11784
+ caps.features.hasCheckoutCustomFields ? "\u2705 Checkout custom fields configured (surcharges may apply)" : "\u274C No checkout custom fields"
11785
+ );
11357
11786
  lines.push("");
11358
11787
  lines.push("## Connection Settings");
11359
11788
  lines.push(
@@ -11399,6 +11828,11 @@ function formatCapabilities(caps) {
11399
11828
  if (caps.features.hasCoupons) {
11400
11829
  suggestions.push('Add a coupon input to the cart. Use get-code-example("coupon-input").');
11401
11830
  }
11831
+ if (caps.features.hasCheckoutCustomFields) {
11832
+ suggestions.push(
11833
+ `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").`
11834
+ );
11835
+ }
11402
11836
  if (caps.connection.requireEmailVerification) {
11403
11837
  suggestions.push(
11404
11838
  'Email verification is required \u2014 ALWAYS build the verify-email page. Use get-code-example("verify-email").'
@@ -11429,13 +11863,9 @@ function formatCapabilities(caps) {
11429
11863
  }
11430
11864
  async function handleGetStoreCapabilities(args) {
11431
11865
  try {
11432
- const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11433
- /\/$/,
11434
- ""
11435
- );
11436
- const caps = await fetchStoreCapabilities(args.connectionId, baseUrl);
11866
+ const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
11437
11867
  return {
11438
- content: [{ type: "text", text: formatCapabilities(caps) }]
11868
+ content: [{ type: "text", text: formatCapabilities(resolved.capabilities) }]
11439
11869
  };
11440
11870
  } catch (error) {
11441
11871
  const message = error instanceof Error ? error.message : "Failed to fetch store capabilities";
@@ -11698,13 +12128,10 @@ var BUILD_STORE_SCHEMA = {
11698
12128
  storeStyle: z7.string().optional().describe('Design style (e.g., "minimalist", "luxury", "playful", "dark mode")')
11699
12129
  };
11700
12130
  async function handleBuildStore(args) {
11701
- const baseUrl = (process.env.BRAINERCE_API_URL || "https://api.brainerce.com").replace(
11702
- /\/$/,
11703
- ""
11704
- );
11705
12131
  let capabilities = null;
11706
12132
  try {
11707
- capabilities = await fetchStoreCapabilities(args.connectionId, baseUrl);
12133
+ const resolved = await resolveStoreCapabilities(args.connectionId, getCandidateApiUrls());
12134
+ capabilities = resolved.capabilities;
11708
12135
  } catch {
11709
12136
  }
11710
12137
  const bundle = buildStoreBundle({