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.
package/dist/index.js CHANGED
@@ -13,6 +13,17 @@ const CATALOGUE_URI = "frameworc://blocks";
13
13
  (0, sites_js_1.warnIfWorldReadable)();
14
14
  const session = new session_js_1.Session();
15
15
  const client = new api_client_js_1.ApiClient(session);
16
+ /** Resolves the block-op target from page_id / prefill_id args. */
17
+ function blockHost(args) {
18
+ const hasPage = args.page_id !== undefined && args.page_id !== null;
19
+ const hasPrefill = args.prefill_id !== undefined && args.prefill_id !== null;
20
+ if (hasPage === hasPrefill) {
21
+ throw new Error("Pass exactly one of page_id / prefill_id.");
22
+ }
23
+ return hasPrefill
24
+ ? { type: "prefill", id: Number(args.prefill_id) }
25
+ : { type: "page", id: Number(args.page_id) };
26
+ }
16
27
  const tools = [
17
28
  {
18
29
  name: "use_site",
@@ -37,12 +48,34 @@ const tools = [
37
48
  properties: {},
38
49
  },
39
50
  },
51
+ {
52
+ name: "list_cms_sites",
53
+ description: "List the multisite sites configured INSIDE the pinned OctoberCMS install (languages, regional variants). Returns [{id, name, code, locale, is_enabled, is_primary}]. Call this before creating content on a multi-language project so you know which site_id to target. Not to be confused with list_sites, which lists separate CMS installs from your local config file.",
54
+ inputSchema: {
55
+ type: "object",
56
+ properties: { site_url: { type: "string" } },
57
+ },
58
+ },
59
+ {
60
+ name: "use_cms_site",
61
+ description: "Pin which multisite site (language) subsequent calls read and write. Pass the id from list_cms_sites, or null to go back to the primary site. Every page/single tool also accepts a one-off site_id argument that overrides this.",
62
+ inputSchema: {
63
+ type: "object",
64
+ properties: {
65
+ site_id: { type: ["integer", "null"], description: "Site id from list_cms_sites, or null for the primary site." },
66
+ },
67
+ required: ["site_id"],
68
+ },
69
+ },
40
70
  {
41
71
  name: "list_pages",
42
- description: "List all FrameworC pages on the pinned site.",
72
+ description: "List all FrameworC pages on the pinned site (and pinned CMS site, if set).",
43
73
  inputSchema: {
44
74
  type: "object",
45
- properties: { site_url: { type: "string", description: "Override the pinned site URL for this call." } },
75
+ properties: {
76
+ site_url: { type: "string", description: "Override the pinned site URL for this call." },
77
+ site_id: { type: "integer", description: "Override the pinned multisite site for this call." },
78
+ },
46
79
  },
47
80
  },
48
81
  {
@@ -53,6 +86,7 @@ const tools = [
53
86
  properties: {
54
87
  id: { type: "integer", description: "Page id." },
55
88
  site_url: { type: "string" },
89
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
56
90
  },
57
91
  required: ["id"],
58
92
  },
@@ -73,6 +107,7 @@ const tools = [
73
107
  required: ["page"],
74
108
  },
75
109
  site_url: { type: "string" },
110
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
76
111
  },
77
112
  required: ["payload"],
78
113
  },
@@ -86,6 +121,7 @@ const tools = [
86
121
  id: { type: "integer" },
87
122
  payload: { type: "object" },
88
123
  site_url: { type: "string" },
124
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
89
125
  },
90
126
  required: ["id", "payload"],
91
127
  },
@@ -98,101 +134,383 @@ const tools = [
98
134
  properties: {
99
135
  id: { type: "integer" },
100
136
  site_url: { type: "string" },
137
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
101
138
  },
102
139
  required: ["id"],
103
140
  },
104
141
  },
