@binaryandthread/mcp 0.4.11 → 0.4.13

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/README.md CHANGED
@@ -117,3 +117,8 @@ fits (regular and oversize, priced apart), `list_products` marks it (`fits`, `pr
117
117
  lists sizes and prices per fit (pass `fit` to narrow), and `add_to_cart` **requires** `fit` for it:
118
118
  without it the tool returns an error naming both fits, and nothing is added. Colours and sizes follow the ship-to region:
119
119
  `get_product` lists the ones you can buy where the cart ships.
120
+
121
+ `add_to_cart` by `handle` also **requires** `size` whenever the colour comes in more than one: without it
122
+ the tool returns the sizes and asks the agent to put the choice to the buyer, and nothing is added. A
123
+ `variant_id` already names its size. `get_cart` and `add_to_cart` list each line's `size` and `colour`,
124
+ and `get_cart` on a cart that was already checked out says so (`completed: true`) instead of listing it.
package/dist/bin.js CHANGED
@@ -220,13 +220,19 @@ var StoreClient = class {
220
220
  } catch {
221
221
  parsed = {};
222
222
  }
223
- if (opts.cartRecovery && (res.status === 404 || res.status === 409 || String(parsed.type ?? "").includes("cart.completed") || /cart.*complete/i.test(String(parsed.message ?? "")))) {
223
+ const idempotencyConflict = res.status === 409 && parsed.code === "IDEMPOTENCY_CONFLICT";
224
+ const parsedType = String(parsed.type ?? "");
225
+ if (opts.cartRecovery && (res.status === 404 || parsedType === "cart_completed" || parsedType.includes("cart.completed") || /cart.*complete/i.test(String(parsed.message ?? "")))) {
224
226
  throw new RecoverableCartError(
225
227
  `cart is stale or completed (HTTP ${res.status})`,
226
228
  res.status
227
229
  );
228
230
  }
229
- if (res.status === 429) {
231
+ if (idempotencyConflict) {
232
+ retryAfterMs = parseRetryAfterMs(res);
233
+ lastErr = new StoreApiError(`request in flight (HTTP 409)`, 409, path);
234
+ this.logWarn("store.request.idempotency_conflict", { path, attempt: attempt + 1, retryAfterMs });
235
+ } else if (res.status === 429) {
230
236
  retryAfterMs = parseRetryAfterMs(res);
231
237
  lastErr = new StoreApiError(`rate limited (HTTP 429)`, 429, path);
232
238
  this.logWarn("store.request.rate_limited", {
@@ -624,12 +630,20 @@ function supplierOf(p) {
624
630
  return v === "gelato" || v === "printful" ? v : null;
625
631
  }
626
632
  var PRINTFUL_BLANK_COLOURS = {
627
- "ascolour-5082": ["black", "white", "coal", "eucalyptus", "bone"]
628
- // TB124
633
+ "ascolour-5082": ["black", "white", "coal", "eucalyptus", "bone", "grey", "khaki", "indigo"],
634
+ // TB124, TB126
635
+ "bellacanvas-4711": ["asphalt", "dust", "navy", "maroon", "forest"]
636
+ // TB128: the sweatshirt
629
637
  };
630
638
  var PRINTFUL_BLANK_SIZES = {
631
- "ascolour-5082": ["s", "m", "l", "xl", "2xl"]
639
+ "ascolour-5082": ["s", "m", "l", "xl", "2xl"],
640
+ "bellacanvas-4711": ["s", "m", "l", "xl", "2xl"]
632
641
  };
642
+ function printfulBlankOf(p) {
643
+ const v = (p.metadata ?? {})["bt_blank"];
644
+ if (v === void 0 || v === null) return "ascolour-5082";
645
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(PRINTFUL_BLANK_COLOURS, v) ? v : null;
646
+ }
633
647
  var PRINTFUL_SERVED_COUNTRIES = [
634
648
  "se",
635
649
  "us",
@@ -660,29 +674,30 @@ var PRINTFUL_SERVED_COUNTRIES = [
660
674
  "si",
661
675
  "es"
662
676
  ];
663
- function printfulBlankFor(fit, countryCode) {
677
+ function printfulBlankFor(fit, countryCode, productBlank = "ascolour-5082") {
664
678
  if (!PRINTFUL_SERVED_COUNTRIES.includes((countryCode ?? "").trim().toLowerCase())) return null;
665
- return fit === "regular" ? "ascolour-5082" : null;
679
+ if (productBlank === null) return null;
680
+ return fit === "regular" ? productBlank : null;
666
681
  }
667
682
  function sizeKey(size) {
668
683
  const s = size.trim().toLowerCase();
669
684
  return s === "xxl" ? "2xl" : s === "xxxl" ? "3xl" : s;
670
685
  }
671
- function blankRule(fit, countryCode, supplier) {
686
+ function blankRule(fit, countryCode, supplier, productBlank) {
672
687
  if (supplier === null) return null;
673
688
  if (supplier === "printful") {
674
- const pb = printfulBlankFor(fit, countryCode);
689
+ const pb = printfulBlankFor(fit, countryCode, productBlank);
675
690
  return pb ? { colours: PRINTFUL_BLANK_COLOURS[pb], sizes: PRINTFUL_BLANK_SIZES[pb] } : null;
676
691
  }
677
692
  const blank = blankFor(fit, countryCode);
678
693
  return blank ? { colours: BLANK_COLOURS[blank], sizes: BLANK_SIZES[blank] } : null;
679
694
  }
680
- function colourBuyable(fit, colour, countryCode, supplier = "gelato") {
681
- const rule = blankRule(fit, countryCode, supplier);
695
+ function colourBuyable(fit, colour, countryCode, supplier = "gelato", printfulBlank = "ascolour-5082") {
696
+ const rule = blankRule(fit, countryCode, supplier, printfulBlank);
682
697
  return !!rule && rule.colours.includes(colour.trim().toLowerCase());
683
698
  }
684
- function sizeBuyable(fit, size, countryCode, supplier = "gelato") {
685
- const rule = blankRule(fit, countryCode, supplier);
699
+ function sizeBuyable(fit, size, countryCode, supplier = "gelato", printfulBlank = "ascolour-5082") {
700
+ const rule = blankRule(fit, countryCode, supplier, printfulBlank);
686
701
  return !!rule && rule.sizes.includes(sizeKey(size));
687
702
  }
688
703
  function legacyFit(p) {
@@ -701,11 +716,12 @@ function variantsOfFit(p, fit) {
701
716
  function coloursFor(p, fit, countryCode) {
702
717
  const gate = gateFit(p, fit);
703
718
  const supplier = supplierOf(p);
719
+ const pb = printfulBlankOf(p);
704
720
  const out = [];
705
721
  for (const v of variantsOfFit(p, fit)) {
706
722
  const c = variantColour(v);
707
723
  if (!c || out.includes(c)) continue;
708
- if (gate && countryCode !== void 0 && !colourBuyable(gate, c, countryCode, supplier)) continue;
724
+ if (gate && countryCode !== void 0 && !colourBuyable(gate, c, countryCode, supplier, pb)) continue;
709
725
  out.push(c);
710
726
  }
711
727
  return out;
@@ -713,13 +729,14 @@ function coloursFor(p, fit, countryCode) {
713
729
  function sizesFor(p, fit, colour, countryCode) {
714
730
  const gate = gateFit(p, fit);
715
731
  const supplier = supplierOf(p);
732
+ const pb = printfulBlankOf(p);
716
733
  const target = colour.trim().toLowerCase();
717
734
  const out = [];
718
735
  for (const v of variantsOfFit(p, fit)) {
719
736
  if (variantColour(v) !== target) continue;
720
737
  const s = sizeOfVariant(v);
721
738
  if (!s || out.includes(s)) continue;
722
- if (gate && countryCode !== void 0 && !sizeBuyable(gate, s, countryCode, supplier)) continue;
739
+ if (gate && countryCode !== void 0 && !sizeBuyable(gate, s, countryCode, supplier, pb)) continue;
723
740
  out.push(s);
724
741
  }
725
742
  return out;
@@ -758,21 +775,22 @@ function resolveVariant(p, opts) {
758
775
  const pool = variantsOfFit(p, fit);
759
776
  const gate = gateFit(p, fit);
760
777
  const supplier = supplierOf(p);
778
+ const pb = printfulBlankOf(p);
761
779
  const colours = coloursFor(p, fit, opts.countryCode);
762
780
  const colour = colourKey(opts.colour ?? "") || (colours[0] ?? null);
763
- if (gate && colour && opts.countryCode !== void 0 && !colourBuyable(gate, colour, opts.countryCode, supplier)) {
781
+ if (gate && colour && opts.countryCode !== void 0 && !colourBuyable(gate, colour, opts.countryCode, supplier, pb)) {
764
782
  return { ok: false, reason: "fit_unavailable", fit: gate, colour };
765
783
  }
766
784
  const size = (opts.size ?? "").trim() || null;
767
785
  const sizeLc = size?.toLowerCase();
768
786
  const buyableSizes = colour ? sizesFor(p, fit, colour, opts.countryCode) : [];
769
787
  const sizeExists = !!size && pool.some((x) => (sizeOfVariant(x) ?? x.title ?? "").toLowerCase() === size.toLowerCase());
770
- if (gate && size && sizeExists && opts.countryCode !== void 0 && !sizeBuyable(gate, size, opts.countryCode, supplier)) {
788
+ if (gate && size && sizeExists && opts.countryCode !== void 0 && !sizeBuyable(gate, size, opts.countryCode, supplier, pb)) {
771
789
  return { ok: false, reason: "size_unavailable", fit: gate, size, sizes: buyableSizes };
772
790
  }
773
791
  const sellable = gate && opts.countryCode !== void 0 ? pool.filter((x) => {
774
792
  const s = sizeOfVariant(x);
775
- return !s || sizeBuyable(gate, s, opts.countryCode, supplier);
793
+ return !s || sizeBuyable(gate, s, opts.countryCode, supplier, pb);
776
794
  }) : pool;
777
795
  const v = sellable.find((x) => {
778
796
  const colourOk = !colour || variantColour(x) === colour;
@@ -789,7 +807,16 @@ var FIT_TOKEN = /^(regular|oversize)$/i;
789
807
  function titleSegments(title) {
790
808
  return (title ?? "").split("/").map((s) => s.trim()).filter(Boolean);
791
809
  }
792
- var COLOUR_LABELS = { black: "Faded Black", white: "Faded White", coal: "Faded Coal", eucalyptus: "Faded Eucalyptus", bone: "Faded Bone" };
810
+ var COLOUR_LABELS = {
811
+ black: "Faded Black",
812
+ white: "Faded White",
813
+ coal: "Faded Coal",
814
+ eucalyptus: "Faded Eucalyptus",
815
+ bone: "Faded Bone",
816
+ grey: "Faded Grey",
817
+ khaki: "Faded Khaki",
818
+ indigo: "Faded Indigo"
819
+ };
793
820
  function colourLabel(colour) {
794
821
  const c = colour.trim();
795
822
  if (!c) return c;
@@ -912,6 +939,31 @@ function firstImageUrl(images) {
912
939
  function escapeRegex(s) {
913
940
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
914
941
  }
942
+ function sizeTokenOf(title) {
943
+ return titleSegments(title).find((s) => SIZE_TOKEN.test(s)) ?? null;
944
+ }
945
+ var SIZE_ORDER = ["xxs", "xs", "s", "m", "l", "xl", "2xl", "xxl", "3xl", "xxxl", "4xl", "5xl"];
946
+ function sortSizes(sizes) {
947
+ const rank = (s) => {
948
+ const i = SIZE_ORDER.indexOf(s.trim().toLowerCase());
949
+ return i === -1 ? SIZE_ORDER.length : i;
950
+ };
951
+ return [...sizes].sort((a, b) => rank(a) - rank(b) || a.localeCompare(b));
952
+ }
953
+ function lineOption(item, titles) {
954
+ const o = item.variant?.options?.find((x) => titles.includes((x.option?.title ?? "").toLowerCase()));
955
+ const v = typeof o?.value === "string" ? o.value.trim() : "";
956
+ return v ? v : null;
957
+ }
958
+ function lineColour(item) {
959
+ const opt = lineOption(item, ["colour", "color"]);
960
+ if (opt) return opt.toLowerCase();
961
+ const vt = (item.variant_title ?? "").toString();
962
+ return vt ? variantColour({ id: "x", title: vt }) : null;
963
+ }
964
+ function lineSize(item) {
965
+ return lineOption(item, ["size"]) ?? sizeTokenOf(item.variant_title);
966
+ }
915
967
  function orderStage(o) {
916
968
  const status = (o.status ?? "").trim().toLowerCase();
917
969
  if (status === "canceled" || status === "refunded") return "reverted";
@@ -1048,7 +1100,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1048
1100
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
1049
1101
 
1050
1102
  // src/version.ts
1051
- var VERSION = "0.4.11";
1103
+ var VERSION = "0.4.13";
1052
1104
 
1053
1105
  // src/onboarding.ts
1054
1106
  var GETTING_STARTED_URI = "bt://getting-started";
@@ -1266,12 +1318,18 @@ function taxSuffix(isTaxInclusive) {
1266
1318
  return isTaxInclusive ? " incl. VAT" : "";
1267
1319
  }
1268
1320
  function cartItems(cart) {
1269
- return (cart.items ?? []).map((li) => ({
1270
- variant_id: li.variant_id ?? null,
1271
- title: sanitize(li.product_title ?? li.title ?? "item", 80),
1272
- qty: li.quantity ?? 1,
1273
- total: li.total ?? null
1274
- }));
1321
+ return (cart.items ?? []).map((li) => {
1322
+ const size = lineSize(li);
1323
+ const colour = lineColour(li);
1324
+ return {
1325
+ variant_id: li.variant_id ?? null,
1326
+ title: sanitize(li.product_title ?? li.title ?? "item", 80),
1327
+ size: size ? sanitize(size, 20) : null,
1328
+ colour: colour ? sanitize(colour, 20) : null,
1329
+ qty: li.quantity ?? 1,
1330
+ total: li.total ?? null
1331
+ };
1332
+ });
1275
1333
  }
1276
1334
  function cartSummary(cart, region) {
1277
1335
  const lines = [];
@@ -1282,7 +1340,10 @@ function cartSummary(cart, region) {
1282
1340
  } else {
1283
1341
  for (const li of items) {
1284
1342
  const title = sanitize(li.product_title ?? li.title ?? "item", 80);
1285
- lines.push(` ${li.quantity}x ${title} \u2014 ${fmtPrice(li.total, cart.currency_code ?? region?.currency_code)}`);
1343
+ const colour = lineColour(li);
1344
+ const size = lineSize(li);
1345
+ const tags = [colour, size].filter((x) => !!x).map((x) => ` \xB7 ${sanitize(x, 20)}`).join("");
1346
+ lines.push(` ${li.quantity}x ${title}${tags} \u2014 ${fmtPrice(li.total, cart.currency_code ?? region?.currency_code)}`);
1286
1347
  }
1287
1348
  }
1288
1349
  const cur = cart.currency_code ?? region?.currency_code;
@@ -1489,12 +1550,12 @@ function makeTools(client, opts = {}) {
1489
1550
  };
1490
1551
  const add_to_cart = {
1491
1552
  name: "add_to_cart",
1492
- description: "Add a piece to a cart. Pass cart_id to reuse an existing cart (the durable handle), or omit it to start a fresh one. Call get_product first to get valid sizes/colours (or an exact variant_id); for a design sold in two fits (regular / oversize, priced apart) you MUST pass `fit`. qty is capped at 20. Returns the cart_id and the server-side cart summary (VAT included where applicable). `region` (ISO-2) sets the ship-to country + currency of a NEW cart. A reused cart ships where it already ships: a `region` that disagrees with it is refused (start a new cart instead). If a reused cart has expired or was already checked out, a fresh one is made in the same region and the reply says cart_replaced: true.",
1553
+ description: "Add a piece to a cart. Pass cart_id to reuse an existing cart (the durable handle), or omit it to start a fresh one. Call get_product first to get valid sizes/colours (or an exact variant_id); for a design sold in two fits (regular / oversize, priced apart) you MUST pass `fit`. size is required when the colour comes in more than one size: ask the buyer, never choose one for them. qty is capped at 20. Returns the cart_id and the server-side cart summary (VAT included where applicable). `region` (ISO-2) sets the ship-to country + currency of a NEW cart. A reused cart ships where it already ships: a `region` that disagrees with it is refused (start a new cart instead). If a reused cart has expired or was already checked out, a fresh one is made in the same region and the reply says cart_replaced: true.",
1493
1554
  inputSchema: {
1494
1555
  handle: z.string().optional().describe("product handle (resolves a variant by size + colour; call get_product for valid values)"),
1495
1556
  variant_id: z.string().optional().describe("a specific variant id from get_product (overrides handle/size/colour)"),
1496
1557
  fit: z.string().optional().describe("regular or oversize; REQUIRED when the design comes in both (get_product lists them)"),
1497
- size: z.string().optional().describe("size label from get_product (it lists the sizes you can buy where the cart ships)"),
1558
+ size: z.string().optional().describe("size label from get_product (it lists the sizes you can buy where the cart ships); required when the colour comes in more than one size, ask the buyer"),
1498
1559
  colour: z.string().optional().describe("piece colour from get_product, e.g. black / white, or the name shown, e.g. Faded Black (default: the first colour you can buy where the cart ships)"),
1499
1560
  qty: z.number().int().positive().max(20).optional().describe("quantity, 1\u201320 (default 1)"),
1500
1561
  cart_id: z.string().optional().describe("reuse this cart; omit to create a new one"),
@@ -1510,6 +1571,7 @@ function makeTools(client, opts = {}) {
1510
1571
  } catch (e) {
1511
1572
  if (!(e instanceof RecoverableCartError)) throw e;
1512
1573
  }
1574
+ if (existing?.completed_at) existing = null;
1513
1575
  }
1514
1576
  let region;
1515
1577
  let shipCC;
@@ -1587,6 +1649,13 @@ function makeTools(client, opts = {}) {
1587
1649
  `"${sanitize(handle, 60)}" isn't sold in fit "${sanitize(r.requested, 20)}"${r.fits.length ? ` \u2014 it comes in ${r.fits.join(" / ")}` : ""}. nothing was added.`
1588
1650
  );
