frameworc-mcp 0.5.1 → 0.7.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/README.md CHANGED
@@ -1,242 +1,246 @@
1
- # @yourorg/frameworc-mcp
2
-
3
- A [Model Context Protocol](https://modelcontextprotocol.io) server that lets Claude / Cursor / opencode / other AI chat clients create and edit pages built with the **FrameworC** OctoberCMS plugin. One MCP process serves any number of OctCMS installs — you tell the chat which site to work on by calling `use_site`.
4
-
5
- ## What it does
6
-
7
- - Composes 15 prebuilt FrameworC blocks (`Header`, `Section`, `Tiles`, `Slider`, `Tabs`, `Accordion`, `Form`, `Gallery`, `Downloads`, `Columns`, `Prefill`, `BlogList`, `MenuBlock`, `ImageStrip`, `InstaFeed`) into pages
8
- - Lists existing pages and their full builder JSON
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
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
15
- - Draft by default — created pages have `is_enabled = false`; the human flips the switch in the OctCMS backend after assigning images
16
-
17
- The chat-side flow (text → blocks) is **LLM-native**: you paste a markdown page (or upload `.docx` / `.pdf` and instruct Claude to convert it to markdown), Claude reads the `frameworc://blocks` resource once, segments the content into blocks, calls `list_forms` / `list_menus` / `list_prefills` as needed, then calls `create_page` with the assembled JSON. The MCP simply forwards authenticated HTTPS calls to the OctCMS install.
18
-
19
- ## Architecture
20
-
21
- ```
22
- chat client (Claude Desktop / opencode / Cursor / Cline / Continue / ...)
23
- │ stdio JSON-RPC
24
- ▼
25
- node dist/index.js (local, spawned by the chat client)
26
- reads ~/.config/frameworc/sites.json (auto-created on first run; hot-reloaded)
27
- │ HTTPS + Authorization: Bearer <token resolved by URL>
28
- ▼
29
- any OctCMS install /api/mcp/v1/* (provided by the crscompany/frameworcmcp plugin v1.1.0+)
30
- ```
31
-
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.
33
-
34
- ## Install
35
-
36
- ```bash
37
- git clone <your-gitlab-url>/frameworc-mcp.git
38
- cd frameworc-mcp
39
- npm install
40
- npm run build
41
- ```
42
-
43
- (Optional) publish to your GitLab group's npm registry so colleagues can `npx` it without a clone — see [Publishing](#publishing) below.
44
-
45
- ## Claude Desktop config
46
-
47
- Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`; Linux: `~/.config/Claude/`):
48
-
49
- ```json
50
- {
51
- "mcpServers": {
52
- "frameworc": {
53
- "command": "npx",
54
- "args": ["-y", "@yourorg/frameworc-mcp"]
55
- }
56
- }
57
- }
58
- ```
59
-
60
- If you cloned instead of `npx`-installed, point `args` to your local `dist/index.js`:
61
-
62
- ```json
63
- {
64
- "mcpServers": {
65
- "frameworc": {
66
- "command": "node",
67
- "args": ["/absolute/path/to/frameworc-mcp/dist/index.js"]
68
- }
69
- }
70
- }
71
- ```
72
-
73
- No env vars. Restart Claude → the `frameworc` tools appear. On first start the MCP creates `~/.config/frameworc/sites.json` with a template; edit it with your real sites (see below).
74
-
75
- ## Per-site token file — `~/.config/frameworc/sites.json`
76
-
77
- The MCP stores one bearer token per OctCMS install in this file on your laptop. It's the only credential store. Format:
78
-
79
- ```json
80
- {
81
- "sites": [
82
- { "label": "Client A prod", "url": "https://clienta.test", "token": "AAA..." },
83
- { "label": "Client A staging", "url": "https://staging.clienta.test", "token": "BBB..." },
84
- { "label": "Myco", "url": "https://myco.example", "token": "CCC..." }
85
- ]
86
- }
87
- ```
88
-
89
- - `label` is optional, shown by the `list_sites` tool so the chat can pick a site by name when you say "use the myco site".
90
- - `url` is matched after normalising trailing slashes — `https://x.test/` and `https://x.test` are the same.
91
- - `token` is sent as `Authorization: Bearer <token>` on every HTTP call to that site. Never committed to git.
92
-
93
- The file is created automatically on first MCP startup (with a template and `0600` perms). Edits are picked up via stat-on-every-tool-call hot-reload — no MCP or chat-client restart needed.
94
-
95
- If the file is missing or empty, `use_site` errors with a pointer to where to add it. The `list_sites` tool enumerates configured sites without exposing tokens.
96
-
97
- ### Adding a new OctCMS site
98
-
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.
101
- 3. Edit `~/.config/frameworc/sites.json` on your laptop, add one entry: `{ "label": "New Client", "url": "https://newsite.test", "token": "<that string>" }`.
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.
103
-
104
- No env var, no chat-client config edit, no ToolHive touch, no repo push. The whole token map lives in one file on your machine.
105
-
106
- ### Security
107
-
108
- - The file should be `0600` (auto-set on first creation). If you `chmod` it looser, the MCP prints a stderr warning on startup.
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.
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.
111
-
112
- ## Tools (0.5.0)
113
-
114
- | Tool | Description |
115
- |---|---|
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. |
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). |
120
- | `list_pages` | List all pages on the pinned site. |
121
- | `get_page(id)` | Full page JSON (page meta + builder blocks + nested repeaters). |
122
- | `create_page(payload)` | Create a draft page. `payload = { page: {...}, builder: [...] }`. |
123
- | `update_page(id, payload)` | Edit page meta, or full-rebuild the builder array (prefer the per-block tools). |
124
- | `delete_page(id)` | Soft-delete a page. |
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`.
145
-
146
- ## Resource
147
-
148
- - `frameworc://blocks` — reference for all 15 block types (fields, defaults, enums, conditional visibility, nested repeaters, reference fields). The chat reads this once per session; you don't invoke it manually.
149
-
150
- ## Page-content JSON shape
151
-
152
- `create_page` / `update_page` `payload`:
153
-
154
- ```json
155
- {
156
- "page": {
157
- "title": "Contact",
158
- "slug": "contact",
159
- "fullslug": "contact",
160
- "is_enabled": false,
161
- "metaTitle": "",
162
- "metaDescription": "",
163
- "menuStyle": "solid",
164
- "menuHide": "no"
165
- },
166
- "builder": [
167
- {
168
- "content_group": "Header",
169
- "base": {
170
- "blockId": "kontakt",
171
- "headline": "<h1>Contact us</h1>",
172
- "elevated": false,
173
- "containerWidth": "default",
174
- "backgroundColor": "default",
175
- "customCssClass": [],
176
- "responsiveHide": []
177
- },
178
- "content": {
179
- "image": "",
180
- "imageMobile": "",
181
- "isVideoBg": false,
182
- "buttonLabel1": "",
183
- "buttonLink1": "",
184
- "buttonBlank1": false,
185
- "fullHeight": true,
186
- "contrast": false,
187
- "overlay": false
188
- }
189
- },
190
- {
191
- "content_group": "Form",
192
- "base": { "blockId": "formular", "headline": "<h2>Write us</h2>" },
193
- "form": 7,
194
- "content": { "variant": "default" }
195
- }
196
- ]
197
- }
198
- ```
199
-
200
- ### Storage encoding (what the MCP understands)
201
-
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`.
203
- - `switch` fields accept booleans; the API normalises to `"1"` / `"0"` for storage.
204
- - `customCssClass` + `responsiveHide` are arrays of strings.
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}`.
207
- - `Slider` accepts an optional `content.breakpoints = { tablet: number, mobile: number }`.
208
- - `Columns` blocks have `content.columns = [{ blockId, builder: [...blocks] }]` — recursive: each column's `builder` follows the exact same shape as the top-level `builder`.
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
-
219
- ## Catalogue drift
220
-
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.
222
-
223
- ## Publishing
224
-
225
- To publish to your GitLab group's npm registry:
226
-
227
- 1. Edit `package.json`:
228
- - Replace `@yourorg` in `name` and `publishConfig["@yourorg:registry"]` with your actual GitLab scope/group.
229
- - Replace `<YOUR-PROJECT-ID>` in `publishConfig["@yourorg:registry"]` with the numeric project id of the `frameworc-mcp` GitLab repo.
230
- 2. Create a GitLab deploy token / project access token with `api` + `write_registry` scope. Add a `.npmrc` at the registry host:
231
-
232
- ```
233
- @yourorg:registry=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/
234
- //gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/:_authToken=<TOKEN>
235
- ```
236
- 3. `npm publish`.
237
-
238
- Colleagues then use `npx -y @yourorg/frameworc-mcp` in their `claude_desktop_config.json` (no clone needed).
239
-
240
- ## License
241
-
1
+ # @yourorg/frameworc-mcp
2
+
3
+ A [Model Context Protocol](https://modelcontextprotocol.io) server that lets Claude / Cursor / opencode / other AI chat clients create and edit pages built with the **FrameworC** OctoberCMS plugin. One MCP process serves any number of OctCMS installs — you tell the chat which site to work on by calling `use_site`.
4
+
5
+ ## What it does
6
+
7
+ - Composes 15 prebuilt FrameworC blocks (`Header`, `Section`, `Tiles`, `Slider`, `Tabs`, `Accordion`, `Form`, `Gallery`, `Downloads`, `Columns`, `Prefill`, `BlogList`, `MenuBlock`, `ImageStrip`, `InstaFeed`) into pages
8
+ - Lists existing pages and their full builder JSON
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 FrameworC settings (global navbar options and SCSS, per-site SCSS; 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
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
15
+ - Draft by default — created pages have `is_enabled = false`; the human flips the switch in the OctCMS backend after assigning images
16
+
17
+ The chat-side flow (text → blocks) is **LLM-native**: you paste a markdown page (or upload `.docx` / `.pdf` and instruct Claude to convert it to markdown), Claude reads the `frameworc://blocks` resource once, segments the content into blocks, calls `list_forms` / `list_menus` / `list_prefills` as needed, then calls `create_page` with the assembled JSON. The MCP simply forwards authenticated HTTPS calls to the OctCMS install.
18
+
19
+ ## Architecture
20
+
21
+ ```
22
+ chat client (Claude Desktop / opencode / Cursor / Cline / Continue / ...)
23
+ │ stdio JSON-RPC
24
+ ▼
25
+ node dist/index.js (local, spawned by the chat client)
26
+ reads ~/.config/frameworc/sites.json (auto-created on first run; hot-reloaded)
27
+ │ HTTPS + Authorization: Bearer <token resolved by URL>
28
+ ▼
29
+ any OctCMS install /api/mcp/v1/* (provided by the crscompany/frameworcmcp plugin v1.1.0+)
30
+ ```
31
+
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.
33
+
34
+ ## Install
35
+
36
+ ```bash
37
+ git clone <your-gitlab-url>/frameworc-mcp.git
38
+ cd frameworc-mcp
39
+ npm install
40
+ npm run build
41
+ ```
42
+
43
+ (Optional) publish to your GitLab group's npm registry so colleagues can `npx` it without a clone — see [Publishing](#publishing) below.
44
+
45
+ ## Claude Desktop config
46
+
47
+ Edit `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/`; Linux: `~/.config/Claude/`):
48
+
49
+ ```json
50
+ {
51
+ "mcpServers": {
52
+ "frameworc": {
53
+ "command": "npx",
54
+ "args": ["-y", "@yourorg/frameworc-mcp"]
55
+ }
56
+ }
57
+ }
58
+ ```
59
+
60
+ If you cloned instead of `npx`-installed, point `args` to your local `dist/index.js`:
61
+
62
+ ```json
63
+ {
64
+ "mcpServers": {
65
+ "frameworc": {
66
+ "command": "node",
67
+ "args": ["/absolute/path/to/frameworc-mcp/dist/index.js"]
68
+ }
69
+ }
70
+ }
71
+ ```
72
+
73
+ No env vars. Restart Claude → the `frameworc` tools appear. On first start the MCP creates `~/.config/frameworc/sites.json` with a template; edit it with your real sites (see below).
74
+
75
+ ## Per-site token file — `~/.config/frameworc/sites.json`
76
+
77
+ The MCP stores one bearer token per OctCMS install in this file on your laptop. It's the only credential store. Format:
78
+
79
+ ```json
80
+ {
81
+ "sites": [
82
+ { "label": "Client A prod", "url": "https://clienta.test", "token": "AAA..." },
83
+ { "label": "Client A staging", "url": "https://staging.clienta.test", "token": "BBB..." },
84
+ { "label": "Myco", "url": "https://myco.example", "token": "CCC..." }
85
+ ]
86
+ }
87
+ ```
88
+
89
+ - `label` is optional, shown by the `list_sites` tool so the chat can pick a site by name when you say "use the myco site".
90
+ - `url` is matched after normalising trailing slashes — `https://x.test/` and `https://x.test` are the same.
91
+ - `token` is sent as `Authorization: Bearer <token>` on every HTTP call to that site. Never committed to git.
92
+
93
+ The file is created automatically on first MCP startup (with a template and `0600` perms). Edits are picked up via stat-on-every-tool-call hot-reload — no MCP or chat-client restart needed.
94
+
95
+ If the file is missing or empty, `use_site` errors with a pointer to where to add it. The `list_sites` tool enumerates configured sites without exposing tokens.
96
+
97
+ ### Adding a new OctCMS site
98
+
99
+ 1. Spin up the new OctCMS install with the FrameworC suite incl. the `crscompany/frameworcmcp` plugin (v1.4.0 or newer; media assignment needs v1.2.0, per-site settings need v1.3.0 with FrameworC v1.10.0, page JSON-LD fields need v1.4.0 with FrameworC v1.13.0).
100
+ 2. Backend → Settings → FrameworC → **MCP API** → paste a freshly generated random string (e.g. `openssl rand -hex 32`) → Save.
101
+ 3. Edit `~/.config/frameworc/sites.json` on your laptop, add one entry: `{ "label": "New Client", "url": "https://newsite.test", "token": "<that string>" }`.
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.
103
+
104
+ No env var, no chat-client config edit, no ToolHive touch, no repo push. The whole token map lives in one file on your machine.
105
+
106
+ ### Security
107
+
108
+ - The file should be `0600` (auto-set on first creation). If you `chmod` it looser, the MCP prints a stderr warning on startup.
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.
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.
111
+
112
+ ## Tools (0.5.0)
113
+
114
+ | Tool | Description |
115
+ |---|---|
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. |
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). |
120
+ | `list_pages` | List all pages on the pinned site. |
121
+ | `get_page(id)` | Full page JSON (page meta + builder blocks + nested repeaters). |
122
+ | `create_page(payload)` | Create a draft page. `payload = { page: {...}, builder: [...] }`. |
123
+ | `update_page(id, payload)` | Edit page meta, or full-rebuild the builder array (prefer the per-block tools). |
124
+ | `delete_page(id)` | Soft-delete a page. |
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 + global custom SCSS (`styles_globalScss`). Integration secrets are not exposed. |
140
+ | `get_site_settings` / `update_site_settings(fields)` | Per-site FrameworC settings: custom SCSS for one site (`siteScss`), compiled after the global SCSS. |
141
+ | `get_block_schema(name)` | Field schema + usage notes for one block (live from the CMS). |
142
+ | `list_media(folder?, type?, sort?, limit?, offset?)` | Browse the media library. Returns files with their `path` — write that into a mediafinder field. |
143
+ | `search_media(q, folder?, type?, ...)` | Find library files by name across all folders. Every whitespace-separated word must appear in the path. |
144
+
145
+ Form and Menu entries have no translation linking — create them per site by passing `site_id`.
146
+
147
+ ## Resource
148
+
149
+ - `frameworc://blocks` — reference for all 15 block types (fields, defaults, enums, conditional visibility, nested repeaters, reference fields). The chat reads this once per session; you don't invoke it manually.
150
+
151
+ ## Page-content JSON shape
152
+
153
+ `create_page` / `update_page` `payload`:
154
+
155
+ ```json
156
+ {
157
+ "page": {
158
+ "title": "Contact",
159
+ "slug": "contact",
160
+ "fullslug": "contact",
161
+ "is_enabled": false,
162
+ "metaTitle": "",
163
+ "metaDescription": "",
164
+ "menuStyle": "solid",
165
+ "menuHide": "no",
166
+ "jsonLdPageType": "ContactPage",
167
+ "jsonLdDisable": false,
168
+ "jsonLdCustom": ""
169
+ },
170
+ "builder": [
171
+ {
172
+ "content_group": "Header",
173
+ "base": {
174
+ "blockId": "kontakt",
175
+ "headline": "<h1>Contact us</h1>",
176
+ "elevated": false,
177
+ "containerWidth": "default",
178
+ "backgroundColor": "default",
179
+ "customCssClass": [],
180
+ "responsiveHide": []
181
+ },
182
+ "content": {
183
+ "image": "",
184
+ "imageMobile": "",
185
+ "isVideoBg": false,
186
+ "buttonLabel1": "",
187
+ "buttonLink1": "",
188
+ "buttonBlank1": false,
189
+ "fullHeight": true,
190
+ "contrast": false,
191
+ "overlay": false
192
+ }
193
+ },
194
+ {
195
+ "content_group": "Form",
196
+ "base": { "blockId": "formular", "headline": "<h2>Write us</h2>" },
197
+ "form": 7,
198
+ "content": { "variant": "default" }
199
+ }
200
+ ]
201
+ }
202
+ ```
203
+
204
+ ### Storage encoding (what the MCP understands)
205
+
206
+ - 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`.
207
+ - `switch` fields accept booleans; the API normalises to `"1"` / `"0"` for storage.
208
+ - `customCssClass` + `responsiveHide` are arrays of strings.
209
+ - 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**.
210
+ - `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}`.
211
+ - `Slider` accepts an optional `content.breakpoints = { tablet: number, mobile: number }`.
212
+ - `Columns` blocks have `content.columns = [{ blockId, builder: [...blocks] }]` — recursive: each column's `builder` follows the exact same shape as the top-level `builder`.
213
+
214
+ ### Working with media
215
+
216
+ `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:
217
+
218
+ - The `Navigation` logo is an SVG and `Header.image` holds an `.mp4` when `isVideoBg` is on. Both are valid.
219
+ - `buttonIcon1..4` and `Downloads`' `file` declare no `mode` at all, so any file type is accepted there.
220
+ - 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.
221
+ - 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.
222
+
223
+ ## Catalogue drift
224
+
225
+ 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.
226
+
227
+ ## Publishing
228
+
229
+ To publish to your GitLab group's npm registry:
230
+
231
+ 1. Edit `package.json`:
232
+ - Replace `@yourorg` in `name` and `publishConfig["@yourorg:registry"]` with your actual GitLab scope/group.
233
+ - Replace `<YOUR-PROJECT-ID>` in `publishConfig["@yourorg:registry"]` with the numeric project id of the `frameworc-mcp` GitLab repo.
234
+ 2. Create a GitLab deploy token / project access token with `api` + `write_registry` scope. Add a `.npmrc` at the registry host:
235
+
236
+ ```
237
+ @yourorg:registry=https://gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/
238
+ //gitlab.com/api/v4/projects/<YOUR-PROJECT-ID>/packages/npm/:_authToken=<TOKEN>
239
+ ```
240
+ 3. `npm publish`.
241
+
242
+ Colleagues then use `npx -y @yourorg/frameworc-mcp` in their `claude_desktop_config.json` (no clone needed).
243
+
244
+ ## License
245
+
242
246
  MIT.
