@binaryandthread/mcp 0.3.1 → 0.4.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/README.md +7 -0
- package/dist/bin.js +314 -76
- package/dist/bin.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,3 +104,10 @@ Claude renders MCP `ImageContent` natively on claude.ai / the API, but Claude Co
|
|
|
104
104
|
bug ([#31208](https://github.com/anthropics/claude-code/issues/31208), ~15–25k tokens/image) and can
|
|
105
105
|
fail to render it. So `get_product` always emits the **Blob URL as a text block too**, the reliable
|
|
106
106
|
floor. The buy is never blocked on rendering.
|
|
107
|
+
|
|
108
|
+
## Fits
|
|
109
|
+
|
|
110
|
+
Some designs are sold in two fits, **regular** and **oversize**, priced apart. `list_products` marks them
|
|
111
|
+
(`fits`, `price_from`), `get_product` lists sizes and prices per fit (pass `fit` to narrow), and
|
|
112
|
+
`add_to_cart` **requires** `fit` for them: without it the tool returns an error naming both fits, and
|
|
113
|
+
nothing is added. Colours follow the ship-to region (khaki is available on the oversize fit in the US only).
|
package/dist/bin.js
CHANGED
|
@@ -545,6 +545,116 @@ function isAllowedImageHost(url) {
|
|
|
545
545
|
(allowed) => host === allowed || host.endsWith("." + allowed)
|
|
546
546
|
);
|
|
547
547
|
}
|
|
548
|
+
var FITS = ["regular", "oversize"];
|
|
549
|
+
function parseFit(v) {
|
|
550
|
+
const s = typeof v === "string" ? v.trim().toLowerCase() : "";
|
|
551
|
+
return s === "regular" || s === "oversize" ? s : null;
|
|
552
|
+
}
|
|
553
|
+
function fitLabel(f) {
|
|
554
|
+
return f === "oversize" ? "Oversize" : "Regular";
|
|
555
|
+
}
|
|
556
|
+
function variantFit(v) {
|
|
557
|
+
const opt = (v.options ?? []).find((o) => (o.option?.title ?? "").toLowerCase() === "fit");
|
|
558
|
+
return opt ? parseFit(opt.value) : null;
|
|
559
|
+
}
|
|
560
|
+
function productFits(p) {
|
|
561
|
+
const seen = /* @__PURE__ */ new Set();
|
|
562
|
+
for (const v of p.variants ?? []) {
|
|
563
|
+
const f = variantFit(v);
|
|
564
|
+
if (f) seen.add(f);
|
|
565
|
+
}
|
|
566
|
+
return FITS.filter((f) => seen.has(f));
|
|
567
|
+
}
|
|
568
|
+
var BLANK_COLOURS = {
|
|
569
|
+
"gildan-5000": ["black", "white"],
|
|
570
|
+
"sols-03996": ["black", "white"],
|
|
571
|
+
"shaka-shmhss": ["black", "white", "khaki"]
|
|
572
|
+
};
|
|
573
|
+
function blankFor(fit, countryCode) {
|
|
574
|
+
if (fit === "regular") return "gildan-5000";
|
|
575
|
+
return (countryCode ?? "").trim().toLowerCase() === "us" ? "shaka-shmhss" : "sols-03996";
|
|
576
|
+
}
|
|
577
|
+
function colourBuyable(fit, colour, countryCode) {
|
|
578
|
+
return BLANK_COLOURS[blankFor(fit, countryCode)].includes(colour.trim().toLowerCase());
|
|
579
|
+
}
|
|
580
|
+
function variantsOfFit(p, fit) {
|
|
581
|
+
const all = p.variants ?? [];
|
|
582
|
+
return fit === null ? all : all.filter((v) => variantFit(v) === fit);
|
|
583
|
+
}
|
|
584
|
+
function coloursFor(p, fit, countryCode) {
|
|
585
|
+
const out = [];
|
|
586
|
+
for (const v of variantsOfFit(p, fit)) {
|
|
587
|
+
const c = variantColour(v);
|
|
588
|
+
if (!c || out.includes(c)) continue;
|
|
589
|
+
if (fit && countryCode !== void 0 && !colourBuyable(fit, c, countryCode)) continue;
|
|
590
|
+
out.push(c);
|
|
591
|
+
}
|
|
592
|
+
return out;
|
|
593
|
+
}
|
|
594
|
+
function sizesFor(p, fit, colour) {
|
|
595
|
+
const target = colour.trim().toLowerCase();
|
|
596
|
+
const out = [];
|
|
597
|
+
for (const v of variantsOfFit(p, fit)) {
|
|
598
|
+
if (variantColour(v) !== target) continue;
|
|
599
|
+
const s = sizeOfVariant(v);
|
|
600
|
+
if (s && !out.includes(s)) out.push(s);
|
|
601
|
+
}
|
|
602
|
+
return out;
|
|
603
|
+
}
|
|
604
|
+
function cheapest(vs) {
|
|
605
|
+
const priced = vs.filter((v2) => typeof v2.calculated_price?.calculated_amount === "number").sort(
|
|
606
|
+
(a, b) => a.calculated_price.calculated_amount - b.calculated_price.calculated_amount
|
|
607
|
+
);
|
|
608
|
+
const v = priced[0];
|
|
609
|
+
return {
|
|
610
|
+
amount: v?.calculated_price?.calculated_amount ?? null,
|
|
611
|
+
currency: v?.calculated_price?.currency_code ?? null,
|
|
612
|
+
taxInclusive: v?.calculated_price?.is_calculated_price_tax_inclusive ?? null
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
function priceFrom(p) {
|
|
616
|
+
const fits = productFits(p);
|
|
617
|
+
const base = cheapest(p.variants ?? []);
|
|
618
|
+
const amounts = new Set(fits.map((f) => cheapest(variantsOfFit(p, f)).amount));
|
|
619
|
+
return { ...base, from: fits.length > 1 && amounts.size > 1 };
|
|
620
|
+
}
|
|
621
|
+
function priceFor(p, fit) {
|
|
622
|
+
return cheapest(variantsOfFit(p, fit));
|
|
623
|
+
}
|
|
624
|
+
function resolveVariant(p, opts) {
|
|
625
|
+
const fits = productFits(p);
|
|
626
|
+
const requested = parseFit(opts.fit);
|
|
627
|
+
const askedFor = typeof opts.fit === "string" ? opts.fit.trim() : "";
|
|
628
|
+
let fit = null;
|
|
629
|
+
if (fits.length > 0) {
|
|
630
|
+
if (requested && fits.includes(requested)) fit = requested;
|
|
631
|
+
else if (askedFor) return { ok: false, reason: "fit_not_offered", requested: askedFor, fits };
|
|
632
|
+
else if (fits.length === 1) fit = fits[0];
|
|
633
|
+
else return { ok: false, reason: "ambiguous_fit", fits: fits.map((f) => ({ fit: f, ...priceFor(p, f) })) };
|
|
634
|
+
}
|
|
635
|
+
const pool = variantsOfFit(p, fit);
|
|
636
|
+
const colours = coloursFor(p, fit, opts.countryCode);
|
|
637
|
+
const colour = (opts.colour ?? "").trim().toLowerCase() || (colours[0] ?? null);
|
|
638
|
+
if (fit && colour && opts.countryCode !== void 0 && !colourBuyable(fit, colour, opts.countryCode)) {
|
|
639
|
+
return { ok: false, reason: "fit_unavailable", fit, colour };
|
|
640
|
+
}
|
|
641
|
+
const size = (opts.size ?? "").trim() || null;
|
|
642
|
+
const sizeLc = size?.toLowerCase();
|
|
643
|
+
const v = pool.find((x) => {
|
|
644
|
+
const colourOk = !colour || variantColour(x) === colour;
|
|
645
|
+
const sizeOk = !sizeLc || (sizeOfVariant(x) ?? "").toLowerCase() === sizeLc || (x.title ?? "").toLowerCase() === sizeLc;
|
|
646
|
+
return colourOk && sizeOk;
|
|
647
|
+
});
|
|
648
|
+
if (!v) {
|
|
649
|
+
return { ok: false, reason: "not_found", fit, colour, size, colours, sizes: colour ? sizesFor(p, fit, colour) : [] };
|
|
650
|
+
}
|
|
651
|
+
return { ok: true, variant: v, fit, colour: variantColour(v), size: sizeOfVariant(v) };
|
|
652
|
+
}
|
|
653
|
+
var SIZE_TOKEN = /^(xxs|xs|s|m|l|xl|xxl|2xl|3xl|xxxl|4xl|5xl)$/i;
|
|
654
|
+
var FIT_TOKEN = /^(regular|oversize)$/i;
|
|
655
|
+
function titleSegments(title) {
|
|
656
|
+
return (title ?? "").split("/").map((s) => s.trim()).filter(Boolean);
|
|
657
|
+
}
|
|
548
658
|
function variantColour(v) {
|
|
549
659
|
const opt = (v.options ?? []).find(
|
|
550
660
|
(o) => (o.option?.title ?? "").toLowerCase() === "colour" || (o.option?.title ?? "").toLowerCase() === "color"
|
|
@@ -552,11 +662,10 @@ function variantColour(v) {
|
|
|
552
662
|
if (opt && typeof opt.value === "string" && opt.value.trim()) {
|
|
553
663
|
return opt.value.trim().toLowerCase();
|
|
554
664
|
}
|
|
555
|
-
const
|
|
556
|
-
if (
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
if (tail) return tail.toLowerCase();
|
|
665
|
+
const segs = titleSegments(v.title);
|
|
666
|
+
if (segs.length >= 2) {
|
|
667
|
+
const seg = segs.find((s) => !SIZE_TOKEN.test(s) && !FIT_TOKEN.test(s));
|
|
668
|
+
if (seg) return seg.toLowerCase();
|
|
560
669
|
}
|
|
561
670
|
return null;
|
|
562
671
|
}
|
|
@@ -587,35 +696,76 @@ function sizeOfVariant(v) {
|
|
|
587
696
|
if (opt && typeof opt.value === "string" && opt.value.trim()) {
|
|
588
697
|
return opt.value.trim();
|
|
589
698
|
}
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
699
|
+
const segs = titleSegments(v.title);
|
|
700
|
+
const token = segs.find((s) => SIZE_TOKEN.test(s));
|
|
701
|
+
if (token) return token;
|
|
702
|
+
return segs.length === 1 ? segs[0] : segs[0] ?? null;
|
|
703
|
+
}
|
|
704
|
+
var PREFERRED_SCENE = "plp";
|
|
705
|
+
function sceneImage(images, scene, colour) {
|
|
706
|
+
for (const img of images ?? []) {
|
|
707
|
+
const u = (img?.url ?? "").trim();
|
|
708
|
+
if (!u || !u.toLowerCase().includes(`-${scene}-`)) continue;
|
|
709
|
+
if (colour === null || new RegExp(`(^|[^a-z])${escapeRegex(colour)}([^a-z]|$)`, "i").test(u.toLowerCase())) {
|
|
710
|
+
return u;
|
|
711
|
+
}
|
|
594
712
|
}
|
|
595
|
-
return
|
|
713
|
+
return null;
|
|
596
714
|
}
|
|
597
|
-
function
|
|
598
|
-
const
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const sizeMatches = (v) => {
|
|
602
|
-
if (!size) return true;
|
|
603
|
-
return (sizeOfVariant(v) ?? "").toLowerCase() === size || (v.title ?? "").toLowerCase() === size;
|
|
604
|
-
};
|
|
605
|
-
const colourMatches = (v) => {
|
|
606
|
-
if (!colour) return true;
|
|
607
|
-
return variantColour(v) === colour;
|
|
608
|
-
};
|
|
609
|
-
if (size && colour) {
|
|
610
|
-
return variants.find((v) => sizeMatches(v) && colourMatches(v));
|
|
715
|
+
function imageForColour(p, colour) {
|
|
716
|
+
const target = colour.trim().toLowerCase();
|
|
717
|
+
if (!target) {
|
|
718
|
+
return sceneImage(p.images, PREFERRED_SCENE, null) ?? p.thumbnail ?? firstImageUrl(p.images) ?? null;
|
|
611
719
|
}
|
|
612
|
-
|
|
613
|
-
|
|
720
|
+
const plp = sceneImage(p.images, PREFERRED_SCENE, target);
|
|
721
|
+
if (plp) return plp;
|
|
722
|
+
for (const v of p.variants ?? []) {
|
|
723
|
+
if (variantColour(v) !== target) continue;
|
|
724
|
+
const url = firstImageUrl(v.images);
|
|
725
|
+
if (url) return url;
|
|
726
|
+
}
|
|
727
|
+
for (const img of p.images ?? []) {
|
|
728
|
+
const tag = (img.metadata?.["colour"] ?? img.metadata?.["color"] ?? "").toString().toLowerCase();
|
|
729
|
+
if (tag === target) {
|
|
730
|
+
const u = (img.url ?? "").trim();
|
|
731
|
+
if (u) return u;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
for (const img of p.images ?? []) {
|
|
735
|
+
const u = (img.url ?? "").trim();
|
|
736
|
+
if (u && new RegExp(`(^|[^a-z])${escapeRegex(target)}([^a-z]|$)`, "i").test(u)) {
|
|
737
|
+
return u;
|
|
738
|
+
}
|
|
614
739
|
}
|
|
615
|
-
|
|
616
|
-
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
function imageFor(p, sel) {
|
|
743
|
+
const colour = sel.colour.trim().toLowerCase();
|
|
744
|
+
if (!colour) return null;
|
|
745
|
+
const metas = (p.images ?? []).map((img) => ({
|
|
746
|
+
url: (img?.url ?? "").trim(),
|
|
747
|
+
scene: typeof img?.metadata?.["scene"] === "string" ? String(img.metadata["scene"]) : null,
|
|
748
|
+
fit: parseFit(img?.metadata?.["fit"]),
|
|
749
|
+
colour: typeof img?.metadata?.["colour"] === "string" ? String(img.metadata["colour"]).toLowerCase() : null
|
|
750
|
+
}));
|
|
751
|
+
const tagged = metas.filter((m) => m.url && m.colour === colour && m.scene === PREFERRED_SCENE);
|
|
752
|
+
const exact = tagged.find((m) => sel.fit !== null && m.fit === sel.fit);
|
|
753
|
+
if (exact) return exact.url;
|
|
754
|
+
const neutral = tagged.find((m) => m.fit === null);
|
|
755
|
+
if (neutral) return neutral.url;
|
|
756
|
+
const otherFitOnly = tagged.length > 0 && sel.fit !== null;
|
|
757
|
+
if (otherFitOnly) return null;
|
|
758
|
+
return imageForColour(p, colour);
|
|
759
|
+
}
|
|
760
|
+
function firstImageUrl(images) {
|
|
761
|
+
for (const img of images ?? []) {
|
|
762
|
+
const u = (img?.url ?? "").trim();
|
|
763
|
+
if (u) return u;
|
|
617
764
|
}
|
|
618
|
-
return
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
function escapeRegex(s) {
|
|
768
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
619
769
|
}
|
|
620
770
|
function str(v, fallback) {
|
|
621
771
|
return typeof v === "string" && v.length > 0 ? v : fallback;
|
|
@@ -623,15 +773,19 @@ function str(v, fallback) {
|
|
|
623
773
|
function productReadme(p) {
|
|
624
774
|
return typeof p.description === "string" ? p.description : "";
|
|
625
775
|
}
|
|
626
|
-
function productSpecs(p) {
|
|
776
|
+
function productSpecs(p, fit) {
|
|
627
777
|
const md = p.metadata ?? {};
|
|
778
|
+
const byFit = md.specs_by_fit ?? null;
|
|
779
|
+
const fitKey = fit ?? (byFit && Object.keys(byFit).length === 1 ? Object.keys(byFit)[0] : void 0);
|
|
780
|
+
const src = fitKey && byFit?.[fitKey] || md;
|
|
781
|
+
const pick = (k, fallback) => str(src[k] ?? md[k], fallback);
|
|
628
782
|
return [
|
|
629
|
-
{ key: "material", value:
|
|
630
|
-
{ key: "weight", value:
|
|
631
|
-
{ key: "fit", value:
|
|
632
|
-
{ key: "print", value:
|
|
633
|
-
{ key: "delivery", value:
|
|
634
|
-
{ key: "care", value:
|
|
783
|
+
{ key: "material", value: pick("material", "100% cotton") },
|
|
784
|
+
{ key: "weight", value: pick("weight", "180 g/m\xB2") },
|
|
785
|
+
{ key: "fit", value: pick("fit", "Unisex heavyweight crewneck") },
|
|
786
|
+
{ key: "print", value: pick("print", "Front print") },
|
|
787
|
+
{ key: "delivery", value: pick("delivery", "Ships in 2\u20135 business days + transit") },
|
|
788
|
+
{ key: "care", value: pick("care", "Cold wash, line dry") }
|
|
635
789
|
];
|
|
636
790
|
}
|
|
637
791
|
function productLore(p) {
|
|
@@ -652,7 +806,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
652
806
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
653
807
|
|
|
654
808
|
// src/version.ts
|
|
655
|
-
var VERSION = "0.
|
|
809
|
+
var VERSION = "0.4.0";
|
|
656
810
|
|
|
657
811
|
// src/onboarding.ts
|
|
658
812
|
var GETTING_STARTED_URI = "bt://getting-started";
|
|
@@ -869,14 +1023,6 @@ function fmtPrice(amount, currency) {
|
|
|
869
1023
|
function taxSuffix(isTaxInclusive) {
|
|
870
1024
|
return isTaxInclusive ? " incl. VAT" : "";
|
|
871
1025
|
}
|
|
872
|
-
function firstPrice(p) {
|
|
873
|
-
const v = (p.variants ?? [])[0];
|
|
874
|
-
return {
|
|
875
|
-
amount: v?.calculated_price?.calculated_amount ?? null,
|
|
876
|
-
currency: v?.calculated_price?.currency_code ?? null,
|
|
877
|
-
taxInclusive: v?.calculated_price?.is_calculated_price_tax_inclusive ?? null
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
1026
|
function cartItems(cart) {
|
|
881
1027
|
return (cart.items ?? []).map((li) => ({
|
|
882
1028
|
variant_id: li.variant_id ?? null,
|
|
@@ -921,14 +1067,25 @@ function makeTools(client, opts = {}) {
|
|
|
921
1067
|
return result("nothing in stock on this branch yet.", { products: [], currency: region.currency_code, region: rr.requested ?? null });
|
|
922
1068
|
}
|
|
923
1069
|
const lines = rows.map(({ p, stage: stage2 }) => {
|
|
924
|
-
const { amount, currency, taxInclusive } =
|
|
1070
|
+
const { amount, currency, taxInclusive, from } = priceFrom(p);
|
|
1071
|
+
const fits = productFits(p);
|
|
925
1072
|
const handle = sanitize(p.handle ?? p.id, 60);
|
|
926
1073
|
const title = sanitize(p.title ?? "", 80);
|
|
927
|
-
|
|
1074
|
+
const fitsNote = fits.length > 1 ? ` fits: ${fits.join("/")}` : "";
|
|
1075
|
+
return `${handle} ${from ? "from " : ""}${fmtPrice(amount, currency)}${taxSuffix(taxInclusive)} [${stage2}] ${title}${fitsNote}`.trimEnd();
|
|
928
1076
|
});
|
|
929
1077
|
const productList = rows.map(({ p, stage: stage2 }) => {
|
|
930
|
-
const { amount, currency, taxInclusive } =
|
|
931
|
-
return {
|
|
1078
|
+
const { amount, currency, taxInclusive, from } = priceFrom(p);
|
|
1079
|
+
return {
|
|
1080
|
+
handle: p.handle ?? p.id,
|
|
1081
|
+
title: sanitize(p.title ?? "", 80),
|
|
1082
|
+
price: amount,
|
|
1083
|
+
price_from: from,
|
|
1084
|
+
currency,
|
|
1085
|
+
tax_inclusive: taxInclusive,
|
|
1086
|
+
stage: stage2,
|
|
1087
|
+
fits: productFits(p)
|
|
1088
|
+
};
|
|
932
1089
|
});
|
|
933
1090
|
return result(lines.join("\n"), { products: productList, currency: region.currency_code, region: rr.requested ?? null });
|
|
934
1091
|
}
|
|
@@ -937,9 +1094,10 @@ function makeTools(client, opts = {}) {
|
|
|
937
1094
|
name: "get_product",
|
|
938
1095
|
// M3 ★ — a hidden line for the schema-readers (devs read tool descriptions). Rewards the
|
|
939
1096
|
// contributor instinct + hints at a real feature (the next branch / drops). Never alters behaviour.
|
|
940
|
-
description: "Get one piece in full: its readme, the spec sheet (material/weight/fit/print/delivery/care), sizes + price (VAT included where applicable), the lore (the story behind it, with sources), and its image (rendered + the Blob URL as a text floor). Prices are in the region's currency \u2014 pass `region` (ISO-2) to price for a market. // you read the schema. that's the contributor instinct. ask me about the next branch.",
|
|
1097
|
+
description: "Get one piece in full: its readme, the spec sheet (material/weight/fit/print/delivery/care), sizes + price (VAT included where applicable), the lore (the story behind it, with sources), and its image (rendered + the Blob URL as a text floor). Prices are in the region's currency \u2014 pass `region` (ISO-2) to price for a market. Designs sold in two fits (regular / oversize) are priced per fit: pass `fit` to narrow, and ALWAYS pass `fit` to add_to_cart for them. // you read the schema. that's the contributor instinct. ask me about the next branch.",
|
|
941
1098
|
inputSchema: {
|
|
942
1099
|
handle: z.string().describe("the product handle/slug (from list_products)"),
|
|
1100
|
+
fit: z.string().optional().describe("regular or oversize: narrow sizes, price and image to that fit (omit to see both)"),
|
|
943
1101
|
region: z.string().optional().describe("ISO-2 ship-to country to price for (e.g. se / us / de); overrides the server default")
|
|
944
1102
|
},
|
|
945
1103
|
handler: async (args) => {
|
|
@@ -960,22 +1118,52 @@ function makeTools(client, opts = {}) {
|
|
|
960
1118
|
if (typeof fabric === "string") {
|
|
961
1119
|
lines.push(`fabric: ${sanitize(fabric, 80)}`);
|
|
962
1120
|
}
|
|
1121
|
+
const fits = productFits(product);
|
|
1122
|
+
const wantedFit = parseFit(args.fit);
|
|
1123
|
+
if (args.fit && (!wantedFit || !fits.includes(wantedFit))) {
|
|
1124
|
+
return errorText(
|
|
1125
|
+
fits.length ? `no fit called "${sanitize(String(args.fit), 20)}" for "${sanitize(handle, 60)}". fits: ${fits.join(" / ")}.` : `"${sanitize(handle, 60)}" comes in one fit; omit \`fit\`.`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
const shipCC = (rr.requested ?? region.countries?.[0]?.iso_2 ?? "").toLowerCase() || null;
|
|
1129
|
+
const shownFits = fits.length ? wantedFit ? [wantedFit] : fits : [null];
|
|
1130
|
+
if (fits.length > 1) {
|
|
1131
|
+
lines.push(`fit: ${fits.map((f) => `${f} (${fmtPrice(priceFor(product, f).amount, priceFor(product, f).currency)})`).join(" \xB7 ")}${wantedFit ? ` (showing ${wantedFit})` : ""}`);
|
|
1132
|
+
}
|
|
963
1133
|
const colours = productColours(product);
|
|
1134
|
+
const availability = {};
|
|
1135
|
+
const unavailableColours = {};
|
|
964
1136
|
if (colours.length > 0) {
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
const
|
|
968
|
-
|
|
1137
|
+
for (const f of shownFits) {
|
|
1138
|
+
const buyable = coloursFor(product, f, shipCC);
|
|
1139
|
+
const notBuyable = coloursFor(product, f).filter((c) => !buyable.includes(c));
|
|
1140
|
+
const indent = f ? " " : "";
|
|
1141
|
+
if (f) lines.push(`${fitLabel(f).toLowerCase()}:`);
|
|
1142
|
+
if (!f) lines.push(`available colours: ${buyable.map((c) => sanitize(c, 20)).join(", ")}`);
|
|
1143
|
+
const perColour = {};
|
|
1144
|
+
for (const c of buyable) {
|
|
1145
|
+
const cs = sizesFor(product, f, c);
|
|
1146
|
+
perColour[c] = cs;
|
|
1147
|
+
lines.push(`${indent} ${sanitize(c, 20)}: ${cs.map((x) => sanitize(x, 20)).join(" ")}`);
|
|
1148
|
+
}
|
|
1149
|
+
for (const c of notBuyable) {
|
|
1150
|
+
lines.push(`${indent} ${sanitize(c, 20)}: not available where you ship`);
|
|
1151
|
+
}
|
|
1152
|
+
if (f) {
|
|
1153
|
+
availability[f] = perColour;
|
|
1154
|
+
if (notBuyable.length) unavailableColours[f] = notBuyable;
|
|
1155
|
+
}
|
|
969
1156
|
}
|
|
970
1157
|
} else {
|
|
971
1158
|
const sizes = (product.variants ?? []).map((v) => sanitize(v.title ?? "?", 20));
|
|
972
1159
|
lines.push(`sizes: ${sizes.join(" ")}`);
|
|
973
1160
|
}
|
|
974
1161
|
const v0 = (product.variants ?? [])[0];
|
|
975
|
-
|
|
976
|
-
|
|
1162
|
+
const shownPrice = wantedFit ? { ...priceFor(product, wantedFit), from: false } : priceFrom(product);
|
|
1163
|
+
if (shownPrice.amount != null) {
|
|
1164
|
+
lines.push(`price: ${shownPrice.from ? "from " : ""}${fmtPrice(shownPrice.amount, shownPrice.currency)}${taxSuffix(shownPrice.taxInclusive)}`);
|
|
977
1165
|
}
|
|
978
|
-
const specs = productSpecs(product);
|
|
1166
|
+
const specs = productSpecs(product, wantedFit ?? (fits.length ? fits[0] : null));
|
|
979
1167
|
lines.push("specs:");
|
|
980
1168
|
for (const s of specs) {
|
|
981
1169
|
lines.push(` ${sanitize(s.key, 20)}: ${sanitize(s.value, 120)}`);
|
|
@@ -988,13 +1176,17 @@ function makeTools(client, opts = {}) {
|
|
|
988
1176
|
}
|
|
989
1177
|
}
|
|
990
1178
|
const content = [{ type: "text", text: lines.join("\n") }];
|
|
991
|
-
|
|
992
|
-
|
|
1179
|
+
const imgFit = wantedFit ?? (fits.length ? fits[0] : null);
|
|
1180
|
+
const imgColour = colours.length ? coloursFor(product, imgFit, shipCC)[0] ?? null : null;
|
|
1181
|
+
const imageUrl = (imgColour ? imageFor(product, { fit: imgFit, colour: imgColour }) : null) ?? product.thumbnail ?? null;
|
|
1182
|
+
if (imageUrl) {
|
|
1183
|
+
const img = await buildImageContent(imageUrl);
|
|
993
1184
|
content.push(...img);
|
|
994
1185
|
}
|
|
995
1186
|
const variantMap = (product.variants ?? []).map((v) => ({
|
|
996
1187
|
variant_id: v.id,
|
|
997
|
-
|
|
1188
|
+
fit: variantFit(v),
|
|
1189
|
+
size: sizeOfVariant(v),
|
|
998
1190
|
colour: variantColour(v) ?? null,
|
|
999
1191
|
price: v.calculated_price?.calculated_amount ?? null
|
|
1000
1192
|
}));
|
|
@@ -1007,14 +1199,20 @@ function makeTools(client, opts = {}) {
|
|
|
1007
1199
|
structuredContent: redactForOutput({
|
|
1008
1200
|
handle: product.handle ?? handle,
|
|
1009
1201
|
title: sanitize(product.title ?? "", 120),
|
|
1010
|
-
price: v0?.calculated_price?.calculated_amount ?? null,
|
|
1011
|
-
|
|
1012
|
-
|
|
1202
|
+
price: shownPrice.amount ?? v0?.calculated_price?.calculated_amount ?? null,
|
|
1203
|
+
price_from: shownPrice.from,
|
|
1204
|
+
prices_by_fit: Object.fromEntries(fits.map((f) => [f, priceFor(product, f).amount])),
|
|
1205
|
+
currency: shownPrice.currency ?? v0?.calculated_price?.currency_code ?? null,
|
|
1206
|
+
tax_inclusive: shownPrice.taxInclusive ?? v0?.calculated_price?.is_calculated_price_tax_inclusive ?? null,
|
|
1013
1207
|
region: rr.requested ?? null,
|
|
1208
|
+
fits,
|
|
1209
|
+
fit: wantedFit ?? null,
|
|
1014
1210
|
colours,
|
|
1015
1211
|
sizes: sizeList,
|
|
1212
|
+
...fits.length ? { availability } : {},
|
|
1213
|
+
...Object.keys(unavailableColours).length ? { unavailable_colours: unavailableColours } : {},
|
|
1016
1214
|
variants: variantMap,
|
|
1017
|
-
image_url:
|
|
1215
|
+
image_url: imageUrl,
|
|
1018
1216
|
// the PDP content, machine-readable: readme + the spec sheet + the lore (+ sources).
|
|
1019
1217
|
readme: sanitize(productReadme(product), 2e3),
|
|
1020
1218
|
specs: specs.map((s) => ({ key: sanitize(s.key, 40), value: sanitize(s.value, 200) })),
|
|
@@ -1028,12 +1226,13 @@ function makeTools(client, opts = {}) {
|
|
|
1028
1226
|
};
|
|
1029
1227
|
const add_to_cart = {
|
|
1030
1228
|
name: "add_to_cart",
|
|
1031
|
-
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)
|
|
1229
|
+
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 currency for a NEW cart only \u2014 a reused cart keeps its original region.",
|
|
1032
1230
|
inputSchema: {
|
|
1033
1231
|
handle: z.string().optional().describe("product handle (resolves a variant by size + colour; call get_product for valid values)"),
|
|
1034
1232
|
variant_id: z.string().optional().describe("a specific variant id from get_product (overrides handle/size/colour)"),
|
|
1035
|
-
|
|
1036
|
-
|
|
1233
|
+
fit: z.string().optional().describe("regular or oversize; REQUIRED when the design comes in both (get_product lists them)"),
|
|
1234
|
+
size: z.string().optional().describe("size label from get_product (Regular S / M / L / XL / 2XL; Oversize XS \u2026 3XL)"),
|
|
1235
|
+
colour: z.string().optional().describe("piece colour from get_product, e.g. black / white / khaki (khaki: US oversize only; default: the first colour you can buy)"),
|
|
1037
1236
|
qty: z.number().int().positive().max(20).optional().describe("quantity, 1\u201320 (default 1)"),
|
|
1038
1237
|
cart_id: z.string().optional().describe("reuse this cart; omit to create a new one"),
|
|
1039
1238
|
region: z.string().optional().describe("ISO-2 ship-to country for a NEW cart (e.g. se / us / de); ignored when reusing a cart_id")
|
|
@@ -1057,16 +1256,37 @@ function makeTools(client, opts = {}) {
|
|
|
1057
1256
|
return errorText(`no product with handle "${sanitize(handle, 60)}".`);
|
|
1058
1257
|
}
|
|
1059
1258
|
const size = typeof args.size === "string" ? args.size : void 0;
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1259
|
+
const shipCC = (rr.requested ?? region.countries?.[0]?.iso_2 ?? "").toLowerCase() || null;
|
|
1260
|
+
const r = resolveVariant(product, {
|
|
1261
|
+
fit: typeof args.fit === "string" ? args.fit : void 0,
|
|
1262
|
+
size,
|
|
1263
|
+
colour: typeof args.colour === "string" ? args.colour : void 0,
|
|
1264
|
+
countryCode: shipCC
|
|
1265
|
+
});
|
|
1266
|
+
if (!r.ok) {
|
|
1267
|
+
if (r.reason === "ambiguous_fit") {
|
|
1268
|
+
const fitList = r.fits.map((f) => `${f.fit} (${fmtPrice(f.amount, f.currency)})`).join(" \xB7 ");
|
|
1269
|
+
return errorText(
|
|
1270
|
+
`fit required for "${sanitize(handle, 60)}": ${fitList}. pass fit: "regular" or fit: "oversize". nothing was added.`
|
|
1271
|
+
);
|
|
1272
|
+
}
|
|
1273
|
+
if (r.reason === "fit_not_offered") {
|
|
1274
|
+
return errorText(
|
|
1275
|
+
`"${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.`
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
if (r.reason === "fit_unavailable") {
|
|
1279
|
+
return errorText(
|
|
1280
|
+
`${sanitize(r.colour ?? "", 20)} is not available in ${r.fit} for ${sanitize(shipCC ?? "this region", 8)}. nothing was added.`
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
const have = r.colours.length ? ` available colours: ${r.colours.map((c) => sanitize(c, 20)).join(", ")}.` : "";
|
|
1284
|
+
const haveSizes = r.sizes.length ? ` sizes: ${r.sizes.map((x) => sanitize(x, 20)).join(" ")}.` : "";
|
|
1065
1285
|
return errorText(
|
|
1066
|
-
`no variant for "${sanitize(handle, 60)}"${size ? ` size ${sanitize(size, 20)}` : ""}${colour ? ` (${sanitize(colour, 20)})` : ""}.${have}`
|
|
1286
|
+
`no variant for "${sanitize(handle, 60)}"${r.fit ? ` fit ${r.fit}` : ""}${size ? ` size ${sanitize(size, 20)}` : ""}${r.colour ? ` (${sanitize(r.colour, 20)})` : ""}.${have}${haveSizes}`
|
|
1067
1287
|
);
|
|
1068
1288
|
}
|
|
1069
|
-
variantId = variant.id;
|
|
1289
|
+
variantId = r.variant.id;
|
|
1070
1290
|
}
|
|
1071
1291
|
const reuse = typeof args.cart_id === "string" ? args.cart_id : void 0;
|
|
1072
1292
|
const addTo = async (cartId) => client.addLineItem(cartId, variantId, qty);
|
|
@@ -1189,8 +1409,26 @@ ${cartSummary(cart, region)}`,
|
|
|
1189
1409
|
}
|
|
1190
1410
|
throw e;
|
|
1191
1411
|
}
|
|
1192
|
-
const
|
|
1193
|
-
const
|
|
1412
|
+
const s = res.summary;
|
|
1413
|
+
const out = redactForOutput({ status: res.status, order_id: res.order_id, summary: s ?? null });
|
|
1414
|
+
let human;
|
|
1415
|
+
if (res.status === "placed") {
|
|
1416
|
+
const lines = [`paid \u2014 order ${s?.display_id ? `#${s.display_id}` : "placed"}. order_id: ${res.order_id ?? "\u2014"}`];
|
|
1417
|
+
if (s) {
|
|
1418
|
+
const cur = s.currency_code;
|
|
1419
|
+
const vat = taxSuffix(s.is_tax_inclusive === true);
|
|
1420
|
+
if (s.item_total != null) lines.push(` subtotal ${fmtPrice(s.item_total, cur)}`);
|
|
1421
|
+
if (s.discount_total) {
|
|
1422
|
+
const codes = s.discount_codes.length ? ` (${s.discount_codes.join(", ")})` : "";
|
|
1423
|
+
lines.push(` discount${codes} \u2212${fmtPrice(s.discount_total, cur)}`);
|
|
1424
|
+
}
|
|
1425
|
+
if (s.shipping_total != null) lines.push(` shipping ${fmtPrice(s.shipping_total, cur)}`);
|
|
1426
|
+
lines.push(` total ${fmtPrice(s.total, cur)}${vat}`);
|
|
1427
|
+
}
|
|
1428
|
+
human = lines.join("\n");
|
|
1429
|
+
} else {
|
|
1430
|
+
human = `status: ${out.status} (call check_payment again in a moment if still pending)`;
|
|
1431
|
+
}
|
|
1194
1432
|
return { content: [{ type: "text", text: human }], structuredContent: out };
|
|
1195
1433
|
}
|
|
1196
1434
|
};
|