105
142
  {
106
143
  name: "add_block",
107
- description: "Append a block to a page's builder. Optional `position` (1-indexed) inserts at that position; otherwise appends at the end. The `block` arg has the shape { content_group, base, content, form_entry? / menu_entry? / prefill_entry? } per the catalogue.",
144
+ description: "Append a block to a page's builder — or a prefill's, by passing prefill_id instead of page_id. Optional `position` (1-indexed) inserts at that position; otherwise appends at the end. The `block` arg has the shape { content_group, base, content, form? } per the catalogue — `form` sits at block level, while Menu and Prefill references live at `content.menu` and `content.block`. If the section you are adding already exists on another page, do NOT copy it: reference its Prefill entry with a `Prefill` block (extract_block_to_prefill turns an existing page block into one).",
108
145
  inputSchema: {
109
146
  type: "object",
110
147
  properties: {
111
- page_id: { type: "integer" },
148
+ page_id: { type: "integer", description: "Page to edit. Pass exactly one of page_id / prefill_id." },
149
+ prefill_id: { type: "integer", description: "Prefill entry to edit instead of a page." },
112
150
  block: { type: "object" },
113
151
  position: { type: "integer" },
114
152
  site_url: { type: "string" },
153
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
115
154
  },
116
- required: ["page_id", "block"],
155
+ required: ["block"],
117
156
  },
118
157
  },
119
158
  {
120
159
  name: "update_block",
121
- description: "Replace a single block by id (the row id returned by get_page's builder[].id). The new `block` must include `content_group` (matching or changing the block type) plus `base` + `content`.",
160
+ description: "Replace a single block by id (the row id from get_page's builder[].id) on a page — or a prefill via prefill_id. The new `block` must include `content_group` plus `base` + `content`. The block is replaced, not patched: the returned row id is NEW. Media a human assigned in the backend is carried over automatically wherever your payload leaves the field empty — unless you change content_group. Other blocks are untouched.",
122
161
  inputSchema: {
123
162
  type: "object",
124
163
  properties: {
125
- page_id: { type: "integer" },
164
+ page_id: { type: "integer", description: "Page to edit. Pass exactly one of page_id / prefill_id." },
165
+ prefill_id: { type: "integer", description: "Prefill entry to edit instead of a page." },
126
166
  block_id: { type: "integer" },
127
167
  block: { type: "object" },
128
168
  site_url: { type: "string" },
169
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
129
170
  },
130
- required: ["page_id", "block_id", "block"],
171
+ required: ["block_id", "block"],
131
172
  },
132
173
  },
133
174
  {
134
175
  name: "remove_block",
135
- description: "Remove a block by its row id from the page's builder.",
176
+ description: "Remove a block by its row id from a page's builder (or a prefill's via prefill_id). Remaining blocks keep their ids; ordering closes the gap.",
136
177
  inputSchema: {
137
178
  type: "object",
138
179
  properties: {
139
- page_id: { type: "integer" },
180
+ page_id: { type: "integer", description: "Page to edit. Pass exactly one of page_id / prefill_id." },
181
+ prefill_id: { type: "integer", description: "Prefill entry to edit instead of a page." },
140
182
  block_id: { type: "integer" },
141
183
  site_url: { type: "string" },
184
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
142
185
  },
143
- required: ["page_id", "block_id"],
186
+ required: ["block_id"],
144
187
  },
145
188
  },
