sbuilder-mcp 0.7.0 → 0.7.2
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 +14 -0
- package/CHANGELOG.vi.md +14 -0
- package/dist/tools/importpage.js +9 -2
- package/dist/vision/capture.js +51 -7
- package/dist/vision/shoot.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,20 @@ 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.7.2] - 2026-09-08
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- sb_import no longer classifies a short block-level link as a button; it previously stopped at "not inline", so a documentation sidebar's list of navigation links came back as dozens of buttons. A real call to action must now be painted with a fill or a border, and a border only counts when it has width, since a Tailwind-built page sets `border-style: solid; border-width: 0` on every element.
|
|
13
|
+
- sb_import's page capture no longer waits a flat 600ms before reading the page; it now reuses sb_look's own settle check, which waits until the page actually stops changing rather than a fixed delay that is too long for a static page and too short for one that builds itself with scripts.
|
|
14
|
+
|
|
15
|
+
## [0.7.1] - 2026-09-08
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- sb_import takes max_images (default 24), bounding how many images it uploads from the imported page, since every image is a real upload and a page shape like a sponsors wall can carry dozens of them in a single tool call.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- sb_import no longer duplicates content that sits inside a nested section; it previously matched every `<section>` on the page, so an outer band and the bands nested inside it were both captured and the inner content came back twice. Only the innermost matching section is now kept, since the outermost is a candidate too and keeping it would reduce the whole page to one band.
|
|
22
|
+
|
|
9
23
|
## [0.7.0] - 2026-09-08
|
|
10
24
|
|
|
11
25
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,20 @@ 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.7.2] - 2026-09-08
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- sb_import không còn xếp một liên kết block-level ngắn vào loại button; trước đây rule chỉ dừng ở "không phải inline", nên danh sách liên kết điều hướng trong sidebar tài liệu bị trả về thành hàng chục button. Giờ một call-to-action thật phải được "tô" bằng màu nền hoặc viền, và viền chỉ được tính khi có độ dày, vì một trang dựng bằng Tailwind đặt `border-style: solid; border-width: 0` trên mọi phần tử.
|
|
13
|
+
- Bước capture trang của sb_import không còn chờ cố định 600ms trước khi đọc trang; giờ nó dùng lại chính cơ chế chờ ổn định (settle) của sb_look, chờ đến khi trang thực sự ngừng thay đổi thay vì một khoảng chờ cố định vừa quá dài với trang tĩnh vừa quá ngắn với trang tự dựng bằng script.
|
|
14
|
+
|
|
15
|
+
## [0.7.1] - 2026-09-08
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- sb_import nhận max_images (mặc định 24), giới hạn số ảnh nó tải lên từ trang được import, vì mỗi ảnh là một lần upload thật và một dạng trang như tường tài trợ có thể chứa hàng chục ảnh trong một lần gọi tool.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
- sb_import không còn nhân đôi nội dung nằm trong một section lồng nhau; trước đây nó khớp với mọi `<section>` trên trang, nên một band bên ngoài và các band lồng bên trong nó đều bị lấy, khiến nội dung bên trong xuất hiện hai lần. Giờ chỉ section trong cùng khớp được giữ lại, vì section ngoài cùng cũng là một ứng viên và giữ nó sẽ làm cả trang chỉ còn một band.
|
|
22
|
+
|
|
9
23
|
## [0.7.0] - 2026-09-08
|
|
10
24
|
|
|
11
25
|
### Added
|
package/dist/tools/importpage.js
CHANGED
|
@@ -34,6 +34,13 @@ export function registerImportTools(server, ctx, session) {
|
|
|
34
34
|
url: z.string().describe('The page to read'),
|
|
35
35
|
site_id: z.string().optional(),
|
|
36
36
|
max_sections: z.number().int().min(1).max(60).optional(),
|
|
37
|
+
max_images: z
|
|
38
|
+
.number()
|
|
39
|
+
.int()
|
|
40
|
+
.min(0)
|
|
41
|
+
.max(100)
|
|
42
|
+
.optional()
|
|
43
|
+
.describe('Default 24 — every image is an upload'),
|
|
37
44
|
upload_images: z
|
|
38
45
|
.boolean()
|
|
39
46
|
.optional()
|
|
@@ -41,13 +48,13 @@ export function registerImportTools(server, ctx, session) {
|
|
|
41
48
|
dry_run: z.boolean().optional(),
|
|
42
49
|
},
|
|
43
50
|
annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
|
|
44
|
-
}, async ({ url, site_id: given, max_sections, upload_images, dry_run }) => {
|
|
51
|
+
}, async ({ url, site_id: given, max_sections, max_images, upload_images, dry_run }) => {
|
|
45
52
|
const siteId = siteFor(ctx, given);
|
|
46
53
|
// THE TARGET PAGE MUST BE OPEN, and not only because that is where the
|
|
47
54
|
// nodes go: its own heading, button and section are where the tokens come
|
|
48
55
|
// from, so an import with no open page is an import with no design.
|
|
49
56
|
const doc = session.current();
|
|
50
|
-
const shot = await capture(url, { maxSections: max_sections });
|
|
57
|
+
const shot = await capture(url, { maxSections: max_sections, maxImages: max_images });
|
|
51
58
|
const tokens = tokensFromPage(doc.doc);
|
|
52
59
|
const images = imageSources(shot.sections);
|
|
53
60
|
if (dry_run !== false) {
|
package/dist/vision/capture.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { chromium } from 'playwright-core';
|
|
2
|
+
import { settleDom } from './shoot.js';
|
|
2
3
|
/**
|
|
3
4
|
* The in-page walk, as a string the browser evaluates.
|
|
4
5
|
*
|
|
@@ -12,6 +13,7 @@ function capturePage(limits) {
|
|
|
12
13
|
// page, as `ReferenceError: HEADINGS is not defined`, by which point the file
|
|
13
14
|
// already carried a comment saying exactly that.
|
|
14
15
|
const HEADINGS = new Set(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
|
|
16
|
+
const taken = { images: 0 };
|
|
15
17
|
const skipped = {};
|
|
16
18
|
const skip = (why) => void (skipped[why] = (skipped[why] ?? 0) + 1);
|
|
17
19
|
const here = location.href;
|
|
@@ -40,11 +42,26 @@ function capturePage(limits) {
|
|
|
40
42
|
// label in a box with a background. Everything else is prose with a link in
|
|
41
43
|
// it, and turning those into buttons produces a page of buttons.
|
|
42
44
|
const looksLikeButton = (el) => {
|
|
45
|
+
const t = clean(el.textContent);
|
|
46
|
+
if (t.length === 0 || t.length > 32)
|
|
47
|
+
return false;
|
|
43
48
|
const cls = typeof el.className === 'string' ? el.className.toLowerCase() : '';
|
|
44
49
|
if (/\b(btn|button|cta)\b/.test(cls))
|
|
45
50
|
return true;
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
// A SHORT BLOCK-LEVEL LINK IS USUALLY NAVIGATION, not a call to action. The
|
|
52
|
+
// rule used to stop at "not inline" and a documentation sidebar came back as
|
|
53
|
+
// 38 buttons — a page of pink pills where the source had a list of links.
|
|
54
|
+
// A real CTA is PAINTED: it has a fill or a border. Nav links have neither.
|
|
55
|
+
const cs = getComputedStyle(el);
|
|
56
|
+
const filled = cs.backgroundColor !== '' &&
|
|
57
|
+
cs.backgroundColor !== 'transparent' &&
|
|
58
|
+
!cs.backgroundColor.startsWith('rgba(0, 0, 0, 0)');
|
|
59
|
+
// A BORDER NEEDS WIDTH, not just a style. Tailwind's preflight sets
|
|
60
|
+
// `border-style: solid; border-width: 0` on every element, so "has a border
|
|
61
|
+
// style" is true of an entire site built with it — which is how a
|
|
62
|
+
// documentation sidebar came back as 38 buttons even after the first fix.
|
|
63
|
+
const bordered = cs.borderStyle !== '' && cs.borderStyle !== 'none' && parseFloat(cs.borderWidth || '0') > 0;
|
|
64
|
+
return cs.display !== 'inline' && (filled || bordered);
|
|
48
65
|
};
|
|
49
66
|
const IGNORE = new Set([
|
|
50
67
|
'SCRIPT', 'STYLE', 'NOSCRIPT', 'TEMPLATE', 'SVG', 'PATH', 'IFRAME', 'CANVAS',
|
|
@@ -73,11 +90,21 @@ function capturePage(limits) {
|
|
|
73
90
|
}
|
|
74
91
|
if (tag === 'IMG') {
|
|
75
92
|
const src = el.getAttribute('src');
|
|
76
|
-
if (src
|
|
77
|
-
out.push({ kind: 'image', src: abs(src), alt: clean(el.getAttribute('alt')) });
|
|
78
|
-
}
|
|
79
|
-
else
|
|
93
|
+
if (!src || src.startsWith('data:')) {
|
|
80
94
|
skip('image-without-src');
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// BOUNDED, because every image is an upload. A sponsors wall is a real
|
|
98
|
+
// page shape — one measured at 36 logos in four sections — and importing
|
|
99
|
+
// it means 36 sequential HTTP round trips inside a single tool call,
|
|
100
|
+
// which is slow, half-fails in interesting ways, and is almost never
|
|
101
|
+
// what the caller wanted from "import this page".
|
|
102
|
+
if (taken.images >= limits.maxImages) {
|
|
103
|
+
skip('over-image-limit');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
taken.images++;
|
|
107
|
+
out.push({ kind: 'image', src: abs(src), alt: clean(el.getAttribute('alt')) });
|
|
81
108
|
return;
|
|
82
109
|
}
|
|
83
110
|
if (tag === 'A' && looksLikeButton(el)) {
|
|
@@ -117,6 +144,16 @@ function capturePage(limits) {
|
|
|
117
144
|
const main = document.querySelectorAll('main')[0] ?? document.body;
|
|
118
145
|
candidates = Array.from(main.children);
|
|
119
146
|
}
|
|
147
|
+
// INNERMOST ONLY. `section` matches nested ones too, so an outer band and the
|
|
148
|
+
// bands inside it were both taken — and the inner content came back TWICE,
|
|
149
|
+
// once through its parent's leaf walk and once on its own. Measured: 15
|
|
150
|
+
// duplicated strings out of 22 on one real page, 12 on another, which on an
|
|
151
|
+
// imported page reads as a stutter nobody typed.
|
|
152
|
+
//
|
|
153
|
+
// Innermost rather than outermost because a `<section>` that wraps the whole
|
|
154
|
+
// document is one candidate too, and keeping THAT would reduce every page to a
|
|
155
|
+
// single band. The finest ones are the page's actual bands.
|
|
156
|
+
candidates = candidates.filter((el) => !candidates.some((o) => o !== el && el.contains(o)));
|
|
120
157
|
const sections = [];
|
|
121
158
|
for (const el of candidates) {
|
|
122
159
|
if (sections.length >= limits.maxSections) {
|
|
@@ -147,6 +184,7 @@ export async function capture(url, opts = {}) {
|
|
|
147
184
|
const limits = {
|
|
148
185
|
maxSections: opts.maxSections ?? 24,
|
|
149
186
|
maxPerSection: opts.maxPerSection ?? 40,
|
|
187
|
+
maxImages: opts.maxImages ?? 24,
|
|
150
188
|
};
|
|
151
189
|
let browser;
|
|
152
190
|
try {
|
|
@@ -160,7 +198,13 @@ export async function capture(url, opts = {}) {
|
|
|
160
198
|
try {
|
|
161
199
|
page = await browser.newPage({ viewport: { width: opts.width ?? 1440, height: 900 } });
|
|
162
200
|
await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
|
|
163
|
-
|
|
201
|
+
// THE SAME SETTLE `sb_look` USES, not a flat sleep. A fixed 600ms is wrong
|
|
202
|
+
// at both ends: example.com is finished long before it, and a page that
|
|
203
|
+
// builds itself with scripts is not finished after it — which is exactly the
|
|
204
|
+
// page an import is most likely to be pointed at. `settleDom` asks the
|
|
205
|
+
// question actually being asked (has the page stopped changing) and answers
|
|
206
|
+
// when it becomes true, bounded so a page that never settles is still read.
|
|
207
|
+
await settleDom(page);
|
|
164
208
|
return (await page.evaluate(capturePage, limits));
|
|
165
209
|
}
|
|
166
210
|
finally {
|
package/dist/vision/shoot.js
CHANGED
|
@@ -161,7 +161,7 @@ export async function shoot(url, opts = {}) {
|
|
|
161
161
|
* polling widget from holding the shot forever. A page that never settles is
|
|
162
162
|
* photographed anyway — a late picture beats none.
|
|
163
163
|
*/
|
|
164
|
-
async function settleDom(page) {
|
|
164
|
+
export async function settleDom(page) {
|
|
165
165
|
await page
|
|
166
166
|
.evaluate(({ quiet, cap }) => new Promise((resolve) => {
|
|
167
167
|
const start = Date.now();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
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",
|