sbuilder-mcp 0.28.0 → 0.29.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,15 @@ 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.29.0] - 2026-09-10
10
+
11
+ ### Added
12
+ - sb_media_upload's `pick` argument now accepts several ids at once, so stocking a new site's empty library from one search result takes one upload call instead of one per photo; a picked id the search did not return refuses the whole call rather than uploading part of it.
13
+ - A layout pattern now declares how many picture slots it has, and sb_template_use reports how many the site's library could fill and names the sb_media_upload calls (query, then pick with several ids) that fill the rest.
14
+
15
+ ### Changed
16
+ - sb_template_use's result field for a library with too few images is now `pictures`, naming the exact shortfall (slots wanted vs. images offered) instead of the old `images` field, which only ever said the library was completely empty.
17
+
9
18
  ## [0.28.0] - 2026-09-10
10
19
 
11
20
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,15 @@ 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.29.0] - 2026-09-10
10
+
11
+ ### Added
12
+ - Tham số `pick` của sb_media_upload giờ nhận nhiều id cùng lúc, nên việc lấp đầy thư viện trống của một site mới từ một lần tìm kiếm chỉ cần một lệnh upload thay vì một lệnh cho mỗi ảnh; một id được chọn mà kết quả tìm kiếm không trả về sẽ từ chối toàn bộ lệnh thay vì chỉ upload một phần.
13
+ - Một layout dựng sẵn giờ khai báo số ô chứa ảnh nó có, và sb_template_use báo lại thư viện của site lấp được bao nhiêu ô cùng tên các lệnh sb_media_upload (query, rồi pick với nhiều id) để lấp phần còn lại.
14
+
15
+ ### Changed
16
+ - Trường kết quả của sb_template_use khi thư viện thiếu ảnh giờ là `pictures`, nêu đúng phần còn thiếu (số ô cần vs. số ảnh có) thay cho trường `images` cũ, vốn chỉ báo khi thư viện hoàn toàn trống.
17
+
9
18
  ## [0.28.0] - 2026-09-10
10
19
 
11
20
  ### Added
package/README.md CHANGED
@@ -101,7 +101,7 @@ in this client: one would put the very key the platform exists to hold back into
101
101
  | `sb_publish` | Compile the draft into the live page (cascades to shared globals) |
102
102
  | `sb_review` | Every defect a visitor would see, each with its fix, plus the five gaps between this store and a paid order |
103
103
  | `sb_media_list` | The site's media library |
104
- | `sb_media_upload` | Add an image and get its URL — a local path, a URL the platform fetches, or a SEARCH for real photographs you read and pick from |
104
+ | `sb_media_upload` | Add an image and get its URL — a local path, a URL the platform fetches, or a SEARCH for real photographs you read and pick from, one or several at a time |
105
105
  | `sb_live_join` | Join the editor's live-edit room as a visible peer — edits then appear live |
106
106
  | `sb_look` | Save, render, and return screenshots plus measured node boxes and layout defects measured on the render |
107
107
  | `sb_event` | Give a node a click action — open the cart, go to a page, open a pop-up |
package/README.vi.md CHANGED
@@ -98,7 +98,7 @@ nhét ngược lại vào mọi bản cài.
98
98
  | `sb_publish` | Biên dịch bản nháp thành trang live (lan sang global dùng chung) |
99
99
  | `sb_review` | Mọi khiếm khuyết người xem sẽ thấy, kèm lệnh sửa từng cái, và năm khoảng trống chắn giữa cửa hàng với một đơn đã thanh toán |
100
100
  | `sb_media_list` | Thư viện ảnh của site |
101
- | `sb_media_upload` | Thêm ảnh và lấy URL — file trên máy, một URL để nền tảng tự tải, hoặc TÌM ảnh chụp thật để đọc mô tả rồi chọn |
101
+ | `sb_media_upload` | Thêm ảnh và lấy URL — file trên máy, một URL để nền tảng tự tải, hoặc TÌM ảnh chụp thật để đọc mô tả rồi chọn, một hoặc nhiều tấm một lượt |
102
102
  | `sb_live_join` | Vào phòng live-edit của editor như một peer nhìn thấy được — sửa gì hiện ngay |
