@solumflow-app/crm-mcp-server 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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.6.0";
9
+ var version = "0.8.0";
10
10
 
11
11
  // src/catalog-migration-tools.ts
12
12
  import { z as z4 } from "zod";
@@ -38,6 +38,11 @@ var CatalogFieldSlugSchema = z.string().max(63).regex(FIELD_SLUG, {
38
38
  )
39
39
  });
40
40
  var cents = z.number().int().min(0).max(MAX_AMOUNT_CENTS);
41
+ var CatalogCategoryPlacementSchema = z.object({
42
+ slug: CatalogWebSlugSchema,
43
+ /** Lowest first; equal places stand in id order. */
44
+ position: z.number().int().min(-1e6).max(1e6).optional()
45
+ }).strict();
41
46
  var text = (max) => z.string().trim().max(max).nullable().optional();
42
47
  var CatalogPriceSchema = z.object({
43
48
  /** The price's own id at the source, so a renamed price stays itself. */
@@ -199,8 +204,13 @@ var CatalogProductUpsertSchema = z.object({
199
204
  socialTitle: text(300),
200
205
  socialDescription: text(1e3),
201
206
  socialImage: CatalogFileRefSchema.nullable().optional(),
202
- /** Replaces the product's categories, by slug. */
203
- categories: z.array(CatalogWebSlugSchema).max(50).optional(),
207
+ /**
208
+ * Replaces the product's categories, by slug, the first one leading. An
209
+ * entry may be `{ slug, position }` instead, to also set where the
210
+ * product stands on that category's page (`?sort=position`, lowest
211
+ * first); a plain slug leaves a stored place as it is.
212
+ */
213
+ categories: z.array(z.union([CatalogWebSlugSchema, CatalogCategoryPlacementSchema])).max(50).optional(),
204
214
  /**
205
215
  * Make a category that does not exist yet instead of refusing. Off unless
206
216
  * asked for, so a typo cannot become a category on a customer's website.
@@ -499,6 +509,13 @@ function more(nextCursor) {
499
509
  function money(cents2, currency2) {
500
510
  return `${(cents2 / 100).toFixed(2)} ${currency2.toUpperCase()}`;
501
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.`;
518
+ }
502
519
 
503
520
  // src/catalog-tools.ts
504
521
  var objectInput = {
@@ -589,7 +606,7 @@ function registerCatalogTools(server, client) {
589
606
  "get_product",
590
607
  {
591
608
  title: "Read one product",
592
- description: "One product as its page needs it: name, prices, pictures, categories, variants and `fields`, keyed by field slug. Published products only unless you pass `status` (needs catalog:write), and a published one may be a minute old: to read back exactly what you just wrote, use get_product_by_ref. Answers that it is not found for an unknown product, or one not in the status asked. " + CONTENT_BLOCKS,
609
+ description: "One product as its page needs it: name, prices, pictures, categories, variants and `fields`, keyed by field slug. Published products only unless you pass `status` (needs a server key), and a published one may be a minute old: to read back exactly what you just wrote, use get_product_by_ref. Answers that it is not found for an unknown product, or one not in the status asked. " + CONTENT_BLOCKS,
593
610
  inputSchema: { slugOrId, currency, status: productStatus },
594
611
  annotations: { readOnlyHint: true, idempotentHint: true }
595
612
  },
@@ -619,7 +636,7 @@ function registerCatalogTools(server, client) {
619
636
  "list_products",
620
637
  {
621
638
  title: "List the products",
622
- description: "One page of the catalogue: name, address (slug), lowest price, whether it is in stock, its first picture and the public fields a listing shows. Narrow it to one category\u2019s whole branch by its id or address, or by the reference upsert_category recorded (`categorySource` + `categoryRef`), to one catalogue by the slug of its object, or to the product carrying an article number (`sku`). Read get_product for everything a product page needs. Published products only, and may be a minute old; `status` lists concepts, scheduled and archived ones too (needs catalog:write).",
639
+ description: "One page of the catalogue: name, address (slug), lowest price, whether it is in stock, its first picture and the public fields a listing shows. Narrow it to one category\u2019s whole branch by its id or address, or by the reference upsert_category recorded (`categorySource` + `categoryRef`), to one catalogue by the slug of its object, or to the product carrying an article number (`sku`). Read get_product for everything a product page needs. Published products only, and may be a minute old; `status` lists concepts, scheduled and archived ones too (needs a server key). `sort` orders the published ones: -publishedAt (newest first, the default), publishedAt, name, or position -- the seller\u2019s own order on the page of `category`, which it needs. Each item carries `sku`, `publishedAt` and `stockQuantity` (null when not counted).",
623
640
  inputSchema: {
624
641
  ...page,
625
642
  category: z3.string().regex(
@@ -636,7 +653,10 @@ function registerCatalogTools(server, client) {
636
653
  'One catalogue, by the slug of its object, such as "product".'
637
654
  ),
638
655
  currency,
639
- status: productStatus
656
+ status: productStatus,
657
+ sort: z3.enum(["-publishedAt", "publishedAt", "name", "position"]).optional().describe(
658
+ "The order: newest first by default; position needs `category`."
659
+ )
640
660
  },
641
661
  annotations: { readOnlyHint: true, idempotentHint: true }
642
662
  },
@@ -664,7 +684,7 @@ function registerCatalogTools(server, client) {
664
684
  "list_categories",
665
685
  {
666
686
  title: "List the categories",
667
- description: 'The categories of the catalogue, each with its path from the top and how many products are in it. Left without `parent`, every category comes back, which is what building a menu in one go needs; "root" asks for the top level, a category id for its children. Published categories only unless you pass `status` (draft or all, needs catalog:write).',
687
+ description: 'The categories of the catalogue, each with its path from the top and how many products are in it. Left without `parent`, every category comes back, which is what building a menu in one go needs; "root" asks for the top level, a category id for its children. Published categories only unless you pass `status` (draft or all, needs a server key).',
668
688
  inputSchema: {
669
689
  ...page,
670
690
  parent: z3.union([z3.literal("root"), z3.string().uuid()]).optional().describe('"root" or the id of a category.'),
@@ -688,7 +708,7 @@ function registerCatalogTools(server, client) {
688
708
  "get_category",
689
709
  {
690
710
  title: "Read one category page",
691
- description: "One category as its page needs it: name, description, the texts for search engines and social media, and the first page of its products. Pass the nextCursor back to page through the products. A category that is not on the website yet needs `status` (draft or all, catalog:write); its products are the published ones.",
711
+ description: "One category as its page needs it: name, description, the texts for search engines and social media, and the first page of its products. Pass the nextCursor back to page through the products. A category that is not on the website yet needs `status` (draft or all, a server key); its products are the published ones.",
692
712
  inputSchema: {
693
713
  slugOrId: z3.string().min(1).max(200).describe("The address (slug) or the id of the category."),
694
714
  ...page,
@@ -852,7 +872,7 @@ function registerCatalogMigrationTools(server, client) {
852
872
  (file) => file.status === "pending"
853
873
  );
854
874
  return answer(
855
- `${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),
856
876
  product
857
877
  );
858
878
  } catch (error) {
@@ -886,12 +906,15 @@ function registerCatalogMigrationTools(server, client) {
886
906
  target(object2)
887
907
  );
888
908
  const count = (kind) => outcomes.filter((outcome) => outcome.outcome === kind).length;
909
+ const offPage = new Set(
910
+ outcomes.flatMap((outcome) => outcome.product?.fieldsNotOnPage ?? [])
911
+ );
889
912
  const refusals = outcomes.filter((outcome) => outcome.outcome === "refused").map(
890
913
  (outcome) => `- ${outcome.externalRef}: ${outcome.error?.message ?? "refused"}` + (outcome.error?.fields.length ? ` (${outcome.error.fields.map((field) => `${field.path}: ${field.message}`).join("; ")})` : "")
891
914
  );
892
915
  return answer(
893
916
  [
894
- `${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.` : ""),
895
918
  ...refusals
896
919
  ].join("\n"),
897
920
  outcomes
@@ -949,7 +972,7 @@ function registerCatalogMigrationTools(server, client) {
949
972
  "get_category_by_ref",
950
973
  {
951
974
  title: "Find a category by its reference",
952
- description: 'The category upsert_category recorded with this `source` and `externalRef` -- the id of a series in your own system -- also after its address was changed in the app. A published category unless you pass `status`: "draft" finds a concept or scheduled one, "all" any (needs catalog:write).',
975
+ description: 'The category upsert_category recorded with this `source` and `externalRef` -- the id of a series in your own system -- also after its address was changed in the app. A published category unless you pass `status`: "draft" finds a concept or scheduled one, "all" any (needs a server key).',
953
976
  inputSchema: {
954
977
  source: CatalogSourceSchema,
955
978
  externalRef: CatalogExternalRefSchema,
@@ -1061,7 +1084,7 @@ function registerCatalogWriteTools(server, client) {
1061
1084
  );
1062
1085
  const verb = product.created ? "Made" : "Changed";
1063
1086
  return answer(
1064
- `${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),
1065
1088
  product
1066
1089
  );
1067
1090
  } catch (error) {