sbuilder-mcp 0.60.4 → 0.61.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,24 @@ 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.61.1] - 2026-09-17
10
+
11
+ ### Added
12
+ - `sb_connect` now reports how old the generated catalog's platform knowledge is, in words (e.g. "45 days old"), along with the web_builder commit it was generated from, so a stale install reads as staleness instead of as a missing element or key.
13
+ - `sb_set`'s precondition warning now also covers `filterMatch: "all"` on filter elements, which only takes effect where a product can hold several of a source's values.
14
+
15
+ ### Fixed
16
+ - `sb_set`'s precondition warning no longer reports a false positive when a sibling `specials` key is simply unset, since a `null` clause now correctly means "not set" instead of being compared as a literal value.
17
+
18
+ ## [0.61.0] - 2026-09-16
19
+
20
+ ### Added
21
+ - `sb_set` now warns when a write on a `specials` key is legal on its own but does nothing because a sibling key it depends on is missing or set to the wrong value, naming both keys, what the node renders as meanwhile, and which value would fix it, read from the platform's own precondition declarations rather than a list kept in this repo.
22
+ - `sb_set` now separately warns when a `specials` key is not honored by the target element's renderer at all, naming the element types that do honor it, so the write is not mistaken for a fixable missing-neighbour case.
23
+
24
+ ### Internal
25
+ - The generated catalog is refreshed against a current platform checkout (a new tool-results API route); operation count moves from 530 to 531 (179 of 238 writes now carrying a body shape read off the handler).
26
+
9
27
  ## [0.60.4] - 2026-09-16
10
28
 
11
29
  ### Internal
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,24 @@ 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.61.1] - 2026-09-17
10
+
11
+ ### Added
12
+ - `sb_connect` giờ báo cáo kiến thức nền tảng của catalog được sinh ra đã cũ đến mức nào, bằng lời (ví dụ "45 days old"), cùng với commit web_builder mà nó được sinh ra từ đó, để một bản cài đặt lỗi thời được đọc đúng là lỗi thời thay vì bị hiểu nhầm là thiếu element hay key.
13
+ - Cảnh báo tiền điều kiện của `sb_set` giờ bao phủ thêm `filterMatch: "all"` trên các element bộ lọc, giá trị này chỉ có tác dụng ở nơi một sản phẩm có thể mang nhiều giá trị của một nguồn.
14
+
15
+ ### Fixed
16
+ - Cảnh báo tiền điều kiện của `sb_set` không còn báo sai (false positive) khi một key `specials` liền kề đơn giản là chưa được thiết lập, vì một clause `null` giờ được hiểu đúng là "chưa thiết lập" thay vì bị so sánh như một giá trị literal.
17
+
18
+ ## [0.61.0] - 2026-09-16
19
+
20
+ ### Added
21
+ - `sb_set` giờ cảnh báo khi một lệnh ghi lên key `specials` tự nó hợp lệ nhưng không làm gì cả vì một key liền kề mà nó phụ thuộc vào đang thiếu hoặc sai giá trị — nêu rõ cả hai key, node sẽ render ra sao trong lúc đó, và giá trị nào sẽ khắc phục, được đọc từ chính khai báo tiền điều kiện của nền tảng thay vì một danh sách tự giữ trong repo này.
22
+ - `sb_set` giờ cảnh báo riêng khi một key `specials` hoàn toàn không được renderer của element đích đọc tới, nêu rõ những element type nào thực sự đọc key đó, để tránh bị nhầm thành trường hợp thiếu key liền kề có thể sửa được.
23
+
24
+ ### Internal
25
+ - Catalog được sinh ra giờ được làm mới dựa trên một bản checkout hiện tại của nền tảng (route API tool-results mới); số operation tăng từ 530 lên 531 (179 trong 238 lệnh ghi giờ mang hình dạng body đọc thẳng từ handler).
26
+
9
27
  ## [0.60.4] - 2026-09-16
10
28
 
11
29
  ### Internal
package/README.md CHANGED
@@ -112,7 +112,7 @@ in this client: one would put the very key the platform exists to hold back into
112
112
  | `sb_store` | Run a store flow that must happen in a fixed order — the four writes that make a working checkout, or any of the platform's 17 form templates (login, register, forgot, contact, subscribe …) with its own field document |
