sbuilder-mcp 0.82.6 → 0.82.7

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,12 @@ 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.82.7] - 2026-10-11
10
+
11
+ ### Fixed
12
+ - `sb_template_use` inserts a site's own template into the open page, instead of reporting success while the page stays empty; `dry_run` sends no request.
13
+ - `sb_page_update` accepts `default_template` and `render_for` on brand template pages.
14
+
9
15
  ## [0.82.6] - 2026-10-10
10
16
 
11
17
  ### Fixed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,12 @@ 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.82.7] - 2026-10-11
10
+
11
+ ### Fixed
12
+ - `sb_template_use` chèn template của chính site vào trang đang mở, thay vì báo thành công mà trang vẫn trống; `dry_run` không gửi request nào.
13
+ - `sb_page_update` nhận `default_template` và `render_for` trên trang template brand.
14
+
9
15
  ## [0.82.6] - 2026-10-10
10
16
 
11
17
  ### Fixed
package/README.md CHANGED
@@ -95,7 +95,7 @@ in this client: one would put the very key the platform exists to hold back into
95
95
  | `sb_remove` | Remove a node and its subtree |
96
96
  | `sb_duplicate` | Copy a node and its subtree under fresh ids, right after the original — or onto another page or site, as the editor's copy/paste does (assets copied, references that cannot travel listed) |
97
97
  | `sb_templates` | The store's saved section templates, plus the BUILT-IN layouts — hero, feature trio, stats, FAQ, CTA band, product shelf, category strip, brand wall, trust band — composed against the page's own tokens |
98
- | `sb_template_use` | Instantiate a template into a page |
98
+ | `sb_template_use` | Add a section template to a page |
99
99
  | `sb_page_list` | Every page on the site |
100
100
  | `sb_page_create` | Create a page — a store type arrives with the editor's own starting document; `type` is the route for checkout, product, category, post, course |
101
101
  | `sb_page_update` | Edit a page's name, slug, home flag, order, SEO (title, description, Open Graph, robots, meta tags, JSON-LD) and members-only flag — merged into `settings`, so the keys it does not name survive. On a template page: make it the type's default, or choose which products/categories/posts render through it |
@@ -1024,6 +1024,20 @@ export class PageSession {
1024
1024
  this.savedRev = d.rev;
1025
1025
  }
1026
1026
  }
1027
+ /**
1028
+ * A foreign subtree as a document `copyNodeInto` can read: a holder root over
1029
+ * it, so the copy re-mints every id and strips the shared-master stamps.
1030
+ */
1031
+ function heldTree(rootId, nodes) {
1032
+ const holder = '__held';
1033
+ const rn = structuredClone(nodes);
1034
+ rn[rootId].data.parent = holder;
1035
+ return PageDoc.from({
1036
+ schema_version: 2,
1037
+ root_node_id: holder,
1038
+ nodes: { ...rn, [holder]: { id: holder, data: { type: 'root', parent: null, nodes: [rootId] } } },
1039
+ });
1040
+ }
1027
1041
  function formDocPath(siteId, formId) {
1028
1042
  return `/api/sites/${encodeURIComponent(siteId)}/forms/${encodeURIComponent(formId)}/document`;
1029
1043
  }
@@ -1902,15 +1916,7 @@ export function registerPageTools(server, ctx) {
1902
1916
  if (!r?.tree?.nodes || !r.tree.rootId || !(r.tree.rootId in r.tree.nodes)) {
1903
1917
  throw new Error('sbuilder: the platform answered the reconcile with no tree — nothing was pasted.');
1904
1918
  }
1905
- // A holder root so the copy can read the reconciled subtree as a document.
1906
- const holder = '__clipboard';
1907
- const rn = structuredClone(r.tree.nodes);
1908
- rn[r.tree.rootId].data.parent = holder;
1909
- src = PageDoc.from({
1910
- schema_version: 2,
1911
- root_node_id: holder,
1912
- nodes: { ...rn, [holder]: { id: holder, data: { type: 'root', parent: null, nodes: [r.tree.rootId] } } },
1913
- });
1919
+ src = heldTree(r.tree.rootId, r.tree.nodes);
1914
1920
  if (r.tree.rootId !== id)
1915
1921
  id = r.tree.rootId;
1916
1922
  reconciled = { copiedAssets: r.copiedAssets ?? 0, droppedRefs: r.droppedRefs ?? [] };
@@ -1950,7 +1956,7 @@ export function registerPageTools(server, ctx) {
1950
1956
  });
1951
1957
  });
