sbuilder-mcp 0.52.0 → 0.53.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.53.0] - 2026-09-15
10
+
11
+ ### Added
12
+ - `sb_store action:"form"` now takes a `page_name` (and optional `headline`) that builds a page and places the seeded form on it in the same call, instead of leaving the caller to create a page, `sb_add` a form element, and `sb_set` its `formId` by hand.
13
+ - `sb_page_list`'s advisory for a missing login, register, forgot-password, or contact page now names the single `sb_store action:"form" template:"..." page_name:"..."` call that builds both the form and its page, instead of pointing at the form template alone.
14
+
15
+ ### Changed
16
+ - A page created for a seeded form is a second write and never undoes the first; if the page create is refused, `sb_store` still reports the form it already made, under a `page_failed` field, rather than rolling the form back.
17
+
9
18
  ## [0.52.0] - 2026-09-15
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.53.0] - 2026-09-15
10
+
11
+ ### Added
12
+ - `sb_store action:"form"` giờ nhận thêm `page_name` (và `headline` tuỳ chọn) để dựng luôn một trang và đặt form vừa tạo lên đó trong cùng một lần gọi, thay vì để người gọi phải tự tạo trang, `sb_add` phần tử form rồi `sb_set` `formId` của nó.
13
+ - Gợi ý của `sb_page_list` khi thiếu trang đăng nhập, đăng ký, quên mật khẩu hoặc liên hệ giờ nêu đích danh một lệnh gọi duy nhất `sb_store action:"form" template:"..." page_name:"..."` để dựng cả form lẫn trang chứa nó, thay vì chỉ trỏ tới mỗi template của form.
14
+
15
+ ### Changed
16
+ - Trang được tạo cho một form vừa dựng là một lần ghi riêng và không bao giờ huỷ lần ghi trước đó; nếu việc tạo trang bị từ chối, `sb_store` vẫn báo cáo form đã tạo thành công, kèm trường `page_failed`, thay vì xoá form đó đi.
17
+
9
18
  ## [0.52.0] - 2026-09-15
10
19
 
11
20
  ### Added
@@ -29,25 +29,25 @@ export const USUAL_PAGES = [
29
29
  key: 'login',
30
30
  match: ['login', 'signin', 'sign-in', 'dang-nhap', 'đăng nhập'],
31
31
  why: 'Nothing for a header, an order email or /account\'s signed-out state to link to. ' +
32
- 'Seed it with sb_store action:"form" template "login" on a page of type "page".',
32
+ 'sb_store action:"form" template:"login" page_name:"Đăng nhập" builds the form AND the page it lives on.',
33
33
  },
34
34
  {
35
35
  key: 'register',
36
36
  match: ['register', 'signup', 'sign-up', 'dang-ky', 'đăng ký'],
37
37
  why: 'A shopper cannot open an account, so order history, addresses and any member-gated ' +
38
- 'page are unreachable. Template "register".',
38
+ 'page are unreachable. sb_store action:"form" template:"register" with a page_name.',
39
39
  },
40
40
  {
41
41
  key: 'forgot',
42
42
  match: ['forgot', 'reset', 'quen-mat-khau', 'quên mật khẩu', 'doi-mat-khau'],
43
43
  why: 'A customer who forgets a password has no way back in and writes to support instead. ' +
44
- 'Template "forgot".',
44
+ 'sb_store action:"form" template:"forgot" with a page_name.',
45
45
  },
46
46
  {
47
47
  key: 'contact',
48
48
  match: ['contact', 'lien-he', 'liên hệ'],
49
49
  why: 'No address, phone or form anywhere, so a shopper with a question about an order has ' +
50
- 'nowhere to put it. Template "contact".',
50
+ 'nowhere to put it. sb_store action:"form" template:"contact" with a page_name.',
51
51
  },
