sbuilder-mcp 0.30.1 → 0.32.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/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ 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.32.0] - 2026-09-11
10
+
11
+ ### Added
12
+ - sb_api_find's call sheet for any `/apps` or `/builtin-apps` operation now explains how to place an installed app's block on a page: build `specials.appBlockRef` as `"<installId>/<blockKey>"` from a row of `GET /api/sites/{siteId}/apps/blocks` and add it with sb_add, which was previously reachable only in the platform's Go source.
13
+ - The same call sheet names the two silent traps around a placed block: never author `specials.appBlockId` or `appBlockHash` (the server's own composition stamps), and an edit made inside a composed block's subtree is stored nowhere.
14
+ - The call sheet also explains installing an app: a built-in app installs with `POST /api/sites/{siteId}/builtin-apps/{key}`, while a marketplace app requires a merchant to approve an OAuth consent screen and cannot be installed through this server.
15
+
16
+ ### Fixed
17
+ - The `key` parameter on the built-in app install and list operations now documents all eight installable keys (mail, multilingual, agent, chat, booking, loyalty, payments, courses) instead of only the first two ever shipped.
18
+
19
+ ## [0.31.0] - 2026-09-11
20
+
21
+ ### Added
22
+ - sb_import and sb_import_site now turn a set of panels with a button row into a real `tab` element instead of flattening it, as long as every panel's label can be read off the page (via `aria-controls`, a shared `data-*` pairing, or position when the button row's clickable count matches the panel count); a panel set whose labels are filled in by script rather than present in the DOM is reported as `panel-set-without-labels` and left flattened rather than guessed at.
23
+
9
24
  ## [0.30.1] - 2026-09-11
10
25
 
11
26
  ### Changed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,21 @@ 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.32.0] - 2026-09-11
10
+
11
+ ### Added
12
+ - Call sheet của sb_api_find cho mọi operation `/apps` hoặc `/builtin-apps` giờ giải thích cách đặt block của một app đã cài lên trang: dựng `specials.appBlockRef` dạng `"<installId>/<blockKey>"` từ một dòng của `GET /api/sites/{siteId}/apps/blocks` rồi thêm bằng sb_add — điều trước đây chỉ có thể biết được qua mã nguồn Go của nền tảng.
13
+ - Cùng call sheet đó nêu rõ hai cái bẫy âm thầm quanh một block đã đặt: không bao giờ tự viết `specials.appBlockId` hay `appBlockHash` (đây là các dấu server tự gắn khi compose), và một chỉnh sửa bên trong subtree đã compose sẽ không được lưu ở đâu cả.
14
+ - Call sheet cũng giải thích cách cài app: một app tích hợp sẵn cài bằng `POST /api/sites/{siteId}/builtin-apps/{key}`, còn một app từ marketplace cần merchant tự duyệt màn hình xác nhận OAuth và không thể cài qua server này.
15
+
16
+ ### Fixed
17
+ - Tham số `key` trên các operation cài và liệt kê app tích hợp sẵn giờ ghi đủ tám key có thể cài (mail, multilingual, agent, chat, booking, loyalty, payments, courses) thay vì chỉ hai key đầu tiên từng có.
18
+
19
+ ## [0.31.0] - 2026-09-11
20
+
21
+ ### Added
22
+ - sb_import và sb_import_site giờ biến một bộ panel có hàng nút thành một element `tab` thật thay vì làm phẳng nó, miễn là nhãn của mọi panel đọc được trên trang (qua `aria-controls`, một cặp giá trị `data-*` chung, hoặc theo vị trí khi số phần tử bấm được trong hàng nút khớp với số panel); một bộ panel có nhãn do script điền vào chứ không có sẵn trong DOM sẽ được báo là `panel-set-without-labels` và giữ nguyên dạng làm phẳng thay vì bị đoán mò.
23
+
9
24
  ## [0.30.1] - 2026-09-11
10
25
 
11
26
  ### Changed
@@ -3971,7 +3971,7 @@ export const API_OPERATIONS = [
3971
3971
  "in": "path",
3972
3972
  "required": true,
3973
3973
  "type": "string",
3974
- "description": "App key (mail | multilingual)"
3974
+ "description": "App key: mail | multilingual | agent | chat | booking | loyalty | payments | courses"
3975
3975
  }
3976
3976
  ],
3977
3977
  "bodyDescribed": false,
@@ -3999,7 +3999,7 @@ export const API_OPERATIONS = [
3999
3999
  "in": "path",
4000
4000
  "required": true,
4001
4001
  "type": "string",
4002
- "description": "App key (mail | multilingual)"
4002
+ "description": "App key: mail | multilingual | agent | chat | booking | loyalty | payments | courses"
4003
4003
  }
4004
4004
  ],
4005
4005
  "bodyDescribed": false,
