frameworc-mcp 0.3.0 → 0.5.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/README.md +46 -20
- package/dist/api-client.js +93 -10
- package/dist/catalogue.js +41 -18
- package/dist/index.js +396 -28
- package/package.json +33 -33
package/README.md
CHANGED
|
@@ -6,8 +6,11 @@ A [Model Context Protocol](https://modelcontextprotocol.io) server that lets Cla
|
|
|
6
6
|
|
|
7
7
|
- Composes 15 prebuilt FrameworC blocks (`Header`, `Section`, `Tiles`, `Slider`, `Tabs`, `Accordion`, `Form`, `Gallery`, `Downloads`, `Columns`, `Prefill`, `BlogList`, `MenuBlock`, `ImageStrip`, `InstaFeed`) into pages
|
|
8
8
|
- Lists existing pages and their full builder JSON
|
|
9
|
-
- Creates pages, adds / updates / removes / reorders individual blocks
|
|
10
|
-
-
|
|
9
|
+
- Creates pages, adds / updates / removes / reorders individual blocks — on pages and on Prefill entries
|
|
10
|
+
- Full CRUD for `Form` entries (incl. their field rows), `Menu` entries (incl. the navigation tree) and `Prefill` entries (incl. their builder blocks)
|
|
11
|
+
- Extracts a repeated page section into a Prefill entry and replaces it with a reference (`extract_block_to_prefill`) — the intended FrameworC de-duplication workflow
|
|
12
|
+
- Reads and writes the per-site singles (Meta & SEO, Navigation, Footer) and the global FrameworC settings (navbar options, SCSS variables; integration secrets are not accessible by design)
|
|
13
|
+
- Multisite-aware: every content tool takes/pins a `site_id`; page and prefill translations are linked for the language switcher
|
|
11
14
|
- Publishes the block catalogue as an MCP resource (`frameworc://blocks`) so the chat agent knows each block's fields, defaults, and when-to-use notes
|
|
12
15
|
- Draft by default — created pages have `is_enabled = false`; the human flips the switch in the OctCMS backend after assigning images
|
|
13
16
|
|
|
@@ -23,10 +26,10 @@ node dist/index.js (local, spawned by the chat client)
|
|
|
23
26
|
reads ~/.config/frameworc/sites.json (auto-created on first run; hot-reloaded)
|
|
24
27
|
│ HTTPS + Authorization: Bearer <token resolved by URL>
|
|
25
28
|
▼
|
|
26
|
-
any OctCMS install /api/mcp/v1/* (provided by the
|
|
29
|
+
any OctCMS install /api/mcp/v1/* (provided by the crscompany/frameworcmcp plugin v1.1.0+)
|
|
27
30
|
```
|
|
28
31
|
|
|
29
|
-
No Docker, no ToolHive, no remote gateway, no second auth layer. Each OctCMS install has its own bearer token stored in its
|
|
32
|
+
No Docker, no ToolHive, no remote gateway, no second auth layer. Each OctCMS install has its own bearer token stored in its backend under `Settings → FrameworC → MCP API`, and your laptop keeps the URL → token map in one local file.
|
|
30
33
|
|
|
31
34
|
## Install
|
|
32
35
|
|
|
@@ -93,8 +96,8 @@ If the file is missing or empty, `use_site` errors with a pointer to where to ad
|
|
|
93
96
|
|
|
94
97
|
### Adding a new OctCMS site
|
|
95
98
|
|
|
96
|
-
1. Spin up the new OctCMS install with the FrameworC plugin (v1.
|
|
97
|
-
2. Backend → Settings → FrameworC →
|
|
99
|
+
1. Spin up the new OctCMS install with the FrameworC suite incl. the `crscompany/frameworcmcp` plugin (v1.2.0 or newer; media assignment needs v1.2.0).
|
|
100
|
+
2. Backend → Settings → FrameworC → **MCP API** → paste a freshly generated random string (e.g. `openssl rand -hex 32`) → Save.
|
|
98
101
|
3. Edit `~/.config/frameworc/sites.json` on your laptop, add one entry: `{ "label": "New Client", "url": "https://newsite.test", "token": "<that string>" }`.
|
|
99
102
|
4. In your next chat (no Claude Desktop restart needed): *"Use `https://newsite.test`."* → MCP hot-reloads the file, finds the token, sends it. Done.
|
|
100
103
|
|
|
@@ -106,25 +109,39 @@ No env var, no chat-client config edit, no ToolHive touch, no repo push. The who
|
|
|
106
109
|
- A leak of the file compromises every site listed in it. Treat it like an SSH private key — back it up, rotate tokens periodically, never commit it to git.
|
|
107
110
|
- One-off override: `use_site("https://X", "token-string")` lets you pass a token inline (without storing it) for the duration of the chat session. Useful for testing a token before saving it.
|
|
108
111
|
|
|
109
|
-
## Tools (
|
|
112
|
+
## Tools (0.5.0)
|
|
110
113
|
|
|
111
114
|
| Tool | Description |
|
|
112
115
|
|---|---|
|
|
113
116
|
| `use_site(url, site_token?)` | Pin the target OctCMS site for the session. Token resolved from `~/.config/frameworc/sites.json` by URL, or supplied via the optional `site_token` arg. |
|
|
114
117
|
| `list_sites` | List configured sites (label + URL only, no tokens) from `sites.json`. |
|
|
118
|
+
| `list_cms_sites` | List the multisite sites (languages) inside the pinned install. |
|
|
119
|
+
| `use_cms_site(site_id)` | Pin the multisite site for subsequent calls (null = primary). |
|
|
115
120
|
| `list_pages` | List all pages on the pinned site. |
|
|
116
121
|
| `get_page(id)` | Full page JSON (page meta + builder blocks + nested repeaters). |
|
|
117
122
|
| `create_page(payload)` | Create a draft page. `payload = { page: {...}, builder: [...] }`. |
|
|
118
|
-
| `update_page(id, payload)` | Edit page meta, or full-rebuild the builder array. |
|
|
123
|
+
| `update_page(id, payload)` | Edit page meta, or full-rebuild the builder array (prefer the per-block tools). |
|
|
119
124
|
| `delete_page(id)` | Soft-delete a page. |
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
125
|
+
| `create_translation(id, target_site_id, prefill?)` | Linked sibling of a page (or Prefill with `prefill:true`) on another multisite site. |
|
|
126
|
+
| `add_block(page_id \| prefill_id, block, position?)` | Append (or insert at `position`) a block. |
|
|
127
|
+
| `update_block(page_id \| prefill_id, block_id, block)` | Replace one block by row id (new id returned). A media field you omit is inherited; one you send is written verbatim, so `""` clears it. |
|
|
128
|
+
| `remove_block(page_id \| prefill_id, block_id)` | Remove one block by row id. |
|
|
129
|
+
| `reorder_blocks(page_id \| prefill_id, order)` | Reorder blocks (order = array of all row ids in new order). |
|
|
130
|
+
| `extract_block_to_prefill(page_id, block_id, title)` | Move a page block into a new Prefill entry (lossless from plugin v1.2.0) and reference it in place. |
|
|
131
|
+
| `list_forms` / `get_form(id)` | Form entries; `get_form` includes the `fwcFields` rows. |
|
|
132
|
+
| `get_form_schema` | Live field-group catalogue for authoring forms. |
|
|
133
|
+
| `create_form` / `update_form` / `delete_form` | Form CRUD. `fwcFields` rows: `{group, label, name, required, width, ...}`; delete guarded unless `force:true`. |
|
|
134
|
+
| `list_menus` / `get_menu(id)` | Menu entries; `get_menu` includes the navigation tree. |
|
|
135
|
+
| `create_menu` / `update_menu` / `delete_menu` | Menu CRUD. Tree items `{title, url \| {page_id}, anchor, blank, children}`, max 2 levels; delete guarded. |
|
|
136
|
+
| `list_prefills` / `get_prefill(id)` | Prefill entries; `get_prefill` includes the builder blocks. |
|
|
137
|
+
| `create_prefill` / `update_prefill` / `delete_prefill` | Prefill CRUD — same block shape as pages; delete guarded. |
|
|
138
|
+
| `get_page_meta(handle)` / `update_page_meta(handle, fields)` | Per-site singles: `Meta` (SEO), `Navigation` (navbar incl. `nav` menu link + buttons), `Footer` (incl. `socials` rows + `nav`). |
|
|
139
|
+
| `get_settings` / `update_settings(fields)` | Global FrameworC settings: navbar options + custom SCSS variables. Integration secrets are not exposed. |
|
|
140
|
+
| `get_block_schema(name)` | Field schema + usage notes for one block (live from the CMS). |
|
|
141
|
+
| `list_media(folder?, type?, sort?, limit?, offset?)` | Browse the media library. Returns files with their `path` — write that into a mediafinder field. |
|
|
142
|
+
| `search_media(q, folder?, type?, ...)` | Find library files by name across all folders. Every whitespace-separated word must appear in the path. |
|
|
143
|
+
|
|
144
|
+
Form and Menu entries have no translation linking — create them per site by passing `site_id`.
|
|
128
145
|
|
|
129
146
|
## Resource
|
|
130
147
|
|
|
@@ -173,7 +190,7 @@ No env var, no chat-client config edit, no ToolHive touch, no repo push. The who
|
|
|
173
190
|
{
|
|
174
191
|
"content_group": "Form",
|
|
175
192
|
"base": { "blockId": "formular", "headline": "<h2>Write us</h2>" },
|
|
176
|
-
"
|
|
193
|
+
"form": 7,
|
|
177
194
|
"content": { "variant": "default" }
|
|
178
195
|
}
|
|
179
196
|
]
|
|
@@ -182,14 +199,23 @@ No env var, no chat-client config edit, no ToolHive touch, no repo push. The who
|
|
|
182
199
|
|
|
183
200
|
### Storage encoding (what the MCP understands)
|
|
184
201
|
|
|
185
|
-
- Media fields (`
|
|
202
|
+
- Media fields hold **media library paths**, root-relative with a leading slash (`/images/hero.jpg`). Assignment only: the API cannot upload, and a path that is not in the library is a 422. Single-item fields (`maxItems: 1`) are strings and read as `""` when unset; multi fields are arrays and read as `[]`. Find paths with `search_media` / `list_media`.
|
|
186
203
|
- `switch` fields accept booleans; the API normalises to `"1"` / `"0"` for storage.
|
|
187
204
|
- `customCssClass` + `responsiveHide` are arrays of strings.
|
|
188
|
-
- Multi-`mediafinder` fields
|
|
189
|
-
- `entries` links are integers: `
|
|
205
|
+
- Multi-`mediafinder` fields are arrays of path strings: `Gallery.images` and `ImageStrip.images`, where **every** item renders, plus `Section.content.image` and the `Footer` single's `logo`, which are arrays in storage but of which the theme renders **only the first item**.
|
|
206
|
+
- `entries` links are integers (or `{id: n}`): `form` (block level for `Form`), `content.menu` (for `MenuBlock`), `content.block` (for `Prefill`). Reads return them as `{id, title}`.
|
|
190
207
|
- `Slider` accepts an optional `content.breakpoints = { tablet: number, mobile: number }`.
|
|
191
208
|
- `Columns` blocks have `content.columns = [{ blockId, builder: [...blocks] }]` — recursive: each column's `builder` follows the exact same shape as the top-level `builder`.
|
|
192
209
|
|
|
210
|
+
### Working with media
|
|
211
|
+
|
|
212
|
+
`mode: image` on a blueprint field is only a hint for the backend widget; October never enforces it, and real FrameworC content relies on the slack. The API therefore accepts any image, SVG or video extension in a `mode: image` field and rejects the rest (a `.pdf` in a hero would break `resize()`), which means:
|
|
213
|
+
|
|
214
|
+
- The `Navigation` logo is an SVG and `Header.image` holds an `.mp4` when `isVideoBg` is on. Both are valid.
|
|
215
|
+
- `buttonIcon1..4` and `Downloads`' `file` declare no `mode` at all, so any file type is accepted there.
|
|
216
|
+
- Whether SVG reports as `file_type: image` or `document` depends on the install's `media.image_extensions` config, so treat `list_media`'s `type` filter as a convenience rather than a guarantee.
|
|
217
|
+
- Folders are not selectable, and filenames containing `+ # % !` are unreachable through the API — an October media library limitation that applies to its own backend manager too.
|
|
218
|
+
|
|
193
219
|
## Catalogue drift
|
|
194
220
|
|
|
195
221
|
The hand-authored block catalogue lives in `src/catalogue.ts`. It mirrors `plugins/crscompany/frameworc/blueprints/Blocks/*.yaml` + `Mixins/Buttons.yaml` + `Mixins/Buttons2.yaml` + `BaseBlock.yaml`. If a block blueprint changes (new field, new enum value, removed field), update `src/catalogue.ts` to match and rebuild. No generator script — kept manual because blocks change rarely per the FrameworC convention.
|
package/dist/api-client.js
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ApiClient = void 0;
|
|
4
|
+
/** Renders the defined params as a leading-`?` query string, or "" when empty. */
|
|
5
|
+
function qs(params) {
|
|
6
|
+
const search = new URLSearchParams();
|
|
7
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
9
|
+
search.set(key, String(value));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const rendered = search.toString();
|
|
13
|
+
return rendered ? `?${rendered}` : "";
|
|
14
|
+
}
|
|
4
15
|
class ApiClient {
|
|
5
16
|
session;
|
|
6
17
|
constructor(session) {
|
|
@@ -60,6 +71,13 @@ class ApiClient {
|
|
|
60
71
|
getBlockSchema(name) {
|
|
61
72
|
return this.request(`/blocks/${encodeURIComponent(name)}`);
|
|
62
73
|
}
|
|
74
|
+
// --- media library (global) -------------------------------------------
|
|
75
|
+
listMedia(params = {}, siteId) {
|
|
76
|
+
return this.request(this.q(`/media${qs(params)}`, siteId));
|
|
77
|
+
}
|
|
78
|
+
searchMedia(query, params = {}, siteId) {
|
|
79
|
+
return this.request(this.q(`/media/search${qs({ q: query, ...params })}`, siteId));
|
|
80
|
+
}
|
|
63
81
|
// --- pages ------------------------------------------------------------
|
|
64
82
|
listPages(siteId) {
|
|
65
83
|
return this.request(this.q("/pages", siteId));
|
|
@@ -84,38 +102,103 @@ class ApiClient {
|
|
|
84
102
|
}
|
|
85
103
|
return this.request(`/pages/${id}/translations`, { method: "POST", body: JSON.stringify(body) });
|
|
86
104
|
}
|
|
87
|
-
// --- blocks (per
|
|
88
|
-
|
|
89
|
-
return
|
|
105
|
+
// --- blocks (per page or per prefill) ---------------------------------
|
|
106
|
+
blockBase(host) {
|
|
107
|
+
return host.type === "prefill" ? `/prefills/${host.id}` : `/pages/${host.id}`;
|
|
108
|
+
}
|
|
109
|
+
addBlock(host, block, position, siteId) {
|
|
110
|
+
return this.request(`${this.blockBase(host)}/blocks`, {
|
|
90
111
|
method: "POST",
|
|
91
112
|
body: this.b({ block, position }, siteId),
|
|
92
113
|
});
|
|
93
114
|
}
|
|
94
|
-
updateBlock(
|
|
95
|
-
return this.request(
|
|
115
|
+
updateBlock(host, blockId, block, siteId) {
|
|
116
|
+
return this.request(`${this.blockBase(host)}/blocks/${blockId}`, {
|
|
96
117
|
method: "PATCH",
|
|
97
118
|
body: this.b(block, siteId),
|
|
98
119
|
});
|
|
99
120
|
}
|
|
100
|
-
removeBlock(
|
|
101
|
-
return this.request(this.q(
|
|
121
|
+
removeBlock(host, blockId, siteId) {
|
|
122
|
+
return this.request(this.q(`${this.blockBase(host)}/blocks/${blockId}`, siteId), { method: "DELETE" });
|
|
102
123
|
}
|
|
103
|
-
reorderBlocks(
|
|
104
|
-
return this.request(
|
|
124
|
+
reorderBlocks(host, order, siteId) {
|
|
125
|
+
return this.request(`${this.blockBase(host)}/blocks/order`, {
|
|
105
126
|
method: "POST",
|
|
106
127
|
body: this.b({ order }, siteId),
|
|
107
128
|
});
|
|
108
129
|
}
|
|
109
|
-
|
|
130
|
+
extractBlockToPrefill(pageId, blockId, title, siteId) {
|
|
131
|
+
return this.request(`/pages/${pageId}/blocks/${blockId}/extract-to-prefill`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
body: this.b({ title }, siteId),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
// --- forms --------------------------------------------------------------
|
|
110
137
|
listForms(siteId) {
|
|
111
138
|
return this.request(this.q("/forms", siteId));
|
|
112
139
|
}
|
|
140
|
+
getFormSchema() {
|
|
141
|
+
return this.request("/forms/schema");
|
|
142
|
+
}
|
|
143
|
+
getForm(id, siteId) {
|
|
144
|
+
return this.request(this.q(`/forms/${id}`, siteId));
|
|
145
|
+
}
|
|
146
|
+
createForm(payload, siteId) {
|
|
147
|
+
return this.request("/forms", { method: "POST", body: this.b(payload, siteId) });
|
|
148
|
+
}
|
|
149
|
+
updateForm(id, payload, siteId) {
|
|
150
|
+
return this.request(`/forms/${id}`, { method: "PATCH", body: this.b(payload, siteId) });
|
|
151
|
+
}
|
|
152
|
+
deleteForm(id, force, siteId) {
|
|
153
|
+
return this.request(this.q(`/forms/${id}${force ? "?force=1" : ""}`, siteId), { method: "DELETE" });
|
|
154
|
+
}
|
|
155
|
+
// --- menus --------------------------------------------------------------
|
|
113
156
|
listMenus(siteId) {
|
|
114
157
|
return this.request(this.q("/menus", siteId));
|
|
115
158
|
}
|
|
159
|
+
getMenu(id, siteId) {
|
|
160
|
+
return this.request(this.q(`/menus/${id}`, siteId));
|
|
161
|
+
}
|
|
162
|
+
createMenu(payload, siteId) {
|
|
163
|
+
return this.request("/menus", { method: "POST", body: this.b(payload, siteId) });
|
|
164
|
+
}
|
|
165
|
+
updateMenu(id, payload, siteId) {
|
|
166
|
+
return this.request(`/menus/${id}`, { method: "PATCH", body: this.b(payload, siteId) });
|
|
167
|
+
}
|
|
168
|
+
deleteMenu(id, force, siteId) {
|
|
169
|
+
return this.request(this.q(`/menus/${id}${force ? "?force=1" : ""}`, siteId), { method: "DELETE" });
|
|
170
|
+
}
|
|
171
|
+
// --- prefills -----------------------------------------------------------
|
|
116
172
|
listPrefills(siteId) {
|
|
117
173
|
return this.request(this.q("/prefills", siteId));
|
|
118
174
|
}
|
|
175
|
+
getPrefill(id, siteId) {
|
|
176
|
+
return this.request(this.q(`/prefills/${id}`, siteId));
|
|
177
|
+
}
|
|
178
|
+
createPrefill(payload, siteId) {
|
|
179
|
+
return this.request("/prefills", { method: "POST", body: this.b(payload, siteId) });
|
|
180
|
+
}
|
|
181
|
+
updatePrefill(id, payload, siteId) {
|
|
182
|
+
return this.request(`/prefills/${id}`, { method: "PATCH", body: this.b(payload, siteId) });
|
|
183
|
+
}
|
|
184
|
+
deletePrefill(id, force, siteId) {
|
|
185
|
+
return this.request(this.q(`/prefills/${id}${force ? "?force=1" : ""}`, siteId), { method: "DELETE" });
|
|
186
|
+
}
|
|
187
|
+
createPrefillTranslation(id, targetSiteId, payload, sourceSiteId) {
|
|
188
|
+
const body = { ...(payload ?? {}), site_id: targetSiteId };
|
|
189
|
+
const source = sourceSiteId ?? this.session.getSiteId();
|
|
190
|
+
if (source !== null && source !== undefined) {
|
|
191
|
+
body.source_site_id = source;
|
|
192
|
+
}
|
|
193
|
+
return this.request(`/prefills/${id}/translations`, { method: "POST", body: JSON.stringify(body) });
|
|
194
|
+
}
|
|
195
|
+
// --- FrameworC settings (global) ---------------------------------------
|
|
196
|
+
getSettings() {
|
|
197
|
+
return this.request("/settings");
|
|
198
|
+
}
|
|
199
|
+
updateSettings(fields) {
|
|
200
|
+
return this.request("/settings", { method: "PATCH", body: JSON.stringify({ fields }) });
|
|
201
|
+
}
|
|
119
202
|
// --- singles ----------------------------------------------------------
|
|
120
203
|
getSingle(handle, siteId) {
|
|
121
204
|
return this.request(this.q(`/singles/${encodeURIComponent(handle)}`, siteId));
|
package/dist/catalogue.js
CHANGED
|
@@ -6,7 +6,7 @@ const buttonsMixin = [
|
|
|
6
6
|
{ name: "buttonVariant1", type: "dropdown", enum: ["default", "outline", "blurLight", "blurDark", "plain"], default: "default" },
|
|
7
7
|
{ name: "buttonContrast1", type: "switch", default: false },
|
|
8
8
|
{ name: "buttonLink1", type: "text", comment: "Pagefinder path or URL for button 1 (left blank = no link)" },
|
|
9
|
-
{ name: "buttonIcon1", type: "media-single", comment: "
|
|
9
|
+
{ name: "buttonIcon1", type: "media-single", comment: "Media library path, e.g. \"/icons/arrow.svg\". Find one with search_media. No mode restriction, so any file type is accepted." },
|
|
10
10
|
{ name: "buttonBlank1", type: "switch", default: false },
|
|
11
11
|
{ name: "buttonLabel2", type: "text", comment: "Text of button 2" },
|
|
12
12
|
{ name: "buttonVariant2", type: "dropdown", enum: ["default", "outline", "blurLight", "blurDark", "plain"], default: "default" },
|
|
@@ -35,7 +35,7 @@ const baseBlock = [
|
|
|
35
35
|
{ name: "elevated", type: "switch", default: false, comment: "Lifts this block up into the previous block visually." },
|
|
36
36
|
{ name: "containerWidth", type: "dropdown", enum: ["default", "narrow", "full", "pop"], default: "default" },
|
|
37
37
|
{ name: "backgroundColor", type: "dropdown", enum: ["default", "accent", "contrast", "image", "containerAccent", "containerContrast", "containerImage"], default: "default" },
|
|
38
|
-
{ name: "backgroundImage", type: "media-single", comment: "
|
|
38
|
+
{ name: "backgroundImage", type: "media-single", comment: "Media library path from list_media/search_media. Only renders when backgroundColor is 'image' or 'containerImage'." },
|
|
39
39
|
{ name: "backgroundImageMobile", type: "media-single" },
|
|
40
40
|
{ name: "customCssClass", type: "taglist", enum: ["animate", "noSpacing", "noSpacingTop", "noSpacingBottom", "narrowSpacing", "centerButtons"], comment: "Array of class keys to apply as Builder-container--<class>." },
|
|
41
41
|
{ name: "aliasOverride", type: "text", comment: "Custom component alias (theme partial hook). Leave blank unless specifically needed." },
|
|
@@ -53,7 +53,7 @@ exports.BLOCKS = [
|
|
|
53
53
|
whenToUse: "Always first block on a page. Use H1/H2 here (other blocks use H2/H3). One per page.",
|
|
54
54
|
base: baseBlock,
|
|
55
55
|
content: [
|
|
56
|
-
{ name: "image", type: "media-single", comment: "
|
|
56
|
+
{ name: "image", type: "media-single", comment: "Media library path. Declared mode:image, but an .mp4 is valid here when isVideoBg is on." },
|
|
57
57
|
{ name: "imageMobile", type: "media-single" },
|
|
58
58
|
{ name: "isVideoBg", type: "switch", default: false, comment: "If true, 'image' is treated as a video file and 'imageMobile' as poster." },
|
|
59
59
|
...buttonsMixin,
|
|
@@ -71,7 +71,7 @@ exports.BLOCKS = [
|
|
|
71
71
|
content: [
|
|
72
72
|
{ name: "variant", type: "dropdown", enum: sectionVariants, default: "halfAndHalf" },
|
|
73
73
|
{ name: "reverse", type: "switch", default: false, hiddenWhen: { field: "variant", equals: ["noText", "noImage", "embedOnly"] }, comment: "Swap sides (image left vs right)." },
|
|
74
|
-
{ name: "image", type: "media-
|
|
74
|
+
{ name: "image", type: "media-multi", comment: "Stored as an ARRAY of paths (the blueprint sets no maxItems) but the theme renders only the FIRST item, so send [\"/photo.jpg\"].", hiddenWhen: { field: "variant", equals: ["noImage", "embedHalfAndHalf", "embedOnly", "embed30", "embed70", "textAndText"] } },
|
|
75
75
|
{ name: "embed", type: "codeeditor", shownWhen: { field: "variant", equals: ["embedHalfAndHalf", "embedOnly", "embed30", "embed70"] }, comment: "Raw embed HTML (iframe etc.)." },
|
|
76
76
|
...buttonsMixin,
|
|
77
77
|
{ name: "headline2", type: "richeditor", shownWhen: { field: "variant", equals: ["textAndText"] }, comment: "Second column text (only when variant is 'textAndText')." },
|
|
@@ -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,
|
|
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: "
|
|
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 },
|
|
@@ -102,7 +102,7 @@ exports.BLOCKS = [
|
|
|
102
102
|
description: "One repeater item per tile/card.",
|
|
103
103
|
items: [
|
|
104
104
|
{ name: "headline", type: "text", required: true, comment: "Tile title." },
|
|
105
|
-
{ name: "image", type: "media-single", comment: "Tile image
|
|
105
|
+
{ name: "image", type: "media-single", comment: "Tile image: a media library path, or \"\" for a text-only tile." },
|
|
106
106
|
{ name: "text", type: "richeditor", comment: "Tile body text." },
|
|
107
107
|
...buttonsMixin,
|
|
108
108
|
],
|
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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." },
|
|
@@ -205,10 +205,10 @@ exports.BLOCKS = [
|
|
|
205
205
|
content_group: "Gallery",
|
|
206
206
|
name: "Gallery (masonry)",
|
|
207
207
|
description: "Masonry image gallery.",
|
|
208
|
-
whenToUse: "Multiple images shown together in a grid
|
|
208
|
+
whenToUse: "Multiple images shown together in a grid, lightbox on expand. 'images' is an array of media library paths and every item renders.",
|
|
209
209
|
base: baseBlock,
|
|
210
210
|
content: [
|
|
211
|
-
{ name: "images", type: "media-multi", comment: "Array of
|
|
211
|
+
{ name: "images", type: "media-multi", comment: "Array of media library paths, e.g. [\"/a.jpg\",\"/b.jpg\"]. Each must already exist; find them with search_media." },
|
|
212
212
|
{ name: "columns", type: "number", default: 3, comment: "Number of columns (blank = 3)." },
|
|
213
213
|
],
|
|
214
214
|
},
|
|
@@ -216,7 +216,7 @@ exports.BLOCKS = [
|
|
|
216
216
|
content_group: "Downloads",
|
|
217
217
|
name: "Downloads",
|
|
218
218
|
description: "List of downloadable files (name + icon + description + file each).",
|
|
219
|
-
whenToUse: "Document / spec / PDF lists. Each row has a name, an icon dropdown, an optional description, and a file
|
|
219
|
+
whenToUse: "Document / spec / PDF lists. Each row has a name, an icon dropdown, an optional description, and a file taken from the media library.",
|
|
220
220
|
base: baseBlock,
|
|
221
221
|
content: [],
|
|
222
222
|
nestedRepeaters: [
|
|
@@ -227,7 +227,7 @@ exports.BLOCKS = [
|
|
|
227
227
|
{ name: "name", type: "text", required: true },
|
|
228
228
|
{ name: "icon", type: "dropdown", enum: ["none", "file", "pdf", "dl"], default: "none" },
|
|
229
229
|
{ name: "description", type: "richeditor" },
|
|
230
|
-
{ name: "file", type: "media-single", comment: "
|
|
230
|
+
{ name: "file", type: "media-single", comment: "Media library path to the document, e.g. \"/docs/spec.pdf\". No mode restriction, so any file type is accepted." },
|
|
231
231
|
],
|
|
232
232
|
},
|
|
233
233
|
],
|
|
@@ -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: "
|
|
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,7 +254,7 @@ 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
260
|
referencing: { kind: "prefill", location: "content", jsonKey: "block", column: "block_id" },
|
|
@@ -288,7 +288,7 @@ exports.BLOCKS = [
|
|
|
288
288
|
whenToUse: "Brand/logos row, partners strip, photo strip with horizontal scroll. Optional auto-scroll.",
|
|
289
289
|
base: baseBlock,
|
|
290
290
|
content: [
|
|
291
|
-
{ name: "images", type: "media-multi", comment: "Array of
|
|
291
|
+
{ name: "images", type: "media-multi", comment: "Array of media library paths; every item renders, side by side." },
|
|
292
292
|
{ name: "centered", type: "switch", default: false, comment: "Center the strip horizontally." },
|
|
293
293
|
{ name: "autoScroll", type: "switch", default: false },
|
|
294
294
|
{ name: "stopOnHover", type: "switch", default: false },
|
|
@@ -308,7 +308,30 @@ exports.BLOCKS = [
|
|
|
308
308
|
],
|
|
309
309
|
},
|
|
310
310
|
];
|
|
311
|
-
|
|
311
|
+
/**
|
|
312
|
+
* Lands once, ahead of every block, rather than being repeated per field.
|
|
313
|
+
* Media is the part of a block payload that models get wrong most often.
|
|
314
|
+
*/
|
|
315
|
+
const MEDIA_PREAMBLE = [
|
|
316
|
+
"## Media fields",
|
|
317
|
+
"",
|
|
318
|
+
"`media-single` takes ONE path string; `media-multi` takes an ARRAY of path",
|
|
319
|
+
"strings. Paths are root-relative with a leading slash, exactly as stored:",
|
|
320
|
+
'"/hero.jpg", "/logos/brand.svg".',
|
|
321
|
+
"",
|
|
322
|
+
"The file must ALREADY EXIST in the library. Call search_media (by name) or",
|
|
323
|
+
"list_media (by folder) and copy the `path` from the result — never the `url`,",
|
|
324
|
+
"and never invent a filename: a path that does not resolve is rejected. This",
|
|
325
|
+
"server cannot upload. If what the user wants is not in the library, say so",
|
|
326
|
+
"and ask them to add it in Backend > Media.",
|
|
327
|
+
"",
|
|
328
|
+
'To leave a field unset send "" (single) or [] (multi). On update_block a',
|
|
329
|
+
"media key you OMIT keeps whatever is stored, while a key you SEND is written",
|
|
330
|
+
'as given — so sending "" is how you clear an image.',
|
|
331
|
+
"",
|
|
332
|
+
"",
|
|
333
|
+
].join("\n");
|
|
334
|
+
exports.BLOCK_CATALOGUE_TEXT = MEDIA_PREAMBLE + exports.BLOCKS.map((b) => {
|
|
312
335
|
const lines = [];
|
|
313
336
|
lines.push(`### ${b.name} (content_group="${b.content_group}")`);
|
|
314
337
|
lines.push(`When to use: ${b.whenToUse}`);
|
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",
|
|
@@ -82,7 +93,7 @@ const tools = [
|
|
|
82
93
|
},
|
|
83
94
|
{
|
|
84
95
|
name: "create_page",
|
|
85
|
-
description: "Create a new FrameworC page. Send a `page` object (title, slug, fullslug, is_enabled, metaTitle, metaDescription, menuStyle, menuHide) and a `builder` array of block objects per the frameworc://blocks resource. Pages are created with is_enabled=false (draft) by default. Media fields
|
|
96
|
+
description: "Create a new FrameworC page. Send a `page` object (title, slug, fullslug, is_enabled, metaTitle, metaDescription, menuStyle, menuHide) and a `builder` array of block objects per the frameworc://blocks resource. Pages are created with is_enabled=false (draft) by default. Media fields take a media library path such as \"/images/hero.jpg\" — find one with search_media or list_media; the file must already exist, as this server cannot upload. Leave a media field as \"\" (single) or [] (multi) to leave it unset. After creating, surface the returned page id so the user can review and flip is_enabled to publish.",
|
|
86
97
|
inputSchema: {
|
|
87
98
|
type: "object",
|
|
88
99
|
properties: {
|
|
@@ -103,7 +114,7 @@ const tools = [
|
|
|
103
114
|
},
|
|
104
115
|
{
|
|
105
116
|
name: "update_page",
|
|
106
|
-
description: "Update an existing page. Partial updates: send only `page` (meta edits) and/or `builder` (full rebuild — all existing blocks are deleted and the new array is written). To
|
|
117
|
+
description: "Update an existing page. Partial updates: send only `page` (meta edits) and/or `builder` (full rebuild — all existing blocks are deleted and the new array is written). A rebuild also rewrites media, so send the builder array exactly as get_page returned it, paths included, or the existing images are cleared. To add, edit or remove one block, prefer add_block / update_block / remove_block. `page.ogImage` takes a media library path.",
|
|
107
118
|
inputSchema: {
|
|
108
119
|
type: "object",
|
|
109
120
|
properties: {
|
|
@@ -130,86 +141,352 @@ const tools = [
|
|
|
130
141
|
},
|
|
131
142
|
{
|
|
132
143
|
name: "add_block",
|
|
133
|
-
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? } per the catalogue — `form` sits at block level, while Menu and Prefill references live at `content.menu` and `content.block`.",
|
|
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).",
|
|
134
145
|
inputSchema: {
|
|
135
146
|
type: "object",
|
|
136
147
|
properties: {
|
|
137
|
-
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." },
|
|
138
150
|
block: { type: "object" },
|
|
139
151
|
position: { type: "integer" },
|
|
140
152
|
site_url: { type: "string" },
|
|
141
153
|
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
142
154
|
},
|
|
143
|
-
required: ["
|
|
155
|
+
required: ["block"],
|
|
144
156
|
},
|
|
145
157
|
},
|
|
146
158
|
{
|
|
147
159
|
name: "update_block",
|
|
148
|
-
description: "Replace a single block by id (the row id
|
|
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. A media field you OMIT is inherited from the old block; one you SEND is written verbatim, so sending \"\" (or [] for a multi field) is how you clear an image. Keep each repeater row's `id` from get_page so media follows the right row. Inheritance is skipped when you change content_group. Other blocks are untouched.",
|
|
149
161
|
inputSchema: {
|
|
150
162
|
type: "object",
|
|
151
163
|
properties: {
|
|
152
|
-
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." },
|
|
153
166
|
block_id: { type: "integer" },
|
|
154
167
|
block: { type: "object" },
|
|
155
168
|
site_url: { type: "string" },
|
|
156
169
|
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
157
170
|
},
|
|
158
|
-
required: ["
|
|
171
|
+
required: ["block_id", "block"],
|
|
159
172
|
},
|
|
160
173
|
},
|
|
161
174
|
{
|
|
162
175
|
name: "remove_block",
|
|
163
|
-
description: "Remove a block by its row id from
|
|
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.",
|
|
164
177
|
inputSchema: {
|
|
165
178
|
type: "object",
|
|
166
179
|
properties: {
|
|
167
|
-
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." },
|
|
168
182
|
block_id: { type: "integer" },
|
|
169
183
|
site_url: { type: "string" },
|
|
170
184
|
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
171
185
|
},
|
|
172
|
-
required: ["
|
|
186
|
+
required: ["block_id"],
|
|
173
187
|
},
|
|
174
188
|
},
|
|
175
189
|
{
|
|
176
190
|
name: "reorder_blocks",
|
|
177
|
-
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).",
|
|
178
192
|
inputSchema: {
|
|
179
193
|
type: "object",
|
|
180
194
|
properties: {
|
|
181
|
-
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." },
|
|
182
197
|
order: { type: "array", items: { type: "integer" } },
|
|
183
198
|
site_url: { type: "string" },
|
|
184
199
|
site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
|
|
185
200
|
},
|
|
186
|
-
required: ["
|
|
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: assigned media survives the move (requires the frameworcmcp plugin at v1.2.0 or newer; older versions silently dropped it). 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." },
|
|
215
|
+
},
|
|
216
|
+
required: ["page_id", "block_id", "title"],
|
|
187
217
|
},
|
|
188
218
|
},
|
|
189
219
|
{
|
|
190
220
|
name: "list_forms",
|
|
191
|
-
description: "List Form
|
|
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.",
|
|
192
233
|
inputSchema: {
|
|
193
234
|
type: "object",
|
|
194
235
|
properties: { site_url: { type: "string" } },
|
|
195
236
|
},
|
|
196
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
|
+
},
|
|
197
296
|
{
|
|
198
297
|
name: "list_menus",
|
|
199
|
-
description: "List Menu
|
|
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.",
|
|
200
299
|
inputSchema: {
|
|
201
300
|
type: "object",
|
|
202
|
-
properties: {
|
|
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"],
|
|
203
363
|
},
|
|
204
364
|
},
|
|
205
365
|
{
|
|
206
366
|
name: "list_prefills",
|
|
207
|
-
description: "List Prefill
|
|
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 rewrites media from your payload, so send the blocks as get_prefill returned them or the images are cleared). 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.",
|
|
208
437
|
inputSchema: {
|
|
209
438
|
type: "object",
|
|
210
439
|
properties: { site_url: { type: "string" } },
|
|
211
440
|
},
|
|
212
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
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "list_media",
|
|
456
|
+
description: "Browse the media library one folder at a time. Returns {folder, parent, folders:[{path,name,item_count}], files:[{path,name,file_type,extension,size,last_modified,url}], total_files, truncated}. To put a file on a page, write its `path` — the root-relative form with a leading slash, e.g. \"/logos/brand.svg\" — into a mediafinder field; never the `url`. This server CANNOT upload: if the file the user wants is not here, say so and ask them to add it in Backend > Media. Prefer search_media when you know part of the name. Note that `file_type` buckets follow the install's media config (SVG may report as image or as document), so treat `type` as a convenience filter rather than a guarantee. The library is global: site_id is accepted and ignored.",
|
|
457
|
+
inputSchema: {
|
|
458
|
+
type: "object",
|
|
459
|
+
properties: {
|
|
460
|
+
folder: { type: "string", description: "Folder to list, root-relative with a leading slash. Defaults to \"/\". A folder that does not exist is a 404." },
|
|
461
|
+
type: { type: "string", enum: ["image", "video", "audio", "document"], description: "Filter files by kind." },
|
|
462
|
+
sort: { type: "string", enum: ["title", "size", "modified"], description: "Defaults to title." },
|
|
463
|
+
direction: { type: "string", enum: ["asc", "desc"], description: "Defaults to asc." },
|
|
464
|
+
limit: { type: "integer", description: "Max files to return (default 100, max 500). Folders are never paged." },
|
|
465
|
+
offset: { type: "integer", description: "Files to skip, for paging." },
|
|
466
|
+
site_url: { type: "string" },
|
|
467
|
+
site_id: { type: "integer", description: "Accepted and ignored; the media library is global." },
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: "search_media",
|
|
473
|
+
description: "Find files anywhere in the media library by name. The query is lowercased and split on spaces, and EVERY word must appear somewhere in the file's full path — so \"logo dark\" matches \"/brand/logo-dark.svg\" and \"2024 pdf\" matches \"/reports/2024-annual.pdf\". It matches the whole path, not just the filename. Returns the same file shape as list_media; write the returned `path` into a mediafinder field. Prefer this over list_media whenever the user names a file. This server cannot upload files. Filenames containing + # % or ! are not addressable through this API (an October media library limit), so do not retry those.",
|
|
474
|
+
inputSchema: {
|
|
475
|
+
type: "object",
|
|
476
|
+
properties: {
|
|
477
|
+
q: { type: "string", description: "Search words; at least 2 characters." },
|
|
478
|
+
folder: { type: "string", description: "Optional: keep only results inside this folder subtree." },
|
|
479
|
+
type: { type: "string", enum: ["image", "video", "audio", "document"] },
|
|
480
|
+
sort: { type: "string", enum: ["title", "size", "modified"] },
|
|
481
|
+
direction: { type: "string", enum: ["asc", "desc"] },
|
|
482
|
+
limit: { type: "integer", description: "Max files (default 50, max 200)." },
|
|
483
|
+
offset: { type: "integer", description: "Files to skip, for paging." },
|
|
484
|
+
site_url: { type: "string" },
|
|
485
|
+
site_id: { type: "integer", description: "Accepted and ignored; the media library is global." },
|
|
486
|
+
},
|
|
487
|
+
required: ["q"],
|
|
488
|
+
},
|
|
489
|
+
},
|
|
213
490
|
{
|
|
214
491
|
name: "get_block_schema",
|
|
215
492
|
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.",
|
|
@@ -238,7 +515,7 @@ const tools = [
|
|
|
238
515
|
},
|
|
239
516
|
{
|
|
240
517
|
name: "update_page_meta",
|
|
241
|
-
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
|
|
518
|
+
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 take a media library path from search_media: Meta.ogImage and Navigation.logo / logoDark are single paths, while Footer.logo is an ARRAY of paths of which only the first renders. Affects every page on that site, so confirm with the user first.",
|
|
242
519
|
inputSchema: {
|
|
243
520
|
type: "object",
|
|
244
521
|
properties: {
|
|
@@ -252,13 +529,14 @@ const tools = [
|
|
|
252
529
|
},
|
|
253
530
|
{
|
|
254
531
|
name: "create_translation",
|
|
255
|
-
description: "Create the sibling of an existing page 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.",
|
|
532
|
+
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.",
|
|
256
533
|
inputSchema: {
|
|
257
534
|
type: "object",
|
|
258
535
|
properties: {
|
|
259
|
-
id: { type: "integer", description: "Source page id." },
|
|
536
|
+
id: { type: "integer", description: "Source page (or prefill) id." },
|
|
260
537
|
target_site_id: { type: "integer", description: "Site to create the translation on." },
|
|
261
|
-
source_site_id: { type: "integer", description: "Site the source
|
|
538
|
+
source_site_id: { type: "integer", description: "Site the source record lives on. Defaults to the pinned CMS site." },
|
|
539
|
+
prefill: { type: "boolean", description: "Set true when id is a Prefill entry, not a page." },
|
|
262
540
|
page: { type: "object" },
|
|
263
541
|
builder: { type: "array" },
|
|
264
542
|
site_url: { type: "string" },
|
|
@@ -268,7 +546,7 @@ const tools = [
|
|
|
268
546
|
},
|
|
269
547
|
},
|
|
270
548
|
];
|
|
271
|
-
const server = new index_js_1.Server({ name: "frameworc-mcp", version: "0.
|
|
549
|
+
const server = new index_js_1.Server({ name: "frameworc-mcp", version: "0.5.0" }, { capabilities: { tools: {}, resources: {} } });
|
|
272
550
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
|
|
273
551
|
tools,
|
|
274
552
|
}));
|
|
@@ -371,37 +649,127 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
|
371
649
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
372
650
|
}
|
|
373
651
|
case "create_translation": {
|
|
374
|
-
const
|
|
652
|
+
const payload = { page: args.page ?? {}, builder: args.builder };
|
|
653
|
+
const source = args.source_site_id !== undefined ? Number(args.source_site_id) : undefined;
|
|
654
|
+
const r = args.prefill
|
|
655
|
+
? await client.createPrefillTranslation(Number(args.id), Number(args.target_site_id), { prefill: args.page ?? {}, builder: args.builder }, source)
|
|
656
|
+
: await client.createTranslation(Number(args.id), Number(args.target_site_id), payload, source);
|
|
375
657
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
376
658
|
}
|
|
377
659
|
case "add_block": {
|
|
378
|
-
const r = await client.addBlock(
|
|
660
|
+
const r = await client.addBlock(blockHost(args), args.block, args.position !== undefined ? Number(args.position) : undefined, site);
|
|
379
661
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
380
662
|
}
|
|
381
663
|
case "update_block": {
|
|
382
|
-
const r = await client.updateBlock(
|
|
664
|
+
const r = await client.updateBlock(blockHost(args), Number(args.block_id), args.block, site);
|
|
383
665
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
384
666
|
}
|
|
385
667
|
case "remove_block": {
|
|
386
|
-
const r = await client.removeBlock(
|
|
668
|
+
const r = await client.removeBlock(blockHost(args), Number(args.block_id), site);
|
|
387
669
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
388
670
|
}
|
|
389
671
|
case "reorder_blocks": {
|
|
390
|
-
const r = await client.reorderBlocks(
|
|
672
|
+
const r = await client.reorderBlocks(blockHost(args), args.order, site);
|
|
673
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
674
|
+
}
|
|
675
|
+
case "extract_block_to_prefill": {
|
|
676
|
+
const r = await client.extractBlockToPrefill(Number(args.page_id), Number(args.block_id), args.title ?? "", site);
|
|
391
677
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
392
678
|
}
|
|
393
679
|
case "list_forms": {
|
|
394
680
|
const r = await client.listForms(site);
|
|
395
681
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
396
682
|
}
|
|
683
|
+
case "get_form_schema": {
|
|
684
|
+
const r = await client.getFormSchema();
|
|
685
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
686
|
+
}
|
|
687
|
+
case "get_form": {
|
|
688
|
+
const r = await client.getForm(Number(args.id), site);
|
|
689
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
690
|
+
}
|
|
691
|
+
case "create_form": {
|
|
692
|
+
const r = await client.createForm(args.payload, site);
|
|
693
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
694
|
+
}
|
|
695
|
+
case "update_form": {
|
|
696
|
+
const r = await client.updateForm(Number(args.id), args.payload, site);
|
|
697
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
698
|
+
}
|
|
699
|
+
case "delete_form": {
|
|
700
|
+
const r = await client.deleteForm(Number(args.id), Boolean(args.force), site);
|
|
701
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
702
|
+
}
|
|
397
703
|
case "list_menus": {
|
|
398
704
|
const r = await client.listMenus(site);
|
|
399
705
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
400
706
|
}
|
|
707
|
+
case "get_menu": {
|
|
708
|
+
const r = await client.getMenu(Number(args.id), site);
|
|
709
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
710
|
+
}
|
|
711
|
+
case "create_menu": {
|
|
712
|
+
const r = await client.createMenu(args.payload, site);
|
|
713
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
714
|
+
}
|
|
715
|
+
case "update_menu": {
|
|
716
|
+
const r = await client.updateMenu(Number(args.id), args.payload, site);
|
|
717
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
718
|
+
}
|
|
719
|
+
case "delete_menu": {
|
|
720
|
+
const r = await client.deleteMenu(Number(args.id), Boolean(args.force), site);
|
|
721
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
722
|
+
}
|
|
401
723
|
case "list_prefills": {
|
|
402
724
|
const r = await client.listPrefills(site);
|
|
403
725
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
404
726
|
}
|
|
727
|
+
case "get_prefill": {
|
|
728
|
+
const r = await client.getPrefill(Number(args.id), site);
|
|
729
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
730
|
+
}
|
|
731
|
+
case "create_prefill": {
|
|
732
|
+
const r = await client.createPrefill(args.payload, site);
|
|
733
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
734
|
+
}
|
|
735
|
+
case "update_prefill": {
|
|
736
|
+
const r = await client.updatePrefill(Number(args.id), args.payload, site);
|
|
737
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
738
|
+
}
|
|
739
|
+
case "delete_prefill": {
|
|
740
|
+
const r = await client.deletePrefill(Number(args.id), Boolean(args.force), site);
|
|
741
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
742
|
+
}
|
|
743
|
+
case "list_media": {
|
|
744
|
+
const r = await client.listMedia({
|
|
745
|
+
folder: args.folder,
|
|
746
|
+
type: args.type,
|
|
747
|
+
sort: args.sort,
|
|
748
|
+
direction: args.direction,
|
|
749
|
+
limit: args.limit,
|
|
750
|
+
offset: args.offset,
|
|
751
|
+
}, site);
|
|
752
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
753
|
+
}
|
|
754
|
+
case "search_media": {
|
|
755
|
+
const r = await client.searchMedia(String(args.q ?? ""), {
|
|
756
|
+
folder: args.folder,
|
|
757
|
+
type: args.type,
|
|
758
|
+
sort: args.sort,
|
|
759
|
+
direction: args.direction,
|
|
760
|
+
limit: args.limit,
|
|
761
|
+
offset: args.offset,
|
|
762
|
+
}, site);
|
|
763
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
764
|
+
}
|
|
765
|
+
case "get_settings": {
|
|
766
|
+
const r = await client.getSettings();
|
|
767
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
768
|
+
}
|
|
769
|
+
case "update_settings": {
|
|
770
|
+
const r = await client.updateSettings(args.fields ?? {});
|
|
771
|
+
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
|
772
|
+
}
|
|
405
773
|
case "get_page_meta": {
|
|
406
774
|
const r = await client.getSingle(String(args.handle), site);
|
|
407
775
|
return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
|
package/package.json
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "frameworc-mcp",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "MCP server for the FrameworC OctoberCMS plugin — programmatically create/edit FrameworC pages and blocks.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "commonjs",
|
|
7
|
-
"bin": {
|
|
8
|
-
"frameworc-mcp": "dist/index.js"
|
|
9
|
-
},
|
|
10
|
-
"main": "dist/index.js",
|
|
11
|
-
"files": [
|
|
12
|
-
"dist",
|
|
13
|
-
"README.md"
|
|
14
|
-
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"build": "tsc",
|
|
17
|
-
"start": "node dist/index.js",
|
|
18
|
-
"prepublishOnly": "npm run build"
|
|
19
|
-
},
|
|
20
|
-
"dependencies": {
|
|
21
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
22
|
-
},
|
|
23
|
-
"devDependencies": {
|
|
24
|
-
"@types/node": "^20.0.0",
|
|
25
|
-
"typescript": "^5.4.0"
|
|
26
|
-
},
|
|
27
|
-
"engines": {
|
|
28
|
-
"node": ">=18.0.0"
|
|
29
|
-
},
|
|
30
|
-
"publishConfig": {
|
|
31
|
-
"@yourorg:registry": "https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/"
|
|
32
|
-
}
|
|
33
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "frameworc-mcp",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "MCP server for the FrameworC OctoberCMS plugin — programmatically create/edit FrameworC pages and blocks.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": {
|
|
8
|
+
"frameworc-mcp": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "dist/index.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.0.0",
|
|
25
|
+
"typescript": "^5.4.0"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18.0.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"@yourorg:registry": "https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/"
|
|
32
|
+
}
|
|
33
|
+
}
|