sbuilder-mcp 0.26.0 → 0.28.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 +20 -0
- package/CHANGELOG.vi.md +20 -0
- package/README.md +8 -0
- package/README.vi.md +7 -5
- package/dist/catalog/api.generated.js +76 -1
- package/dist/catalog/shapes.generated.js +19 -4
- package/dist/tools/live.js +25 -18
- package/dist/transport/stock.js +45 -87
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,26 @@ 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.28.0] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- The API catalog gained the platform's own image-search route, `GET /api/sites/{siteId}/images/search`, reachable through sb_api_call as well as through sb_media_upload.
|
|
13
|
+
- The API catalog gained the admin key-pool pair, `GET/POST /api/admin/image-keys`, for managing the platform's Pexels credentials.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- sb_media_upload's "search unavailable" instruction now names the admin Settings area's Pexels keys screen as the way to turn image search on, alongside the PEXELS_API_KEYS server variable, and notes that several keys are walked round-robin because the provider rate-limits per key.
|
|
17
|
+
|
|
18
|
+
## [0.27.0] - 2026-09-10
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- 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.
|
|
28
|
+
|
|
9
29
|
## [0.26.0] - 2026-09-10
|
|
10
30
|
|
|
11
31
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,26 @@ 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.28.0] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Catalog API có thêm route tìm ảnh của chính nền tảng, `GET /api/sites/{siteId}/images/search`, gọi được qua sb_api_call cũng như qua sb_media_upload.
|
|
13
|
+
- Catalog API có thêm cặp quản lý pool key phía admin, `GET/POST /api/admin/image-keys`, để quản lý key Pexels của nền tảng.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Chỉ dẫn "chưa cấu hình tìm ảnh" của sb_media_upload giờ nêu tên màn Cài đặt phía admin (khoá Pexels) như một cách bật tìm ảnh, bên cạnh biến môi trường PEXELS_API_KEYS trên server, và ghi chú rằng nhiều khoá được xoay vòng vì nhà cung cấp giới hạn theo từng khoá.
|
|
17
|
+
|
|
18
|
+
## [0.27.0] - 2026-09-10
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- 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.
|
|
22
|
+
|
|
23
|
+
### Removed
|
|
24
|
+
- 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.
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
- 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.
|
|
28
|
+
|
|
9
29
|
## [0.26.0] - 2026-09-10
|
|
10
30
|
|
|
11
31
|
### 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 |
|
package/README.vi.md
CHANGED
|
@@ -65,11 +65,13 @@ 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
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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.
|
|
73
75
|
|
|
74
76
|
## Bộ tool
|
|
75
77
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const SWAGGER_SOURCE = {
|
|
2
|
-
"operations":
|
|
2
|
+
"operations": 501,
|
|
3
3
|
"definitions": 104,
|
|
4
4
|
"bodyCarrying": 178,
|
|
5
5
|
"bodyUndescribed": 64,
|
|
@@ -1211,6 +1211,32 @@ export const API_OPERATIONS = [
|
|
|
1211
1211
|
"bodyRef": null,
|
|
1212
1212
|
"credential": "siteScoped"
|
|
1213
1213
|
},
|
|
1214
|
+
{
|
|
1215
|
+
"id": "get:/api/admin/image-keys",
|
|
1216
|
+
"method": "GET",
|
|
1217
|
+
"path": "/api/admin/image-keys",
|
|
1218
|
+
"tags": [
|
|
1219
|
+
"admin"
|
|
1220
|
+
],
|
|
1221
|
+
"summary": "Manage the platform's Pexels credentials",
|
|
1222
|
+
"params": [],
|
|
1223
|
+
"bodyDescribed": false,
|
|
1224
|
+
"bodyRef": null,
|
|
1225
|
+
"credential": "siteScoped"
|
|
1226
|
+
},
|
|
1227
|
+
{
|
|
1228
|
+
"id": "post:/api/admin/image-keys",
|
|
1229
|
+
"method": "POST",
|
|
1230
|
+
"path": "/api/admin/image-keys",
|
|
1231
|
+
"tags": [
|
|
1232
|
+
"admin"
|
|
1233
|
+
],
|
|
1234
|
+
"summary": "Manage the platform's Pexels credentials",
|
|
1235
|
+
"params": [],
|
|
1236
|
+
"bodyDescribed": false,
|
|
1237
|
+
"bodyRef": null,
|
|
1238
|
+
"credential": "siteScoped"
|
|
1239
|
+
},
|
|
1214
1240
|
{
|
|
1215
1241
|
"id": "get:/api/admin/plans/export",
|
|
1216
1242
|
"method": "GET",
|
|
@@ -7542,6 +7568,55 @@ export const API_OPERATIONS = [
|
|
|
7542
7568
|
"bodyRef": null,
|
|
7543
7569
|
"credential": "siteScoped"
|
|
7544
7570
|
},
|
|
7571
|
+
{
|
|
7572
|
+
"id": "get:/api/sites/{siteId}/images/search",
|
|
7573
|
+
"method": "GET",
|
|
7574
|
+
"path": "/api/sites/{siteId}/images/search",
|
|
7575
|
+
"tags": [
|
|
7576
|
+
"images"
|
|
7577
|
+
],
|
|
7578
|
+
"summary": "Search stock photographs",
|
|
7579
|
+
"params": [
|
|
7580
|
+
{
|
|
7581
|
+
"name": "siteId",
|
|
7582
|
+
"in": "path",
|
|
7583
|
+
"required": true,
|
|
7584
|
+
"type": "string",
|
|
7585
|
+
"description": "Site ID"
|
|
7586
|
+
},
|
|
7587
|
+
{
|
|
7588
|
+
"name": "query",
|
|
7589
|
+
"in": "query",
|
|
7590
|
+
"required": true,
|
|
7591
|
+
"type": "string",
|
|
7592
|
+
"description": "What to search for"
|
|
7593
|
+
},
|
|
7594
|
+
{
|
|
7595
|
+
"name": "per_page",
|
|
7596
|
+
"in": "query",
|
|
7597
|
+
"required": false,
|
|
7598
|
+
"type": "integer",
|
|
7599
|
+
"description": "1-30, default 8"
|
|
7600
|
+
},
|
|
7601
|
+
{
|
|
7602
|
+
"name": "orientation",
|
|
7603
|
+
"in": "query",
|
|
7604
|
+
"required": false,
|
|
7605
|
+
"type": "string",
|
|
7606
|
+
"description": "landscape, portrait or square"
|
|
7607
|
+
},
|
|
7608
|
+
{
|
|
7609
|
+
"name": "locale",
|
|
7610
|
+
"in": "query",
|
|
7611
|
+
"required": false,
|
|
7612
|
+
"type": "string",
|
|
7613
|
+
"description": "Provider locale, e.g. vi-VN"
|
|
7614
|
+
}
|
|
7615
|
+
],
|
|
7616
|
+
"bodyDescribed": false,
|
|
7617
|
+
"bodyRef": null,
|
|
7618
|
+
"credential": "siteScoped"
|
|
7619
|
+
},
|
|
7545
7620
|
{
|
|
7546
7621
|
"id": "get:/api/sites/{siteId}/integrations",
|
|
7547
7622
|
"method": "GET",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const SHAPE_SOURCE = {
|
|
2
|
-
"writeOperations":
|
|
3
|
-
"shaped":
|
|
4
|
-
"fromHandlers":
|
|
2
|
+
"writeOperations": 219,
|
|
3
|
+
"shaped": 169,
|
|
4
|
+
"fromHandlers": 169,
|
|
5
5
|
"fromSwaggerOnly": 0,
|
|
6
6
|
"withReadOnly": 26,
|
|
7
|
-
"structsRead":
|
|
7
|
+
"structsRead": 1658
|
|
8
8
|
};
|
|
9
9
|
export const REQUEST_SHAPES = {
|
|
10
10
|
"post:/api/sites/{siteId}/api-keys": {
|
|
@@ -4672,6 +4672,21 @@ export const REQUEST_SHAPES = {
|
|
|
4672
4672
|
"source": "go",
|
|
4673
4673
|
"goType": "(inline)"
|
|
4674
4674
|
},
|
|
4675
|
+
"post:/api/admin/image-keys": {
|
|
4676
|
+
"fields": [
|
|
4677
|
+
{
|
|
4678
|
+
"name": "label",
|
|
4679
|
+
"type": "string"
|
|
4680
|
+
},
|
|
4681
|
+
{
|
|
4682
|
+
"name": "key",
|
|
4683
|
+
"type": "string",
|
|
4684
|
+
"note": "`key`, not `secret`: it is what Pexels's own dashboard calls the value the operator is copying, and a form field named for the thing you are pasting is one less thing to guess at."
|
|
4685
|
+
}
|
|
4686
|
+
],
|
|
4687
|
+
"source": "go",
|
|
4688
|
+
"goType": "(inline)"
|
|
4689
|
+
},
|
|
4675
4690
|
"post:/api/admin/translation-keys": {
|
|
4676
4691
|
"fields": [
|
|
4677
4692
|
{
|
package/dist/tools/live.js
CHANGED
|
@@ -27,7 +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 } from '../transport/stock.js';
|
|
30
|
+
import { searchStock, SearchUnavailable, NO_SEARCH_NEXT } from '../transport/stock.js';
|
|
31
31
|
import { siteFor } from './context.js';
|
|
32
32
|
import { projectList, MEDIA_FIELDS } from './project.js';
|
|
33
33
|
/**
|
|
@@ -428,33 +428,40 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
428
428
|
// descriptions and CHOOSES; uploading the first hit unread would rebuild
|
|
429
429
|
// the cat statue with better plumbing.
|
|
430
430
|
if (query) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
orientation
|
|
434
|
-
}
|
|
435
|
-
|
|
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;
|
|
436
450
|
if (!chosen) {
|
|
437
451
|
return text({
|
|
438
452
|
...(pick !== undefined ? { no_such_pick: pick } : {}),
|
|
439
|
-
found:
|
|
453
|
+
found: photos.map((p) => ({
|
|
440
454
|
pick: p.id,
|
|
441
455
|
shows: p.alt || '(the photographer left no description)',
|
|
442
456
|
size: `${p.width}x${p.height}`,
|
|
443
457
|
by: p.photographer,
|
|
444
458
|
})),
|
|
445
|
-
via: found.via,
|
|
446
459
|
next: 'Read what each one SHOWS, then re-call with pick:<id> and dry_run:false. The photo ' +
|
|
447
460
|
"is uploaded into this site's own library, never hotlinked.",
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
453
|
-
: {}),
|
|
454
|
-
licence: ctx.notices.once('stock_licence', 'Pexels photographs are free for commercial use and attribution is appreciated ' +
|
|
455
|
-
'rather than required, so a storefront can carry one without printing a credit ' +
|
|
456
|
-
'line. The photographer and the photo page come back with each result if you ' +
|
|
457
|
-
'want to credit anyway.'),
|
|
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.'),
|
|
458
465
|
});
|
|
459
466
|
}
|
|
460
467
|
if (dry_run !== false) {
|
package/dist/transport/stock.js
CHANGED
|
@@ -1,95 +1,53 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* it actually SHOWS. Pexels answers "coffee shop" with
|
|
12
|
-
* "Three young adults engage at a cafe counter, using mobile devices" — so the
|
|
13
|
-
* caller can read the descriptions and CHOOSE, which is what a person does and
|
|
14
|
-
* what the cat statue proves nobody was doing.
|
|
15
|
-
*
|
|
16
|
-
* PEXELS, because it is already this family's answer: `webcake-landing-mcp`
|
|
17
|
-
* ships the same client, down to the shared proxy below, and a second house
|
|
18
|
-
* standard for the same job is a second place for it to drift. Its licence is
|
|
19
|
-
* free for commercial use with attribution appreciated rather than required,
|
|
20
|
-
* which is what makes an image usable on a merchant's storefront without
|
|
21
|
-
* printing a credit line they did not ask for.
|
|
22
|
-
*
|
|
23
|
-
* THE KEY IS OPTIONAL, and that is deliberate: with `PEXELS_API_KEY` this calls
|
|
24
|
-
* Pexels directly, and without one it calls the shared proxy the sibling repo
|
|
25
|
-
* already runs (`https://mcp.toolvn.io.vn/api/images/search`), which holds a key
|
|
26
|
-
* and answers the same shape. An `npx` install with no configuration at all
|
|
27
|
-
* still finds images.
|
|
28
|
-
*/
|
|
29
|
-
const PEXELS_SEARCH = 'https://api.pexels.com/v1/search';
|
|
30
|
-
const PROXY_DEFAULT = 'https://mcp.toolvn.io.vn';
|
|
31
|
-
const PROXY_PATH = '/api/images/search';
|
|
32
|
-
const TIMEOUT_MS = 20_000;
|
|
33
|
-
/** The shape both doors answer, normalised here so the caller cannot tell them apart. */
|
|
34
|
-
function normalise(p) {
|
|
35
|
-
const src = (p.src ?? {});
|
|
36
|
-
// `large2x` is ~1880px wide, which is a hero at 1440 and still a few hundred
|
|
37
|
-
// KB; `original` is the native file and on a 3800px photograph that is tens of
|
|
38
|
-
// megabytes for a band nobody will view above 1440.
|
|
39
|
-
const url = src.large2x || src.large || src.original || '';
|
|
40
|
-
if (!url)
|
|
41
|
-
return null;
|
|
42
|
-
return {
|
|
43
|
-
id: Number(p.id ?? 0),
|
|
44
|
-
alt: typeof p.alt === 'string' ? p.alt : '',
|
|
45
|
-
width: Number(p.width ?? 0),
|
|
46
|
-
height: Number(p.height ?? 0),
|
|
47
|
-
photographer: typeof p.photographer === 'string' ? p.photographer : '',
|
|
48
|
-
photographer_url: typeof p.photographer_url === 'string' ? p.photographer_url : '',
|
|
49
|
-
page_url: typeof p.url === 'string' ? p.url : '',
|
|
50
|
-
url,
|
|
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
|
+
}
|
|
52
11
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const q = new URLSearchParams({
|
|
62
|
-
query,
|
|
63
|
-
per_page: String(Math.min(Math.max(opts.perPage ?? 8, 1), 30)),
|
|
64
|
-
});
|
|
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 turns the search on by adding a Pexels key in the ' +
|
|
15
|
+
'admin Settings area (Pexels keys), or by setting PEXELS_API_KEYS on the server; several keys ' +
|
|
16
|
+
'are walked round-robin, because that provider rate-limits per key. Either way the key lives ' +
|
|
17
|
+
'there rather than here, so one place holds it and one quota is spent.';
|
|
18
|
+
export async function searchStock(ctx, siteId, query, opts = {}) {
|
|
19
|
+
const q = new URLSearchParams({ query, per_page: String(opts.perPage ?? 8) });
|
|
65
20
|
if (opts.orientation)
|
|
66
21
|
q.set('orientation', opts.orientation);
|
|
67
|
-
const key = (process.env.PEXELS_API_KEY ?? '').trim();
|
|
68
|
-
const proxy = (process.env.PEXELS_PROXY_BASE ?? PROXY_DEFAULT).replace(/\/+$/, '');
|
|
69
|
-
const [url, headers, via] = key
|
|
70
|
-
? [`${PEXELS_SEARCH}?${q}`, { Authorization: key }, 'pexels']
|
|
71
|
-
: [`${proxy}${PROXY_PATH}?${q}`, {}, 'proxy'];
|
|
72
|
-
let res;
|
|
73
22
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
23
|
+
const out = (await request({
|
|
24
|
+
base: ctx.base,
|
|
25
|
+
method: 'GET',
|
|
26
|
+
path: `/api/sites/${encodeURIComponent(siteId)}/images/search?${q}`,
|
|
27
|
+
token: siteToken(ctx),
|
|
28
|
+
fetchImpl: ctx.fetchImpl,
|
|
29
|
+
}));
|
|
30
|
+
return (out.photos ?? []).map((p) => ({
|
|
31
|
+
id: Number(p.id ?? 0),
|
|
32
|
+
alt: typeof p.alt === 'string' ? p.alt : '',
|
|
33
|
+
width: Number(p.width ?? 0),
|
|
34
|
+
height: Number(p.height ?? 0),
|
|
35
|
+
photographer: typeof p.photographer === 'string' ? p.photographer : '',
|
|
36
|
+
photographer_url: typeof p.photographerUrl === 'string' ? p.photographerUrl : '',
|
|
37
|
+
page_url: typeof p.pageUrl === 'string' ? p.pageUrl : '',
|
|
38
|
+
url: typeof p.url === 'string' ? p.url : '',
|
|
39
|
+
})).filter((p) => p.url);
|
|
78
40
|
}
|
|
79
41
|
catch (e) {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
(
|
|
86
|
-
? '
|
|
87
|
-
|
|
88
|
-
|
|
42
|
+
// 503 is the platform saying it has no usable key; 404 is a deployment older
|
|
43
|
+
// than the route. Both mean the same thing to the caller — no search here —
|
|
44
|
+
// and both deserve the same answer, which is not an error but an instruction.
|
|
45
|
+
const err = e;
|
|
46
|
+
if (err?.status === 503 || err?.status === 404 || err?.code === 'image_search_unavailable') {
|
|
47
|
+
throw new SearchUnavailable(err?.status === 404
|
|
48
|
+
? 'this deployment has no image-search route yet'
|
|
49
|
+
: 'no image provider key is configured or usable on this platform');
|
|
50
|
+
}
|
|
51
|
+
throw e;
|
|
89
52
|
}
|
|
90
|
-
const body = (await res.json());
|
|
91
|
-
const photos = (body.photos ?? [])
|
|
92
|
-
.map(normalise)
|
|
93
|
-
.filter((p) => p !== null);
|
|
94
|
-
return { photos, via };
|
|
95
53
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.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",
|