sbuilder-mcp 0.25.0 → 0.27.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 +19 -0
- package/CHANGELOG.vi.md +19 -0
- package/README.md +9 -1
- package/README.vi.md +9 -1
- package/dist/tools/live.js +80 -5
- package/dist/transport/stock.js +51 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ 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.27.0] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- sb_media_upload's photo search now runs through the platform (`GET /api/sites/{siteId}/images/search`) instead of calling Pexels or a shared proxy directly, so the provider key lives in one place and no install shares its quota with another product.
|
|
13
|
+
|
|
14
|
+
### Removed
|
|
15
|
+
- sb_media_upload no longer reads a `PEXELS_API_KEY` or a proxy base from the environment; this client keeps no stock-photo key of its own.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- sb_media_upload now reports `search_unavailable` with an explicit next step (pass a URL to sb_media_upload instead) when the platform has no image search configured, rather than raising an error.
|
|
19
|
+
|
|
20
|
+
## [0.26.0] - 2026-09-10
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- sb_media_upload can now search for a real photograph: `query` returns real photographs with the description each photographer wrote, and `pick` uploads the chosen one into the site's own library instead of hotlinking it.
|
|
24
|
+
- sb_media_upload's search takes `orientation` (landscape, portrait, square) to ask the search itself for the right shape instead of cropping the result afterward.
|
|
25
|
+
- sb_media_upload's search results carry photographer credit (name, profile, photo page), and the uploaded asset's name defaults to the photograph's own description so the library stays searchable by what each image shows.
|
|
26
|
+
- An optional `PEXELS_API_KEY` calls Pexels directly for the image search; without one it falls back to a shared proxy, so an `npx` install with no configuration still finds real images.
|
|
27
|
+
|
|
9
28
|
## [0.25.0] - 2026-09-10
|
|
10
29
|
|
|
11
30
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,25 @@ 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.27.0] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- Phần tìm ảnh của sb_media_upload giờ chạy qua platform (`GET /api/sites/{siteId}/images/search`) thay vì gọi thẳng Pexels hay một proxy chung, nên key của nhà cung cấp nằm ở một nơi duy nhất và không bản cài nào phải chia sẻ quota với sản phẩm khác.
|
|
13
|
+
|
|
14
|
+
### Removed
|
|
15
|
+
- sb_media_upload không còn đọc `PEXELS_API_KEY` hay proxy base từ biến môi trường; client này không giữ key ảnh stock nào của riêng nó nữa.
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- sb_media_upload giờ trả về `search_unavailable` kèm bước tiếp theo rõ ràng (truyền URL cho sb_media_upload thay vì tìm) khi platform chưa cấu hình tìm ảnh, thay vì báo lỗi.
|
|
19
|
+
|
|
20
|
+
## [0.26.0] - 2026-09-10
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- sb_media_upload giờ có thể tìm ảnh chụp thật: `query` trả về ảnh chụp thật kèm mô tả do chính người chụp viết, và `pick` upload đúng tấm được chọn vào thư viện của site thay vì hotlink.
|
|
24
|
+
- Phần tìm ảnh của sb_media_upload nhận `orientation` (landscape, portrait, square) để hỏi thẳng bộ tìm kiếm về hình dạng phù hợp thay vì cắt ảnh lại sau đó.
|
|
25
|
+
- Kết quả tìm ảnh của sb_media_upload mang theo thông tin ghi công (tên người chụp, trang cá nhân, trang ảnh), và tên của asset sau khi upload mặc định lấy theo mô tả của bức ảnh để thư viện tìm được theo nội dung ảnh.
|
|
26
|
+
- `PEXELS_API_KEY` là tuỳ chọn, dùng để gọi thẳng Pexels cho phần tìm ảnh; không có key thì dùng một proxy chung, nên một bản cài `npx` không cấu hình gì vẫn tìm được ảnh thật.
|
|
27
|
+
|
|
9
28
|
## [0.25.0] - 2026-09-10
|
|
10
29
|
|
|
11
30
|
### Added
|
package/README.md
CHANGED
|
@@ -68,6 +68,14 @@ make, because those mean "this person's account".
|
|
|
68
68
|
|
|
69
69
|
`SB_API` defaults to `http://localhost:8080`. Secrets are read from the environment only.
|
|
70
70
|
|
|
71
|
+
`sb_media_upload`'s photo search needs **no key here**: it calls the platform's own
|
|
72
|
+
`GET /api/sites/{siteId}/images/search`, which runs a rotated pool of provider keys behind the
|
|
73
|
+
credential this server already holds. An operator enables it by setting `PEXELS_API_KEYS` on the
|
|
74
|
+
SERVER (comma separated; free keys at <https://www.pexels.com/api/>). With none configured the
|
|
75
|
+
search answers "unavailable" and tells the caller to find a photograph by its own means and pass
|
|
76
|
+
the URL — which the platform then fetches server-side. There is deliberately no fallback provider
|
|
77
|
+
in this client: one would put the very key the platform exists to hold back into every install.
|
|
78
|
+
|
|
71
79
|
## Tools
|
|
72
80
|
|
|
73
81
|
| Tool | What it does |
|
|
@@ -93,7 +101,7 @@ make, because those mean "this person's account".
|
|
|
93
101
|
| `sb_publish` | Compile the draft into the live page (cascades to shared globals) |
|
|
94
102
|
| `sb_review` | Every defect a visitor would see, each with its fix, plus the five gaps between this store and a paid order |
|
|
95
103
|
| `sb_media_list` | The site's media library |
|
|
96
|
-
| `sb_media_upload` | Add an image and get its URL —
|
|
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 |
|
|
97
105
|
| `sb_live_join` | Join the editor's live-edit room as a visible peer — edits then appear live |
|
|
98
106
|
| `sb_look` | Save, render, and return screenshots plus measured node boxes and layout defects measured on the render |
|
|
99
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
|
@@ -65,6 +65,14 @@ là "tài khoản của người này".
|
|
|
65
65
|
|
|
66
66
|
`SB_API` mặc định `http://localhost:8080`. Bí mật chỉ đọc từ biến môi trường.
|
|
67
67
|
|
|
68
|
+
Phần tìm ảnh của `sb_media_upload` **không cần key ở đây**: nó gọi route của chính nền tảng,
|
|
69
|
+
`GET /api/sites/{siteId}/images/search`, nơi có một pool key xoay vòng nằm sau đúng credential mà
|
|
70
|
+
server này đang cầm. Operator bật nó bằng cách đặt `PEXELS_API_KEYS` **trên SERVER** (ngăn cách
|
|
71
|
+
bằng dấu phẩy; key miễn phí ở <https://www.pexels.com/api/>). Không cấu hình gì thì tìm kiếm trả
|
|
72
|
+
lời "không khả dụng" và bảo người gọi tự tìm ảnh rồi đưa URL — nền tảng sẽ tự tải về. Client này
|
|
73
|
+
cố ý **không có nhà cung cấp dự phòng**: có nó là đem đúng cái key mà nền tảng sinh ra để giữ,
|
|
74
|
+
nhét ngược lại vào mọi bản cài.
|
|
75
|
+
|
|
68
76
|
## Bộ tool
|
|
69
77
|
|
|
70
78
|
| Tool | Làm gì |
|
|
@@ -90,7 +98,7 @@ là "tài khoản của người này".
|
|
|
90
98
|
| `sb_publish` | Biên dịch bản nháp thành trang live (lan sang global dùng chung) |
|
|
91
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 |
|
|
92
100
|
| `sb_media_list` | Thư viện ảnh của site |
|
|
93
|
-
| `sb_media_upload` | Thêm ảnh và lấy URL —
|
|
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 |
|
|
94
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 |
|
|
95
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 |
|
|
96
104
|
| `sb_event` | Gắn click action cho một node — mở giỏ, sang trang, mở pop-up |
|
package/dist/tools/live.js
CHANGED
|
@@ -27,6 +27,7 @@ import { LiveSession } from '../live/session.js';
|
|
|
27
27
|
import { refuseAppBlockInterior } from '../domains/site/builder.js';
|
|
28
28
|
import { childrenOf, isOverlay, overlayRoot, subtreeIds } from '../core/tree.js';
|
|
29
29
|
import { siteToken } from './credentialpick.js';
|
|
30
|
+
import { searchStock, SearchUnavailable, NO_SEARCH_NEXT } from '../transport/stock.js';
|
|
30
31
|
import { siteFor } from './context.js';
|
|
31
32
|
import { projectList, MEDIA_FIELDS } from './project.js';
|
|
32
33
|
/**
|
|
@@ -403,21 +404,95 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
403
404
|
}), 'assets', MEDIA_FIELDS)));
|
|
404
405
|
server.registerTool('sb_media_upload', {
|
|
405
406
|
description: 'Put an image into the media library and get its URL back, ready for sb_set. Takes a ' +
|
|
406
|
-
'local
|
|
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
409
|
inputSchema: {
|
|
409
410
|
site_id: z.string().optional(),
|
|
410
411
|
path: z.string().optional().describe('A file on this machine'),
|
|
411
412
|
url: z.string().optional().describe('Fetched, then uploaded'),
|
|
413
|
+
query: z.string().optional().describe('Search real photographs; read the descriptions, then pick'),
|
|
414
|
+
orientation: z.enum(['landscape', 'portrait', 'square']).optional(),
|
|
415
|
+
pick: z.number().int().optional().describe('The id of the search result to upload'),
|
|
412
416
|
name: z.string().optional(),
|
|
413
417
|
folder_id: z.string().optional(),
|
|
414
418
|
dry_run: z.boolean().optional(),
|
|
415
419
|
},
|
|
416
420
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
417
|
-
}, async ({ site_id: given, path, url, name, folder_id, dry_run }) => {
|
|
421
|
+
}, async ({ site_id: given, path, url, name, folder_id, query, orientation, pick, dry_run }) => {
|
|
418
422
|
const site_id = siteFor(ctx, given);
|
|
419
|
-
|
|
420
|
-
|
|
423
|
+
// A SEARCH IS NOT A GUESS, and the difference is the whole reason this is
|
|
424
|
+
// two steps. Rule 7 records what a keyword glued into a URL returns —
|
|
425
|
+
// `loremflickr` answered "kids,clothing" with a cat statue — and the fault
|
|
426
|
+
// was never stock photography, it was that nobody looked. Every result
|
|
427
|
+
// here carries what it actually SHOWS, so the caller reads the
|
|
428
|
+
// descriptions and CHOOSES; uploading the first hit unread would rebuild
|
|
429
|
+
// the cat statue with better plumbing.
|
|
430
|
+
if (query) {
|
|
431
|
+
let photos;
|
|
432
|
+
try {
|
|
433
|
+
photos = await searchStock(ctx, site_id, query, { perPage: 8, orientation });
|
|
434
|
+
}
|
|
435
|
+
catch (e) {
|
|
436
|
+
if (e instanceof SearchUnavailable) {
|
|
437
|
+
// NOT AN ERROR, AN INSTRUCTION. There is deliberately no fallback
|
|
438
|
+
// provider here: one would put the very key the platform exists to
|
|
439
|
+
// hold back into every install. An agent with a web search of its
|
|
440
|
+
// own loses nothing — it finds a photograph and passes the URL, and
|
|
441
|
+
// the platform fetches it server-side exactly as it would have.
|
|
442
|
+
return text({
|
|
443
|
+
search_unavailable: e.why,
|
|
444
|
+
next: NO_SEARCH_NEXT,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
throw e;
|
|
448
|
+
}
|
|
449
|
+
const chosen = pick !== undefined ? photos.find((p) => p.id === pick) : undefined;
|
|
450
|
+
if (!chosen) {
|
|
451
|
+
return text({
|
|
452
|
+
...(pick !== undefined ? { no_such_pick: pick } : {}),
|
|
453
|
+
found: photos.map((p) => ({
|
|
454
|
+
pick: p.id,
|
|
455
|
+
shows: p.alt || '(the photographer left no description)',
|
|
456
|
+
size: `${p.width}x${p.height}`,
|
|
457
|
+
by: p.photographer,
|
|
458
|
+
})),
|
|
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.",
|
|
461
|
+
licence: ctx.notices.once('stock_licence', 'These are Pexels photographs: free for commercial use, with attribution ' +
|
|
462
|
+
'appreciated rather than required, so a storefront can carry one without printing ' +
|
|
463
|
+
'a credit line nobody asked for. The photographer comes back with each result if ' +
|
|
464
|
+
'you want to credit anyway.'),
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (dry_run !== false) {
|
|
468
|
+
return text({
|
|
469
|
+
dry_run: true,
|
|
470
|
+
would_upload: chosen.url,
|
|
471
|
+
shows: chosen.alt,
|
|
472
|
+
by: chosen.photographer,
|
|
473
|
+
into: site_id,
|
|
474
|
+
note: 'Nothing was sent. Re-call with dry_run:false to upload.',
|
|
475
|
+
});
|
|
476
|
+
}
|
|
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
|
+
});
|
|
484
|
+
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.',
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
if (!path && !url) {
|
|
494
|
+
throw new Error('sbuilder: give sb_media_upload a path, a url, or a query to search');
|
|
495
|
+
}
|
|
421
496
|
if (dry_run !== false) {
|
|
422
497
|
return text({
|
|
423
498
|
dry_run: true,
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { request } from './http.js';
|
|
2
|
+
import { siteToken } from '../tools/credentialpick.js';
|
|
3
|
+
/** The platform has no usable provider key, which is not the same as no results. */
|
|
4
|
+
export class SearchUnavailable extends Error {
|
|
5
|
+
why;
|
|
6
|
+
constructor(why) {
|
|
7
|
+
super(why);
|
|
8
|
+
this.why = why;
|
|
9
|
+
this.name = 'SearchUnavailable';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export const NO_SEARCH_NEXT = 'This platform has no image search available, so find a photograph by your own means and pass ' +
|
|
13
|
+
'its URL to sb_media_upload — the platform fetches it server-side and it lands in this site\'s ' +
|
|
14
|
+
'own library, never hotlinked. An operator enables the search by setting PEXELS_API_KEYS on the ' +
|
|
15
|
+
'server; the key belongs there rather than here, so one place holds it and one quota is spent.';
|
|
16
|
+
export async function searchStock(ctx, siteId, query, opts = {}) {
|
|
17
|
+
const q = new URLSearchParams({ query, per_page: String(opts.perPage ?? 8) });
|
|
18
|
+
if (opts.orientation)
|
|
19
|
+
q.set('orientation', opts.orientation);
|
|
20
|
+
try {
|
|
21
|
+
const out = (await request({
|
|
22
|
+
base: ctx.base,
|
|
23
|
+
method: 'GET',
|
|
24
|
+
path: `/api/sites/${encodeURIComponent(siteId)}/images/search?${q}`,
|
|
25
|
+
token: siteToken(ctx),
|
|
26
|
+
fetchImpl: ctx.fetchImpl,
|
|
27
|
+
}));
|
|
28
|
+
return (out.photos ?? []).map((p) => ({
|
|
29
|
+
id: Number(p.id ?? 0),
|
|
30
|
+
alt: typeof p.alt === 'string' ? p.alt : '',
|
|
31
|
+
width: Number(p.width ?? 0),
|
|
32
|
+
height: Number(p.height ?? 0),
|
|
33
|
+
photographer: typeof p.photographer === 'string' ? p.photographer : '',
|
|
34
|
+
photographer_url: typeof p.photographerUrl === 'string' ? p.photographerUrl : '',
|
|
35
|
+
page_url: typeof p.pageUrl === 'string' ? p.pageUrl : '',
|
|
36
|
+
url: typeof p.url === 'string' ? p.url : '',
|
|
37
|
+
})).filter((p) => p.url);
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
// 503 is the platform saying it has no usable key; 404 is a deployment older
|
|
41
|
+
// than the route. Both mean the same thing to the caller — no search here —
|
|
42
|
+
// and both deserve the same answer, which is not an error but an instruction.
|
|
43
|
+
const err = e;
|
|
44
|
+
if (err?.status === 503 || err?.status === 404 || err?.code === 'image_search_unavailable') {
|
|
45
|
+
throw new SearchUnavailable(err?.status === 404
|
|
46
|
+
? 'this deployment has no image-search route yet'
|
|
47
|
+
: 'no image provider key is configured or usable on this platform');
|
|
48
|
+
}
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.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",
|