1589
1651
  }
1652
+ const colourAsked = "colour" in r ? r.colour : null;
1653
+ const madeIn = coloursFor(product, productFits(product).length > 0 ? r.fit ?? null : null);
1654
+ if (colourAsked && (r.reason === "fit_unavailable" || r.reason === "not_found") && !madeIn.includes(colourAsked)) {
1655
+ return errorText(
1656
+ `"${sanitize(handle, 60)}" isn't made in ${sanitize(colourAsked, 20)}. colours: ${madeIn.map((c) => sanitize(c, 20)).join(" \xB7 ")} (pass one as colour). nothing was added.`
1657
+ );
1658
+ }
1590
1659
  if (r.reason === "fit_unavailable") {
1591
1660
  return errorText(
1592
1661
  `${sanitize(colourLabel(r.colour ?? ""), 20)} is not available in ${r.fit} for ${sanitize(shipCC ?? "this region", 8)}. nothing was added.`
@@ -1597,12 +1666,27 @@ function makeTools(client, opts = {}) {
1597
1666
  `size ${sanitize(r.size, 20)} is not made in ${r.fit} for ${sanitize(shipCC ?? "this region", 8)}${r.sizes.length ? ` \u2014 sizes here: ${r.sizes.map((x) => sanitize(x, 20)).join(" ")}` : ""}. nothing was added.`
1598
1667
  );
1599
1668
  }
1669
+ if (size && r.colour && r.sizes.length) {
1670
+ return errorText(
1671
+ `"${sanitize(handle, 60)}" doesn't come in size ${sanitize(size, 20)} in ${sanitize(r.colour, 20)}. sizes: ${sortSizes(r.sizes).map((x) => sanitize(x, 20)).join(" ")}. nothing was added.`
1672
+ );
1673
+ }
1600
1674
  const have = r.colours.length ? ` available colours: ${r.colours.map((c) => sanitize(colourLabel(c), 20)).join(", ")}.` : "";
1601
1675
  const haveSizes = r.sizes.length ? ` sizes: ${r.sizes.map((x) => sanitize(x, 20)).join(" ")}.` : "";
1602
1676
  return errorText(
1603
1677
  `no variant for "${sanitize(handle, 60)}"${r.fit ? ` fit ${r.fit}` : ""}${size ? ` size ${sanitize(size, 20)}` : ""}${r.colour ? ` (${sanitize(colourLabel(r.colour), 20)})` : ""}.${have}${haveSizes}`
1604
1678
  );
1605
1679
  }
