@solumflow-app/crm-mcp-server 0.7.0 → 0.9.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.js CHANGED
@@ -6,7 +6,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  import { createClient } from "@solumflow-app/crm-client";
7
7
 
8
8
  // package.json
9
- var version = "0.7.0";
9
+ var version = "0.9.0";
10
10
 
11
11
  // src/catalog-migration-tools.ts
12
12
  import { z as z4 } from "zod";
@@ -506,8 +506,15 @@ var page = {
506
506
  function more(nextCursor) {
507
507
  return nextCursor ? `More on the next page: cursor ${nextCursor}` : "";
508
508
  }
509
- function money(cents2, currency2) {
510
- return `${(cents2 / 100).toFixed(2)} ${currency2.toUpperCase()}`;
509
+ function money(cents3, currency2) {
510
+ return `${(cents3 / 100).toFixed(2)} ${currency2.toUpperCase()}`;
511
+ }
512
+ function fieldsNotOnPageNote(slugs) {
513
+ if (!slugs?.length) {
514
+ return "";
515
+ }
516
+ const count = slugs.length === 1 ? "1 public field has" : `${slugs.length} public fields have`;
517
+ return ` ${count} a value but ${slugs.length === 1 ? "is" : "are"} not on this product's page: ${slugs.join(", ")}; send them in publicFields to show them.`;
511
518
  }
512
519
 
513
520
  // src/catalog-tools.ts
@@ -865,7 +872,7 @@ function registerCatalogMigrationTools(server, client) {
865
872
  (file) => file.status === "pending"
866
873
  );
867
874
  return answer(
868
- `${product.name} (${product.slug || "no address"}), ${product.status} (${product.availability}); ${Object.keys(product.fields).length} fields, ${product.categories.length} categories, ${product.images.length} pictures; files: ${pending.length} pending, ${refused.length} refused${refused.length ? ` (${refused.map((file) => `${file.ref}: ${file.reason}`).join("; ")})` : ""}.`,
875
+ `${product.name} (${product.slug || "no address"}), ${product.status} (${product.availability}); ${Object.keys(product.fields).length} fields, ${product.categories.length} categories, ${product.images.length} pictures; files: ${pending.length} pending, ${refused.length} refused${refused.length ? ` (${refused.map((file) => `${file.ref}: ${file.reason}`).join("; ")})` : ""}.` + fieldsNotOnPageNote(product.fieldsNotOnPage),
869
876
  product
870
877
  );
871
878
  } catch (error) {
@@ -899,12 +906,15 @@ function registerCatalogMigrationTools(server, client) {
899
906
  target(object2)
900
907
  );
901
908
  const count = (kind) => outcomes.filter((outcome) => outcome.outcome === kind).length;
909
+ const offPage = new Set(
910
+ outcomes.flatMap((outcome) => outcome.product?.fieldsNotOnPage ?? [])
911
+ );
902
912
  const refusals = outcomes.filter((outcome) => outcome.outcome === "refused").map(
903
913
  (outcome) => `- ${outcome.externalRef}: ${outcome.error?.message ?? "refused"}` + (outcome.error?.fields.length ? ` (${outcome.error.fields.map((field) => `${field.path}: ${field.message}`).join("; ")})` : "")
904
914
  );
905
915
  return answer(
906
916
  [
907
- `${count("created")} made, ${count("changed")} changed, ${count("refused")} refused, ${count("skipped")} skipped.`,
917
+ `${count("created")} made, ${count("changed")} changed, ${count("refused")} refused, ${count("skipped")} skipped.` + (offPage.size ? ` Some products hold public fields that are not on their page (${[...offPage].join(", ")}); send them in publicFields to show them.` : ""),
908
918
  ...refusals
909
919
  ].join("\n"),
910
920
  outcomes
@@ -1074,7 +1084,7 @@ function registerCatalogWriteTools(server, client) {
1074
1084
  );
1075
1085
  const verb = product.created ? "Made" : "Changed";
1076
1086
  return answer(
1077
- `${verb} ${product.name} (${product.slug ?? "no address"}), ${product.availability}; ${product.variants.length} variants; pictures ${product.images}.`,
1087
+ `${verb} ${product.name} (${product.slug ?? "no address"}), ${product.availability}; ${product.variants.length} variants; pictures ${product.images}.` + fieldsNotOnPageNote(product.fieldsNotOnPage),
1078
1088
  product
1079
1089
  );
1080
1090
  } catch (error) {
@@ -1270,7 +1280,11 @@ var CONFIGURATOR_LIMITS = {
1270
1280
  expandedParts: 200,
1271
1281
  repeatCount: 12,
1272
1282
  repeatDepth: 2,
1273
- annotations: 30
1283
+ annotations: 30,
1284
+ roomWallOptions: 12,
1285
+ roomFloorOptions: 8,
1286
+ focus: 60,
1287
+ focusNames: 20
1274
1288
  };
1275
1289
  function jsonByteLength(value) {
1276
1290
  return new TextEncoder().encode(JSON.stringify(value)).length + jsonbSpacing(value);
@@ -1639,8 +1653,10 @@ var PRIMITIVE_PARAMS = {
1639
1653
  // A quarter of an elliptical shell in plan view, filling the block's width
1640
1654
  // and depth; `mirror` 0 bends it towards the right, 1 towards the left.
1641
1655
  arc_panel: { required: ["thickness"], optional: ["mirror"] },
1642
- // A thin strip that gives light.
1643
- led_strip: { required: [], optional: [] },
1656
+ // A thin strip that gives light. `facing` is where the light goes: 0 down
1657
+ // (the default: under a shelf, under a floating cabinet), 1 up, 2 forwards
1658
+ // into the room, 3 back against the wall.
1659
+ led_strip: { required: [], optional: ["facing"] },
1644
1660
  // A wall with a rectangular opening, measured from the block's own corner.
1645
1661
  niche_panel: {
1646
1662
  required: ["niche_x", "niche_y", "niche_width", "niche_height"],
@@ -1656,13 +1672,24 @@ var PRIMITIVE_PARAMS = {
1656
1672
  };
1657
1673
  var CONFIGURATOR_MODEL_PATH_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.glb$/;
1658
1674
  var CONFIGURATOR_FOLLOWS_PRESET = "preset";
1675
+ var MOTION_GROUP_PATTERN = /^[a-z](?:[a-z0-9_]|\{[ij]\}){0,39}$/;
1676
+ var MOTION_KINDS = ["hinge", "slide"];
1677
+ var MOTION_EDGES = ["bottom", "top", "left", "right"];
1678
+ var GeometryMotionSchema = z5.strictObject({
1679
+ group: z5.string().regex(MOTION_GROUP_PATTERN, "configurator.invalidName"),
1680
+ kind: z5.enum(MOTION_KINDS),
1681
+ edge: z5.enum(MOTION_EDGES).optional(),
1682
+ angle: FormulaSchema.optional(),
1683
+ distance: FormulaSchema.optional()
1684
+ });
1659
1685
  var PrimitiveNodeFields = {
1660
1686
  id: ConfiguratorNameSchema,
1661
1687
  slot: ConfiguratorNameSchema,
1662
1688
  size: Vector3Schema,
1663
1689
  at: Vector3Schema,
1664
1690
  visible: FormulaSchema.optional(),
1665
- params: z5.record(ConfiguratorNameSchema, FormulaSchema).optional()
1691
+ params: z5.record(ConfiguratorNameSchema, FormulaSchema).optional(),
1692
+ motion: GeometryMotionSchema.optional()
1666
1693
  };
1667
1694
  var PrimitiveNodeSchema = z5.strictObject({
1668
1695
  ...PrimitiveNodeFields,
@@ -1752,6 +1779,26 @@ function geometryDocumentSchema(modelFile) {
1752
1779
  });
1753
1780
  }
1754
1781
  var GeometryDocumentSchema = geometryDocumentSchema(ModelPathSchema);
1782
+ function motionNames(nodes) {
1783
+ const ids = /* @__PURE__ */ new Set();
1784
+ const groups = /* @__PURE__ */ new Set();
1785
+ const walk = (list) => {
1786
+ for (const node of list) {
1787
+ ids.add(node.id);
1788
+ if (node.type === "repeat") {
1789
+ walk(node.children);
1790
+ } else if (node.motion) {
1791
+ groups.add(node.motion.group);
1792
+ }
1793
+ }
1794
+ };
1795
+ walk(nodes);
1796
+ return { ids, groups };
1797
+ }
1798
+ function motionGroupMatches(template, name) {
1799
+ const pattern = template.split(/\{[ij]\}/).join("[1-9][0-9]?");
1800
+ return new RegExp(`^${pattern}$`).test(name);
1801
+ }
1755
1802
  function choiceModelBlocks(nodes) {
1756
1803
  const found = [];
1757
1804
  const walk = (list) => {
@@ -1991,6 +2038,32 @@ function geometryDocumentProblems(document, context = {}) {
1991
2038
  }
1992
2039
  checkFormula2(formula, [...nodePath, "params", key], scope);
1993
2040
  }
2041
+ if (node.motion) {
2042
+ const motion = node.motion;
2043
+ const motionPath = [...nodePath, "motion"];
2044
+ if (motion.angle !== void 0) {
2045
+ checkFormula2(motion.angle, [...motionPath, "angle"], scope);
2046
+ }
2047
+ if (motion.distance !== void 0) {
2048
+ checkFormula2(motion.distance, [...motionPath, "distance"], scope);
2049
+ }
2050
+ for (const placeholder of motion.group.match(/\{([ij])\}/g) ?? []) {
2051
+ if (!scope.has(placeholder.slice(1, 2))) {
2052
+ problems.push(
2053
+ issue([...motionPath, "group"], "configurator.unknownName", {
2054
+ name: placeholder
2055
+ })
2056
+ );
2057
+ }
2058
+ }
2059
+ if (motion.kind === "hinge" && motion.distance !== void 0 || motion.kind === "slide" && (motion.edge !== void 0 || motion.angle !== void 0)) {
2060
+ problems.push(
2061
+ issue(motionPath, "configurator.motionMixed", {
2062
+ kind: motion.kind
2063
+ })
2064
+ );
2065
+ }
2066
+ }
1994
2067
  if (node.follows !== void 0 && context.parameters) {
1995
2068
  problems.push(
1996
2069
  ...followProblems(node.follows, nodePath, {
@@ -2039,6 +2112,34 @@ function followProblems(follows, path, context) {
2039
2112
 
2040
2113
  // ../features/product-configurator/src/shared/configurator-structure.schema.ts
2041
2114
  import { z as z6 } from "zod";
2115
+
2116
+ // ../features/product-configurator/src/shared/room-palette.ts
2117
+ var ROOM_WALL_PALETTE = [
2118
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2119
+ { code: "gebroken-wit", label: "Gebroken wit", color: "#ece8e1" },
2120
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2121
+ { code: "warm-grijs", label: "Warm grijs", color: "#b9b2a8" },
2122
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2123
+ { code: "zand", label: "Zand", color: "#d6c5a8" },
2124
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2125
+ { code: "saliegroen", label: "Saliegroen", color: "#a7b29c" },
2126
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2127
+ { code: "nachtblauw", label: "Nachtblauw", color: "#2f3a4c" },
2128
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2129
+ { code: "antraciet", label: "Antraciet", color: "#3d3d3f" }
2130
+ ];
2131
+ var ROOM_FLOOR_PALETTE = [
2132
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2133
+ { code: "licht-eiken", label: "Licht eiken", color: "#c8a77e" },
2134
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2135
+ { code: "donker-eiken", label: "Donker eiken", color: "#6b4f38" },
2136
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2137
+ { code: "betongrijs", label: "Betongrijs", color: "#9a9893" },
2138
+ // i18n-exempt: the Dutch name of a colour in a palette that is also served to customers' own sites, where there is no catalogue
2139
+ { code: "warm-beige", label: "Warm beige tegel", color: "#d9cbb7" }
2140
+ ];
2141
+
2142
+ // ../features/product-configurator/src/shared/configurator-structure.schema.ts
2042
2143
  var LabelSchema = z6.string().trim().min(1).max(80);
2043
2144
  var CONFIGURATOR_TEXTURE_PATH_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(webp|jpg|jpeg)$/;
2044
2145
  var TexturePathSchema = z6.string().regex(CONFIGURATOR_TEXTURE_PATH_PATTERN, "configurator.invalidTexturePath");
@@ -2096,6 +2197,13 @@ var materialSchema = (picture) => z6.strictObject({
2096
2197
  opacity: z6.number().min(0.05).max(1).optional(),
2097
2198
  /** Gives light rather than reflecting it: a flame, a LED. */
2098
2199
  emissive: z6.boolean().optional(),
2200
+ /**
2201
+ * The colour temperature of a light, in kelvin: a LED strip sold as
2202
+ * 3000 K glows and lights in that colour rather than in `color`, which
2203
+ * stays the swatch's fallback. A strip that can do several (2700-6500 K)
2204
+ * names the one to show: 4000 K.
2205
+ */
2206
+ kelvin: z6.number().int().min(1500).max(1e4).optional(),
2099
2207
  priceGroup: ConfiguratorCodeSchema.optional()
2100
2208
  });
2101
2209
  var ConfiguratorChoiceListSchema = choiceListSchema(TexturePathSchema);
@@ -2176,9 +2284,37 @@ var CONFIGURATOR_SCENE_LIGHTINGS = [
2176
2284
  "daylight"
2177
2285
  ];
2178
2286
  var CONFIGURATOR_SCENE_EDGES = ["soft", "sharp"];
2287
+ var RoomColorSchema = z6.strictObject({
2288
+ code: ConfiguratorCodeSchema,
2289
+ label: LabelSchema,
2290
+ color: z6.string().regex(/^#[0-9a-f]{6}$/i, "configurator.invalidColor")
2291
+ });
2292
+ var roomSurfaceSchema = (max) => z6.strictObject({
2293
+ /** The code shown first; absent is the first option. */
2294
+ default: ConfiguratorCodeSchema.optional(),
2295
+ /** Absent offers the package's own palette (`room-palette.ts`). */
2296
+ options: z6.array(RoomColorSchema).min(1).max(max).optional()
2297
+ });
2298
+ var ConfiguratorRoomSchema = z6.strictObject({
2299
+ wall: roomSurfaceSchema(CONFIGURATOR_LIMITS.roomWallOptions).optional(),
2300
+ floor: roomSurfaceSchema(CONFIGURATOR_LIMITS.roomFloorOptions).optional(),
2301
+ /** Millimetres of room beside the furniture on each side; 1500 by default. */
2302
+ margin: z6.number().min(0).max(5e3).optional(),
2303
+ /** Millimetres of floor in front of the wall; 3500 by default. */
2304
+ depth: z6.number().min(1e3).max(1e4).optional()
2305
+ });
2179
2306
  var ConfiguratorSceneSchema = z6.strictObject({
2180
2307
  lighting: z6.enum(CONFIGURATOR_SCENE_LIGHTINGS).optional(),
2181
- edges: z6.enum(CONFIGURATOR_SCENE_EDGES).optional()
2308
+ edges: z6.enum(CONFIGURATOR_SCENE_EDGES).optional(),
2309
+ room: ConfiguratorRoomSchema.optional()
2310
+ });
2311
+ var FocusNameSchema = z6.string().regex(/^[a-z][a-z0-9_]{0,39}$/, "configurator.invalidName");
2312
+ var ConfiguratorFocusSchema = z6.strictObject({
2313
+ param: ConfiguratorNameSchema,
2314
+ when: ConfiguratorConditionSchema.optional(),
2315
+ open: z6.array(FocusNameSchema).max(CONFIGURATOR_LIMITS.focusNames).optional(),
2316
+ frame: z6.array(FocusNameSchema).min(1).max(CONFIGURATOR_LIMITS.focusNames).optional(),
2317
+ night: z6.boolean().optional()
2182
2318
  });
2183
2319
  var structureObjectSchema = (files) => z6.strictObject({
2184
2320
  schemaVersion: z6.literal(1),
@@ -2212,6 +2348,8 @@ var structureObjectSchema = (files) => z6.strictObject({
2212
2348
  required: z6.boolean()
2213
2349
  }).optional(),
2214
2350
  rules: z6.array(ConfiguratorRuleSchema).max(CONFIGURATOR_LIMITS.rules),
2351
+ /** What the picture opens and looks at when a choice changes. */
2352
+ focus: z6.array(ConfiguratorFocusSchema).max(CONFIGURATOR_LIMITS.focus).optional(),
2215
2353
  priceRules: z6.array(ConfiguratorPriceRuleSchema).max(CONFIGURATOR_LIMITS.priceRules)
2216
2354
  });
2217
2355
  var ConfiguratorStructureObjectSchema = structureObjectSchema({
@@ -2495,6 +2633,78 @@ function configuratorStructureProblems(structure) {
2495
2633
  );
2496
2634
  }
2497
2635
  });
2636
+ const shape = motionNames(structure.geometry.root);
2637
+ const isGroup = (name) => [...shape.groups].some((template) => motionGroupMatches(template, name));
2638
+ (structure.focus ?? []).forEach((focus, index) => {
2639
+ const path = ["focus", index];
2640
+ if (!parameters.has(focus.param)) {
2641
+ problems.push(
2642
+ issue([...path, "param"], "configurator.unknownParam", {
2643
+ key: focus.param
2644
+ })
2645
+ );
2646
+ }
2647
+ if (focus.when) {
2648
+ const conditionParam = parameters.get(focus.when.param);
2649
+ const valueProblem = conditionParam ? conditionValueProblem(
2650
+ focus.when.op,
2651
+ focus.when.value,
2652
+ conditionParam,
2653
+ lists,
2654
+ materials
2655
+ ) : "configurator.unknownParam";
2656
+ if (valueProblem) {
2657
+ problems.push(
2658
+ issue([...path, "when"], valueProblem, { key: focus.when.param })
2659
+ );
2660
+ }
2661
+ }
2662
+ (focus.open ?? []).forEach((name, nameIndex) => {
2663
+ if (!isGroup(name)) {
2664
+ problems.push(
2665
+ issue(
2666
+ [...path, "open", nameIndex],
2667
+ "configurator.unknownMotionGroup",
2668
+ {
2669
+ name
2670
+ }
2671
+ )
2672
+ );
2673
+ }
2674
+ });
2675
+ (focus.frame ?? []).forEach((name, nameIndex) => {
2676
+ if (!shape.ids.has(name) && !isGroup(name)) {
2677
+ problems.push(
2678
+ issue([...path, "frame", nameIndex], "configurator.unknownFrame", {
2679
+ name
2680
+ })
2681
+ );
2682
+ }
2683
+ });
2684
+ });
2685
+ const room = structure.scene?.room;
2686
+ for (const surface of ["wall", "floor"]) {
2687
+ const own = room?.[surface];
2688
+ const path = ["scene", "room", surface];
2689
+ const codes = own?.options ? uniqueKeys(
2690
+ own.options,
2691
+ (option) => option.code,
2692
+ [...path, "options"],
2693
+ "code",
2694
+ problems
2695
+ ) : new Set(
2696
+ (surface === "wall" ? ROOM_WALL_PALETTE : ROOM_FLOOR_PALETTE).map(
2697
+ (option) => option.code
2698
+ )
2699
+ );
2700
+ if (own?.default !== void 0 && !codes.has(own.default)) {
2701
+ problems.push(
2702
+ issue([...path, "default"], "configurator.unknownCode", {
2703
+ code: own.default
2704
+ })
2705
+ );
2706
+ }
2707
+ }
2498
2708
  const geometryParameters = new Map(
2499
2709
  structure.parameters.map((parameter) => [
2500
2710
  parameter.key,
@@ -2952,6 +3162,13 @@ function onStep(value, step) {
2952
3162
  }
2953
3163
 
2954
3164
  // ../features/product-configurator/src/shared/expand-geometry.ts
3165
+ function motionGroupName(template, scope) {
3166
+ return template.replace(
3167
+ /\{([ij])\}/g,
3168
+ (_, index) => String((scope[index] ?? 0) + 1)
3169
+ );
3170
+ }
3171
+ var DEFAULT_HINGE_ANGLE = 90;
2955
3172
  function expandGeometry(document, inputs, chosen = { choices: {} }) {
2956
3173
  const issues = [];
2957
3174
  const parts = [];
@@ -3064,6 +3281,29 @@ function expandGeometry(document, inputs, chosen = { choices: {} }) {
3064
3281
  }
3065
3282
  params[key] = number2;
3066
3283
  }
3284
+ let motion;
3285
+ if (node.motion) {
3286
+ const angle = node.motion.angle === void 0 ? DEFAULT_HINGE_ANGLE : evaluate(node.motion.angle, scope, [
3287
+ ...nodePath,
3288
+ "motion",
3289
+ "angle"
3290
+ ]);
3291
+ const distance = node.motion.distance === void 0 ? null : evaluate(node.motion.distance, scope, [
3292
+ ...nodePath,
3293
+ "motion",
3294
+ "distance"
3295
+ ]);
3296
+ if (angle === null || node.motion.distance !== void 0 && distance === null) {
3297
+ return;
3298
+ }
3299
+ motion = {
3300
+ group: motionGroupName(node.motion.group, scope),
3301
+ kind: node.motion.kind,
3302
+ edge: node.motion.edge ?? "bottom",
3303
+ angle,
3304
+ distance
3305
+ };
3306
+ }
3067
3307
  parts.push({
3068
3308
  key: `${node.id}${keySuffix}`,
3069
3309
  id: node.id,
@@ -3072,7 +3312,8 @@ function expandGeometry(document, inputs, chosen = { choices: {} }) {
3072
3312
  size,
3073
3313
  at,
3074
3314
  params,
3075
- ...model === void 0 ? {} : { model }
3315
+ ...model === void 0 ? {} : { model },
3316
+ ...motion === void 0 ? {} : { motion }
3076
3317
  });
3077
3318
  if (parts.length > CONFIGURATOR_LIMITS.expandedParts) {
3078
3319
  issues.push(
@@ -3691,15 +3932,422 @@ function registerServerInfoTool(server, info) {
3691
3932
  );
3692
3933
  }
3693
3934
 
3694
- // src/tool-definitions-version.ts
3935
+ // src/shop-setup-tools.ts
3695
3936
  import { z as z11 } from "zod";
3937
+ var id = z11.string().uuid();
3938
+ var cents2 = z11.number().int().min(0).max(99999999);
3939
+ var region = z11.object({
3940
+ countryCode: z11.string().regex(/^[A-Z]{2}$/).describe("Two capital letters, ISO 3166 (NL, BE, DE)."),
3941
+ postcodeFrom: z11.string().max(20).nullable().default(null).describe("With postcodeTo: only this range of the country."),
3942
+ postcodeTo: z11.string().max(20).nullable().default(null)
3943
+ });
3944
+ var discount = z11.object({
3945
+ name: z11.string().min(1).max(120).describe("What the team sees it as."),
3946
+ code: z11.string().max(40).nullable().describe(
3947
+ "What a buyer types (upper-cased), or null for one that applies on its own."
3948
+ ),
3949
+ effect: z11.enum([
3950
+ "tier_price",
3951
+ "percent_off_lines",
3952
+ "amount_off_lines",
3953
+ "percent_off_order",
3954
+ "amount_off_order",
3955
+ "free_shipping"
3956
+ ]).describe(
3957
+ "tier_price: a lower price from tierMinQuantity of one product; *_lines: off each matching line; *_order: once off the whole order; free_shipping: no postage."
3958
+ ),
3959
+ percent: z11.number().nullable().describe("For a percentage: above 0, at most 100."),
3960
+ amountCents: z11.number().int().nullable().describe("For an amount, in currency, as the account enters prices."),
3961
+ currency: z11.string().regex(/^[A-Z]{3}$/).nullable().describe("Required beside amountCents (EUR)."),
3962
+ tierMinQuantity: z11.number().int().nullable(),
3963
+ minSubtotalCents: z11.number().int().nullable().describe("Only from this basket value, before discount."),
3964
+ productIds: z11.array(id).max(500).describe("Empty: every product."),
3965
+ categoryIds: z11.array(id).max(100),
3966
+ objectIds: z11.array(id).max(20),
3967
+ startsAt: z11.string().datetime({ offset: true }).nullable(),
3968
+ endsAt: z11.string().datetime({ offset: true }).nullable(),
3969
+ maxUses: z11.number().int().positive().nullable(),
3970
+ maxUsesPerEmail: z11.number().int().positive().nullable(),
3971
+ combinable: z11.boolean().describe(
3972
+ "Whether it may count beside another discount on the same order."
3973
+ )
3974
+ });
3975
+ function registerShopSetupTools(server, client) {
3976
+ server.registerTool(
3977
+ "check_shop_readiness",
3978
+ {
3979
+ title: "What the shop still misses",
3980
+ description: "Everything that stops the shop from selling (blocker) or costs it sales (warning): no product for sale, products without a price, no shipping method, Stripe not connected or still in test mode, no revalidation webhook, no cookie banner, no visit tracking. Each finding names the tool that fixes it, or the screen in the app. Run it first, and again when you think you are done.",
3981
+ annotations: { readOnlyHint: true, idempotentHint: true }
3982
+ },
3983
+ async () => {
3984
+ try {
3985
+ const readiness = await client.getShopReadiness();
3986
+ return answer(
3987
+ [
3988
+ readiness.ready ? "Nothing blocks a sale." : "The shop cannot sell yet.",
3989
+ ...readiness.findings.map(describeFinding)
3990
+ ].join("\n"),
3991
+ readiness
3992
+ );
3993
+ } catch (error) {
3994
+ return refusal(error, "shop:manage");
3995
+ }
3996
+ }
3997
+ );
3998
+ server.registerTool(
3999
+ "list_shipping_methods",
4000
+ {
4001
+ title: "List shipping zones and methods",
4002
+ description: "Every shipping zone with the countries it covers and the methods under it (what each charges, whether it is on), and the shipping classes a flat rate may charge per class. Amounts in cents, as the account enters prices.",
4003
+ annotations: { readOnlyHint: true, idempotentHint: true }
4004
+ },
4005
+ async () => {
4006
+ try {
4007
+ const setup = await client.getShippingSetup();
4008
+ return answer(
4009
+ setup.zones.length === 0 ? "No shipping zones yet: make one with upsert_shipping_zone." : setup.zones.map(describeZone).join("\n"),
4010
+ setup
4011
+ );
4012
+ } catch (error) {
4013
+ return refusal(error, "shop:manage");
4014
+ }
4015
+ }
4016
+ );
4017
+ server.registerTool(
4018
+ "upsert_shipping_zone",
4019
+ {
4020
+ title: "Make or change a shipping zone",
4021
+ description: "A zone is a name and the countries (or postcode ranges) it covers. Without id a new zone; with one, that zone renamed and its places replaced by these. Answers the zone id, for upsert_shipping_method.",
4022
+ inputSchema: {
4023
+ id: id.optional().describe("The zone to change; leave out for a new one."),
4024
+ name: z11.string().min(1).max(80),
4025
+ regions: z11.array(region).max(300).default([])
4026
+ },
4027
+ annotations: { idempotentHint: true }
4028
+ },
4029
+ async (input) => {
4030
+ try {
4031
+ const zone = await client.upsertShippingZone(input);
4032
+ return answer(
4033
+ `${input.id ? "Changed" : "Made"} zone "${input.name}" (id ${zone.id}).`,
4034
+ zone
4035
+ );
4036
+ } catch (error) {
4037
+ return refusal(error, "shop:manage");
4038
+ }
4039
+ }
4040
+ );
4041
+ server.registerTool(
4042
+ "upsert_shipping_method",
4043
+ {
4044
+ title: "Make or change a shipping method",
4045
+ description: "A way to post under a zone. flat_rate charges amountCents (and per class through classRates, noClassAmountCents for a product without one); free_shipping charges nothing, from freeThresholdCents of goods if given; local_pickup charges nothing. Without id a new method under zoneId; with one, that method changed -- its kind and zone are fixed once made.",
4046
+ inputSchema: {
4047
+ id: id.optional(),
4048
+ zoneId: id,
4049
+ kind: z11.enum(["flat_rate", "free_shipping", "local_pickup"]),
4050
+ name: z11.string().min(1).max(80).describe("What the buyer chooses."),
4051
+ enabled: z11.boolean().default(true),
4052
+ amountCents: cents2.default(0),
4053
+ noClassAmountCents: cents2.default(0),
4054
+ taxRateId: id.nullish(),
4055
+ freeThresholdCents: cents2.nullish(),
4056
+ classRates: z11.array(z11.object({ classId: id, amountCents: cents2 })).max(100).default([])
4057
+ },
4058
+ annotations: { idempotentHint: true }
4059
+ },
4060
+ async (input) => {
4061
+ try {
4062
+ const method = await client.upsertShippingMethod(input);
4063
+ return answer(
4064
+ `${input.id ? "Changed" : "Made"} method "${input.name}" (id ${method.id}).`,
4065
+ method
4066
+ );
4067
+ } catch (error) {
4068
+ return refusal(error, "shop:manage");
4069
+ }
4070
+ }
4071
+ );
4072
+ server.registerTool(
4073
+ "list_discounts",
4074
+ {
4075
+ title: "List discounts",
4076
+ description: "The account discounts, newest first, at most 200: codes, the ones that apply on their own, and tier prices, with every condition and how many paid orders used each. Archived ones only with archived: true.",
4077
+ inputSchema: { archived: z11.boolean().optional() },
4078
+ annotations: { readOnlyHint: true, idempotentHint: true }
4079
+ },
4080
+ async ({ archived }) => {
4081
+ try {
4082
+ const discounts = await client.getDiscounts({ archived });
4083
+ return answer(
4084
+ discounts.length === 0 ? "No discounts." : discounts.map(describeDiscount).join("\n"),
4085
+ discounts
4086
+ );
4087
+ } catch (error) {
4088
+ return refusal(error, "shop:manage");
4089
+ }
4090
+ }
4091
+ );
4092
+ server.registerTool(
4093
+ "upsert_discount",
4094
+ {
4095
+ title: "Make or change a discount",
4096
+ description: "A code, a discount that applies on its own (code null), or a tier price. Every field is sent, null where it does not apply. Without id a new discount; with one, that discount replaced field by field. A code the account already has is refused (conflict). A discount is counted against maxUses when an order with it is paid.",
4097
+ inputSchema: {
4098
+ id: id.optional(),
4099
+ discount
4100
+ },
4101
+ annotations: { idempotentHint: true }
4102
+ },
4103
+ async (input) => {
4104
+ try {
4105
+ const made = await client.upsertDiscount(input);
4106
+ return answer(describeDiscount(made), made);
4107
+ } catch (error) {
4108
+ return refusal(error, "shop:manage");
4109
+ }
4110
+ }
4111
+ );
4112
+ server.registerTool(
4113
+ "archive_discount",
4114
+ {
4115
+ title: "Archive a discount",
4116
+ description: "Stops a discount from giving anything to a new cart or order; orders that used it keep it. archived: false brings it back. Deleting is left to a person in the app.",
4117
+ inputSchema: {
4118
+ id,
4119
+ archived: z11.boolean().default(true)
4120
+ },
4121
+ annotations: { idempotentHint: true }
4122
+ },
4123
+ async ({ id: discountId, archived }) => {
4124
+ try {
4125
+ const result = await client.archiveDiscount(discountId, archived);
4126
+ return answer(
4127
+ archived ? "Archived." : "Brought back from the archive.",
4128
+ result
4129
+ );
4130
+ } catch (error) {
4131
+ return refusal(error, "shop:manage");
4132
+ }
4133
+ }
4134
+ );
4135
+ server.registerTool(
4136
+ "get_payment_status",
4137
+ {
4138
+ title: "Can the shop take a payment",
4139
+ description: "Whether a Stripe account is connected and ready for cards, whether Stripe still asks for something, and whether the account is in test mode. Read only: connecting Stripe and switching to live happen in the app, at the screen named in connectIn -- tell the person.",
4140
+ annotations: { readOnlyHint: true, idempotentHint: true }
4141
+ },
4142
+ async () => {
4143
+ try {
4144
+ const status = await client.getPaymentStatus();
4145
+ const sentence = !status.connected ? `Stripe is not connected (${status.status}). A person connects it in the app: ${status.connectIn}.` : [
4146
+ "Stripe is connected",
4147
+ status.testMode ? "in test mode: orders are test orders and no money moves" : "and live",
4148
+ status.requirementsDue ? "Stripe still asks for details" : ""
4149
+ ].filter(Boolean).join(", ") + ".";
4150
+ return answer(sentence, status);
4151
+ } catch (error) {
4152
+ return refusal(error, "shop:manage");
4153
+ }
4154
+ }
4155
+ );
4156
+ server.registerTool(
4157
+ "list_api_webhooks",
4158
+ {
4159
+ title: "List webhook addresses",
4160
+ description: "The addresses the account tells about changes (products, categories, events, orders), with whether each is on, paused after failures, and how its last delivery went. The signing key is never shown again.",
4161
+ annotations: { readOnlyHint: true, idempotentHint: true }
4162
+ },
4163
+ async () => {
4164
+ try {
4165
+ const hooks = await client.getApiWebhooks();
4166
+ return answer(
4167
+ hooks.length === 0 ? "No webhook addresses." : hooks.map(
4168
+ (hook) => `- ${hook.name} (id ${hook.id}): ${hook.url}, ${hook.events.join(", ")}` + (hook.paused ? `, paused after ${hook.consecutiveFailures} failures` : hook.active ? "" : ", off")
4169
+ ).join("\n"),
4170
+ hooks
4171
+ );
4172
+ } catch (error) {
4173
+ return refusal(error, "shop:manage");
4174
+ }
4175
+ }
4176
+ );
4177
+ server.registerTool(
4178
+ "upsert_api_webhook",
4179
+ {
4180
+ title: "Make or change a webhook address",
4181
+ description: "Point the account at the site revalidation route (with the SDK: https://<site>/api/solumflow/revalidate) so its cached pages follow a product the moment it changes. Without id a new address: the answer carries its signing key once -- put it in the site environment as SOLUMFLOW_WEBHOOK_KEY, it cannot be read back. https only, never a private network.",
4182
+ inputSchema: {
4183
+ id: id.optional(),
4184
+ name: z11.string().min(1).max(80),
4185
+ url: z11.string().url().max(2048),
4186
+ events: z11.array(z11.string().min(1).max(60)).min(1).max(20).describe(
4187
+ "For a shop: product.changed, product.deleted, category.changed."
4188
+ )
4189
+ },
4190
+ annotations: { idempotentHint: false }
4191
+ },
4192
+ async (input) => {
4193
+ try {
4194
+ const result = await client.upsertApiWebhook(input);
4195
+ return answer(
4196
+ result.signingSecret ? `Made address ${result.id}. Its signing key is in this answer and nowhere else: set it as SOLUMFLOW_WEBHOOK_KEY in the site environment now.` : `Changed address ${result.id}.`,
4197
+ result
4198
+ );
4199
+ } catch (error) {
4200
+ return refusal(error, "shop:manage");
4201
+ }
4202
+ }
4203
+ );
4204
+ }
4205
+ function describeFinding(finding) {
4206
+ const examples = finding.examples?.length ? ` (${finding.examples.join(", ")})` : "";
4207
+ return `- ${finding.severity}: ${finding.message}${examples} Fixed by ${finding.fixedBy}.`;
4208
+ }
4209
+ function describeZone(zone) {
4210
+ const where = zone.isFallback ? "everywhere else" : zone.regions.map((place) => place.countryCode).join(", ") || "no places";
4211
+ const methods = zone.methods.map(
4212
+ (method) => ` - ${method.name} (${method.kind}, id ${method.id})${method.enabled ? "" : ", off"}: ` + (method.kind === "flat_rate" ? `${(method.amountCents / 100).toFixed(2)}` : method.freeThresholdCents !== null ? `free from ${(method.freeThresholdCents / 100).toFixed(2)}` : "free")
4213
+ );
4214
+ return [`- ${zone.name} (id ${zone.id}): ${where}`, ...methods].join("\n");
4215
+ }
4216
+ function describeDiscount(entry) {
4217
+ const what = entry.percent !== null ? `${entry.percent}%` : entry.amountCents !== null && entry.currency ? money(entry.amountCents, entry.currency) : "";
4218
+ return `- ${entry.name} (id ${entry.id}): ${entry.code ?? "no code"}, ${entry.effect}${what ? ` ${what}` : ""}, used ${entry.timesUsed}x` + (entry.archived ? ", archived" : "");
4219
+ }
4220
+
4221
+ // src/shop-test-order-tool.ts
4222
+ import { z as z12 } from "zod";
4223
+ var PAID_POLLS = 10;
4224
+ var PAID_POLL_MS = 1500;
4225
+ function registerShopTestOrderTool(server, client, options = {}) {
4226
+ const wait = options.wait ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
4227
+ server.registerTool(
4228
+ "run_test_order",
4229
+ {
4230
+ title: "Place and pay a test order",
4231
+ description: "Proves the shop end to end in Stripe test mode: puts one product in a cart, chooses postage, places the order, pays it with Stripe test card 4242, and reads the order back until it is paid (the invoice and the confirmation mail follow from there). Refused when the account is live -- no real money can move through this tool. Needs a key with shop:manage, cart:write, orders:write and orders:read.",
4232
+ inputSchema: {
4233
+ email: z12.string().email().describe(
4234
+ "Where the confirmation goes: the shop owner, never a customer."
4235
+ ),
4236
+ productId: z12.string().uuid().optional().describe(
4237
+ "The product to buy; leave out for the first one in stock."
4238
+ ),
4239
+ countryCode: z12.string().regex(/^[A-Z]{2}$/).default("NL").describe("Where it is posted, for a product that is posted."),
4240
+ postalCode: z12.string().max(20).optional()
4241
+ },
4242
+ annotations: { idempotentHint: false }
4243
+ },
4244
+ async (input) => {
4245
+ let scope = "shop:manage";
4246
+ try {
4247
+ const status = await client.getPaymentStatus();
4248
+ if (!status.connected) {
4249
+ return answer(
4250
+ `Stripe is not connected, so there is nothing to pay with. A person connects it in the app: ${status.connectIn}.`,
4251
+ status
4252
+ );
4253
+ }
4254
+ if (!status.testMode) {
4255
+ return answer(
4256
+ `The account is live: a test order would be a real one. Switch the account to Stripe test mode in the app first (${status.connectIn}), or prove the shop with a real purchase by hand.`,
4257
+ status
4258
+ );
4259
+ }
4260
+ scope = "catalog:read";
4261
+ const productId = input.productId ?? await firstProductForSale();
4262
+ if (!productId) {
4263
+ return answer(
4264
+ "No product is for sale and in stock: there is nothing to order. check_shop_readiness says what is missing.",
4265
+ null
4266
+ );
4267
+ }
4268
+ scope = "cart:write";
4269
+ const handle = await client.createCart({ email: input.email });
4270
+ let cart = await client.addCartLine(handle.token, {
4271
+ productId,
4272
+ quantity: 1
4273
+ });
4274
+ if (cart.shipping.required) {
4275
+ cart = await client.setCartShipping(handle.token, {
4276
+ countryCode: input.countryCode,
4277
+ postalCode: input.postalCode ?? null
4278
+ });
4279
+ const method = cart.shipping.methods[0];
4280
+ if (!method) {
4281
+ return answer(
4282
+ `No shipping method delivers to ${input.countryCode}: add one with upsert_shipping_method.`,
4283
+ cart
4284
+ );
4285
+ }
4286
+ cart = await client.setCartShipping(handle.token, {
4287
+ countryCode: input.countryCode,
4288
+ postalCode: input.postalCode ?? null,
4289
+ methodId: method.id
4290
+ });
4291
+ }
4292
+ if (cart.problem) {
4293
+ return answer(
4294
+ `The cart cannot be ordered: ${cart.problem.message}`,
4295
+ cart
4296
+ );
4297
+ }
4298
+ scope = "orders:write";
4299
+ const order = await client.submitCartOrder({
4300
+ buyer: { email: input.email },
4301
+ cartToken: handle.token,
4302
+ ...cart.shipping.required ? {
4303
+ shippingAddress: {
4304
+ countryCode: input.countryCode,
4305
+ ...input.postalCode ? { postalCode: input.postalCode } : {}
4306
+ }
4307
+ } : {}
4308
+ });
4309
+ scope = "shop:manage";
4310
+ const payment = await client.payTestOrder(order.id);
4311
+ scope = "orders:read";
4312
+ const settled = await readUntilPaid(order.id);
4313
+ const total = money(order.totalCents, order.currency);
4314
+ return answer(
4315
+ settled?.status === "paid" ? `Test order ${order.number} for ${total} is paid. The invoice and the confirmation mail to ${input.email} follow from here.` : `Test order ${order.number} for ${total}: Stripe says the payment ${payment.paymentStatus}, but the order still reads ${settled?.status ?? "unknown"}. The platform marks it paid when Stripe reaches its webhook; if that never happens, the webhook between Stripe and the platform is the place to look.`,
4316
+ { order, payment, settled }
4317
+ );
4318
+ } catch (error) {
4319
+ return refusal(error, scope);
4320
+ }
4321
+ }
4322
+ );
4323
+ async function firstProductForSale() {
4324
+ const list = await client.getProducts({ limit: 50 });
4325
+ return list.data.find(
4326
+ (product) => product.inStock && product.priceFromCents !== null
4327
+ )?.id;
4328
+ }
4329
+ async function readUntilPaid(orderId) {
4330
+ let order = null;
4331
+ for (let poll = 0; poll < PAID_POLLS; poll += 1) {
4332
+ order = await client.getOrder(orderId);
4333
+ if (order?.status === "paid") {
4334
+ return order;
4335
+ }
4336
+ await wait(PAID_POLL_MS);
4337
+ }
4338
+ return order;
4339
+ }
4340
+ }
4341
+
4342
+ // src/tool-definitions-version.ts
4343
+ import { z as z13 } from "zod";
3696
4344
  import { createHash } from "crypto";
3697
4345
  function toolDefinitionsHash(tools) {
3698
4346
  const definitions = [...tools].sort((left, right) => left.name.localeCompare(right.name)).map(({ name, config }) => ({
3699
4347
  name,
3700
4348
  title: config.title ?? null,
3701
4349
  description: config.description ?? null,
3702
- inputSchema: config.inputSchema ? z11.toJSONSchema(z11.object(config.inputSchema), {
4350
+ inputSchema: config.inputSchema ? z13.toJSONSchema(z13.object(config.inputSchema), {
3703
4351
  io: "input",
3704
4352
  unrepresentable: "any"
3705
4353
  }) : null,
@@ -3747,7 +4395,7 @@ function holdToolRegistrations(real) {
3747
4395
  description: `${config.description ?? ""} Definitions version ${version2}: send it as schemaHash and the answer tells you when these tool definitions are older than the server.`,
3748
4396
  inputSchema: {
3749
4397
  ...config.inputSchema,
3750
- schemaHash: z11.string().max(64).optional().describe(
4398
+ schemaHash: z13.string().max(64).optional().describe(
3751
4399
  `The definitions version named in this tool's description (${version2}).`
3752
4400
  )
3753
4401
  }
@@ -3808,6 +4456,8 @@ async function main() {
3808
4456
  registerCatalogMigrationTools(held.server, client);
3809
4457
  registerEventTools(held.server, client);
3810
4458
  registerConfiguratorTools(held.server, client);
4459
+ registerShopSetupTools(held.server, client);
4460
+ registerShopTestOrderTool(held.server, client);
3811
4461
  held.publish();
3812
4462
  await server.connect(new StdioServerTransport());
3813
4463
  try {