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 +9 -0
- package/CHANGELOG.vi.md +9 -0
- package/README.md +1 -1
- package/README.vi.md +1 -1
- package/dist/domains/site/patterns.js +2 -0
- package/dist/tools/live.js +85 -23
- package/dist/tools/page.js +13 -4
- package/package.json +1 -1
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();
|
package/dist/tools/live.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
450
|
-
|
|
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
|
-
...(
|
|
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
|
|
460
|
-
|
|
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
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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) {
|
package/dist/tools/page.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
'the
|
|
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.
|
|
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",
|