146
189
  {
147
190
  name: "reorder_blocks",
148
- description: "Reorder blocks. Send every block row id in the desired order (all existing block ids must be present, no extras).",
191
+ description: "Reorder blocks on a page (or a prefill via prefill_id). Send every block row id in the desired order (all existing block ids must be present, no extras).",
149
192
  inputSchema: {
150
193
  type: "object",
151
194
  properties: {
152
- page_id: { type: "integer" },
195
+ page_id: { type: "integer", description: "Page to edit. Pass exactly one of page_id / prefill_id." },
196
+ prefill_id: { type: "integer", description: "Prefill entry to edit instead of a page." },
153
197
  order: { type: "array", items: { type: "integer" } },
154
198
  site_url: { type: "string" },
199
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
200
+ },
201
+ required: ["order"],
202
+ },
203
+ },
204
+ {
205
+ name: "extract_block_to_prefill",
206
+ description: "Move an existing page block into a NEW Prefill entry and replace it in place with a `Prefill` reference block. Lossless — media a human assigned survives the move. Use this whenever a section should appear on more than one page: extract it once, then add_block a `Prefill` block ({content_group:'Prefill', content:{block:<prefill id>}}) on the other pages. This is the intended FrameworC workflow; never copy the same section onto multiple pages.",
207
+ inputSchema: {
208
+ type: "object",
209
+ properties: {
210
+ page_id: { type: "integer" },
211
+ block_id: { type: "integer", description: "Row id of the block to extract." },
212
+ title: { type: "string", description: "Title for the new Prefill entry, e.g. 'Newsletter CTA'." },
213
+ site_url: { type: "string" },
214
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
155
215
  },
156
- required: ["page_id", "order"],
216
+ required: ["page_id", "block_id", "title"],
157
217
  },
158
218
  },
159
219
  {
160
220
  name: "list_forms",
161
- description: "List Form Tailor entries available for the `Form` block's `form_entry` argument.",
221
+ description: "List Form entries (id, title, slug, field_count, ...). Use get_form for a form's fields, create_form / update_form / delete_form for CRUD, and the id for the `Form` block's block-level `form` key.",
222
+ inputSchema: {
223
+ type: "object",
224
+ properties: {
225
+ site_url: { type: "string" },
226
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
227
+ },
228
+ },
229
+ },
230
+ {
231
+ name: "get_form_schema",
232
+ description: "Fetch the live field-group catalogue for Form entries from the CMS: every fwcFields group with its exact fields, enums and defaults, plus the form-level fields. Use before create_form / update_form when unsure of the shape.",
162
233
  inputSchema: {
163
234
  type: "object",
164
235
  properties: { site_url: { type: "string" } },
165
236
  },
166
237
  },