@@ -199,6 +199,13 @@ class ApiClient {
199
199
  updateSettings(fields) {
200
200
  return this.request("/settings", { method: "PATCH", body: JSON.stringify({ fields }) });
201
201
  }
202
+ // --- FrameworC settings (per site) -------------------------------------
203
+ getSiteSettings(siteId) {
204
+ return this.request(this.q("/site-settings", siteId));
205
+ }
206
+ updateSiteSettings(fields, siteId) {
207
+ return this.request("/site-settings", { method: "PATCH", body: this.b({ fields }, siteId) });
208
+ }
202
209
  // --- singles ----------------------------------------------------------
203
210
  getSingle(handle, siteId) {
204
211
  return this.request(this.q(`/singles/${encodeURIComponent(handle)}`, siteId));
package/dist/catalogue.js CHANGED
@@ -42,7 +42,7 @@ const baseBlock = [
42
42
  { name: "responsiveHide", type: "checkboxlist", enum: ["desktop", "tablet", "mobile"], comment: "Which devices to hide this block on." },
43
43
  ];
44
44
  const sectionVariants = [
45
- "halfAndHalf", "noText", "noImage", "textAndText", "img70", "text70",
45
+ "halfAndHalf", "imgBleed", "noText", "noImage", "textAndText", "img70", "text70",
46
46
  "embedHalfAndHalf", "embed70", "embed30", "embedOnly",
47
47
  ];
48
48
  exports.BLOCKS = [
@@ -56,6 +56,8 @@ exports.BLOCKS = [
56
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
+ { name: "fullVideo", type: "media-single", comment: "Only used when isVideoBg is on. Media library path to the full-length video; a play button opens it in a lightbox popup over the page." },
60
+ { name: "fullVideoLabel", type: "text", comment: "Only used when isVideoBg is on. Label of the button that opens fullVideo." },
59
61
  ...buttonsMixin,
60
62
  { name: "fullHeight", type: "switch", default: true },
61
63
  { name: "contrast", type: "switch", default: false, comment: "Use contrast (light-on-dark) text for the headline when the background image is dark." },
@@ -66,11 +68,12 @@ exports.BLOCKS = [
66
68
  content_group: "Section",
67
69
  name: "Section",
68
70
  description: "Text + image / embed in a configurable column layout. The most common content block.",
69
- whenToUse: "Default choice for any paragraph + image content. Pick a variant that matches the proportion of text to visual. Use 'textAndText' for two text columns (e.g. a feature comparison). Use an 'embed*' variant when you have a YouTube video, map iframe, etc.",
71
+ whenToUse: "Default choice for any paragraph + image content. Pick a variant that matches the proportion of text to visual. Use 'imgBleed' for an image running to the edge of the viewport next to half-width text. Use 'textAndText' for two text columns (e.g. a feature comparison). Use an 'embed*' variant when you have a YouTube video, map iframe, etc.",
70
72
  base: baseBlock,
71
73
  content: [
72
74
  { name: "variant", type: "dropdown", enum: sectionVariants, default: "halfAndHalf" },
73
75
  { name: "reverse", type: "switch", default: false, hiddenWhen: { field: "variant", equals: ["noText", "noImage", "embedOnly"] }, comment: "Swap sides (image left vs right)." },
76
+ { name: "reverseMobile", type: "switch", default: false, hiddenWhen: { field: "variant", equals: ["noText", "noImage", "embedOnly"] }, comment: "On mobile the columns stack; this flips their stacking order." },
74
77
  { 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
78
  { name: "embed", type: "codeeditor", shownWhen: { field: "variant", equals: ["embedHalfAndHalf", "embedOnly", "embed30", "embed70"] }, comment: "Raw embed HTML (iframe etc.)." },
76
79
  ...buttonsMixin,
package/dist/index.js CHANGED
@@ -93,7 +93,7 @@ const tools = [
93
93
  },
94
94
  {
95
95
  name: "create_page",
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.",
96
+ description: "Create a new FrameworC page. Send a `page` object (title, slug, fullslug, is_enabled, metaTitle, metaDescription, menuStyle, menuHide, jsonLdPageType, jsonLdDisable, jsonLdCustom) 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.",
97
97
  inputSchema: {
98
98
  type: "object",
99
99
  properties: {
@@ -114,7 +114,7 @@ const tools = [
114
114
  },
115
115
  {
116
116
  name: "update_page",
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.",
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. Structured data: the site renders schema.org JSON-LD automatically; `page.jsonLdPageType` is WebPage / AboutPage / ContactPage / CollectionPage / FAQPage, `page.jsonLdDisable` turns the generated graph off, and `page.jsonLdCustom` is an extra JSON-LD document as a JSON string (no script tag; invalid JSON is rejected).",
118
118
  inputSchema: {
119
119
  type: "object",
120
120
  properties: {
@@ -433,7 +433,7 @@ const tools = [
433
433
  },
434
434
  {
435
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.",
436
+ description: "Read the FrameworC plugin settings exposed over the API: navbar layout options and the global custom SCSS. Settings are GLOBAL (not per multisite site); per-site SCSS lives in get_site_settings. Integration/secret settings are not accessible by design.",
437
437
  inputSchema: {
438
438
  type: "object",
439
439
  properties: { site_url: { type: "string" } },
@@ -441,7 +441,7 @@ const tools = [
441
441
  },
442
442
  {
443
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.",
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), styles_globalScss (SCSS compiled into every site before that site's own SCSS, so its $variables and mixins are usable there; SCSS that does not compile is rejected). 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
445
  inputSchema: {
446
446
  type: "object",
447
447
  properties: {
@@ -451,6 +451,30 @@ const tools = [
451
451
  required: ["fields"],
452
452
  },
453
453
  },
454
+ {
455
+ name: "get_site_settings",
456
+ description: "Read the per-site FrameworC settings: siteScss, the custom SCSS for one multisite site. It is compiled after the global styles_globalScss from get_settings, so it can use that SCSS's $variables and mixins.",
457
+ inputSchema: {
458
+ type: "object",
459
+ properties: {
460
+ site_url: { type: "string" },
461
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
462
+ },
463
+ },
464
+ },
465
+ {
466
+ name: "update_site_settings",
467
+ description: "Update the per-site FrameworC settings. `fields` is a partial map; writable key: siteScss (SCSS for this site only, compiled after the global SCSS; SCSS that does not compile is rejected). Affects every page on that site, so confirm with the user first.",
468
+ inputSchema: {
469
+ type: "object",
470
+ properties: {
471
+ fields: { type: "object", description: "Field name => value map (partial)." },
472
+ site_url: { type: "string" },
473
+ site_id: { type: "integer", description: "Target multisite site id; defaults to the pinned CMS site." },
474
+ },
475
+ required: ["fields"],
476
+ },
477
+ },
454
478
  {
455
479
  name: "list_media",
456
480
  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.",
@@ -502,7 +526,7 @@ const tools = [
502
526
  },
503
527
  {
504
528
  name: "get_page_meta",
505
- 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.",
529
+ description: "Read a per-site single: Meta (SEO defaults, blog base path, organization data for JSON-LD), Navigation (navbar) or Footer. Use before composing pages so titles and descriptions match the site's conventions.",
506
530
  inputSchema: {
507
531
  type: "object",
508
532
  properties: {
@@ -515,7 +539,7 @@ const tools = [
515
539
  },
516
540
  {
517
541
  name: "update_page_meta",
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.",
542
+ 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. Meta also holds the organization data rendered as JSON-LD on every page (orgType, orgName, orgLegalName, orgIco, orgLogo, orgPhone, orgEmail, orgStreet, orgCity, orgZip, orgCountry, orgLat, orgLng, orgOpeningHours, jsonLdCustom); read get_page_meta for the exact schema. Affects every page on that site, so confirm with the user first.",
519
543
  inputSchema: {
520
544
  type: "object",
521
545
  properties: {
@@ -770,6 +794,14 @@ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
770
794
  const r = await client.updateSettings(args.fields ?? {});
771
795
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
772
796
  }
797
+ case "get_site_settings": {
798
+ const r = await client.getSiteSettings(site);
799
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
800
+ }
801
+ case "update_site_settings": {
802
+ const r = await client.updateSiteSettings(args.fields ?? {}, site);
803
+ return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
804
+ }
773
805
  case "get_page_meta": {
774
806
  const r = await client.getSingle(String(args.handle), site);
775
807
  return { content: [{ type: "text", text: JSON.stringify(r, null, 2) }] };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frameworc-mcp",
3
- "version": "0.5.1",
3
+ "version": "0.7.0",
4
4
  "description": "MCP server for the FrameworC OctoberCMS plugin — programmatically create/edit FrameworC pages and blocks.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",