1680
+ if (!size?.trim()) {
1681
+ const sizes = sortSizes(
1682
+ r.colour ? sizesFor(product, r.fit, r.colour, shipCC) : [...new Set((product.variants ?? []).map((v) => sizeOfVariant(v)).filter((s) => !!s))]
1683
+ );
1684
+ if (sizes.length > 1) {
1685
+ return errorText(
1686
+ `size required for "${sanitize(handle, 60)}": ${sizes.map((x) => sanitize(x, 20)).join(" ")}${r.colour ? ` in ${sanitize(r.colour, 20)}` : ""}. ask the buyer which size, don't choose for them, then pass size. nothing was added.`
1687
+ );
1688
+ }
1689
+ }
1606
1690
  variantId = r.variant.id;
1607
1691
  }
1608
1692
  const addTo = async (cartId) => client.addLineItem(cartId, variantId, qty);
@@ -1649,6 +1733,14 @@ ${cartSummary(cart, region)}`,
1649
1733
  const region = await client.resolveRegion(regionCC);
1650
1734
  try {
1651
1735
  const cart = await client.getCart(cartId);
1736
+ if (cart.completed_at) {
1737
+ return {
1738
+ ...errorText(
1739
+ "that cart was already checked out and became an order, so it cannot be edited. omit cart_id on the next add_to_cart to start a new one."
1740
+ ),
1741
+ structuredContent: { completed: true }
1742
+ };
1743
+ }
1652
1744
  const itemCount = (cart.items ?? []).reduce((n, li) => n + (li.quantity ?? 1), 0);
1653
1745
  return result(cartSummary(cart, region), {
1654
1746
  cart_id: cart.id,
@@ -1686,12 +1778,28 @@ ${cartSummary(cart, region)}`,
1686
1778
  }
