sbuilder-mcp 0.9.1 → 0.10.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,22 @@ 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.10.0] - 2026-09-09
10
+
11
+ ### Added
12
+ - sb_set now takes `unset`, an array of key names removed from the same slot a write would target — base, a breakpoint, or either home of a state — giving every finding that names "remove the override" as its fix a tool that can actually perform it. `keys` is now optional when `unset` carries the work, and a pure removal is exempt from the sticky-host guard so it can repair a `stuck_no_host` finding rather than being refused by it.
13
+
14
+ ### Fixed
15
+ - sb_review's `stuck_no_host` check now counts the keys inside a stuck state slot instead of only checking whether the slot exists, matching the platform's own `HasStuckOverrides`; an empty slot (the state a repair with `unset` leaves behind) no longer keeps reporting the finding it was used to fix.
16
+
17
+ ## [0.9.2] - 2026-09-09
18
+
19
+ ### Added
20
+ - sb_look now reports `stuck_note` once per process when the page pins something (`position: sticky` or `fixed`), explaining that a screenshot cannot show whether the element is actually stuck and pointing at a browser server (Playwright MCP or Chrome DevTools MCP) to scroll and check for the `wb-stuck` class.
21
+
22
+ ### Internal
23
+ - docs/tools.md and the sbuilder-site-design skill now document which of the other MCP servers (Figma, Google Stitch, Chrome DevTools, Playwright) answers which question, alongside sb_look, covering six cases a screenshot alone cannot settle: a sticky header actually engaging, a style that "did not apply", a cart drawer's click trigger, a checkout submission, an entity template previewed with a real record, and layout stability (CLS).
24
+
9
25
  ## [0.9.1] - 2026-09-09
10
26
 
11
27
  ### Fixed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,22 @@ 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.10.0] - 2026-09-09
10
+
11
+ ### Added
12
+ - sb_set giờ nhận `unset`, một mảng tên key cần xoá khỏi đúng slot mà một lần ghi sẽ nhắm tới — base, một breakpoint, hoặc một trong hai nơi lưu của một state — nhờ đó mọi finding nêu "xoá override đi" làm cách sửa giờ có công cụ để thực sự làm điều đó. `keys` giờ có thể bỏ qua khi `unset` đã làm phần việc, và một lần xoá thuần tuý được miễn khỏi kiểm tra sticky-host để nó có thể sửa một finding `stuck_no_host` thay vì bị chính kiểm tra đó từ chối.
13
+
14
+ ### Fixed
15
+ - Kiểm tra `stuck_no_host` của sb_review giờ đếm số key bên trong slot trạng thái stuck thay vì chỉ kiểm tra slot có tồn tại hay không, khớp với `HasStuckOverrides` của chính nền tảng; một slot rỗng (trạng thái mà một lần sửa bằng `unset` để lại) không còn tiếp tục báo cáo finding mà nó vừa được dùng để khắc phục.
16
+
17
+ ## [0.9.2] - 2026-09-09
18
+
19
+ ### Added
20
+ - sb_look giờ báo cáo `stuck_note` một lần mỗi tiến trình khi trang có ghim một phần tử (`position: sticky` hoặc `fixed`), giải thích rằng một ảnh chụp màn hình không thể cho biết phần tử đó có thực sự bị "stuck" hay không, và chỉ ra một trình duyệt server (Playwright MCP hoặc Chrome DevTools MCP) để cuộn trang và kiểm tra class `wb-stuck`.
21
+
22
+ ### Internal
23
+ - docs/tools.md và skill sbuilder-site-design giờ ghi lại server MCP nào (Figma, Google Stitch, Chrome DevTools, Playwright) trả lời câu hỏi nào, bên cạnh sb_look, bao quát sáu trường hợp mà chỉ riêng ảnh chụp màn hình không thể trả lời: một header sticky có thực sự ghim hay không, một style "không được áp dụng", nút bấm mở giỏ hàng, việc submit checkout, xem trước một template entity với dữ liệu thật, và độ ổn định bố cục (CLS).
24
+
9
25
  ## [0.9.1] - 2026-09-09
10
26
 
11
27
  ### Fixed
@@ -188,6 +188,21 @@ export function setKeys(doc, id, keys, opts) {
188
188
  // a breakpoint, never inside a stuck slot.
189
189
  if (namespace === 'config')
190
190
  refuseStuckAfter(doc.doc, id, keys);
191
+ // The paths `keys` and `unset` share. Computed once so a removal can never
192
+ // land somewhere a write would not have.
193
+ const slot = () => {
194
+ if (namespace === 'specials')
195
+ return ['nodes', id, 'specials'];
196
+ if (opts.state) {
197
+ return opts.base
198
+ ? ['nodes', id, 'states', opts.state, namespace]
199
+ : ['nodes', id, 'responsive', opts.breakpoint ?? 'desktop', 'states', opts.state, namespace];
200
+ }
201
+ if (opts.base)
202
+ return ['nodes', id, namespace];
203
+ return ['nodes', id, 'responsive', opts.breakpoint ?? 'desktop', namespace];
204
+ };
205
+ const removals = (opts.unset ?? []).map((k) => ({ op: 'unset', path: [...slot(), k] }));
191
206
  if (namespace === 'specials') {
192
207
  refuseComposedStamp(keys);
193
208
  // `specials` is content and identity, base-only by definition, and states
@@ -198,11 +213,14 @@ export function setKeys(doc, id, keys, opts) {
198
213
  'specials is content and identity (text, htmlTag, bound…), which do not vary by ' +
199
214
  'state. Style is what has states.');
200
215
  }
201
- return Object.entries(keys).map(([k, v]) => ({
202
- op: 'set',
203
- path: ['nodes', id, 'specials', k],
204
- value: v,
205
- }));
216
+ return [
217
+ ...Object.entries(keys).map(([k, v]) => ({
218
+ op: 'set',
219
+ path: ['nodes', id, 'specials', k],
220
+ value: v,
221
+ })),
222
+ ...removals,
223
+ ];
206
224
  }
207
225
  // A STATE IS ANSWERED BEFORE `base`, because `base` used to be tested first
208
226
  // and swallowed it: `base:true state:"hover"` wrote the hover value straight
@@ -216,7 +234,10 @@ export function setKeys(doc, id, keys, opts) {
216
234
  // class a runtime island toggles on the PINNED element, and the renderer
217
235
  // emits no rule at all when there is nothing pinned to hang it off. See
218
236
  // sticky.ts — this is the whole reason that module exists.
219
- if (opts.state === STUCK_STATE) {
237
+ // A pure REMOVAL is exempt: unsetting a hostless stuck override is exactly
238
+ // the repair `sb_review` asks for, and refusing it would leave the caller
239
+ // holding a finding they cannot act on.
240
+ if (opts.state === STUCK_STATE && Object.keys(keys).length) {
220
241
  requireStuckHost(doc.doc, id);
221
242
  if (namespace === 'config')
222
243
  refuseStuckConfig(keys);
@@ -233,6 +254,7 @@ export function setKeys(doc, id, keys, opts) {
233
254
  path: [...prefix, namespace, k],
234
255
  value: v,
235
256
  })),
257
+ ...removals,
236
258
  ...(rebind ? [rebind] : []),
237
259
  ];
238
260
  }
@@ -270,6 +292,7 @@ export function setKeys(doc, id, keys, opts) {
270
292
  path: ['nodes', id, namespace, k],
271
293
  value: v,
272
294
  })),
295
+ ...removals,
273
296
  ...(rebind ? [rebind] : []),
274
297
  ];
275
298
  }
@@ -280,6 +303,7 @@ export function setKeys(doc, id, keys, opts) {
280
303
  path: ['nodes', id, 'responsive', bp, namespace, k],
281
304
  value: v,
282
305
  })),
306
+ ...removals,
283
307
  ...(rebind ? [rebind] : []),
284
308
  ];
285
309
  }
@@ -492,7 +516,7 @@ export function setMany(doc, edits) {
492
516
  const patches = [];
493
517
  const touched = [];
494
518
  for (const e of edits) {
495
- patches.push(...setKeys(doc, e.id, e.keys, { namespace: e.namespace, breakpoint: e.breakpoint, base: e.base, state: e.state }));
519
+ patches.push(...setKeys(doc, e.id, e.keys, { namespace: e.namespace, breakpoint: e.breakpoint, base: e.base, state: e.state, unset: e.unset }));
496
520
  touched.push({ id: e.id, keys: Object.keys(e.keys) });
497
521
  }
498
522
  return { patches, touched };
@@ -214,8 +214,20 @@ export function reviewDesign(doc) {
214
214
  });
215
215
  }
216
216
  }
217
- const hasStuck = styled.states?.[STUCK_STATE] !== undefined ||
218
- Object.values(styled.responsive ?? {}).some((r) => r?.states?.[STUCK_STATE] !== undefined);
217
+ // COUNT THE KEYS, don't ask whether the slot exists — the platform's own
218
+ // `HasStuckOverrides` tests `len(slot.Style) > 0`, and an EMPTY slot paints
219
+ // nothing whether or not there is a host. Asking the weaker question made
220
+ // this a false positive on the one document the fix produces: unsetting the
221
+ // last key leaves `{}` behind, so the repair the finding names left the
222
+ // finding standing.
223
+ const stuckSlots = [
224
+ styled.states?.[STUCK_STATE],
225
+ ...Object.values(styled.responsive ?? {}).map((r) => r?.states?.[STUCK_STATE]),
226
+ ];
227
+ const hasStuck = stuckSlots.some((slot) => {
228
+ const s = slot;
229
+ return !!s && (Object.keys(s.style ?? {}).length > 0 || Object.keys(s.config ?? {}).length > 0);
230
+ });
219
231
  if (hasStuck && !stuckHostOf(d, id)) {
220
232
  out.push({
221
233
  code: 'stuck_no_host',
@@ -7,6 +7,7 @@ import { uploadMedia } from '../transport/media.js';
7
7
  import { request } from '../transport/http.js';
8
8
  import { shoot, DEFAULT_WIDTHS } from '../vision/shoot.js';
9
9
  import { measure, MEASURE_NOTICE } from '../vision/measure.js';
10
+ import { isPinnedNode } from '../domains/site/sticky.js';
10
11
  import { compactFindings } from '../domains/site/findings.js';
11
12
  import { reviewField } from './page.js';
12
13
  import { boxesForResponse, BOXES_FORMAT } from '../vision/boxes.js';
@@ -340,10 +341,30 @@ export function registerLiveTools(server, ctx, session) {
340
341
  '("Color / Size", "Red / S") rather than the product\'s own. That is the preview, ' +
341
342
  'not the page. Pass a published storefront URL as `url` to judge a template.')
342
343
  : undefined;
344
+ // A STILL PICTURE CANNOT SHOW A PINNED ELEMENT ENGAGING, and this is the
345
+ // one tool a caller would expect to. `position: sticky` looks identical at
346
+ // rest and while stuck — that is the whole reason the platform needs a
347
+ // runtime class for it — so a page carrying a pinned node has a look this
348
+ // tool is structurally unable to photograph, however many widths it shoots.
349
+ //
350
+ // Gated on the document actually carrying one, and said once, for the same
351
+ // reason `preview_note` is: a directive that fires on pages it cannot
352
+ // apply to is noise, and noise is what makes the real notes unread.
353
+ const pinned = Object.values(session.current().doc.nodes).some((n) => isPinnedNode(n));
354
+ const stuckNote = pinned
355
+ ? ctx.notices.once('stuck-scroll', 'This page pins something (position sticky/fixed). A screenshot is ONE scroll ' +
356
+ 'position, so nothing here can show whether it engages or what it looks like ' +
357
+ 'once it does — the platform styles that moment through a class a runtime island ' +
358
+ 'toggles, "wb-stuck", and CSS alone cannot express it. To check: open the ' +
359
+ 'PUBLISHED page in a browser server (Playwright or Chrome DevTools MCP), scroll, ' +
360
+ 'and read classList for "wb-stuck". If it never appears, every stuck override on ' +
361
+ 'the page is stored and never painted.')
362
+ : undefined;
343
363
  return images(shots.map((s) => ({ dataBase64: s.imageBase64, mimeType: s.mimeType })), {
344
364
  widths: shots.map((s) => s.width),
345
365
  ...(url ? { shot: url } : {}),
346
366
  ...(previewNote ? { preview_note: previewNote } : {}),
367
+ ...(stuckNote ? { stuck_note: stuckNote } : {}),
347
368
  ...(node_id ? { framed: node_id } : {}),
348
369
  ...(with_boxes === false
349
370
  ? {}
@@ -262,6 +262,11 @@ export function registerPageTools(server, ctx) {
262
262
  id: z.string().optional(),
263
263
  namespace: z.enum(['style', 'config', 'specials']).optional(),
264
264
  keys: z.record(z.unknown()).optional(),
265
+ unset: z
266
+ .array(z.string())
267
+ .optional()
268
+ .describe('Keys to REMOVE from the same slot — the only way to undo a write. Setting null ' +
269
+ 'is not the same: null is a stored value, so the override still counts as present.'),
265
270
  breakpoint: z.enum(['desktop', 'laptop', 'tablet', 'mobile']).optional(),
266
271
  base: z.boolean().optional(),
267
272
  state: z
@@ -278,20 +283,24 @@ export function registerPageTools(server, ctx) {
278
283
  breakpoint: z.enum(['desktop', 'laptop', 'tablet', 'mobile']).optional(),
279
284
  base: z.boolean().optional(),
280
285
  state: z.string().optional(),
286
+ unset: z.array(z.string()).optional(),
281
287
  }))
282
288
  .optional(),
283
289
  dry_run: z.boolean().optional(),
284
290
  },
285
291
  annotations: { readOnlyHint: false, destructiveHint: false },
286
- }, async ({ id, namespace, keys, breakpoint, base, state, edits, dry_run }) => {
292
+ }, async ({ id, namespace, keys, breakpoint, base, state, unset, edits, dry_run }) => {
287
293
  const d = session.current();
288
294
  // One shape inside: a single edit is a batch of one.
289
295
  const batch = edits ?? [];
290
296
  if (!edits) {
291
- if (!id || !namespace || !keys) {
292
- throw new Error('sbuilder: sb_set needs id + namespace + keys, or edits[]');
297
+ // `keys` is optional when `unset` carries the work: a pure removal is a
298
+ // legitimate edit, and demanding an empty object alongside it would make
299
+ // the repair `sb_review` names read like a workaround.
300
+ if (!id || !namespace || (!keys && !unset?.length)) {
301
+ throw new Error('sbuilder: sb_set needs id + namespace + keys (or unset), or edits[]');
293
302
  }
294
- batch.push({ id, namespace, keys, breakpoint: breakpoint, base, state });
303
+ batch.push({ id, namespace, keys: keys ?? {}, breakpoint: breakpoint, base, state, unset });
295
304
  }
296
305
  const { patches, touched } = setMany(d, batch);
297
306
  // THE STICKY WARNING IS COMPUTED AGAINST THE DOCUMENT AS IT WILL BE, so
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.9.1",
3
+ "version": "0.10.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",