@@ -91,6 +91,34 @@ export function describeOperation(op) {
91
91
  if (/\/translations(\/|$)/.test(op.path)) {
92
92
  out.translation_fields = translationCallSheet();
93
93
  }
94
+ // AN APP'S BLOCKS WERE REACHABLE AND UNUSABLE, because the one thing needed to
95
+ // place one is a string format that exists only in Go. `page/appblocks.go`
96
+ // spells it out — SpecAppBlockRef is "<installId>/<blockKey>" — and every
97
+ // field to build it comes back from `/apps/blocks` (`installId`, `key`). An
98
+ // agent had the list of blocks, the route that returns it, and no way to turn
99
+ // a row into a node.
100
+ //
101
+ // Attached to the call sheet for the same reason the translation table is:
102
+ // that is where the agent already is when it decides what to send. No new
103
+ // tool — placing one is `sb_add`, which already takes the specials it needs.
104
+ if (/\/(apps|builtin-apps)(\/|$)/.test(op.path)) {
105
+ out.app_blocks = {
106
+ place: 'A block from an installed app goes on a page as ONE node carrying ' +
107
+ 'specials.appBlockRef = "<installId>/<blockKey>" — both fields come back from ' +
108
+ 'GET /api/sites/{siteId}/apps/blocks. Add it with sb_add; the platform composes the ' +
109
+ "app's markup underneath on read.",
110
+ never: 'Never author specials.appBlockId or appBlockHash. Those are the stamps the SERVER ' +
111
+ 'writes when it composes, and writing one makes the next save decompose your node over ' +
112
+ 'the app instead.',
113
+ interior: 'An edit INSIDE a composed block is stored nowhere and reported nowhere — the save ' +
114
+ 'reduces the subtree back to the reference. Configure the block through its own ' +
115
+ 'props/slots, never by editing what it rendered.',
116
+ installing: 'A BUILT-IN app installs with POST /api/sites/{siteId}/builtin-apps/{key} and that key ' +
117
+ "parameter's description names every installable one. A MARKETPLACE app cannot be " +
118
+ 'installed from here at all: it goes through an OAuth consent screen a person has to ' +
119
+ 'approve, so ask the merchant to install it and then read /apps/blocks again.',
120
+ };
121
+ }
94
122
  const shape = REQUEST_SHAPES[op.id];
95
123
  if (shape) {
96
124
  out.body_shape = shape;
@@ -167,6 +167,29 @@ function one(c, t) {
167
167
  children: body,
168
168
  };
169
169
  }
170
+ case 'tab': {
171
+ const items = (c.children ?? [])
172
+ .map((item) => one(item, t))
173
+ .filter((n) => n !== null);
174
+ // Two is the floor for a tab: one panel with a button over it is a
175
+ // heading the visitor cannot dismiss.
176
+ if (items.length < 2)
177
+ return null;
178
+ return { type: 'tab', children: items };
179
+ }
180
+ case 'tab-item': {
181
+ const label = c.text?.trim();
182
+ const body = (c.children ?? [])
183
+ .map((k) => one(k, t))
184
+ .filter((n) => n !== null);
185
+ // NO LABEL, NO PANEL — and this is stricter than the accordion beside it
186
+ // on purpose. `accordion-content` seeds its own summary, so an unlabelled
187
+ // one still opens; `tab` builds its BUTTON ROW from these labels, so an
188
+ // unlabelled panel is one the visitor has no way to reach.
189
+ if (!label || !body.length)
190
+ return null;
191
+ return { type: 'tab-content', specials: { label }, children: body };
192
+ }
170
193
  case 'divider': {
171
194
  return { type: 'divider', style: { width: '100%' } };
172
195
  }
@@ -211,6 +211,21 @@ function capturePage(limits) {
211
211
  }
212
212
  if (!hidden.length)
213
213
  continue;
214
+ // STAMPED WHILE WE STILL KNOW. Once revealed, a panel is indistinguishable
215
+ // from an ordinary visible child, so the split is recorded here — and
216
+ // ONLY the ones that were actually hidden. A first version stamped every
217
+ // child that shared a tag with a visible peer, which swept in the BUTTON
218
+ // ROW itself: it is a `<div>` with children like the panels are, so the
219
+ // tab came out with its own controls as one of its panes.
220
+ let stamped = 0;
221
+ for (const k of hidden) {
222
+ if (!shownByTag[k.tagName] || k.children.length === 0)
223
+ continue;
224
+ k.setAttribute('data-sb-panel', '');
225
+ stamped += 1;
226
+ }
227
+ if (stamped >= 1)
228
+ parent.setAttribute('data-sb-panelset', '');
214
229
  for (const k of hidden) {
215
230
  // Real content only — an empty slot or a script-filled placeholder is
216
231
  // not a panel worth showing, and revealing it costs a skip either way.
@@ -458,6 +473,116 @@ function capturePage(limits) {
458
473
  skip('hidden');
459
474
  return [];
460
475
  }
476
+ // A TAB SET, WHEN — AND ONLY WHEN — ITS LABELS CAN BE READ.
477
+ //
478
+ // The platform's `tab` synthesizes its whole button row from each
479
+ // `tab-content` child's `specials.label` (render/nodes/tab/html.go), so a
480
+ // tab with no labels is a stack of panels wearing a control nobody can
481
+ // use. That makes "are there labels" the right question AND the right
482
+ // discriminator: MEASURED on ttgshop.vn, the panel-set stamp also lands on
483
+ // a `grid-cols-5` product grid that happens to hide one card, and a label
484
+ // row is exactly what that grid does not have.
485
+ //
486
+ // Three pairings, strongest first, because a wrong label is worse than
487
+ // none — it puts one collection's name over another's products:
488
+ // 1. ARIA. `[role=tab][aria-controls=<panel id>]` says it outright.
489
+ // 2. A SHARED data-* VALUE. `<button data-id="7">` beside
490
+ // `<div data-id="7">` is how most hand-rolled tab scripts wire
491
+ // themselves, and it survives reordering.
492
+ // 3. POSITION, and only from a row that holds exactly as many
493
+ // clickable items as there are panels — anything else is a guess
494
+ // dressed as a match.
495
+ //
496
+ // Every panel must come out with a label or this is not a tab. ttgshop.vn
497
+ // itself does NOT become one: its tab buttons are empty in the DOM and
498
+ // filled by script, and its section titles are images. Deriving a label
499
+ // from the `data-url` slug was available and refused — "hot-sale" is not
500
+ // what the page says, and a Vietnamese slug comes back stripped of its
501
+ // diacritics, which is the invented-copy defect this repo reports on
502
+ // everybody else's seeds.
503
+ const tabSetOf = (host) => {
504
+ const kids = Array.from(host.children);
505
+ const revealed = kids.filter((c) => c.getAttribute('data-sb-panel') !== null);
506
+ if (revealed.length === 0)
507
+ return null;
508
+ // THE BUTTON ROW IS A SIBLING OF THE PANELS, and it has to be taken out
509
+ // of the set before anything is counted — it is a container of the same
510
+ // shape, and leaving it in makes the panel count wrong and the pairing
511
+ // meaningless. It is the child that holds the clickable words.
512
+ // INNERMOST ONLY. `<li><button>Một</button></li>` is the ordinary way a
513
+ // tab bar is written, and counting both gives four clickables for two
514
+ // panels — which fails the count test that position pairing depends on,
515
+ // so a perfectly labelled tab bar read as unlabelled.
516
+ const clicksIn = (c) => {
517
+ const all = Array.from(c.querySelectorAll('button, a, li, [role="tab"]')).filter((b) => clean(b.textContent).length > 0);
518
+ return all.filter((b) => !all.some((o) => o !== b && b.contains(o)));
519
+ };
520
+ let bar = null;
521
+ for (const c of kids) {
522
+ if (c.getAttribute('data-sb-panel') !== null)
523
+ continue;
524
+ if (clicksIn(c).length >= 2) {
525
+ bar = c;
526
+ break;
527
+ }
528
+ }
529
+ const tagOf = revealed[0].tagName;
530
+ const panels = kids.filter((c) => c !== bar && c.tagName === tagOf && c.children.length > 0);
531
+ if (panels.length < 2)
532
+ return null;
533
+ const clickable = bar ? clicksIn(bar) : [];
534
+ const labels = [];
535
+ for (let i = 0; i < panels.length; i += 1) {
536
+ const panel = panels[i];
537
+ const id = panel.getAttribute('id');
538
+ let label = '';
539
+ if (id) {
540
+ for (const b of Array.from(host.querySelectorAll(`[aria-controls="${id}"]`))) {
541
+ const t = clean(b.textContent);
542
+ if (t) {
543
+ label = t;
544
+ break;
545
+ }
546
+ }
547
+ }
548
+ if (!label) {
549
+ for (const attr of Array.from(panel.attributes ?? [])) {
550
+ if (attr.name.indexOf('data-') !== 0 || attr.name === 'data-sb-panel' || !attr.value)
551
+ continue;
552
+ for (const b of clickable) {
553
+ if (b.getAttribute(attr.name) === attr.value) {
554
+ label = clean(b.textContent);
555
+ break;
556
+ }
557
+ }
558
+ if (label)
559
+ break;
560
+ }
561
+ }
562
+ if (!label && clickable.length === panels.length)
563
+ label = clean(clickable[i].textContent);
564
+ if (!label)
565
+ return null;
566
+ labels.push(label);
567
+ }
568
+ const children = [];
569
+ for (let i = 0; i < panels.length; i += 1) {
570
+ const body = walk(panels[i]);
571
+ if (body.length === 0)
572
+ continue;
573
+ children.push({ kind: 'tab-item', text: labels[i], children: body });
574
+ }
575
+ if (children.length < 2)
576
+ return null;
577
+ taken.nodes++;
578
+ return { kind: 'tab', children };
579
+ };
580
+ if (el.getAttribute('data-sb-panelset') !== null) {
581
+ const asTabs = tabSetOf(el);
582
+ if (asTabs)
583
+ return [asTabs];
584
+ skip('panel-set-without-labels');
585
+ }
461
586
  if (tag === 'DETAILS') {
462
587
  const kids2 = Array.from(el.children);
463
588
  const summary = kids2.filter((c) => c.tagName === 'SUMMARY')[0];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.30.1",
3
+ "version": "0.32.0",
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",