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