sbuilder-mcp 0.22.1 → 0.23.1

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,23 @@ 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.23.1] - 2026-09-10
10
+
11
+ ### Added
12
+ - sb_media_upload now asks the platform to fetch a URL source itself (`POST /api/media/{siteId}/from-url`) before falling back to downloading and re-uploading the bytes, so the file makes one network hop instead of two and its content type comes from the origin's own answer instead of being guessed from a header or extension.
13
+
14
+ ### Changed
15
+ - sb_media_upload now raises rather than working around it when the platform refuses a URL source as `remote_blocked` (an address outside the public internet, such as a private range or the cloud metadata endpoint), since fetching that same URL from this server's own machine would defeat the platform's guard rather than respect it.
16
+
17
+ ### Fixed
18
+ - sb_media_upload now reports the platform's own reason when it cannot reach a URL source (for example, "www.blender.org answered 404") instead of the generic "the platform could not read it".
19
+
20
+ ## [0.23.0] - 2026-09-10
21
+
22
+ ### Added
23
+ - sb_templates now lists six built-in layout patterns (two hero variants, a feature trio, a stats row, a CTA band, and an FAQ) under `built_in`, alongside the site's own saved section templates, so a page can start from a designed composition instead of an invented one.
24
+ - sb_template_use can now instantiate a built-in layout by id: the section is composed against the target page's own tokens (heading color, button fill, section padding) instead of being copied verbatim, and falls back to the site's theme tokens, carried as `var(--wb-color-…)`, when the page has no look of its own yet.
25
+
9
26
  ## [0.22.1] - 2026-09-10
10
27
 
11
28
  ### Fixed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,23 @@ 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.23.1] - 2026-09-10
10
+
11
+ ### Added
12
+ - sb_media_upload giờ yêu cầu chính nền tảng tải nguồn URL trước (`POST /api/media/{siteId}/from-url`) rồi mới lùi về cách cũ là tải xuống rồi upload lại, nên file chỉ đi một chặng mạng thay vì hai, và content type được lấy từ câu trả lời của chính nguồn thay vì đoán từ header hay phần mở rộng.
13
+
14
+ ### Changed
15
+ - sb_media_upload giờ báo lỗi thay vì tìm cách lách khi nền tảng từ chối một nguồn URL với mã `remote_blocked` (địa chỉ không thuộc internet công khai, như dải private hoặc endpoint metadata của cloud), vì việc tự tải URL đó từ máy chạy server này sẽ phá vỡ lớp bảo vệ của nền tảng thay vì tuân theo nó.
16
+
17
+ ### Fixed
18
+ - sb_media_upload giờ báo đúng lý do của chính nền tảng khi không tải được một nguồn URL (ví dụ "www.blender.org answered 404") thay vì thông báo chung "the platform could not read it".
19
+
20
+ ## [0.23.0] - 2026-09-10
21
+
22
+ ### Added
23
+ - sb_templates giờ liệt kê sáu layout dựng sẵn (hai kiểu hero, ba lợi ích, dải số liệu, dải kêu gọi hành động và FAQ) trong `built_in`, cùng với các section template đã lưu của chính site, để một trang có thể bắt đầu từ một bố cục được thiết kế thay vì tự bịa ra.
24
+ - sb_template_use giờ có thể thả một layout dựng sẵn theo id: section được soạn theo token của chính trang đích (màu chữ tiêu đề, nền nút, padding section) thay vì được copy nguyên văn, và lấy theo token theme của site, mang dạng `var(--wb-color-…)`, khi trang chưa có phong cách riêng.
25
+
9
26
  ## [0.22.1] - 2026-09-10
10
27
 
11
28
  ### Fixed
package/README.md CHANGED
@@ -86,7 +86,7 @@ make, because those mean "this person's account".
86
86
  | `sb_move` | Move a node to another parent |
87
87
  | `sb_remove` | Remove a node and its subtree |
88
88
  | `sb_duplicate` | Copy a node and its subtree under fresh ids, right after the original |