1952
1958
  server.registerTool('sb_template_use', {
1953
- description: 'Instantiate a section template into a page — the site\'s own (the server copies it) or ' +
1959
+ description: 'Add a section template to a page, above the footer — a sb_templates row (another site\'s is copied in first) or ' +
1954
1960
  'one of the BUILT-IN layouts sb_templates lists, which are composed against this ' +
1955
1961
  "page's own tokens rather than copied.",
1956
1962
  inputSchema: {
@@ -1962,9 +1968,9 @@ export function registerPageTools(server, ctx) {
1962
1968
  annotations: { readOnlyHint: false, destructiveHint: false },
1963
1969
  }, async ({ site_id: given, template_id, page_id, dry_run }) => {
1964
1970
  const site_id = siteFor(ctx, given);
1965
- // A BUILT-IN IS NOT A SERVER COPY. The platform's own templates are
1966
- // instantiated by the platform, which is why the section arrives exactly
1967
- // as designed; a built-in has no row on the server, so it is composed HERE
1971
+ // A BUILT-IN IS NOT A SERVER COPY. A server row is a stored tree
1972
+ // (copied in by instantiate when another site owns it), which is why the
1973
+ // section arrives exactly as designed; a built-in has no row on the server, so it is composed HERE
1968
1974
  // — against the target page's own tokens, which is the whole point of it
1969
1975
  // being a pattern rather than a snapshot.
1970
1976
  const pattern = PATTERN_BY_ID.get(template_id);
@@ -2068,23 +2074,68 @@ export function registerPageTools(server, ctx) {
2068
2074
  : {}),
2069
2075
  });
2070
2076
  }
2071
- const path = `/api/sites/${encodeURIComponent(site_id)}/section-templates/${encodeURIComponent(template_id)}/instantiate`;
2072
- if (dry_run !== false) {
2073
- return text({ dry_run: true, would_post: path, body: { pageId: page_id } });
2074
- }
2075
- const out = await request({
2077
+ // A SERVER-SIDE TEMPLATE IS A TREE THIS SERVER INSERTS, as the editor does
2078
+ // (TemplateCards.vue). The site's own is placed straight from the list's
2079
+ // `document`; anyone else's (org, platform) goes through instantiate, which
2080
+ // reads NO body, touches no page, copies the template's images into this
2081
+ // site and answers `{ document, droppedRefs }`
2082
+ // (sectiontemplates/rest/rest.go). Either way the ids are minted fresh here,
2083
+ // as the editor's `cloneTree` does.
2084
+ const tplBase = `/api/sites/${encodeURIComponent(site_id)}/section-templates`;
2085
+ const listed = (await request({
2076
2086
  base: ctx.base,
2077
- method: 'POST',
2078
- path,
2087
+ method: 'GET',
2088
+ path: tplBase,
2079
2089
  token: siteToken(ctx),
2080
- body: { pageId: page_id },
2081
2090
  fetchImpl: ctx.fetchImpl,
2082
- });
2091
+ }));
2092
+ const row = (listed.sectionTemplates ?? []).find((t) => t.id === template_id);
2093
+ if (!row) {
2094
+ throw new Error(`sbuilder: no template "${template_id}" on this site's shelf or among the built-ins — list them with sb_templates.`);
2095
+ }
2096
+ const own = row.source === 'site';
2097
+ const path = `${tplBase}/${encodeURIComponent(template_id)}/instantiate`;
2098
+ const insert = (tree, target) => {
2099
+ if (!tree?.nodes || !tree.root_node_id || !(tree.root_node_id in tree.nodes))
2100
+ return null;
2101
+ return copyNodeInto(heldTree(tree.root_node_id, tree.nodes), tree.root_node_id, target, target.doc.root_node_id, middleEnd(target.doc));
2102
+ };
2103
+ const doc = dry_run !== false
2104
+ ? PageDoc.from((await loadSource(ctx, site_id, page_id)).document)
2105
+ : (await session.open(site_id, page_id), session.current());
2106
+ // Checked on the listed document BEFORE instantiate: that call copies media,
2107
+ // and a refusal after it would leave orphaned images in the library.
2108
+ const planned = insert(row.document, doc);
2109
+ const refuse = planned ? saveRefusal(doc, planned.patches).refusal : undefined;
2110
+ if (dry_run !== false) {
2111
+ return text({
2112
+ dry_run: true,
2113
+ ...(refuse ? { would_refuse: refuse } : {}),
2114
+ would_add: row.name ?? template_id,
2115
+ ...(planned ? { nodes: planned.ids.length } : {}),
2116
+ into: page_id,
2117
+ ...(own ? {} : { would_post: path, note: "instantiate copies the template's images into this site first" }),
2118
+ });
2119
+ }
2120
+ if (refuse)
2121
+ throw new Error(`sbuilder: ${refuse}`);
2122
+ let placed = planned;
2123
+ let dropped = [];
2124
+ if (!own) {
2125
+ const out = (await request({ base: ctx.base, method: 'POST', path, token: siteToken(ctx), fetchImpl: ctx.fetchImpl }));
2126
+ placed = insert(out.document, doc);
2127
+ dropped = out.droppedRefs ?? [];
2128
+ }
2129
+ if (!placed)
2130
+ throw new Error(`sbuilder: template "${template_id}" carries no usable document — nothing was added.`);
2131
+ await session.applyAndSave(placed.patches);
2083
2132
  return text({
2084
- instantiated: template_id,
2133
+ added: row.name ?? template_id,
2134
+ section: placed.ids[0],
2135
+ nodes: placed.ids.length,
2085
2136
  into: page_id,
2086
- result: out,
2087
- note: 'Re-open the page with sb_page_open — this session still holds the old tree.',
2137
+ rev: doc.rev,
2138
+ ...(dropped.length ? { dropped_refs: dropped } : {}),
2088
2139
  });
2089
2140
  });
2090
2141
  server.registerTool('sb_page_list', {
@@ -39,13 +39,14 @@ const LINK_TYPE_FOR_PAGE_TYPE = {
39
39
  post: 'article',
40
40
  blog: 'blogCategory',
41
41
  course: 'course',
42
+ brand: 'brand',
42
43
  };
43
44
  const PUBLISH_NOTICE = 'Page settings are published verbatim: the storefront shows this change after sb_publish.';
44
45
  export function registerPageUpdateTools(server, ctx) {
45
46
  server.registerTool('sb_page_update', {
46
47
  description: "Edit a page's name, slug, home flag, order, SEO (merged key by key) or members_only. " +
47
48
  'Reads the page first and merges `settings`, so keys it does not name survive. On a ' +
48
- 'template page (product, category, post, blog, course): default_template makes it the ' +
49
+ 'template page (product, category, post, blog, course, brand): default_template makes it the ' +
49
50
  'type\'s default, render_for/render_default move entities onto it or back to the default.',
50
51
  inputSchema: {
51
52
  site_id: z.string().optional(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.82.6",
3
+ "version": "0.82.7",
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",