sbuilder-mcp 0.47.0 → 0.47.2
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 +10 -0
- package/CHANGELOG.vi.md +10 -0
- package/dist/tools/page.js +138 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ 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.47.2] - 2026-09-14
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `sb_page_create` no longer stores an HTML entity verbatim in a page name; a name like `Chính sách giao hàng & đổi trả`, lifted undecoded from a page's own source, is now decoded once before it is sent, on both the create and the home-page-adopt rename path.
|
|
13
|
+
|
|
14
|
+
## [0.47.1] - 2026-09-14
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `sb_page_create` with `is_homepage` set on a site that already has one no longer creates a duplicate that steals the star and leaves the platform's own original home page reachable at no address; it now adopts the existing home page instead, renaming it when the caller named it something else, and reports what it did instead of creating anything.
|
|
18
|
+
|
|
9
19
|
## [0.47.0] - 2026-09-14
|
|
10
20
|
|
|
11
21
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,16 @@ 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.47.2] - 2026-09-14
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `sb_page_create` không còn lưu nguyên một HTML entity trong tên trang; một tên như `Chính sách giao hàng & đổi trả`, lấy chưa giải mã từ chính nguồn của một trang, giờ được giải mã một lần trước khi gửi đi, trên cả đường tạo trang lẫn đường đổi tên khi nhận lại trang chủ.
|
|
13
|
+
|
|
14
|
+
## [0.47.1] - 2026-09-14
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `sb_page_create` với `is_homepage` được bật trên một site đã có sẵn trang chủ giờ không còn tạo ra một trang trùng lặp cướp mất ngôi sao và khiến trang chủ gốc của nền tảng trở nên không thể truy cập ở bất kỳ địa chỉ nào; giờ nó nhận lại trang chủ hiện có, đổi tên nếu người gọi đặt tên khác, và báo cáo việc đã làm thay vì tạo trang mới.
|
|
18
|
+
|
|
9
19
|
## [0.47.0] - 2026-09-14
|
|
10
20
|
|
|
11
21
|
### Added
|
package/dist/tools/page.js
CHANGED
|
@@ -311,6 +311,63 @@ async function siteChrome(ctx, siteId) {
|
|
|
311
311
|
return {};
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
|
+
/**
|
|
315
|
+
* Plain text, for a field the platform renders AS text.
|
|
316
|
+
*
|
|
317
|
+
* A page name is shown in the pages panel and in the browser tab and is never
|
|
318
|
+
* parsed as markup, so an HTML entity in one is always a mistake: text lifted
|
|
319
|
+
* from a page's source and handed on without being decoded. Measured on a real
|
|
320
|
+
* store, read back through sb_page_list: a page stored as "Chính sách giao hàng
|
|
321
|
+
* & đổi trả" — which is what the merchant then reads in their own editor,
|
|
322
|
+
* and what a browser tab then shows.
|
|
323
|
+
*
|
|
324
|
+
* ONE PASS, and only over what escaping actually produces. "&" decodes to
|
|
325
|
+
* "&" and stops there, because a second pass would be inventing an intent
|
|
326
|
+
* nobody expressed; an unknown entity is left exactly as it arrived. This
|
|
327
|
+
* un-escapes text that was escaped once. It does not interpret markup.
|
|
328
|
+
*/
|
|
329
|
+
const ENTITIES = {
|
|
330
|
+
amp: '&',
|
|
331
|
+
lt: '<',
|
|
332
|
+
gt: '>',
|
|
333
|
+
quot: '"',
|
|
334
|
+
apos: "'",
|
|
335
|
+
nbsp: ' ',
|
|
336
|
+
};
|
|
337
|
+
export function plainText(s) {
|
|
338
|
+
return s.replace(/&(#x[0-9a-fA-F]+|#\d+|[a-zA-Z]+);/g, (whole, body) => {
|
|
339
|
+
if (body.startsWith('#')) {
|
|
340
|
+
const code = body.startsWith('#x') ? parseInt(body.slice(2), 16) : parseInt(body.slice(1), 10);
|
|
341
|
+
// A code point that is not one is not a reference — leave the text alone
|
|
342
|
+
// rather than writing a replacement character into somebody's page name.
|
|
343
|
+
return Number.isFinite(code) && code > 0 && code <= 0x10ffff ? String.fromCodePoint(code) : whole;
|
|
344
|
+
}
|
|
345
|
+
return ENTITIES[body.toLowerCase()] ?? whole;
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* The site's own home page, or null when it genuinely has none.
|
|
350
|
+
*
|
|
351
|
+
* THROWS RATHER THAN GUESSES. Answering "none" for a listing that could not be
|
|
352
|
+
* read is the one wrong answer available here: it is indistinguishable from an
|
|
353
|
+
* empty site, and the caller acts on it by creating a home page the site
|
|
354
|
+
* already had. `siteChrome` above may swallow its read because a page created
|
|
355
|
+
* without chrome is a page a person can fix; this one may not, because the page
|
|
356
|
+
* it creates cannot be un-created and takes the star off whichever page held it.
|
|
357
|
+
*/
|
|
358
|
+
async function existingHomepage(ctx, siteId) {
|
|
359
|
+
const listed = (await request({
|
|
360
|
+
base: ctx.base,
|
|
361
|
+
method: 'GET',
|
|
362
|
+
path: `/api/sites/${encodeURIComponent(siteId)}/pages`,
|
|
363
|
+
token: siteToken(ctx),
|
|
364
|
+
fetchImpl: ctx.fetchImpl,
|
|
365
|
+
}));
|
|
366
|
+
const home = (listed.pages ?? []).find((p) => p.isHomepage === true);
|
|
367
|
+
if (!home || typeof home.id !== 'string' || !home.id)
|
|
368
|
+
return null;
|
|
369
|
+
return { id: home.id, name: typeof home.name === 'string' ? home.name : '' };
|
|
370
|
+
}
|
|
314
371
|
export function registerPageTools(server, ctx) {
|
|
315
372
|
const session = new PageSession(ctx);
|
|
316
373
|
server.registerTool('sb_page_open', {
|
|
@@ -981,6 +1038,8 @@ export function registerPageTools(server, ctx) {
|
|
|
981
1038
|
}, async ({ site_id: given, name, type, slug, is_homepage, settings, seed, chrome, locale, headline, dry_run }) => {
|
|
982
1039
|
const site_id = siteFor(ctx, given);
|
|
983
1040
|
const path = `/api/sites/${encodeURIComponent(site_id)}/pages`;
|
|
1041
|
+
// The platform stores this verbatim and renders it as text — see plainText.
|
|
1042
|
+
name = plainText(name);
|
|
984
1043
|
// TYPE IS THE ROUTE for several kinds of page: /checkout and
|
|
985
1044
|
// /products/{slug} resolve to the site's PUBLISHED page of that type and
|
|
986
1045
|
// fall through to a 404 when there is none. Without this argument the
|
|
@@ -1012,7 +1071,85 @@ export function registerPageTools(server, ctx) {
|
|
|
1012
1071
|
// fine, while `siteChrome` asks whether the SITE has globals and it does.
|
|
1013
1072
|
// Measured: three pages built with these tools, every one of them bare,
|
|
1014
1073
|
// beside a store page carrying its header as ROOT's first child.
|
|
1015
|
-
|
|
1074
|
+
// A SITE HAS ONE HOME PAGE, AND BY THE TIME AN AGENT ASKS FOR ONE IT
|
|
1075
|
+
// USUALLY EXISTS ALREADY.
|
|
1076
|
+
//
|
|
1077
|
+
// `isHomepage: true` does not mean "make this the home page" to the
|
|
1078
|
+
// platform. It means MOVE THE STAR: CreatePage demotes whoever holds it
|
|
1079
|
+
// and promotes this one. An agent building a store reads the flag the
|
|
1080
|
+
// first way and asks for it on a site the editor already gave a home page
|
|
1081
|
+
// to, so the site ends up with two — the new one on "/", the old one
|
|
1082
|
+
// demoted and holding nothing. Measured on a real store: pg_439cb121
|
|
1083
|
+
// "Home" and pg_237719d4 "Trang chủ", both slug "", both path "/", the
|
|
1084
|
+
// first reachable at no address at all and still listed as a page.
|
|
1085
|
+
//
|
|
1086
|
+
// So the flag is honoured as what the caller meant — the site's home page
|
|
1087
|
+
// — which is the one it already has. Adopted, renamed when the caller
|
|
1088
|
+
// named it something else, never duplicated. The same rule sb_import_site
|
|
1089
|
+
// already follows when its entry page lands on a site with a home page.
|
|
1090
|
+
//
|
|
1091
|
+
// REPLACING the home page with a DIFFERENT page stays possible and stays
|
|
1092
|
+
// explicit: create it without the flag, build it, then PATCH isHomepage
|
|
1093
|
+
// through sb_api_call. That is a decision, and it should read like one.
|
|
1094
|
+
const adopt = is_homepage === true ? await existingHomepage(ctx, site_id) : null;
|
|
1095
|
+
const rename = adopt && name.trim() !== '' && name !== adopt.name ? name : '';
|
|
1096
|
+
// Read AFTER the adopt decision and skipped when it holds: siteChrome
|
|
1097
|
+
// reads the header and footer off the home page, so asking it which
|
|
1098
|
+
// chrome to dress the home page in is two requests to answer "its own".
|
|
1099
|
+
const wear = chrome !== false && !adopt ? await siteChrome(ctx, site_id) : {};
|
|
1100
|
+
if (adopt && dry_run !== false) {
|
|
1101
|
+
return text({
|
|
1102
|
+
dry_run: true,
|
|
1103
|
+
into: 'the existing home page',
|
|
1104
|
+
page: adopt,
|
|
1105
|
+
...(rename ? { would_rename: { from: adopt.name, to: rename } } : {}),
|
|
1106
|
+
note: 'Nothing would be created. This site already has a home page and a site has ' +
|
|
1107
|
+
'exactly one, so a create carrying isHomepage would have taken the star off ' +
|
|
1108
|
+
`${JSON.stringify(adopt.name)} and left it with no address. Open ${adopt.id} ` +
|
|
1109
|
+
'with sb_page_open and build it.',
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
if (adopt) {
|
|
1113
|
+
// THE RENAME IS THE ONLY WRITE. Not the seed — this page may already be
|
|
1114
|
+
// the site's front door, and overwriting a document nobody asked to
|
|
1115
|
+
// replace is the one thing worse than the duplicate this branch exists
|
|
1116
|
+
// to prevent. Not the chrome either: siteChrome reads the header and
|
|
1117
|
+
// footer OFF the home page, so this page is where they came from.
|
|
1118
|
+
let renamed_to;
|
|
1119
|
+
let rename_failed;
|
|
1120
|
+
if (rename) {
|
|
1121
|
+
try {
|
|
1122
|
+
await request({
|
|
1123
|
+
base: ctx.base,
|
|
1124
|
+
method: 'PATCH',
|
|
1125
|
+
path: `${path}/${encodeURIComponent(adopt.id)}`,
|
|
1126
|
+
token: siteToken(ctx),
|
|
1127
|
+
body: { name: rename },
|
|
1128
|
+
fetchImpl: ctx.fetchImpl,
|
|
1129
|
+
});
|
|
1130
|
+
renamed_to = rename;
|
|
1131
|
+
}
|
|
1132
|
+
catch (e) {
|
|
1133
|
+
// The page is still the right one to build on, so a failed rename
|
|
1134
|
+
// is reported, never raised — the caller asked for a home page and
|
|
1135
|
+
// this is it, under its old name.
|
|
1136
|
+
rename_failed = e.message.replace(/^sbuilder:\s*/, '').slice(0, 160);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
return text({
|
|
1140
|
+
into: 'the existing home page',
|
|
1141
|
+
page: { id: adopt.id, name: renamed_to ?? adopt.name },
|
|
1142
|
+
...(renamed_to ? { renamed: { from: adopt.name, to: renamed_to } } : {}),
|
|
1143
|
+
...(rename_failed ? { rename_failed } : {}),
|
|
1144
|
+
...(slug ? { slug_ignored: 'A home page is served at "/" and carries no slug.' } : {}),
|
|
1145
|
+
...(type && type !== 'page' ? { type_ignored: `Kept as it is; adopting does not retype a page to "${type}".` } : {}),
|
|
1146
|
+
note: 'Nothing was created. This site already had a home page and a site has exactly ' +
|
|
1147
|
+
'one, so creating another would have taken the star off this page and left it ' +
|
|
1148
|
+
`with no address. Open ${adopt.id} with sb_page_open and build it. To hand the ` +
|
|
1149
|
+
'home page over to a DIFFERENT page instead, create that page WITHOUT ' +
|
|
1150
|
+
'is_homepage and then PATCH isHomepage on it through sb_api_call.',
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1016
1153
|
if (dry_run !== false) {
|
|
1017
1154
|
return text({
|
|
1018
1155
|
dry_run: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.47.
|
|
3
|
+
"version": "0.47.2",
|
|
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",
|