frameworc-mcp 0.2.0 → 0.4.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.
@@ -28,56 +28,168 @@ class ApiClient {
28
28
  }
29
29
  if (!res.ok) {
30
30
  const msg = json && typeof json === "object" && json.errors
31
- ? `HTTP ${res.status}: ${JSON.stringify(json.errors)}`
31
+ ? `HTTP ${res.status}: ${json.message ?? ""} ${JSON.stringify(json.errors)}`
32
32
  : `HTTP ${res.status}: ${typeof json === "string" ? json : JSON.stringify(json)}`;
33
- throw new Error(msg);
33
+ throw new Error(msg.trim());
34
34
  }
35
35
  return json;
36
36
  }
37
- listPages() {
38
- return this.request("/pages");
37
+ /** Appends the effective CMS site id to a query string. */
38
+ q(path, siteId) {
39
+ const id = siteId ?? this.session.getSiteId();
40
+ if (id === null || id === undefined)
41
+ return path;
42
+ return path + (path.includes("?") ? "&" : "?") + `site_id=${id}`;
39
43
  }
40
- getPage(id) {
41
- return this.request(`/pages/${id}`);
44
+ /** Merges the effective CMS site id into a JSON body. */
45
+ b(body, siteId) {
46
+ const id = siteId ?? this.session.getSiteId();
47
+ const merged = { ...(body ?? {}) };
48
+ if (id !== null && id !== undefined && merged.site_id === undefined) {
49
+ merged.site_id = id;
50
+ }
51
+ return JSON.stringify(merged);
52
+ }
53
+ // --- sites & schema ---------------------------------------------------
54
+ listCmsSites() {
55
+ return this.request("/sites");
56
+ }
57
+ listBlocks() {
58
+ return this.request("/blocks");
59
+ }
60
+ getBlockSchema(name) {
61
+ return this.request(`/blocks/${encodeURIComponent(name)}`);
62
+ }
63
+ // --- pages ------------------------------------------------------------
64
+ listPages(siteId) {
65
+ return this.request(this.q("/pages", siteId));
42
66
  }
43
- createPage(payload) {
44
- return this.request("/pages", { method: "POST", body: JSON.stringify(payload) });
67
+ getPage(id, siteId) {
68
+ return this.request(this.q(`/pages/${id}`, siteId));
45
69
  }
46
- updatePage(id, payload) {
47
- return this.request(`/pages/${id}`, { method: "PATCH", body: JSON.stringify(payload) });
70
+ createPage(payload, siteId) {
71
+ return this.request("/pages", { method: "POST", body: this.b(payload, siteId) });
72
+ }
73
+ updatePage(id, payload, siteId) {
74
+ return this.request(`/pages/${id}`, { method: "PATCH", body: this.b(payload, siteId) });
75
+ }
76
+ deletePage(id, siteId) {
77
+ return this.request(this.q(`/pages/${id}`, siteId), { method: "DELETE" });
78
+ }
79
+ createTranslation(id, targetSiteId, payload, sourceSiteId) {
80
+ const body = { ...(payload ?? {}), site_id: targetSiteId };
81
+ const source = sourceSiteId ?? this.session.getSiteId();
82
+ if (source !== null && source !== undefined) {
83
+ body.source_site_id = source;
84
+ }
85
+ return this.request(`/pages/${id}/translations`, { method: "POST", body: JSON.stringify(body) });
48
86
  }
49
- deletePage(id) {
50
- return this.request(`/pages/${id}`, { method: "DELETE" });
87
+ // --- blocks (per page or per prefill) ---------------------------------
88
+ blockBase(host) {
89
+ return host.type === "prefill" ? `/prefills/${host.id}` : `/pages/${host.id}`;
51
90
  }
52
- addBlock(pageId, block, position) {
53
- return this.request(`/pages/${pageId}/blocks`, {
91
+ addBlock(host, block, position, siteId) {
92
+ return this.request(`${this.blockBase(host)}/blocks`, {
54
93
  method: "POST",
55
- body: JSON.stringify({ block, position }),
94
+ body: this.b({ block, position }, siteId),
56
95
  });
57
96
  }
58
- updateBlock(pageId, blockId, block) {
59
- return this.request(`/pages/${pageId}/blocks/${blockId}`, {
97
+ updateBlock(host, blockId, block, siteId) {
98
+ return this.request(`${this.blockBase(host)}/blocks/${blockId}`, {
60
99
  method: "PATCH",
61
- body: JSON.stringify(block),
100
+ body: this.b(block, siteId),
62
101
  });
63
102
  }
64
- removeBlock(pageId, blockId) {
65
- return this.request(`/pages/${pageId}/blocks/${blockId}`, { method: "DELETE" });
103
+ removeBlock(host, blockId, siteId) {
104
+ return this.request(this.q(`${this.blockBase(host)}/blocks/${blockId}`, siteId), { method: "DELETE" });
66
105
  }
67
- reorderBlocks(pageId, order) {
68
- return this.request(`/pages/${pageId}/blocks/order`, {
106
+ reorderBlocks(host, order, siteId) {
107
+ return this.request(`${this.blockBase(host)}/blocks/order`, {
69
108
  method: "POST",
70
- body: JSON.stringify({ order }),
109
+ body: this.b({ order }, siteId),
71
110
  });
72
111
  }
73
- listForms() {
74
- return this.request("/forms");
112
+ extractBlockToPrefill(pageId, blockId, title, siteId) {
113
+ return this.request(`/pages/${pageId}/blocks/${blockId}/extract-to-prefill`, {
114
+ method: "POST",
115
+ body: this.b({ title }, siteId),
116
+ });
117
+ }
118
+ // --- forms --------------------------------------------------------------
119
+ listForms(siteId) {
120
+ return this.request(this.q("/forms", siteId));
121
+ }
122
+ getFormSchema() {
123
+ return this.request("/forms/schema");
124
+ }
125
+ getForm(id, siteId) {
126
+ return this.request(this.q(`/forms/${id}`, siteId));
127
+ }
128
+ createForm(payload, siteId) {
129
+ return this.request("/forms", { method: "POST", body: this.b(payload, siteId) });
130
+ }
131
+ updateForm(id, payload, siteId) {
132
+ return this.request(`/forms/${id}`, { method: "PATCH", body: this.b(payload, siteId) });
133
+ }
134
+ deleteForm(id, force, siteId) {
135
+ return this.request(this.q(`/forms/${id}${force ? "?force=1" : ""}`, siteId), { method: "DELETE" });
75
136
  }
76
- listMenus() {
77
- return this.request("/menus");
137
+ // --- menus --------------------------------------------------------------
138
+ listMenus(siteId) {
139
+ return this.request(this.q("/menus", siteId));
78
140
  }
79
- listPrefills() {
80
- return this.request("/prefills");
141
+ getMenu(id, siteId) {
142
+ return this.request(this.q(`/menus/${id}`, siteId));
143
+ }
144
+ createMenu(payload, siteId) {
145
+ return this.request("/menus", { method: "POST", body: this.b(payload, siteId) });
146
+ }
147
+ updateMenu(id, payload, siteId) {
148
+ return this.request(`/menus/${id}`, { method: "PATCH", body: this.b(payload, siteId) });
149
+ }
150
+ deleteMenu(id, force, siteId) {
151
+ return this.request(this.q(`/menus/${id}${force ? "?force=1" : ""}`, siteId), { method: "DELETE" });
152
+ }
153
+ // --- prefills -----------------------------------------------------------
154
+ listPrefills(siteId) {
155
+ return this.request(this.q("/prefills", siteId));
156
+ }
157
+ getPrefill(id, siteId) {
158
+ return this.request(this.q(`/prefills/${id}`, siteId));
159
+ }
160
+ createPrefill(payload, siteId) {
161
+ return this.request("/prefills", { method: "POST", body: this.b(payload, siteId) });
162
+ }
163
+ updatePrefill(id, payload, siteId) {
164
+ return this.request(`/prefills/${id}`, { method: "PATCH", body: this.b(payload, siteId) });
165
+ }
166
+ deletePrefill(id, force, siteId) {
167
+ return this.request(this.q(`/prefills/${id}${force ? "?force=1" : ""}`, siteId), { method: "DELETE" });
168
+ }
169
+ createPrefillTranslation(id, targetSiteId, payload, sourceSiteId) {
170
+ const body = { ...(payload ?? {}), site_id: targetSiteId };
171
+ const source = sourceSiteId ?? this.session.getSiteId();
172
+ if (source !== null && source !== undefined) {
173
+ body.source_site_id = source;
174
+ }
175
+ return this.request(`/prefills/${id}/translations`, { method: "POST", body: JSON.stringify(body) });
176
+ }
177
+ // --- FrameworC settings (global) ---------------------------------------
178
+ getSettings() {
179
+ return this.request("/settings");
180
+ }
181
+ updateSettings(fields) {
182
+ return this.request("/settings", { method: "PATCH", body: JSON.stringify({ fields }) });
183
+ }
184
+ // --- singles ----------------------------------------------------------
185
+ getSingle(handle, siteId) {
186
+ return this.request(this.q(`/singles/${encodeURIComponent(handle)}`, siteId));
187
+ }
188
+ updateSingle(handle, fields, siteId) {
189
+ return this.request(`/singles/${encodeURIComponent(handle)}`, {
190
+ method: "PATCH",
191
+ body: this.b(fields, siteId),
192
+ });
81
193
  }
82
194
  }
83
195
  exports.ApiClient = ApiClient;
package/dist/catalogue.js CHANGED
@@ -83,10 +83,10 @@ exports.BLOCKS = [
83
83
  content_group: "Tiles",
84
84
  name: "Tiles (grid of cards)",
85
85
  description: "Grid of cards (icon + headline + rich text + button each).",
86
- whenToUse: "Feature lists, service grids, 'value proposition' card rows,ething lists of related items.",
86
+ whenToUse: "Feature lists, service grids, 'value proposition' card rows, or any list of related items.",
87
87
  base: baseBlock,
88
88
  content: [
89
- { name: "tiles", type: "taglist", comment: "Array of tile items, each {headline, image, text, buttonLabel1, buttonVariant1, buttonContrast1, buttonLink1, buttonIcon1, buttonBlank1, buttonLabel2, ...}. Use this to list the tiles." },
89
+ { name: "tiles", type: "repeater", comment: "Array of tile items, each {headline, image, text, buttonLabel1, buttonVariant1, buttonContrast1, buttonLink1, buttonIcon1, buttonBlank1, buttonLabel2, ...}. Use this to list the tiles." },
90
90
  { name: "tileLink", type: "switch", default: false, comment: "Whole tile acts as a link." },
91
91
  { name: "tileIcon", type: "switch", default: false, comment: "Display icon-only tiles." },
92
92
  { name: "contrastHover", type: "switch", default: false },
@@ -116,7 +116,7 @@ exports.BLOCKS = [
116
116
  whenToUse: "Image-heavy content that benefits from sequential browsing — testimonials, step-by-step walkthroughs, gallery-like rows.",
117
117
  base: baseBlock,
118
118
  content: [
119
- { name: "slides", type: "taglist", comment: "Array of slide items, each {image, headline, text, buttonLabel1, buttonVariant1, ...}." },
119
+ { name: "slides", type: "repeater", comment: "Array of slide items, each {image, headline, text, buttonLabel1, buttonVariant1, ...}." },
120
120
  { name: "gallery", type: "switch", default: false, comment: "Clicking a slide opens the gallery lightbox." },
121
121
  { name: "loop", type: "switch", default: false, comment: "Infinite loop playback." },
122
122
  { name: "autoWidth", type: "switch", default: false, comment: "Slide width matches image width." },
@@ -149,7 +149,7 @@ exports.BLOCKS = [
149
149
  whenToUse: "Long content split into topic tabs (e.g. 'What we do' / 'Who we are' / 'Contact'). Don't use if there are only 1–2 logical sections — use Section instead.",
150
150
  base: baseBlock,
151
151
  content: [
152
- { name: "slides", type: "taglist", comment: "Array of tab items, each {image, headline, text, buttonLabel1, ...}." },
152
+ { name: "slides", type: "repeater", comment: "Array of tab items, each {image, headline, text, buttonLabel1, ...}." },
153
153
  { name: "gallery", type: "switch", default: false },
154
154
  { name: "variant", type: "dropdown", enum: ["tabs-horizontal", "tabs-vertical"], default: "tabs-horizontal" },
155
155
  ],
@@ -173,7 +173,7 @@ exports.BLOCKS = [
173
173
  whenToUse: "FAQs, 'Common questions', or any content the user wants collapsed by default. Each item has a headline + rich text.",
174
174
  base: baseBlock,
175
175
  content: [
176
- { name: "items", type: "taglist", comment: "Array of items, each {headline, text}." },
176
+ { name: "items", type: "repeater", comment: "Array of items, each {headline, text}." },
177
177
  { name: "variant", type: "dropdown", enum: ["default", "outline", "shadow"], default: "default" },
178
178
  { name: "firstItemOpen", type: "switch", default: false, comment: "First item is expanded on load." },
179
179
  { name: "singleMode", type: "switch", default: false, comment: "Only one item open at a time." },
@@ -194,12 +194,12 @@ exports.BLOCKS = [
194
194
  content_group: "Form",
195
195
  name: "Form",
196
196
  description: "Contact / inquiry form wired to a separately-defined Form Tailor entry.",
197
- whenToUse: "Only when a contact form is needed AND the referenced Form entry already exists. Get the form_entry id via the `list_forms` tool. The actual form fields live on the linked Form entry, not on this block.",
197
+ whenToUse: "Only when a contact form is needed AND the referenced Form entry already exists. Set the block-level `form` key to the entry id from the `list_forms` tool. The actual form fields live on the linked Form entry, not on this block.",
198
198
  base: baseBlock,
199
199
  content: [
200
200
  { name: "variant", type: "dropdown", enum: ["default", "outline", "card"], default: "default" },
201
201
  ],
202
- referencing: { kind: "form", location: "block", jsonKey: "form_entry", column: "form_id" },
202
+ referencing: { kind: "form", location: "block", jsonKey: "form", column: "form_id" },
203
203
  },
204
204
  {
205
205
  content_group: "Gallery",
@@ -245,7 +245,7 @@ exports.BLOCKS = [
245
245
  description: "One repeater item per column. Each column has {blockId, builder: [...blocks]} where builder is a full Builder array.",
246
246
  items: [
247
247
  { name: "blockId", type: "text", comment: "Optional column anchor id (used for the column wrapper)." },
248
- { name: "builder", type: "taglist", comment: "Array of block objects (same shape as top-level 'builder' in a page). You can nest any block here, including another Columns (avoid deep nesting)." },
248
+ { name: "builder", type: "repeater", comment: "Array of block objects (same shape as top-level 'builder' in a page). You can nest any block here, including another Columns (avoid deep nesting)." },
249
249
  ],
250
250
  },
251
251
  ],
@@ -254,10 +254,10 @@ exports.BLOCKS = [
254
254
  content_group: "Prefill",
255
255
  name: "Prefill (reusable block template)",
256
256
  description: "References a Prefill Tailor entry whose builder content is shared across pages — edit once, updates everywhere it is used.",
257
- whenToUse: "For shared/global blocks like a newsletter CTA, a contact form snippet, or a footer-style band that should appear the same on many pages. The actual block content lives on the Prefill entry — get its id via `list_prefills`. Don't use this for one-off content.",
257
+ whenToUse: "For shared/global blocks like a newsletter CTA, a contact form snippet, or a footer-style band that should appear the same on many pages. The actual block content lives on the Prefill entry — get its id via `list_prefills`. Whenever the same section would appear on more than one page, do NOT copy it: keep it in one Prefill (create_prefill, or extract_block_to_prefill for an existing page block) and insert this block on every page that needs it. Don't use this for one-off content.",
258
258
  base: baseBlock,
259
259
  content: [],
260
- referencing: { kind: "prefill", location: "content", jsonKey: "prefill_entry", column: "block_id" },
260
+ referencing: { kind: "prefill", location: "content", jsonKey: "block", column: "block_id" },
261
261
  },
262
262
  {
263
263
  content_group: "BlogList",
@@ -276,10 +276,10 @@ exports.BLOCKS = [
276
276
  content_group: "MenuBlock",
277
277
  name: "Menu block",
278
278
  description: "Renders an existing Menu Tailor entry inline in the page body (flattened to 2 levels).",
279
- whenToUse: "When you need to embed navigation as content (e.g. an on-page section that lists links). Get the menu_entry id via `list_menus`.",
279
+ whenToUse: "When you need to embed navigation as content (e.g. an on-page section that lists links). Set `content.menu` to the entry id from `list_menus`.",
280
280
  base: baseBlock,
281
281
  content: [],
282
- referencing: { kind: "menu", location: "content", jsonKey: "menu_entry", column: "menu_id" },
282
+ referencing: { kind: "menu", location: "content", jsonKey: "menu", column: "menu_id" },
283
283
  },
284
284
  {
285
285
  content_group: "ImageStrip",