238
+ {
239
+ name: "get_form",
240
+ description: "Fetch one Form entry including its fwcFields rows (each {id, group, ...fields}).",
241
+ inputSchema: {
242
+ type: "object",
243
+ properties: {
244
+ id: { type: "integer" },
245
+ site_url: { type: "string" },
246
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
247
+ },
248
+ required: ["id"],
249
+ },
250
+ },
251
+ {
252
+ name: "create_form",
253
+ description: "Create a Form entry. Payload: {form: {title (required), headline?, recipients?: [emails], is_enabled?}, fwcFields?: [rows]}. Each row names its `group` — one of: section, text, number, email, phone, textarea, file, select, checkbox, radio, agreement — plus that group's fields: input groups take {label, placeholder?, name (required, unique), fieldId?, required?, width?: full|half|third|twoThirds}; select/checkbox/radio add options: [{value, label}]; agreement adds text (rich HTML); section takes only content (rich HTML, not an input). Call get_form_schema when unsure of a group's exact fields. Reference the created form from a page with a `Form` block ({content_group:'Form', form:<id>, content:{variant:...}}).",
254
+ inputSchema: {
255
+ type: "object",
256
+ properties: {
257
+ payload: {
258
+ type: "object",
259
+ properties: { form: { type: "object" }, fwcFields: { type: "array" } },
260
+ required: ["form"],
261
+ },
262
+ site_url: { type: "string" },
263
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
264
+ },
265
+ required: ["payload"],
266
+ },
267
+ },
268
+ {
269
+ name: "update_form",
270
+ description: "Update a Form entry. `form` merges scalar fields; if `fwcFields` is present the field rows are fully rebuilt from the array (send ALL rows, not a diff — they carry no media, so nothing is lost).",
271
+ inputSchema: {
272
+ type: "object",
273
+ properties: {
274
+ id: { type: "integer" },
275
+ payload: { type: "object", properties: { form: { type: "object" }, fwcFields: { type: "array" } } },
276
+ site_url: { type: "string" },
277
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
278
+ },
279
+ required: ["id", "payload"],
280
+ },
281
+ },
282
+ {
283
+ name: "delete_form",
284
+ description: "Delete a Form entry. Refused with a 422 naming the referencing pages if any `Form` block still points at it; pass force:true to delete anyway.",
285
+ inputSchema: {
286
+ type: "object",
287
+ properties: {
288
+ id: { type: "integer" },
289
+ force: { type: "boolean" },
290
+ site_url: { type: "string" },
291
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
292
+ },
293
+ required: ["id"],
294
+ },
295
+ },
167
296
  {
168
297
  name: "list_menus",
169
- description: "List Menu Tailor entries available for the `MenuBlock`'s `content.menu_entry` argument.",
298
+ description: "List Menu entries (id, title, item_count, ...). Use get_menu / create_menu / update_menu / delete_menu for CRUD, and the id for `content.menu` on a MenuBlock or the `nav` field of the Navigation/Footer singles.",
170
299
  inputSchema: {
171
300
  type: "object",
172
- properties: { site_url: { type: "string" } },
301
+ properties: {
302
+ site_url: { type: "string" },
303
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
304
+ },
305
+ },
306
+ },
307
+ {
308
+ name: "get_menu",
309
+ description: "Fetch one Menu entry including its navigation tree (items {id, title, url, anchor, blank, children?}).",
310
+ inputSchema: {
311
+ type: "object",
312
+ properties: {
313
+ id: { type: "integer" },
314
+ site_url: { type: "string" },
315
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
316
+ },
317
+ required: ["id"],
318
+ },
319
+ },
320
+ {
321
+ name: "create_menu",
322
+ description: "Create a Menu entry. Payload: {menu: {title (required)}, navigation?: [items]}. Each item: {title (required), url, anchor?, blank?, children?: [items]}. `url` is a string ('/', '/kontakt#form', full URL) OR {page_id: n} to link a FrameworC page robustly. Max 2 levels — children may not have children.",
323
+ inputSchema: {
324
+ type: "object",
325
+ properties: {
326
+ payload: {
327
+ type: "object",
328
+ properties: { menu: { type: "object" }, navigation: { type: "array" } },
329
+ required: ["menu"],
330
+ },
331
+ site_url: { type: "string" },
332
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
333
+ },
334
+ required: ["payload"],
335
+ },
336
+ },
337
+ {
338
+ name: "update_menu",
339
+ description: "Update a Menu entry. `menu` merges title/slug/is_enabled; if `navigation` is present the whole tree is replaced from the array (send ALL items — they carry no media).",
340
+ inputSchema: {
341
+ type: "object",
342
+ properties: {
343
+ id: { type: "integer" },
344
+ payload: { type: "object", properties: { menu: { type: "object" }, navigation: { type: "array" } } },
345
+ site_url: { type: "string" },
346
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
347
+ },
348
+ required: ["id", "payload"],
349
+ },
350
+ },
351
+ {
352
+ name: "delete_menu",
353
+ description: "Delete a Menu entry. Refused with a 422 naming the referencing hosts (MenuBlocks, Navigation/Footer singles) if still referenced; pass force:true to delete anyway.",
354
+ inputSchema: {
355
+ type: "object",
356
+ properties: {
357
+ id: { type: "integer" },
358
+ force: { type: "boolean" },
359
+ site_url: { type: "string" },
360
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
361
+ },
362
+ required: ["id"],
173
363
  },
174
364
  },