89
- | `sb_templates` | The store's saved section templates — designed sections to start from |
89
+ | `sb_templates` | The store's saved section templates, plus the BUILT-IN layouts — hero, feature trio, stats, FAQ, CTA band — composed against the page's own tokens |
90
90
  | `sb_template_use` | Instantiate a template into a page |
91
91
  | `sb_page_list` | Every page on the site |
92
92
  | `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 |
package/README.vi.md CHANGED
@@ -83,7 +83,7 @@ là "tài khoản của người này".
83
83
  | `sb_move` | Chuyển node sang cha khác |
84
84
  | `sb_remove` | Xoá node và cả cây con |
85
85
  | `sb_duplicate` | Nhân bản một node và cả cây con dưới id mới, ngay sau bản gốc |
86
- | `sb_templates` | Section template đã lưu của cửa hàng — section thiết kế sẵn để bắt đầu |
86
+ | `sb_templates` | Section template đã lưu của cửa hàng, cộng bộ layout DỰNG SẴN — hero, ba lợi ích, số liệu, FAQ, dải kêu gọi — soạn theo token của chính trang |
87
87
  | `sb_template_use` | Thả một template vào trang |
88
88
  | `sb_page_list` | Mọi trang của site |
89
89
  | `sb_page_create` | Tạo một trang — trang cửa hàng sinh ra đã có sẵn tài liệu như trong editor; `type` là đường đi cho checkout, product, category, post, course |
@@ -294,6 +294,23 @@ function one(c, t) {
294
294
  const kids = (c.children ?? []).map((k) => one(k, t)).filter((n) => n !== null);
295
295
  if (kids.length === 0)
296
296
  return null;
297
+ // A COLUMN IS A STACK, NOT A ROW, and this was read by nothing.
298
+ //
299
+ // The capture never emits one — a column is what a page already is, so it
300
+ // is flattened in the browser — and `direction` sat in `Captured` unread
301
+ // for exactly that reason. The moment a caller composes a tree by hand
302
+ // (a layout pattern, a design ported out of Figma or Stitch) that stops
303
+ // being true, and every column it asked for came back as a row: a hero's
304
+ // heading, its sentence and its button side by side instead of stacked.
305
+ if (c.direction === 'column') {
306
+ if (kids.length === 1)
307
+ return kids[0];
308
+ return {
309
+ type: 'flex-block',
310
+ style: { width: '100%', display: 'flex', flexDirection: 'column', gap: '12px' },
311
+ children: kids,
312
+ };
313
+ }
297
314
  if (kids.length === 1)
298
315
  return kids[0];
299
316
  // A ROW OF TWO OR MORE COLUMNS NEEDS AN EXPLICIT STACK BREAKPOINT, and
@@ -0,0 +1,130 @@
1
+ import { toSpecs } from './importmap.js';
2
+ /**
3
+ * THE LOOK A PAGE HAS BEFORE IT HAS ANYTHING, and it is not nothing.
4
+ *
5
+ * Rule 0 says read the page's own pattern. A blank page has none — and the next
6
+ * authority is not invention, it is the SITE'S THEME, which is where every
7
+ * element's style preset resolves from anyway.
8
+ *
9
+ * Written as `var(--wb-color-…)` rather than as the hex those resolve to, which
10
+ * is the whole point: a literal on a node OUTRANKS the preset beneath it
11
+ * permanently, so a pattern that baked today's `#171717` in would stop following
12
+ * the theme the moment the merchant changed it — the exact detachment this repo
13
+ * already records for imported icons. The variable is what the theme's own
14
+ * scheme roles use.
15
+ */
16
+ export const THEME_TOKENS = {
17
+ headingColor: 'var(--wb-color-heading)',
18
+ textColor: 'var(--wb-color-text)',
19
+ buttonBg: 'var(--wb-color-primary)',
20
+ buttonColor: '#ffffff',
21
+ };
22
+ /** One section, through the same mapper an import goes through. */
23
+ function section(children, t, style, innerStyle) {
24
+ const [spec] = toSpecs([{ kind: 'section', children }], t);
25
+ if (!spec)
26
+ return null;
27
+ const outer = style ? { ...spec, style: { ...spec.style, ...style } } : spec;
28
+ if (!innerStyle)
29
+ return outer;
30
+ // THE SECTION CENTRES ITS BLOCK; THE BLOCK CENTRES ITS CHILDREN. `textAlign`
31
+ // moves the words and leaves a `width: fit-content` button where it was, so a
32
+ // centred band came out with its call to action against the left margin.
33
+ const [inner, ...rest] = outer.children ?? [];
34
+ if (!inner)
35
+ return outer;
36
+ return {
37
+ ...outer,
38
+ children: [{ ...inner, style: { ...inner.style, ...innerStyle } }, ...rest],
39
+ };
40
+ }
41
+ const row = (children, wrap = false) => ({
42
+ kind: 'group',
43
+ direction: 'row',
44
+ wrap,
45
+ children,
46
+ });
47
+ const h = (text, level = 2) => ({ kind: 'heading', level, text });
48
+ const p = (text) => ({ kind: 'text', text });
49
+ const cta = (text, href = '#') => ({ kind: 'button', variant: 'cta', text, href });
50
+ const img = (alt) => ({
51
+ // A PLACEHOLDER WITH NO SRC IS DROPPED by the mapper, which is right — an
52
+ // empty frame is not content. So a pattern that wants a picture says so in
53
+ // words and the caller points sb_set at it, rather than the page shipping a
54
+ // grey box nobody chose.
55
+ kind: 'image',
56
+ src: '',
57
+ alt,
58
+ });
59
+ /**
60
+ * The set. Deliberately small and deliberately ordinary: these are the bands
61
+ * every commercial page is built from, and a library that tried to be clever
62
+ * would be a library nobody could predict.
63
+ */
64
+ export const LAYOUT_PATTERNS = [
65
+ {
66
+ id: 'sb_hero_split',
67
+ name: 'Hero — hai cột',
68
+ use: 'Mở đầu trang: tiêu đề, một câu, nút hành động bên trái; chỗ cho ảnh bên phải',
69
+ build: (t) => section([
70
+ row([
71
+ { kind: 'group', direction: 'column', children: [h('Tiêu đề chính', 1), p('Một câu nói rõ bạn bán gì và cho ai.'), cta('Mua ngay')] },
72
+ { kind: 'group', direction: 'column', children: [p('Đặt ảnh vào đây — sb_add một image, hoặc sb_set src lên khối này.')] },
73
+ ]),
74
+ ], t),
75
+ },
76
+ {
77
+ id: 'sb_hero_centered',
78
+ name: 'Hero — canh giữa',
79
+ use: 'Mở đầu trang khi chưa có ảnh: tiêu đề lớn, một câu, một nút',
80
+ build: (t) => section([h('Tiêu đề chính', 1), p('Một câu nói rõ bạn bán gì và cho ai.'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
81
+ },
82
+ {
83
+ id: 'sb_feature_trio',
84
+ name: 'Ba lợi ích',
85
+ use: 'Ba cột ngang nhau: mỗi cột một tiêu đề nhỏ và một đoạn — lý do nên mua',
86
+ build: (t) => section([
87
+ h('Vì sao chọn chúng tôi'),
88
+ row([
89
+ { kind: 'group', direction: 'column', children: [h('Lợi ích một', 3), p('Một hoặc hai câu.')] },
90
+ { kind: 'group', direction: 'column', children: [h('Lợi ích hai', 3), p('Một hoặc hai câu.')] },
91
+ { kind: 'group', direction: 'column', children: [h('Lợi ích ba', 3), p('Một hoặc hai câu.')] },
92
+ ]),
93
+ ], t),
94
+ },
95
+ {
96
+ id: 'sb_stats_row',
97
+ name: 'Dải số liệu',
98
+ use: 'Ba đến bốn con số lớn với nhãn bên dưới — bằng chứng, không phải lời hứa',
99
+ build: (t) => section([
100
+ row([
101
+ { kind: 'group', direction: 'column', children: [h('1.000+', 2), p('Khách hàng')] },
102
+ { kind: 'group', direction: 'column', children: [h('4,9/5', 2), p('Đánh giá trung bình')] },
103
+ { kind: 'group', direction: 'column', children: [h('24h', 2), p('Giao trong nội thành')] },
104
+ ]),
105
+ ], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
106
+ },
107
+ {
108
+ id: 'sb_cta_band',
109
+ name: 'Dải kêu gọi hành động',
110
+ use: 'Một câu và một nút, đặt cuối trang hoặc giữa hai band nội dung',
111
+ build: (t) => section([h('Sẵn sàng bắt đầu?'), p('Một câu nhắc lại lời hứa chính.'), cta('Mua ngay')], t, { alignItems: 'center', textAlign: 'center' }, { alignItems: 'center' }),
112
+ },
113
+ {
114
+ id: 'sb_faq',
115
+ name: 'Câu hỏi thường gặp',
116
+ use: 'Ba câu hỏi mở/đóng — nơi trả lời những gì cản trở việc mua',
117
+ build: (t) => section([
118
+ h('Câu hỏi thường gặp'),
119
+ {
120
+ kind: 'accordion',
121
+ children: [
122
+ { kind: 'accordion-item', text: 'Giao hàng mất bao lâu?', children: [p('Trả lời ngắn gọn.')] },
123
+ { kind: 'accordion-item', text: 'Đổi trả thế nào?', children: [p('Trả lời ngắn gọn.')] },
124
+ { kind: 'accordion-item', text: 'Thanh toán ra sao?', children: [p('Trả lời ngắn gọn.')] },
125
+ ],
126
+ },
127
+ ], t),
128
+ },
129
+ ];
130
+ export const PATTERN_BY_ID = new Map(LAYOUT_PATTERNS.map((p2) => [p2.id, p2]));
@@ -20,6 +20,9 @@ import { readinessGaps, READINESS_NOTICE } from '../domains/site/readiness.js';
20
20
  import { gatherReadiness } from '../domains/site/readiness-fetch.js';
21
21
  import { globalWarning, restampPatches, RESPONSIVE_NOTICE } from '../domains/site/traps.js';
22
22
  import { catalogMatches, traitsFor } from '../catalog/element-search.js';
23
+ import { LAYOUT_PATTERNS, PATTERN_BY_ID, THEME_TOKENS } from '../domains/site/patterns.js';
24
+ import { tokensFromPage } from '../domains/site/importmap.js';
25
+ import { middleEnd } from '../domains/site/traps.js';
23
26
  import { applyPatches } from '../core/patch.js';
24
27
  import { stickyWarning } from '../domains/site/sticky.js';
25
28
  import { HOVER_STATE, PARENT_HOVER_STATE, hoverHostNote, hoverRoutingNote } from '../domains/site/hover.js';
@@ -654,16 +657,27 @@ export function registerPageTools(server, ctx) {
654
657
  'than assembling one. Use sb_template_use to drop one into the open page.',
655
658
  inputSchema: { site_id: z.string().optional() },
656
659
  annotations: { readOnlyHint: true },
657
- }, async ({ site_id: given }) => text(projectList(await request({
658
- base: ctx.base,
659
- method: 'GET',
660
- path: `/api/sites/${encodeURIComponent(siteFor(ctx, given))}/section-templates`,
661
- token: siteToken(ctx),
662
- fetchImpl: ctx.fetchImpl,
663
- }), 'sectionTemplates', TEMPLATE_FIELDS)));
660
+ }, async ({ site_id: given }) => {
661
+ const listed = projectList(await request({
662
+ base: ctx.base,
663
+ method: 'GET',
664
+ path: `/api/sites/${encodeURIComponent(siteFor(ctx, given))}/section-templates`,
665
+ token: siteToken(ctx),
666
+ fetchImpl: ctx.fetchImpl,
667
+ }), 'sectionTemplates', TEMPLATE_FIELDS);
668
+ // THE SITE'S OWN FIRST, ALWAYS. A template a merchant designed is this
669
+ // site's answer; these are defaults for a page that has none. Measured on
670
+ // a live site, the platform's library held TWO — which is why an agent
671
+ // asked for "a hero" was inventing one from flex-blocks every time.
672
+ return text({
673
+ ...listed,
674
+ built_in: LAYOUT_PATTERNS.map((p) => ({ id: p.id, name: p.name, use: p.use })),
675
+ });
676
+ });
664
677
  server.registerTool('sb_template_use', {
665
- description: 'Instantiate a saved section template into a page. The server does the copy, so the ' +
666
- 'section arrives exactly as it was designed — then re-open the page to see it.',
678
+ description: 'Instantiate a section template into a page — the site\'s own (the server copies it) or ' +
679
+ 'one of the BUILT-IN layouts sb_templates lists, which are composed against this ' +
680
+ "page's own tokens rather than copied.",
667
681
  inputSchema: {
668
682
  site_id: z.string().optional(),
669
683
  template_id: z.string(),
@@ -673,6 +687,54 @@ export function registerPageTools(server, ctx) {
673
687
  annotations: { readOnlyHint: false, destructiveHint: false },
674
688
  }, async ({ site_id: given, template_id, page_id, dry_run }) => {
675
689
  const site_id = siteFor(ctx, given);
690
+ // A BUILT-IN IS NOT A SERVER COPY. The platform's own templates are
691
+ // instantiated by the platform, which is why the section arrives exactly
692
+ // as designed; a built-in has no row on the server, so it is composed HERE
693
+ // — against the target page's own tokens, which is the whole point of it
694
+ // being a pattern rather than a snapshot.
695
+ const pattern = PATTERN_BY_ID.get(template_id);
696
+ if (pattern) {
697
+ await session.open(site_id, page_id);
698
+ const doc = session.current();
699
+ const read = tokensFromPage(doc.doc);
700
+ // A BLANK PAGE HAS NO PATTERN TO READ, and the next authority is the
701
+ // site's THEME rather than nothing — every element's style preset
702
+ // resolves from it anyway. Carried as `var(--wb-color-…)`, so the band
703
+ // goes on following the theme instead of freezing today's hex into it.
704
+ const fromTheme = Object.keys(read).length === 0;
705
+ const tokens = fromTheme ? THEME_TOKENS : read;
706
+ const spec = pattern.build(tokens);
707
+ if (!spec) {
708
+ throw new Error(`sbuilder: the built-in "${template_id}" produced nothing to add.`);
709
+ }
710
+ const { patches, ids } = addSubtree(doc, doc.doc.root_node_id, spec, middleEnd(doc.doc));
711
+ if (dry_run !== false) {
712
+ return text({
713
+ dry_run: true,
714
+ would_add: pattern.name,
715
+ nodes: ids.length,
716
+ into: page_id,
717
+ tokens_from: fromTheme ? "this site's theme — the page has no look of its own yet" : 'this page',
718
+ note: 'Composed against THIS page\'s tokens, not copied — the same heading ink, button ' +
719
+ 'fill and section padding the page already uses. Pass dry_run:false to add it.',
720
+ });
721
+ }
722
+ await session.applyAndSave(patches);
723
+ return text({
724
+ added: pattern.name,
725
+ section: ids[0],
726
+ nodes: ids.length,
727
+ into: page_id,
728
+ rev: doc.rev,
729
+ ...(fromTheme
730
+ ? {
731
+ tokens_from: "this site's theme — the page had no heading, button or section to read a look " +
732
+ 'off, so the band carries var(--wb-color-…) and follows the theme rather than a ' +
733
+ 'frozen colour. Style this first band and every pattern after it reads THAT.',
734
+ }
735
+ : {}),
736
+ });
737
+ }
676
738
  const path = `/api/sites/${encodeURIComponent(site_id)}/section-templates/${encodeURIComponent(template_id)}/instantiate`;
677
739
  if (dry_run !== false) {
678
740
  return text({ dry_run: true, would_post: path, body: { pageId: page_id } });
@@ -41,8 +41,87 @@ function typeForName(name) {
41
41
  const dot = name.lastIndexOf('.');
42
42
  return dot < 0 ? '' : (TYPE_BY_EXT[name.slice(dot).toLowerCase()] ?? '');
43
43
  }
44
+ /**
45
+ * Ask the platform to fetch the URL itself.
46
+ *
47
+ * Returns the asset when the server took it, or null when the ROUTE is not
48
+ * there — an older deployment — which is the one case the caller may answer by
49
+ * downloading the bytes and posting them the old way.
50
+ *
51
+ * A REFUSAL IS TERMINAL, and that is a security rule rather than tidiness. The
52
+ * server refuses an address that is not on the public internet (`remote_blocked`
53
+ * — loopback, private ranges, the cloud metadata endpoint), and a client that
54
+ * answered by fetching that same URL from its OWN machine and uploading the
55
+ * bytes would walk straight around the guard. The agent's network is not the
56
+ * server's, but "the caller does it instead" is exactly the bypass the check
57
+ * exists to prevent, so the error is raised rather than worked around.
58
+ */
59
+ async function fromUrl(ctx, siteId, url, source) {
60
+ const doFetch = ctx.fetchImpl ?? fetch;
61
+ const res = await doFetch(`${ctx.base.replace(/\/$/, '')}/api/media/${encodeURIComponent(siteId)}/from-url`, {
62
+ method: 'POST',
63
+ headers: {
64
+ Authorization: `Bearer ${siteToken(ctx)}`,
65
+ 'Content-Type': 'application/json',
66
+ Accept: 'application/json',
67
+ ...identityHeaders(),
68
+ },
69
+ body: JSON.stringify({
70
+ url,
71
+ ...(source.name ? { name: source.name } : {}),
72
+ ...(source.folderId ? { folderId: source.folderId } : {}),
73
+ }),
74
+ });
75
+ // 404/405 is "this build has no such route". Anything else is an answer.
76
+ if (res.status === 404 || res.status === 405)
77
+ return null;
78
+ const raw = await res.text();
79
+ let parsed = {};
80
+ try {
81
+ parsed = raw ? JSON.parse(raw) : {};
82
+ }
83
+ catch {
84
+ // A build that routes this path to something else entirely. Fall back rather
85
+ // than reporting a parse error the caller cannot act on.
86
+ return null;
87
+ }
88
+ if (!res.ok) {
89
+ const env = (parsed ?? {});
90
+ if (env.code === 'remote_blocked') {
91
+ throw new ApiError(res.status, 'remote_blocked', `sbuilder: the platform refuses to fetch ${url} — it is not an address on the public ` +
92
+ 'internet. This server will not fetch it on the platform\'s behalf either: that would ' +
93
+ 'walk around the check rather than satisfy it. Give a public URL, or upload the file ' +
94
+ 'with a local path.');
95
+ }
96
+ if (env.code === 'remote_unreachable') {
97
+ // THE PLATFORM'S OWN REASON, not a sentence of ours over the top of it. It
98
+ // says which host answered what — a 404 is a wrong URL, a 403 is an origin
99
+ // refusing this fetcher, a timeout is worth retrying — and replacing that
100
+ // with "could not read it" sends all of them to check their network.
101
+ throw new ApiError(res.status, 'remote_unreachable', `sbuilder: the platform could not fetch ${url} — ${env.error ?? 'no reason given'}.`);
102
+ }
103
+ // Any OTHER refusal — 401, 403, 413, an unsupported type — is one the older
104
+ // path answers with its own, better-worded diagnosis. Let it try.
105
+ return null;
106
+ }
107
+ const body = (parsed ?? {});
108
+ return body.asset ?? parsed;
109
+ }
44
110
  export async function uploadMedia(ctx, siteId, source) {
45
111
  const doFetch = ctx.fetchImpl ?? fetch;
112
+ // THE SERVER'S OWN DOOR FIRST, when the source is a URL.
113
+ //
114
+ // `POST /api/media/{siteId}/from-url` fetches it where the platform already
115
+ // guards outbound requests, so the bytes make ONE hop instead of two and the
116
+ // content type is decided by the origin's own answer rather than reconstructed
117
+ // here. Everything below this is the older path, kept because a deployment
118
+ // without that route must still be able to upload — the same shape the partner
119
+ // -surface retry below has, and for the same reason.
120
+ if (source.url) {
121
+ const viaServer = await fromUrl(ctx, siteId, source.url, source);
122
+ if (viaServer)
123
+ return viaServer;
124
+ }
46
125
  let bytes;
47
126
  let filename;
48
127
  let declared = '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.22.1",
3
+ "version": "0.23.1",
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",