sbuilder-mcp 0.3.0 → 0.4.1

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/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ All notable changes to this project are documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.4.1] - 2026-09-08
10
+
11
+ ### Fixed
12
+ - sb_review now reports findings inside the cart drawer and other site overlays, tagged with overlay: true, instead of silently skipping them on the belief that an overlay is not the page's to fix; the review notice explains that an overlay:true finding is fixed the same way but is site-wide, so it only needs fixing once.
13
+ - sb_api_call's path_params now matches a parameter name case-insensitively as a fallback, so a call using the common siteId spelling no longer fails against the 8 operations that spell it siteID; an exact match still wins and a genuinely missing parameter is still refused.
14
+
15
+ ## [0.4.0] - 2026-09-08
16
+
17
+ ### Added
18
+ - sb_review reports a catalogue gap when a site has no active products, since every repeater on the site renders its empty state and the product template is bound to nothing on a store that otherwise reports ready.
19
+ - sb_review reports the same catalogue gap when every active product is priced at zero, since a zero-priced product still renders, adds to the cart and totals nothing.
20
+
9
21
  ## [0.3.0] - 2026-09-08
10
22
 
11
23
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,18 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
6
6
  Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.4.1] - 2026-09-08
10
+
11
+ ### Fixed
12
+ - sb_review giờ báo cáo các finding bên trong cart drawer và các overlay khác của site, gắn cờ overlay: true, thay vì âm thầm bỏ qua chúng vì cho rằng một overlay không phải trách nhiệm của trang; thông báo review giải thích rằng một finding có overlay: true được sửa theo cùng cách nhưng có phạm vi toàn site, nên chỉ cần sửa một lần.
13
+ - path_params của sb_api_call giờ khớp tên tham số không phân biệt hoa thường như một phương án dự phòng, nên một lời gọi dùng cách viết phổ biến siteId không còn thất bại với 8 operation viết là siteID; một khớp chính xác vẫn được ưu tiên và một tham số thực sự thiếu vẫn bị từ chối.
14
+
15
+ ## [0.4.0] - 2026-09-08
16
+
17
+ ### Added
18
+ - sb_review giờ báo cáo gap catalogue khi site chưa có sản phẩm active nào, vì mọi repeater trên site sẽ render trạng thái rỗng và product template không bind vào đâu cả trên một cửa hàng vốn báo cáo là đã sẵn sàng.
19
+ - sb_review giờ báo cáo cùng gap catalogue khi mọi sản phẩm active đều có giá bằng không, vì một sản phẩm giá 0 vẫn render, vẫn thêm được vào giỏ hàng và tổng đơn hàng bằng không.
20
+
9
21
  ## [0.3.0] - 2026-09-08
10
22
 
11
23
  ### Added
@@ -24,11 +24,16 @@ export async function gatherReadiness(ctx, siteId, pageNodes) {
24
24
  }
25
25
  };
26
26
  const site = encodeURIComponent(siteId);