175
365
  {
176
366
  name: "list_prefills",
177
- description: "List Prefill Tailor entries available for the `Prefill` block's `content.prefill_entry` argument.",
367
+ description: "List Prefill entries (id, title, block_count, ...). Prefills hold sections shared across pages; reference one from a page with a `Prefill` block ({content_group:'Prefill', content:{block:<id>}}). Use get_prefill / create_prefill / update_prefill / delete_prefill for CRUD.",
368
+ inputSchema: {
369
+ type: "object",
370
+ properties: {
371
+ site_url: { type: "string" },
372
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
373
+ },
374
+ },
375
+ },
376
+ {
377
+ name: "get_prefill",
378
+ description: "Fetch one Prefill entry including its builder blocks — same block shape as get_page.",
379
+ inputSchema: {
380
+ type: "object",
381
+ properties: {
382
+ id: { type: "integer" },
383
+ site_url: { type: "string" },
384
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
385
+ },
386
+ required: ["id"],
387
+ },
388
+ },
389
+ {
390
+ name: "create_prefill",
391
+ description: "Create a Prefill entry — a reusable section shared across pages. Payload: {prefill: {title (required), slug?}, builder?: [blocks]} with the SAME block shape as create_page. When several pages need the same section, create the Prefill FIRST and reference it from each page with a `Prefill` block instead of duplicating the section. (To factor out a section that already exists on a page, use extract_block_to_prefill instead.) Per-block edits work via add_block/update_block/remove_block/reorder_blocks with prefill_id.",
392
+ inputSchema: {
393
+ type: "object",
394
+ properties: {
395
+ payload: {
396
+ type: "object",
397
+ properties: { prefill: { type: "object" }, builder: { type: "array" } },
398
+ required: ["prefill"],
399
+ },
400
+ site_url: { type: "string" },
401
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
402
+ },
403
+ required: ["payload"],
404
+ },
405
+ },
406
+ {
407
+ name: "update_prefill",
408
+ description: "Update a Prefill entry. `prefill` merges title/slug/is_enabled; a `builder` array fully rebuilds the blocks (prefer the per-block tools with prefill_id — a rebuild loses human-assigned media). Edits apply everywhere the prefill is referenced.",
409
+ inputSchema: {
410
+ type: "object",
411
+ properties: {
412
+ id: { type: "integer" },
413
+ payload: { type: "object", properties: { prefill: { type: "object" }, builder: { type: "array" } } },
414
+ site_url: { type: "string" },
415
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
416
+ },
417
+ required: ["id", "payload"],
418
+ },
419
+ },
420
+ {
421
+ name: "delete_prefill",
422
+ description: "Delete a Prefill entry. Refused with a 422 naming the referencing pages if any `Prefill` block still points at it; pass force:true to delete anyway (the referencing blocks would break).",
423
+ inputSchema: {
424
+ type: "object",
425
+ properties: {
426
+ id: { type: "integer" },
427
+ force: { type: "boolean" },
428
+ site_url: { type: "string" },
429
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
430
+ },
431
+ required: ["id"],
432
+ },
433
+ },
434
+ {
435
+ name: "get_settings",
436
+ description: "Read the FrameworC plugin settings exposed over the API: navbar layout options and the custom SCSS variables. Settings are GLOBAL (not per multisite site). Integration/secret settings are not accessible by design.",
178
437
  inputSchema: {
179
438
  type: "object",
180
439
  properties: { site_url: { type: "string" } },
181
440
  },
182
441
  },