103
103
  | `sb_look` | Lưu, render, trả về ảnh chụp kèm box đo được của node và lỗi bố cục đo trên bản render |
104
104
  | `sb_event` | Gắn click action cho một node — mở giỏ, sang trang, mở pop-up |
@@ -93,6 +93,7 @@ export const LAYOUT_PATTERNS = [
93
93
  id: 'sb_hero_split',
94
94
  name: 'Hero — hai cột',
95
95
  use: 'Mở đầu trang: tiêu đề, một câu, nút hành động bên trái; chỗ cho ảnh bên phải',
96
+ images: 1,
96
97
  build: (t, pool) => {
97
98
  const used = new Set();
98
99
  return section([
@@ -149,6 +150,7 @@ export const LAYOUT_PATTERNS = [
149
150
  {
150
151
  id: 'sb_gallery',
151
152
  name: 'Dải ảnh',
153
+ images: 6,
152
154
  use: 'Một hàng ảnh cuốn dòng, lấy từ thư viện ảnh của chính site',
153
155
  build: (t, pool) => {
154
156
  const used = new Set();
@@ -405,14 +405,18 @@ export function registerLiveTools(server, ctx, session) {
405
405
  server.registerTool('sb_media_upload', {
406
406
  description: 'Put an image into the media library and get its URL back, ready for sb_set. Takes a ' +
407
407
  'local path, a URL, or a SEARCH — `query` returns real photographs with their own ' +
408
- 'descriptions, and `pick` uploads the one you chose. The only way to add an image.',
408
+ 'descriptions, and `pick` uploads the one you chose, or several at once to stock a ' +
409
+ 'site you just built. The only way to add an image.',
409
410
  inputSchema: {
410
411
  site_id: z.string().optional(),
411
412
  path: z.string().optional().describe('A file on this machine'),
412
413
  url: z.string().optional().describe('Fetched, then uploaded'),
413
414
  query: z.string().optional().describe('Search real photographs; read the descriptions, then pick'),
414
415
  orientation: z.enum(['landscape', 'portrait', 'square']).optional(),
415
- pick: z.number().int().optional().describe('The id of the search result to upload'),
416
+ pick: z
417
+ .union([z.number().int(), z.array(z.number().int())])
418
+ .optional()
419
+ .describe('The id of the search result to upload — or several ids, which stocks a site in one call'),
416
420
  name: z.string().optional(),
417
421
  folder_id: z.string().optional(),
418
422
  dry_run: z.boolean().optional(),
@@ -446,48 +450,106 @@ export function registerLiveTools(server, ctx, session) {
446
450
  }
447
451
  throw e;
448
452
  }
449
- const chosen = pick !== undefined ? photos.find((p) => p.id === pick) : undefined;
450
- if (!chosen) {
453
+ // STOCKING A SITE IS WHY THIS TAKES SEVERAL. One search answers with
454
+ // eight photographs and a gallery band wants six of them — and a site
455
+ // this server has just built has an EMPTY library, so every picture
456
+ // slot in every pattern is a sentence until somebody fills it. One
457
+ // pick per call made that twelve round trips for one band, which is
458
+ // how a correct rule becomes a rule nobody follows.
459
+ //
460
+ // It does NOT weaken rule 7. What that rule protects is that somebody
461
+ // LOOKED: reading eight descriptions and choosing six is the same act
462
+ // of choosing as reading eight and choosing one. What it forbids is
463
+ // uploading a hit nobody read, and no `pick` still uploads nothing.
464
+ const wanted = pick === undefined ? [] : Array.isArray(pick) ? pick : [pick];
465
+ const chosen = wanted
466
+ .map((id) => photos.find((p) => p.id === id))
467
+ .filter((p) => p !== undefined);
468
+ const absent = wanted.filter((id) => !photos.some((p) => p.id === id));
469
+ // A PARTIAL PICK IS REFUSED WHOLE rather than half-uploaded. The caller
470
+ // named a set; delivering some of it and reporting the rest as a note
471
+ // leaves them to work out which slots they can still fill.
472
+ if (chosen.length === 0 || absent.length > 0) {
451
473
  return text({
452
- ...(pick !== undefined ? { no_such_pick: pick } : {}),
474
+ ...(absent.length === 1 ? { no_such_pick: absent[0] } : {}),
475
+ ...(absent.length > 1 ? { no_such_picks: absent } : {}),
453
476
  found: photos.map((p) => ({
454
477
  pick: p.id,
455
478
  shows: p.alt || '(the photographer left no description)',
456
479
  size: `${p.width}x${p.height}`,
457
480
  by: p.photographer,
458
481
  })),
459
- next: 'Read what each one SHOWS, then re-call with pick:<id> and dry_run:false. The photo ' +
460
- "is uploaded into this site's own library, never hotlinked.",
482
+ next: 'Read what each one SHOWS, then re-call with pick:<id> and dry_run:false or ' +
483
+ 'pick:[<id>,<id>,…] to take several in one call, which is what fills a gallery ' +
484
+ "band. Each photo is uploaded into this site's own library, never hotlinked.",
461
485
  licence: ctx.notices.once('stock_licence', 'These are Pexels photographs: free for commercial use, with attribution ' +
462
486
  'appreciated rather than required, so a storefront can carry one without printing ' +
463
487
  'a credit line nobody asked for. The photographer comes back with each result if ' +
464
488
  'you want to credit anyway.'),
465
489
  });
466
490
  }
491
+ // ONE PICK KEEPS THE SHAPE IT HAS ALWAYS HAD. A caller that asked for
492
+ // one photograph gets one answer about one photograph; only a caller
493
+ // that asked for several is handed a list to read.
494
+ const solo = chosen.length === 1 ? chosen[0] : undefined;
467
495
  if (dry_run !== false) {
468
496
  return text({
469
497
  dry_run: true,
470
- would_upload: chosen.url,
471
- shows: chosen.alt,
472
- by: chosen.photographer,
498
+ would_upload: solo ? solo.url : chosen.map((c) => c.url),
499
+ shows: solo ? solo.alt : chosen.map((c) => c.alt),
500
+ by: solo ? solo.photographer : chosen.map((c) => c.photographer),
473
501
  into: site_id,
474
502
  note: 'Nothing was sent. Re-call with dry_run:false to upload.',
475
503
  });
476
504
  }
477
- const asset = await uploadMedia(ctx, site_id, {
478
- url: chosen.url,
479
- // THE DESCRIPTION BECOMES THE NAME, so the library is searchable by
480
- // what the photographs show and the alt on the page means something.
481
- name: name ?? chosen.alt ?? undefined,
482
- folderId: folder_id,
483
- });
505
+ // NOT ATOMIC, AND IT MUST NOT PRETEND TO BE. Each photo is its own
506
+ // upload, so the fifth failing does not un-upload the four that
507
+ // landed throwing here would leave the caller with four images in
508
+ // the library, no idea which, and an error that names none of them.
509
+ // The same reasoning `sb_import_site` records for its per-page report.
510
+ const uploaded = [];
511
+ const failed = [];
512
+ for (const one of chosen) {
513
+ try {
514
+ const asset = await uploadMedia(ctx, site_id, {
515
+ url: one.url,
516
+ // THE DESCRIPTION BECOMES THE NAME, so the library is searchable
517
+ // by what the photographs show and the alt on the page means
518
+ // something. A caller-supplied name can only speak for ONE photo,
519
+ // so it is honoured only when one was picked.
520
+ name: (solo ? name : undefined) ?? one.alt ?? undefined,
521
+ folderId: folder_id,
522
+ });
523
+ uploaded.push({
524
+ asset,
525
+ shows: one.alt,
526
+ credit: { by: one.photographer, profile: one.photographer_url, photo: one.page_url },
527
+ });
528
+ }
529
+ catch (e) {
530
+ failed.push({ pick: one.id, why: e.message });
531
+ }
532
+ }
533
+ if (solo) {
534
+ const only = uploaded[0];
535
+ if (!only)
536
+ throw new Error(`sbuilder: ${failed[0]?.why ?? 'the upload failed'}`);
537
+ const asset = only.asset;
538
+ return text({
539
+ asset,
540
+ shows: solo.alt,
541
+ credit: only.credit,
542
+ next: asset.url
543
+ ? `Use it: sb_set id "<node>", namespace specials, keys { "src": ${JSON.stringify(asset.url)} }`
544
+ : 'Uploaded, but the server returned no url — read it back with sb_media_list.',
545
+ });
546
+ }
484
547
  return text({
485
- asset,
486
- shows: chosen.alt,
487
- credit: { by: chosen.photographer, profile: chosen.photographer_url, photo: chosen.page_url },
488
- next: asset.url
489
- ? `Use it: sb_set id "<node>", namespace specials, keys { "src": ${JSON.stringify(asset.url)} }`
490
- : 'Uploaded, but the server returned no url — read it back with sb_media_list.',
548
+ uploaded,
549
+ ...(failed.length ? { failed } : {}),
550
+ next: `${uploaded.length} in this site's library now. A layout pattern reads the library ` +
551
+ 'when it builds, so sb_template_use will put these into its picture slots — or set ' +
552
+ 'one on a node directly with sb_set namespace specials, keys { "src": "<url>" }.',
491
553
  });
492
554
  }
493
555
  if (!path && !url) {
@@ -794,6 +794,7 @@ export function registerPageTools(server, ctx) {
794
794
  into: page_id,
795
795
  tokens_from: fromTheme ? "this site's theme — the page has no look of its own yet" : 'this page',
796
796
  images_available: pool.length,
797
+ ...(pattern.images ? { pictures_wanted: pattern.images } : {}),
797
798
  note: 'Composed against THIS page\'s tokens, not copied — the same heading ink, button ' +
798
799
  'fill and section padding the page already uses. Pass dry_run:false to add it.',
799
800
  });
@@ -805,11 +806,19 @@ export function registerPageTools(server, ctx) {
805
806
  nodes: ids.length,
806
807
  into: page_id,
807
808
  rev: doc.rev,
808
- ...(pool.length === 0
809
+ // A SHORT LIBRARY IS THE ORDINARY STATE OF A SITE THIS SERVER BUILT,
810
+ // not an edge case — nothing has been uploaded yet, so every picture
811
+ // slot in every pattern is a sentence. Saying "there are no images"
812
+ // and stopping leaves the agent to rediscover the fix; saying how
813
+ // many this band wanted and naming the two calls that fill them is
814
+ // the same fact with the next step attached.
815
+ ...(pattern.images && pool.length < pattern.images
809
816
  ? {
810
- images: 'This site has no images in its library, so any picture slot in this band says ' +
811
- 'so in words rather than showing a grey box. sb_media_upload takes a URL and ' +
812
- 'the platform fetches it server-side.',
817
+ pictures: `This band has ${pattern.images} picture slot${pattern.images > 1 ? 's' : ''} and the ` +
818
+ `library offered ${pool.length}; the rest say so in words rather than showing a grey ` +
819
+ 'box. Fill them: sb_media_upload query:"<what the band should show>" reads back real ' +
820
+ 'photographs with their own descriptions, then pick:[…] uploads the ones you chose in ' +
821
+ 'one call. Re-run this pattern afterwards and it takes them.',
813
822
  }
814
823
  : {}),
815
824
  ...(fromTheme
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.28.0",
3
+ "version": "0.29.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",