113
113
  | `sb_undo` | Put back what a PUT replaced. The SECOND answer for a page, not the only one: the platform has versions, history and restore (`sb_api_find` "page versions"), which outlive this process — reach for those first and use this for every other shaped PUT |
114
114
 
115
- Twenty-eight tools, **530 API operations** (178 of the 237 writes carrying a body shape read
115
+ Twenty-eight tools, **531 API operations** (179 of the 238 writes carrying a body shape read
116
116
  off the handler), 113 elements, 79 binding sources. `sb_api_find`
117
117
  is an index rather than a tool per endpoint, so the tool list stays short while everything
118
118
  the platform can do stays reachable — and operations added to the platform arrive with the
package/README.vi.md CHANGED
@@ -109,7 +109,7 @@ nhét ngược lại vào mọi bản cài.
109
109
  | `sb_store` | Chạy một luồng cửa hàng bắt buộc đúng thứ tự — bốn lệnh ghi tạo nên trang thanh toán, hoặc gieo bất kỳ template nào trong 17 form của nền tảng (login, register, forgot, contact, subscribe …) kèm field document của nó |
110
110
  | `sb_undo` | Trả lại thứ mà một lệnh PUT đã ghi đè. Với TRANG thì đây là đường về thứ hai chứ không phải duy nhất: nền tảng có version, history và restore (`sb_api_find` "page versions"), và chúng sống lâu hơn tiến trình này — hãy dùng chúng trước, còn tool này cho mọi PUT có hình dạng khác |
111
111
 
112
- Hai mươi tám tool, **530 operation API** (178 trong 237 lệnh ghi có hình dạng body đọc thẳng
112
+ Hai mươi tám tool, **531 operation API** (179 trong 238 lệnh ghi có hình dạng body đọc thẳng
113
113
  từ handler), 113 element, 79 nguồn binding. `sb_api_find` là
114
114
  một chỉ mục chứ không phải mỗi endpoint một tool, nên danh sách tool vẫn ngắn trong khi mọi
115
115
  thứ nền tảng làm được vẫn với tới — và operation mới thêm bên nền tảng sẽ tự có sau lần
@@ -1,8 +1,8 @@
1
1
  export const SWAGGER_SOURCE = {
2
- "operations": 530,
2
+ "operations": 531,
3
3
  "definitions": 107,
4
- "bodyCarrying": 196,
5
- "bodyUndescribed": 77,
4
+ "bodyCarrying": 197,
5
+ "bodyUndescribed": 78,
6
6
  "generatedFrom": "server/docs/swagger.json"
7
7
  };
8
8
  export const API_OPERATIONS = [
@@ -3489,6 +3489,34 @@ export const API_OPERATIONS = [
3489
3489
  "bodyRef": null,
3490
3490
  "credential": "siteScoped"
3491
3491
  },
3492
+ {
3493
+ "id": "post:/api/sites/{siteId}/ai/tool-results",
3494
+ "method": "POST",
3495
+ "path": "/api/sites/{siteId}/ai/tool-results",
3496
+ "tags": [
3497
+ "sites"
3498
+ ],
3499
+ "summary": "Deliver the result of a client-side tool call",
3500
+ "params": [
3501
+ {
3502
+ "name": "siteId",
3503
+ "in": "path",
3504
+ "required": true,
3505
+ "type": "string",
3506
+ "description": "Site ID"
3507
+ },
3508
+ {
3509
+ "name": "result",
3510
+ "in": "body",
3511
+ "required": true,
3512
+ "type": "object",
3513
+ "description": "id (the tool_request's own id, from the SSE event) and result (what the tool answered, as a string)"
3514
+ }
3515
+ ],
3516
+ "bodyDescribed": false,
3517
+ "bodyRef": null,
3518
+ "credential": "siteScoped"
3519
+ },
3492
3520
  {
3493
3521
  "id": "get:/api/sites/{siteId}/api-keys",
3494
3522
  "method": "GET",
@@ -1,6 +1,6 @@
1
1
  export const DEAD_KEY_SOURCE = {
2
- "files": 4063,
3
- "identifiers": 77938,
2
+ "files": 4065,
3
+ "identifiers": 77979,
4
4
  "seededKeys": 388,
5
5
  "dead": 0
6
6
  };
@@ -36830,6 +36830,115 @@ export const ELEMENT_SEEDS = {
36830
36830
  export const FIRST_CHILD_ONLY = [
36831
36831
  "list-dataset"
36832
36832
  ];
36833
+ /**
36834
+ * What a setting needs from its NEIGHBOURS before it does anything.
36835
+ *
36836
+ * Every other table here publishes one key's legal values, which cannot express
36837
+ * "these two are legal apart and meaningless together" — and that is a real
36838
+ * write: filterValueMode "auto" with filterSource "blog_category" stores,
36839
+ * saves, publishes and renders an empty filter.
36840
+ *
36841
+ * Declared by the platform (schema/src/filters/preconditions.ts) and pinned
36842
+ * there against both Go renderers.
36843
+ */
36844
+ export const WRITE_PRECONDITIONS = [
36845
+ {
36846
+ "key": "filterValueMode",
36847
+ "value": "auto",
36848
+ "types": [
36849
+ "filter-checkbox",
36850
+ "filter-radio",
36851
+ "filter-color",
36852
+ "filter-tag"
36853
+ ],
36854
+ "requires": [
36855
+ {
36856
+ "key": "filterSource",
36857
+ "anyOf": [
36858
+ "category"
36859
+ ]
36860
+ }
36861
+ ],
36862
+ "otherwise": "the filter offers NOTHING and renders nothing at all. What this mode follows is the page's own PRODUCT collection (`/collections/{slug}`), so the blog's taxonomy — a tree too, which is why it looks like it should work — has nothing here to follow."
36863
+ },
36864
+ {
36865
+ "key": "filterBehavior",
36866
+ "value": "navigate",
36867
+ "types": [
36868
+ "filter-checkbox",
36869
+ "filter-radio",
36870
+ "filter-color",
36871
+ "filter-tag"
36872
+ ],
36873
+ "requires": [
36874
+ {
36875
+ "key": "filterSource",
36876
+ "anyOf": [
36877
+ "category",
36878
+ "blog_category"
36879
+ ]
36880
+ }
36881
+ ],
36882
+ "otherwise": "every row renders as an ordinary filtering label instead of a link. A row becomes an `<a href>` only where its value HAS a page, and both renderers decide that from the source's link kind — a brand, a tag or a price band is a way of narrowing a shelf, not a place."
36883
+ },
36884
+ {
36885
+ "key": "filterShowAll",
36886
+ "value": true,
36887
+ "types": [
36888
+ "filter-checkbox",
36889
+ "filter-radio",
36890
+ "filter-color",
36891
+ "filter-tag"
36892
+ ],
36893
+ "requires": [
36894
+ {
36895
+ "key": "filterValueMode",
36896
+ "anyOf": [
36897
+ "auto"
36898
+ ]
36899
+ },
36900
+ {
36901
+ "key": "filterBehavior",
36902
+ "anyOf": [
36903
+ "navigate"
36904
+ ]
36905
+ }
36906
+ ],
36907
+ "otherwise": "no extra row is drawn. It is the way BACK OUT of a browse tree — a link to the category the page is serving — so it needs the page to be followed and the rows to be links. Under in-place filtering, \"everything\" is already what an untouched filter shows."
36908
+ },
36909
+ {
36910
+ "key": "filterMatch",
36911
+ "value": "all",
36912
+ "types": [
36913
+ "filter-checkbox",
36914
+ "filter-radio",
36915
+ "filter-color",
36916
+ "filter-tag"
36917
+ ],
36918
+ "requires": [
36919
+ {
36920
+ "key": "filterSource",
36921
+ "anyOf": [
36922
+ "category",
36923
+ "attribute",
36924
+ "tag",
36925
+ "blog_category",
36926
+ "course_tag"
36927
+ ]
36928
+ },
36929
+ {
36930
+ "key": "filterBehavior",
36931
+ "anyOf": [
36932
+ "filter",
36933
+ "navigate",
36934
+ "",
36935
+ null
36936
+ ]
36937
+ }
36938
+ ],
36939
+ "otherwise": "the filter combines ticked values with OR, as it always did. Both predicates IGNORE the mode outside these cases rather than honouring it — a product holds ONE brand, one price and one stock state, so \"match all\" on two ticked values could only return an empty shelf, and a stale URL degrades to the ordinary OR instead of to a blank page."
36940
+ }
36941
+ ];
36833
36942
  export const HOVER_HOMES = {
36834
36943
  "button": {
36835
36944
  "home": "legacy"
@@ -1,7 +1,7 @@
1
1
  export const SHAPE_SOURCE = {
2
- "writeOperations": 237,
3
- "shaped": 178,
4
- "fromHandlers": 178,
2
+ "writeOperations": 238,
3
+ "shaped": 179,
4
+ "fromHandlers": 179,
5
5
  "fromSwaggerOnly": 0,
6
6
  "withReadOnly": 26,
7
7
  "structsRead": 1726
@@ -3038,6 +3038,20 @@ export const REQUEST_SHAPES = {
3038
3038
  "source": "go",
3039
3039
  "goType": "(inline)"
3040
3040
  },
3041
+ "post:/api/sites/{siteId}/ai/tool-results": {
3042
+ "fields": [
3043
+ {
3044
+ "name": "id",
3045
+ "type": "string"
3046
+ },
3047
+ {
3048
+ "name": "result",
3049
+ "type": "string"
3050
+ }
3051
+ ],
3052
+ "source": "go",
3053
+ "goType": "(inline)"
3054
+ },
3041
3055
  "put:/api/sites/{siteId}/ai/settings": {
3042
3056
  "fields": [
3043
3057
  {
@@ -0,0 +1,7 @@
1
+ // GENERATED by scripts/gen-catalog.ts — do not edit by hand.
2
+ // The platform commit every other generated file here describes.
3
+ export const PLATFORM_SOURCE = {
4
+ "commit": "d6a68b77a7a00a6a9ca66da5161d57e845aae628",
5
+ "committedAt": "2026-09-17T00:01:02+07:00",
6
+ "dirty": false
7
+ };
@@ -1,5 +1,5 @@
1
1
  import { DEAD_KEYS } from '../../catalog/deadkeys.generated.js';
2
- import { ANIMATION, CONFIG_VALUES, ELEMENT_VALUES } from '../../catalog/elements.generated.js';
2
+ import { ANIMATION, CONFIG_VALUES, ELEMENT_VALUES, WRITE_PRECONDITIONS, } from '../../catalog/elements.generated.js';
3
3
  /** The vocabulary for a config key, or null when the key has no fixed one. */
4
4
  export function vocabularyFor(key) {
5
5
  return CONFIG_VALUES[key] ?? null;
@@ -395,3 +395,91 @@ export function deadKeyNote(namespace, key) {
395
395
  'draws no row for it either, so only an agent can reach it. Nothing here can fix that — the ' +
396
396
  `fix is upstream, in schema/src/elements/${dead.seededBy[0]}/meta.ts.`);
397
397
  }
398
+ /**
399
+ * THE WARNING A PER-KEY VOCABULARY CANNOT GIVE.
400
+ *
401
+ * Every table above publishes ONE key's legal values, so a write whose values
402
+ * are each legal passes in silence — and several combinations in this platform
403
+ * are legal apart and meaningless together. `filterValueMode: "auto"` with
404
+ * `filterSource: "blog_category"` is the one that prompted this: both values
405
+ * are real, the pair stores, saves, publishes, and renders an empty filter,
406
+ * with no error at any layer. A caller had no way to see it coming.
407
+ *
408
+ * Takes the node AS IT WILL BE — the caller merges its write in first — because
409
+ * these are questions about the finished node. Asking about one key alone is
410
+ * exactly the blindness being fixed.
411
+ *
412
+ * A WARNING AND NEVER A REFUSAL, the rule its siblings above already follow:
413
+ * the platform stores what it is given, so refusing would invent a rule it does
414
+ * not have, and a newer deployment may honour a combination this catalog was
415
+ * generated before. The sentence says what the PAGE does, never "invalid".
416
+ */
417
+ /**
418
+ * One clause against one stored value — a MIRROR of `holds` in
419
+ * schema/src/filters/preconditions.ts, and the two must agree.
420
+ *
421
+ * `null` in a clause means NOT SET, which is a real state rather than a hole:
422
+ * a node written before a key existed does not carry it, and both renderers
423
+ * then read their own fallback. Without this mapping the two copies disagreed
424
+ * within an hour of being written — this one called a perfectly good tag filter
425
+ * broken, which is a FALSE POSITIVE and worse than the silence the whole
426
+ * mechanism replaced, because a caller warned about correct work stops reading
427
+ * the warnings.
428
+ *
429
+ * It is a copy because this package has no runtime dependency on the schema —
430
+ * the table arrives as generated data. The cases that separate the two are
431
+ * asserted on both sides (`test/write-preconditions.test.ts` here,
432
+ * `schema/test/filter-preconditions.test.ts` there) rather than left to the
433
+ * reading.
434
+ */
435
+ function holdsClause(c, stored) {
436
+ return c.anyOf.includes(stored === undefined ? null : stored);
437
+ }
438
+ export function preconditionNotes(type, specials) {
439
+ const out = [];
440
+ for (const p of WRITE_PRECONDITIONS) {
441
+ if (!p.types.includes(type))
442
+ continue;
443
+ if (specials[p.key] !== p.value)
444
+ continue;
445
+ const missing = p.requires.filter((c) => !holdsClause(c, specials[c.key]));
446
+ if (missing.length === 0)
447
+ continue;
448
+ const held = missing
449
+ .map((c) => {
450
+ const now = specials[c.key];
451
+ const want = c.anyOf
452
+ .map((v) => (v === null ? 'to be left unset' : JSON.stringify(v)))
453
+ .join(' or ');
454
+ // An ABSENT key is not `null`, and saying so would send a caller looking
455
+ // for a null they never wrote. The platform's own default applies here,
456
+ // which is a different thing to fix than a wrong value.
457
+ const has = now === undefined ? 'is not set' : `is ${JSON.stringify(now)}`;
458
+ return `${c.key} ${has}, and this needs ${want}`;
459
+ })
460
+ .join('; ');
461
+ out.push(`specials.${p.key} = ${JSON.stringify(p.value)} does nothing on this node: ${held}. ` +
462
+ `Left as it is, ${p.otherwise}`);
463
+ }
464
+ return out;
465
+ }
466
+ /**
467
+ * The same question for a shape that does not honour the setting AT ALL.
468
+ *
469
+ * Split from the clause check because the answer is different in kind: a
470
+ * missing neighbour is something the caller can fix with another write, while a
471
+ * `select` will never read the auto value mode however the rest of the node is
472
+ * arranged.
473
+ */
474
+ export function unsupportedSettingNote(type, key, value) {
475
+ const p = WRITE_PRECONDITIONS.find((x) => x.key === key && x.value === value);
476
+ if (!p || p.types.includes(type))
477
+ return null;
478
+ // NOT `p.otherwise`. That sentence explains what a node MISSING A NEIGHBOUR
479
+ // renders as, and borrowing it here would hand a caller the wrong diagnosis —
480
+ // it would send them to fix a sibling key on a shape that will never read
481
+ // this one however the rest of the node is arranged.
482
+ return (`specials.${key} = ${JSON.stringify(value)} is not a setting ${type} reads — ` +
483
+ `only ${p.types.join(', ')} honour it. It stores, saves and publishes with no error, ` +
484
+ `and this element's renderer never looks at it.`);
485
+ }
@@ -9,7 +9,7 @@ import { detachNote, presetIdOf, presetLayer } from '../domains/site/theme.js';
9
9
  import { inertHintsFor } from '../domains/site/inert.js';
10
10
  import { hasSeed, layoutDocument, seedDocument, seedSummary, seededTypes, } from '../domains/site/storepage.js';
11
11
  import { PAGE_TYPES } from '../catalog/storepages.generated.js';
12
- import { animationNote, deadKeyNote, unknownValueNote, unknownWriteNote, } from '../domains/site/vocabulary.js';
12
+ import { animationNote, deadKeyNote, unknownValueNote, unknownWriteNote, preconditionNotes, unsupportedSettingNote, } from '../domains/site/vocabulary.js';
13
13
  import { skinLevelNote } from '../domains/site/fieldskin.js';
14
14
  import { siteTheme } from '../domains/site/theme-fetch.js';
15
15
  import { request, redact } from '../transport/http.js';
@@ -787,6 +787,31 @@ export function registerPageTools(server, ctx) {
787
787
  if (once)
788
788
  valueNotes.push(once);
789
789
  }
790
+ // …AND THE ONE A PER-KEY TABLE CANNOT GIVE: values that are each legal
791
+ // and mean nothing TOGETHER. `filterValueMode: "auto"` with
792
+ // `filterSource: "blog_category"` passed every check above and
793
+ // published a filter that renders nothing.
794
+ //
795
+ // Asked of the node AS IT WILL BE, not of the write: the node may
796
+ // already carry the neighbour this write needs, and the write may
797
+ // supply the neighbour a stored value was missing — so a question
798
+ // about one key alone answers the wrong thing in both directions.
799
+ if (e.namespace === 'specials') {
800
+ const after = { ...(d.doc.nodes[e.id]?.specials ?? {}), ...e.keys };
801
+ for (const [k, v] of Object.entries(e.keys)) {
802
+ const un = unsupportedSettingNote(type, k, v);
803
+ if (!un)
804
+ continue;
805
+ const once = ctx.notices.once(`unsupported:${type}.${k}=${JSON.stringify(v)}`, un);
806
+ if (once)
807
+ valueNotes.push(once);
808
+ }
809
+ for (const n of preconditionNotes(type, after)) {
810
+ const once = ctx.notices.once(`precondition:${type}:${n.slice(0, 60)}`, n);
811
+ if (once)
812
+ valueNotes.push(once);
813
+ }
814
+ }
790
815
  }
791
816
  // AND THE QUIETER ONE: a key an element SEEDS that no renderer anywhere
792
817
  // reads. The vocabularies above answer "this value means something other
@@ -2,6 +2,26 @@ import { z } from 'zod';
2
2
  import { request } from '../transport/http.js';
3
3
  import { text } from '../mcp/response.js';
4
4
  import { API_OPERATIONS } from '../catalog/api.generated.js';
5
+ import { PLATFORM_SOURCE } from '../catalog/source.generated.js';
6
+ /**
7
+ * The provenance line, or '' when the generator had no git to ask.
8
+ *
9
+ * Reads as an AGE rather than a commit id, because the age is the actionable
10
+ * half — nobody can tell whether `d6a68b7` is recent, and everybody can tell
11
+ * whether four months is. The id rides along for the person who then has to
12
+ * find it.
13
+ */
14
+ export function catalogAge(now = new Date()) {
15
+ const { commit, committedAt, dirty } = PLATFORM_SOURCE;
16
+ if (!commit || !committedAt)
17
+ return '';
18
+ const days = Math.floor((now.getTime() - new Date(committedAt).getTime()) / 86_400_000);
19
+ const age = days <= 1 ? 'today' : days < 45 ? `${days} days old` : `${Math.floor(days / 30)} months old`;
20
+ return (`Built from web_builder ${commit.slice(0, 9)} (${committedAt.slice(0, 10)}, ${age})` +
21
+ (dirty ? ', from an UNCOMMITTED tree — it may describe work no deployment has' : '') +
22
+ `. The platform reports no version of its own, so nothing here can compare the two: if an ` +
23
+ `element or key you expect is missing, regenerate rather than working around it.`);
24
+ }
5
25
  /**
6
26
  * Log in and report what this server can actually reach.
7
27
  *
@@ -26,6 +46,7 @@ export async function connect(ctx, args) {
26
46
  ...(ctx.siteId ? { site: ctx.siteId } : {}),
27
47
  ...(ctx.siteId && ctx.siteName ? { site_name: ctx.siteName } : {}),
28
48
  operations: API_OPERATIONS.length,
49
+ ...(catalogAge() ? { catalog: catalogAge() } : {}),
29
50
  note: ctx.siteId
30
51
  ? `Connected with an API key alone, on site ${ctx.siteId} (SB_SITE)${ctx.siteName ? `, the store called ${JSON.stringify(ctx.siteName)}` : ''}. Every tool ` +
31
52
  'defaults to it, so site_id is optional. Set SB_EMAIL and SB_PASSWORD as well if ' +
@@ -71,6 +92,7 @@ export async function connect(ctx, args) {
71
92
  sites: (listed?.sites ?? []).map((s) => ({ id: s.id, name: s.name })),
72
93
  api_key: ctx.apiKey ? 'present' : 'missing',
73
94
  operations: API_OPERATIONS.length,
95
+ ...(catalogAge() ? { catalog: catalogAge() } : {}),
74
96
  };
75
97
  if (!ctx.apiKey) {
76
98
  result.note =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.60.4",
3
+ "version": "0.61.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",