52
52
  {
53
53
  key: 'about',
@@ -33,6 +33,7 @@ import { request, redact } from '../transport/http.js';
33
33
  import { siteToken } from './credentialpick.js';
34
34
  import { siteFor } from './context.js';
35
35
  import { genId } from '../domains/site/ids.js';
36
+ import { addSubtree } from '../domains/site/builder.js';
36
37
  import { chromeLinks, hasGlobal, shareChrome, sitePages } from './chrome.js';
37
38
  import { tokensFromPage } from '../domains/site/importmap.js';
38
39
  import { CHECKOUT_FORM, CHECKOUT_FORM_DOCUMENT, CHECKOUT_PAGE_DOCUMENT, CHECKOUT_TEXT, FORM_ID_SENTINEL, HEADLINE_SENTINEL, FORM_TEMPLATES, } from '../catalog/checkout.generated.js';
@@ -207,7 +208,54 @@ const FORM_TEMPLATE_KEYS = Object.keys(FORM_TEMPLATES).sort();
207
208
  * returns — and `/account` is the one page that is not a free choice, because
208
209
  * `membersOnlyRedirectTarget` sends every gated visitor there.
209
210
  */
210
- async function seedForm(ctx, siteId, key, formName, dryRun) {
211
+ /**
212
+ * The page a seeded form goes on, built and saved in the same call.
213
+ *
214
+ * WHY THIS EXISTS. seedForm made a form and stopped, and said so: "No page is
215
+ * made. Where a login form belongs is a design decision." True, and it left the
216
+ * caller three steps — create a page, sb_add a form element, sb_set its formId —
217
+ * with nothing insisting they belong together. Measured: a store built with
218
+ * these tools had no login page, no register page and no forgot page, and its
219
+ * own header had nothing to link to. The design decision was never the
220
+ * obstacle; the three steps were.
221
+ *
222
+ * A BLANK PAGE FIRST, THEN THE SUBTREE, rather than a document posted with the
223
+ * create. The checkout path can post one because it has a whole seeded document
224
+ * to post; here the document is one element, and going through the session is
225
+ * how every other builder in this server writes a page — band rules, id
226
+ * minting and the save contract all come with it instead of being re-derived.
227
+ */
228
+ async function placeFormOnPage(ctx, session, siteId, formId, pageName, headline) {
229
+ const made = (await request({
230
+ base: ctx.base,
231
+ method: 'POST',
232
+ path: `/api/sites/${encodeURIComponent(siteId)}/pages`,
233
+ token: siteToken(ctx),
234
+ body: { name: pageName, type: 'page' },
235
+ fetchImpl: ctx.fetchImpl,
236
+ }));
237
+ const id = made.page?.id;
238
+ if (typeof id !== 'string' || !id) {
239
+ throw new Error('sbuilder: the platform accepted the page create and returned no page');
240
+ }
241
+ await session.open(siteId, id);
242
+ const doc = session.current();
243
+ const { patches } = addSubtree(doc, doc.doc.root_node_id, {
244
+ type: 'flex-section',
245
+ children: [
246
+ {
247
+ type: 'flex-block',
248
+ children: [
249
+ ...(headline ? [{ type: 'heading', specials: { text: headline } }] : []),
250
+ { type: 'form', specials: { formId } },
251
+ ],
252
+ },
253
+ ],
254
+ });
255
+ await session.applyAndSave(patches);
256
+ return { id, name: typeof made.page?.name === 'string' ? made.page.name : pageName };
257
+ }
258
+ async function seedForm(ctx, session, siteId, key, formName, pageName, headline, dryRun) {
211
259
  const tpl = FORM_TEMPLATES[key];
212
260
  const site = encodeURIComponent(siteId);
213
261
  const name = formName ?? tpl.key;
@@ -236,6 +284,15 @@ async function seedForm(ctx, siteId, key, formName, dryRun) {
236
284
  path: `/api/sites/${site}/forms/{formId}/document`,
237
285
  },
238
286
  ],
287
+ ...(pageName
288
+ ? {
289
+ would_also: `create a page named ${JSON.stringify(pageName)} of type "page" and put ` +
290
+ 'the form on it, so the form has an address a header can link to',
291
+ }
292
+ : {
293
+ no_page: 'Only the form. Pass page_name to have the page made and the form placed ' +
294
+ 'on it in this same call — the three steps that otherwise get skipped.',
295
+ }),
239
296
  preview: redact({ name, type: tpl.type }),
240
297
  };
241
298
  }
@@ -268,12 +325,34 @@ async function seedForm(ctx, siteId, key, formName, dryRun) {
268
325
  await send('DELETE', `/api/sites/${site}/forms/${encodeURIComponent(form.id)}`).catch(() => undefined);
269
326
  throw e;
270
327
  }
328
+ // THE PAGE IS A SECOND WRITE AND MUST NOT UNDO THE FIRST. The form EXISTS the
329
+ // moment its three calls land; a refused page create leaves a form the caller
330
+ // can still place by hand, which is exactly what they had before this
331
+ // argument existed. Reporting the failure beats deleting a form they asked
332
+ // for — the same rule the seed follows in sb_page_create.
333
+ let page;
334
+ let page_failed;
335
+ if (pageName) {
336
+ try {
337
+ page = await placeFormOnPage(ctx, session, siteId, form.id, pageName, headline);
338
+ }
339
+ catch (e) {
340
+ page_failed = e.message.replace(/^sbuilder:\s*/, '').slice(0, 160);
341
+ }
342
+ }
271
343
  return {
272
344
  form: { id: form.id, type: form.type, name: form.name },
273
345
  fields,
274
- next: `Place it: sb_add a "form" element, then sb_set its specials.formId to "${form.id}". ` +
275
- 'The submit button lives in the FORM DOCUMENT, not on the page, and a page republish is ' +
276
- 'what makes a form-document edit visible.',
346
+ ...(page ? { page } : {}),
347
+ ...(page_failed ? { page_failed } : {}),
348
+ next: page
349
+ ? `The form is on page ${page.id}. Publish it with sb_publish, and link to it from the ` +
350
+ 'header. The submit button lives in the FORM DOCUMENT, not on the page, and a page ' +
351
+ 'republish is what makes a form-document edit visible.'
352
+ : `Place it: sb_add a "form" element, then sb_set its specials.formId to "${form.id}" — ` +
353
+ 'or pass page_name to have that page made for you. The submit button lives in the FORM ' +
354
+ 'DOCUMENT, not on the page, and a page republish is what makes a form-document edit ' +
355
+ 'visible.',
277
356
  };
278
357
  }
279
358
  export function registerStoreTools(server, ctx, session) {
@@ -350,7 +429,7 @@ export function registerStoreTools(server, ctx, session) {
350
429
  if (!template) {
351
430
  throw new Error(`sbuilder: action:"form" needs a template. One of: ${FORM_TEMPLATE_KEYS.join(', ')}.`);
352
431
  }
353
- return text(await seedForm(ctx, siteId, template, name, dry_run !== false));
432
+ return text(await seedForm(ctx, session, siteId, template, name, page_name, headline, dry_run !== false));
354
433
  }
355
434
  const lang = (language ?? 'vi');
356
435
  const t = CHECKOUT_TEXT[lang];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.52.0",
3
+ "version": "0.53.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",