442
+ {
443
+ name: "update_settings",
444
+ description: "Update FrameworC plugin settings. `fields` is a partial map; writable keys: navigation_width (full|container), navigation_align (left|center|right), navigation_mobile_extra_links (navbar|open), variable_variablesScss (SCSS overriding the design tokens). Settings are GLOBAL and affect the whole install — confirm with the user first. Integration/secret settings cannot be read or written through this API by design.",
445
+ inputSchema: {
446
+ type: "object",
447
+ properties: {
448
+ fields: { type: "object", description: "Field name => value map (partial)." },
449
+ site_url: { type: "string" },
450
+ },
451
+ required: ["fields"],
452
+ },
453
+ },
183
454
  {
184
455
  name: "get_block_schema",
185
- description: "Get the JSON schema + usage notes for a single block by content_group name. Use this when you need to recall field enums / required fields / conditional visibility for one block.",
456
+ description: "Get the JSON schema + usage notes for one block by content_group name. Fetched live from the CMS, which derives it from the actual Tailor blueprints, so it is always current. Use this to recall field enums, defaults and conditional visibility.",
186
457
  inputSchema: {
187
458
  type: "object",
188
459
  properties: {
189
460
  name: { type: "string", enum: catalogue_js_1.BLOCKS.map((b) => b.content_group) },
461
+ site_url: { type: "string" },
462
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
190
463
  },
191
464
  required: ["name"],
192
465
  },
193
466
  },
467
+ {
468
+ name: "get_page_meta",
469
+ description: "Read a per-site single: Meta (SEO defaults, blog base path), Navigation (navbar) or Footer. Use before composing pages so titles and descriptions match the site's conventions.",
470
+ inputSchema: {
471
+ type: "object",
472
+ properties: {
473
+ handle: { type: "string", enum: ["Meta", "Navigation", "Footer"] },
474
+ site_url: { type: "string" },
475
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
476
+ },
477
+ required: ["handle"],
478
+ },
479
+ },
480
+ {
481
+ name: "update_page_meta",
482
+ description: "Update a per-site single (Meta / Navigation / Footer). Send only the fields you want to change, e.g. {handle:'Meta', fields:{metaTitle:'...', description:'...'}}. Media fields must be left empty. Affects every page on that site, so confirm with the user first.",
483
+ inputSchema: {
484
+ type: "object",
485
+ properties: {
486
+ handle: { type: "string", enum: ["Meta", "Navigation", "Footer"] },
487
+ fields: { type: "object", description: "Field name => value map." },
488
+ site_url: { type: "string" },
489
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
490
+ },
491
+ required: ["handle", "fields"],
492
+ },
493
+ },
494
+ {
495
+ name: "create_translation",
496
+ description: "Create the sibling of an existing page — or Prefill entry, with prefill:true — on another multisite site, linked to the original so the language switcher connects them. Pass target_site_id, and optionally `page` (title/slug overrides) and `builder` (the translated blocks). Omitting builder copies nothing — the translation starts empty, ready for you to fill. Form and Menu entries have no translation linking; create them per site with site_id instead.",
497
+ inputSchema: {
498
+ type: "object",
499
+ properties: {
500
+ id: { type: "integer", description: "Source page (or prefill) id." },
501
+ target_site_id: { type: "integer", description: "Site to create the translation on." },
502
+ source_site_id: { type: "integer", description: "Site the source record lives on. Defaults to the pinned CMS site." },
503
+ prefill: { type: "boolean", description: "Set true when id is a Prefill entry, not a page." },
504
+ page: { type: "object" },
505
+ builder: { type: "array" },
506
+ site_url: { type: "string" },
507
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
508
+ },
509
+ required: ["id", "target_site_id"],
510
+ },
511
+ },
194
512
  ];
195
- const server = new index_js_1.Server({ name: "frameworc-mcp", version: "0.2.0" }, { capabilities: { tools: {}, resources: {} } });
513
+ const server = new index_js_1.Server({ name: "frameworc-mcp", version: "0.4.0" }, { capabilities: { tools: {}, resources: {} } });
196
514
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
197
515
  tools,
198
516
  }));
@@ -200,6 +518,10 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
200
518
  const name = req.params.name;
201
519
  const args = (req.params.arguments ?? {});
202
520
  (0, sites_js_1.refreshIfChanged)();