1687
1779
  return errorText("couldn't attach a shipping method to this cart \u2014 checkout isn't available for it right now.");
1688
1780
  }
1689
- const mint = await client.mintCheckout(cartId, email, "mcp");
1781
+ const completedAnswer = () => ({
1782
+ ...errorText(
1783
+ "that cart was already checked out and became an order. call check_payment with its session_id, or omit cart_id on add_to_cart to start a new one."
1784
+ ),
1785
+ structuredContent: { completed: true }
1786
+ });
1787
+ if (shipped.completed_at) {
1788
+ return completedAnswer();
1789
+ }
1790
+ let mint;
1791
+ try {
1792
+ mint = await client.mintCheckout(cartId, email, "mcp");
1793
+ } catch (e) {
1794
+ if (e instanceof StoreApiError && e.status === 409) {
1795
+ return completedAnswer();
1796
+ }
1797
+ throw e;
1798
+ }
1690
1799
  const { checkout_url, session_id } = mint;
1691
1800
  const finalTotal = mint.total ?? shipped.total ?? null;
1692
1801
  const finalCurrency = mint.currency ?? shipped.currency_code ?? null;
1693
1802
  const totalStr = `${fmtPrice(finalTotal, finalCurrency)}${taxSuffix(isCartTaxInclusive(shipped))}`;
1694
- const discountLine = mint.discount && mint.discount.amount > 0 ? [` dev channel: \u2212${fmtPrice(mint.discount.amount, finalCurrency)} (${mint.discount.pct}% off \u2014 for buying from the terminal/agent)`] : [];
1695
1803
  const moneyLines = cartMoneyRows({
1696
1804
  itemTotal: shipped.item_total,
1697
1805
  discountTotal: mint.discount?.amount,