sbuilder-mcp 0.7.0 → 0.7.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,14 @@ 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.1] - 2026-09-08
10
+
11
+ ### Added
12
+ - 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.
13
+
14
+ ### Fixed
15
+ - 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.
16
+
9
17
  ## [0.7.0] - 2026-09-08
10
18
 
11
19
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,14 @@ 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.1] - 2026-09-08
10
+
11
+ ### Added
12
+ - 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.
13
+
14
+ ### Fixed
15
+ - 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.
16
+
9
17
  ## [0.7.0] - 2026-09-08
10
18
 
11
19
  ### Added
@@ -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) {
@@ -12,6 +12,7 @@ function capturePage(limits) {
12
12
  // page, as `ReferenceError: HEADINGS is not defined`, by which point the file
13
13
  // already carried a comment saying exactly that.
14
14
  const HEADINGS = new Set(['H1', 'H2', 'H3', 'H4', 'H5', 'H6']);
15
+ const taken = { images: 0 };
15
16
  const skipped = {};
16
17
  const skip = (why) => void (skipped[why] = (skipped[why] ?? 0) + 1);
17
18
  const here = location.href;
@@ -73,11 +74,21 @@ function capturePage(limits) {
73
74
  }
74
75
  if (tag === 'IMG') {
75
76
  const src = el.getAttribute('src');
76
- if (src && !src.startsWith('data:')) {
77
- out.push({ kind: 'image', src: abs(src), alt: clean(el.getAttribute('alt')) });
78
- }
79
- else
77
+ if (!src || src.startsWith('data:')) {
80
78
  skip('image-without-src');
79
+ return;
80
+ }
81
+ // BOUNDED, because every image is an upload. A sponsors wall is a real
82
+ // page shape — one measured at 36 logos in four sections — and importing
83
+ // it means 36 sequential HTTP round trips inside a single tool call,
84
+ // which is slow, half-fails in interesting ways, and is almost never
85
+ // what the caller wanted from "import this page".
86
+ if (taken.images >= limits.maxImages) {
87
+ skip('over-image-limit');
88
+ return;
89
+ }
90
+ taken.images++;
91
+ out.push({ kind: 'image', src: abs(src), alt: clean(el.getAttribute('alt')) });
81
92
  return;
82
93
  }
83
94
  if (tag === 'A' && looksLikeButton(el)) {
@@ -117,6 +128,16 @@ function capturePage(limits) {
117
128
  const main = document.querySelectorAll('main')[0] ?? document.body;
118
129
  candidates = Array.from(main.children);
119
130
  }
131
+ // INNERMOST ONLY. `section` matches nested ones too, so an outer band and the
132
+ // bands inside it were both taken — and the inner content came back TWICE,
133
+ // once through its parent's leaf walk and once on its own. Measured: 15
134
+ // duplicated strings out of 22 on one real page, 12 on another, which on an
135
+ // imported page reads as a stutter nobody typed.
136
+ //
137
+ // Innermost rather than outermost because a `<section>` that wraps the whole
138
+ // document is one candidate too, and keeping THAT would reduce every page to a
139
+ // single band. The finest ones are the page's actual bands.
140
+ candidates = candidates.filter((el) => !candidates.some((o) => o !== el && el.contains(o)));
120
141
  const sections = [];
121
142
  for (const el of candidates) {
122
143
  if (sections.length >= limits.maxSections) {
@@ -147,6 +168,7 @@ export async function capture(url, opts = {}) {
147
168
  const limits = {
148
169
  maxSections: opts.maxSections ?? 24,
149
170
  maxPerSection: opts.maxPerSection ?? 40,
171
+ maxImages: opts.maxImages ?? 24,
150
172
  };
151
173
  let browser;
152
174
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.7.0",
3
+ "version": "0.7.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",