27
- const [pageList, gateways, shipping, globals] = await Promise.all([
27
+ const [pageList, gateways, shipping, globals, productList] = await Promise.all([
28
28
  get(`/api/sites/${site}/pages`),
29
29
  get(`/api/sites/${site}/payment-gateways`),
30
30
  get(`/api/sites/${site}/shipping-methods`),
31
31
  get(`/api/sites/${site}/global-sections`),
32
+ // The SITE-SCOPED list, not /api/v1/products: this one takes either
33
+ // credential, so the check answers for a session install as well as a
34
+ // key-only one. A page is enough to tell empty from not; `total` carries the
35
+ // real count when the platform sends it.
36
+ get(`/api/sites/${site}/products?limit=200`),
32
37
  ]);
33
38
  // A gateway counts only when it is BOTH enabled and configured — the editor's
34
39
  // `live` getter also filters by the store's currency, which is a narrowing:
@@ -42,8 +47,19 @@ export async function gatherReadiness(ctx, siteId, pageNodes) {
42
47
  const globalNodes = globals?.globalSections
43
48
  ? globals.globalSections.flatMap((g) => Object.values(g.document?.nodes ?? {}))
44
49
  : null;
50
+ // ACTIVE means a shopper can see it; PURCHASABLE adds a price above zero. A
51
+ // product priced at zero renders, adds to the cart, and totals nothing — which
52
+ // reads as a working store right up to the money.
53
+ const rows = productList?.products;
54
+ const products = Array.isArray(rows)
55
+ ? {
56
+ active: rows.filter((p) => (p.status ?? 'active') === 'active').length,
57
+ purchasable: rows.filter((p) => (p.status ?? 'active') === 'active' && (p.priceCents ?? 0) > 0).length,
58
+ }
59
+ : null;
45
60
  return {
46
61
  pages: pageList?.pages ?? null,
62
+ products,
47
63
  liveGateways,
48
64
  shippingMethods,
49
65
  pageNodes,
@@ -100,6 +100,30 @@ export function readinessGaps(input) {
100
100
  : 'Create a page of type "product" and publish it.',
101
101
  });
102
102
  }
103
+ // NOTHING TO SELL. Checked before the delivery option, because a shipping
104
+ // method for an empty catalogue is furniture.
105
+ if (input.products) {
106
+ if (input.products.active === 0) {
107
+ gaps.push({
108
+ id: 'catalogue',
109
+ draft: false,
110
+ problem: 'The store has no active products. Every product list on the site renders its empty ' +
111
+ 'state, the product template has nothing to bind to, and there is nothing to add to a ' +
112
+ 'cart — on a site that otherwise reports ready.',
113
+ fix: 'Create products (sb_api_find "create product"). priceCents is minor units — VND × 100.',
114
+ });
115
+ }
116
+ else if (input.products.purchasable === 0) {
117
+ gaps.push({
118
+ id: 'catalogue',
119
+ draft: false,
120
+ problem: `All ${input.products.active} active products are priced at zero. They render, they add ` +
121
+ 'to the cart, and the order totals nothing — which reads as a working store right up ' +
122
+ 'to the money.',
123
+ fix: 'Set priceCents on each product and its variants. Minor units: VND × 100.',
124
+ });
125
+ }
126
+ }
103
127
  if (input.shippingMethods === 0) {
104
128
  gaps.push({
105
129
  id: 'shipping',
@@ -12,7 +12,9 @@ import { fill } from './findings.js';
12
12
  export const REVIEW_NOTICE = 'FIX THESE. Each one is a defect a visitor will see on the published page, not a ' +
13
13
  'suggestion — a blank band, a placeholder sentence, a broken image. Apply the fix each ' +
14
14
  'finding names, then review again until the list is empty. Do not report the page as done ' +
15
- 'while findings stand; if you believe one is a false positive, say which and why.';
15
+ 'while findings stand; if you believe one is a false positive, say which and why. ' +
16
+ 'A finding marked overlay:true is in the cart drawer or a pop-up — fix it the same way ' +
17
+ '(sb_set lands there), but it is SITE-WIDE, so fix it once rather than once per page.';
16
18
  /** The specials keys an element seeds that hold its visible content. */
17
19
  function contentKeys(type) {
18
20
  const seeded = ELEMENTS[type]?.defaults.specials ?? {};
@@ -56,8 +58,19 @@ function drawsItsOwnContent(type) {
56
58
  /**
57
59
  * Everything wrong with this page that a person would notice.
58
60
  *
59
- * Overlays are skipped: the cart drawer is composed onto ROOT on read and is not
60
- * this page's to fix. So is the INSIDE of an app block (trap 5): its
61
+ * OVERLAYS ARE WALKED, and used not to be on the stated reasoning that the
62
+ * cart drawer "is not this page's to fix", which is false. An overlay's content
63
+ * reaches storage THROUGH THE PAGE SAVE (`overlays/rest/rest.go`: content is
64
+ * "deliberately NOT written here"), so `sb_set` on a drawer node lands, and the
65
+ * skip meant nothing ever reported what shipped inside one. Measured: a
66
+ * rose-and-ink storefront whose drawer carried a static mock row reading
67
+ * "Product name / 0₫", a duplicate cart list, and English copy — none of it
68
+ * mentioned by any check, on a site that reviewed clean ten pages running.
69
+ *
70
+ * Their findings carry `overlay: true`, because the master is SHARED: without
71
+ * the flag the same drawer defect reads as ten problems on a ten-page site.
72
+ *
73
+ * The INSIDE of an app block is still skipped (trap 5): its
61
74
  * placeholders are the app's, and no fix this page could apply would survive a
62
75
  * save. The block root itself is still walked — it is a node the page owns and
63
76
  * can be an empty container. Findings are ordered by document order so a caller working
@@ -84,10 +97,14 @@ export function reviewDesign(doc) {
84
97
  // itself belongs to the scope OUTSIDE it, which is why this is set before the
85
98
  // scope for the children is computed.
86
99
  const inRepeater = new Map();
87
- const go = (id, repeater) => {
88
- if (seen.has(id) || overlayIds.has(id))
100
+ // Which nodes sit inside an overlay, so their findings can say so.
101
+ const inOverlay = new Set();
102
+ const go = (id, repeater, overlay = false) => {
103
+ if (seen.has(id))
89
104
  return;
90
105
  seen.add(id);
106
+ if (overlay)
107
+ inOverlay.add(id);
91
108
  walkOrder.push(id);
92
109
  if (repeater)
93
110
  inRepeater.set(id, repeater);
@@ -95,7 +112,7 @@ export function reviewDesign(doc) {
95
112
  return;
96
113
  const inner = repeats(d.nodes[id]?.data.type ?? '') ? (repeater ?? id) : repeater;
97
114
  for (const k of childrenOf(d, id))
98
- go(k, inner);
115
+ go(k, inner, overlay || overlayIds.has(k));
99
116
  };
100
117
  go(d.root_node_id);
101
118
  for (const id of walkOrder) {
@@ -301,5 +318,12 @@ export function reviewDesign(doc) {
301
318
  }
302
319
  }
303
320
  }
321
+ // Tagged in ONE place rather than at eight push sites: whether a node sits
322
+ // inside an overlay is a fact about where it is, not about what is wrong with
323
+ // it, and threading it through every rule would put the same argument in eight
324
+ // signatures.
325
+ for (const f of out)
326
+ if (inOverlay.has(f.nodeId))
327
+ f.overlay = true;
304
328
  return out;
305
329
  }
package/dist/tools/api.js CHANGED
@@ -171,7 +171,19 @@ export async function callOperation(ctx, args) {
171
171
  let path = op.path;
172
172
  for (const m of op.path.matchAll(/\{([^}]+)\}/g)) {
173
173
  const name = m[1];
174
- const value = args.path_params?.[name];
174
+ // CASE-INSENSITIVE, because the platform spells one parameter two ways:
175
+ // `{siteId}` in 281 operations and `{siteID}` in 8. A caller who learned the
176
+ // common spelling passes the wrong key on those eight and is refused for a
177
+ // difference of one letter — a distinction no reader of the call sheet has
178
+ // any reason to notice, and one this tool has nothing to gain by enforcing.
179
+ // The exact name still wins; the fold is only a fallback.
180
+ const given = args.path_params ?? {};
181
+ let value = given[name];
182
+ if (value === undefined) {
183
+ const folded = Object.keys(given).find((k) => k.toLowerCase() === name.toLowerCase());
184
+ if (folded !== undefined)
185
+ value = given[folded];
186
+ }
175
187
  if (value === undefined) {
176
188
  // NAME THE ARGUMENT, not just the parameter. The call sheet lists these
177
189
  // under `params` while the call takes them in `path_params`, and a caller
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
5
5
  "mcpName": "io.github.vuluu2k/sbuilder-mcp",
6
6
  "type": "module",