521
+ // A one-off site_id argument overrides the pinned CMS site for this call.
522
+ const site = args.site_id !== undefined && args.site_id !== null
523
+ ? Number(args.site_id)
524
+ : undefined;
203
525
  if (name !== "use_site" && name !== "list_sites" && !args.site_url && !session.getUrl()) {
204
526
  return {
205
527
  content: [
@@ -254,68 +576,176 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
254
576
  content: [{ type: "text", text: JSON.stringify({ sites }, null, 2) }],
255
577
  };
256
578
  }
579
+ case "list_cms_sites": {
580
+ const r = await client.listCmsSites();
581
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
582
+ }
583
+ case "use_cms_site": {
584
+ const id = args.site_id === null || args.site_id === undefined ? null : Number(args.site_id);
585
+ session.setSiteId(id);
586
+ return {
587
+ content: [{
588
+ type: "text",
589
+ text: id === null
590
+ ? "Using the primary CMS site."
591
+ : `Using CMS site id ${id} for subsequent calls.`,
592
+ }],
593
+ };
594
+ }
257
595
  case "list_pages": {
258
- const r = await client.listPages();
596
+ const r = await client.listPages(site);
259
597
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
260
598
  }
261
599
  case "get_page": {
262
- const r = await client.getPage(Number(args.id));
600
+ const r = await client.getPage(Number(args.id), site);
263
601
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
264
602
  }
265
603
  case "create_page": {
266
- const r = await client.createPage(args.payload);
604
+ const r = await client.createPage(args.payload, site);
267
605
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
268
606
  }
269
607
  case "update_page": {
270
- const r = await client.updatePage(Number(args.id), args.payload);
608
+ const r = await client.updatePage(Number(args.id), args.payload, site);
271
609
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
272
610
  }
273
611
  case "delete_page": {
274
- const r = await client.deletePage(Number(args.id));
612
+ const r = await client.deletePage(Number(args.id), site);
613
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
614
+ }
615
+ case "create_translation": {
616
+ const payload = { page: args.page ?? {}, builder: args.builder };
617
+ const source = args.source_site_id !== undefined ? Number(args.source_site_id) : undefined;
618
+ const r = args.prefill
619
+ ? await client.createPrefillTranslation(Number(args.id), Number(args.target_site_id), { prefill: args.page ?? {}, builder: args.builder }, source)
620
+ : await client.createTranslation(Number(args.id), Number(args.target_site_id), payload, source);
275
621
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
276
622
  }
277
623
  case "add_block": {
278
- const r = await client.addBlock(Number(args.page_id), args.block, args.position !== undefined ? Number(args.position) : undefined);
624
+ const r = await client.addBlock(blockHost(args), args.block, args.position !== undefined ? Number(args.position) : undefined, site);
279
625
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
280
626
  }
281
627
  case "update_block": {
282
- const r = await client.updateBlock(Number(args.page_id), Number(args.block_id), args.block);
628
+ const r = await client.updateBlock(blockHost(args), Number(args.block_id), args.block, site);
283
629
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
284
630
  }
285
631
  case "remove_block": {
286
- const r = await client.removeBlock(Number(args.page_id), Number(args.block_id));
632
+ const r = await client.removeBlock(blockHost(args), Number(args.block_id), site);
287
633
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
288
634
  }
289
635
  case "reorder_blocks": {
290
- const r = await client.reorderBlocks(Number(args.page_id), args.order);
636
+ const r = await client.reorderBlocks(blockHost(args), args.order, site);
637
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
638
+ }
639
+ case "extract_block_to_prefill": {
640
+ const r = await client.extractBlockToPrefill(Number(args.page_id), Number(args.block_id), args.title ?? "", site);
291
641
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
292
642
  }
293
643
  case "list_forms": {
294
- const r = await client.listForms();
644
+ const r = await client.listForms(site);
645
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
646
+ }
647
+ case "get_form_schema": {
648
+ const r = await client.getFormSchema();
649
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
650
+ }
651
+ case "get_form": {
652
+ const r = await client.getForm(Number(args.id), site);
653
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
654
+ }
655
+ case "create_form": {
656
+ const r = await client.createForm(args.payload, site);
657
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
658
+ }
659
+ case "update_form": {
660
+ const r = await client.updateForm(Number(args.id), args.payload, site);
661
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
662
+ }
663
+ case "delete_form": {
664
+ const r = await client.deleteForm(Number(args.id), Boolean(args.force), site);
295
665
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
296
666
  }
297
667
  case "list_menus": {
298
- const r = await client.listMenus();
668
+ const r = await client.listMenus(site);
669
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
670
+ }
671
+ case "get_menu": {
672
+ const r = await client.getMenu(Number(args.id), site);
673
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
674
+ }
675
+ case "create_menu": {
676
+ const r = await client.createMenu(args.payload, site);
677
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
678
+ }
679
+ case "update_menu": {
680
+ const r = await client.updateMenu(Number(args.id), args.payload, site);
681
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
682
+ }
683
+ case "delete_menu": {
684
+ const r = await client.deleteMenu(Number(args.id), Boolean(args.force), site);
299
685
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
300
686
  }
301
687
  case "list_prefills": {
302
- const r = await client.listPrefills();
688
+ const r = await client.listPrefills(site);
689
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
690
+ }
691
+ case "get_prefill": {
692
+ const r = await client.getPrefill(Number(args.id), site);
693
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
694
+ }
695
+ case "create_prefill": {
696
+ const r = await client.createPrefill(args.payload, site);
697
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
698
+ }
699
+ case "update_prefill": {
700
+ const r = await client.updatePrefill(Number(args.id), args.payload, site);
701
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
702
+ }
703
+ case "delete_prefill": {
704
+ const r = await client.deletePrefill(Number(args.id), Boolean(args.force), site);
705
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
706
+ }
707
+ case "get_settings": {
708
+ const r = await client.getSettings();
709
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
710
+ }
711
+ case "update_settings": {
712
+ const r = await client.updateSettings(args.fields ?? {});
713
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
714
+ }
715
+ case "get_page_meta": {
716
+ const r = await client.getSingle(String(args.handle), site);
717
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
718
+ }
719
+ case "update_page_meta": {
720
+ const r = await client.updateSingle(String(args.handle), args.fields ?? {}, site);
303
721
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
304
722
  }
305
723
  case "get_block_schema": {
306
- const block = catalogue_js_1.BLOCKS.find((b) => b.content_group === args.name);
307
- if (!block) {
724
+ // Served by the CMS so it always matches the live blueprints; the
725
+ // bundled catalogue is only a fallback for an older plugin version.
726
+ try {
727
+ const r = await client.getBlockSchema(String(args.name));
728
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
729
+ }
730
+ catch (err) {
731
+ const block = catalogue_js_1.BLOCKS.find((b) => b.content_group === args.name);
732
+ if (!block) {
733
+ return {
734
+ content: [{
735
+ type: "text",
736
+ text: `Unknown block "${args.name}". Available: ${catalogue_js_1.BLOCKS.map((b) => b.content_group).join(", ")}`,
737
+ }],
738
+ isError: true,
739
+ };
740
+ }
308
741
  return {
309
- content: [
310
- {
742
+ content: [{
311
743
  type: "text",
312
- text: `Unknown block "${args.name}". Available: ${catalogue_js_1.BLOCKS.map((b) => b.content_group).join(", ")}`,
313
- },
314
- ],
315
- isError: true,
744
+ text: `(served from the bundled catalogue; live schema unavailable: ${err?.message ?? err})\n`
745
+ + JSON.stringify(block, null, 2),
746
+ }],
316
747
  };
317
748
  }
318
- return { content: [{ type: "text", text: JSON.stringify(block, null, 2) }] };
319
749
  }
320
750
  default: {
321
751
  return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };