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 +16 -0
- package/CHANGELOG.vi.md +16 -0
- package/dist/domains/site/builder.js +31 -7
- package/dist/domains/site/review.js +14 -2
- package/dist/tools/live.js +21 -0
- package/dist/tools/page.js +13 -4
- package/package.json +1 -1
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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',
|
package/dist/tools/live.js
CHANGED
|
@@ -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
|
? {}
|
package/dist/tools/page.js
CHANGED
|
@@ -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
|
-
|
|
292
|
-
|
|
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.
|
|
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",
|